forked from ebhomengo/niki
Merge pull request 'Implement patient list endpoints' (#258) from feature-patient-analytic into develop
Reviewed-on: ebhomengo/niki#258 Reviewed-by: hossein <h.nazari1990@gmail.com>
This commit is contained in:
commit
05b06df7eb
|
|
@ -18,8 +18,8 @@ type TestContainer struct {
|
||||||
dockerPool *dockertest.Pool // the connection pool to Docker.
|
dockerPool *dockertest.Pool // the connection pool to Docker.
|
||||||
mariaResource *dockertest.Resource // MariaDB Docker container resource.
|
mariaResource *dockertest.Resource // MariaDB Docker container resource.
|
||||||
redisResource *dockertest.Resource // Redis Docker container resource.
|
redisResource *dockertest.Resource // Redis Docker container resource.
|
||||||
mariaDBConn *mysql.DB // Connection to the MariaDB database.
|
mariaDBConn *mysql.DB // Connection to the MariaDB mysql.
|
||||||
redisDBConn *redisadapter.Adapter // Connection to the Redis database.
|
redisDBConn *redisadapter.Adapter // Connection to the Redis mysql.
|
||||||
containerExpiryInSeconds uint
|
containerExpiryInSeconds uint
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,7 +158,7 @@ func (t *TestContainer) Start() {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Fatalf("Could not connect to database: %s", err)
|
log.Fatalf("Could not connect to mysql: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
2
main.go
2
main.go
|
|
@ -43,7 +43,7 @@ func Config() config.Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
func MariaDB(cfg config.Config) *mysql.DB {
|
func MariaDB(cfg config.Config) *mysql.DB {
|
||||||
migrate := flag.Bool("migrate", false, "perform database migration")
|
migrate := flag.Bool("migrate", false, "perform mysql migration")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
if *migrate {
|
if *migrate {
|
||||||
migrator.New(migrator.Config{
|
migrator.New(migrator.Config{
|
||||||
|
|
|
||||||
|
|
@ -1 +1,37 @@
|
||||||
package patientapp
|
package patientapp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.gocasts.ir/ebhomengo/niki/patientapp/config"
|
||||||
|
"git.gocasts.ir/ebhomengo/niki/patientapp/delivery/http/analytic"
|
||||||
|
"git.gocasts.ir/ebhomengo/niki/patientapp/repository/mysql"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Application struct {
|
||||||
|
//Config Config
|
||||||
|
HTTPServer *config.EchoServer
|
||||||
|
DB *mysql.DataBase
|
||||||
|
}
|
||||||
|
|
||||||
|
func Setup(cfg config.Config, conn *mysql.DataBase) Application {
|
||||||
|
|
||||||
|
e := echo.New()
|
||||||
|
|
||||||
|
server := config.EchoServer{
|
||||||
|
Router: e,
|
||||||
|
Config: cfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
return Application{
|
||||||
|
//Config: config,
|
||||||
|
HTTPServer: &server,
|
||||||
|
DB: conn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Application) Start() {
|
||||||
|
|
||||||
|
server := analytic.NewServer(a.HTTPServer)
|
||||||
|
|
||||||
|
_ = server.Serve()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.gocasts.ir/ebhomengo/niki/patientapp"
|
||||||
|
"git.gocasts.ir/ebhomengo/niki/patientapp/config"
|
||||||
|
"git.gocasts.ir/ebhomengo/niki/patientapp/repository/mysql"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
db := mysql.DataBase{}
|
||||||
|
|
||||||
|
cfg := config.Config{
|
||||||
|
Port: 8080,
|
||||||
|
Cors: config.Cors{
|
||||||
|
AllowOrigins: []string{"*"},
|
||||||
|
},
|
||||||
|
ShutDownCtxTimeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
app := patientapp.Setup(cfg, &db)
|
||||||
|
|
||||||
|
app.Start()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Port int `koanf:"port"`
|
||||||
|
Cors Cors `koanf:"cors"`
|
||||||
|
ShutDownCtxTimeout time.Duration `koanf:"shutdown_context_timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Cors struct {
|
||||||
|
AllowOrigins []string `koanf:"allow_origins"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EchoServer struct {
|
||||||
|
Router *echo.Echo
|
||||||
|
Config Config
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
package analytic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
svc "git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic"
|
||||||
|
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
service svc.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandler(service svc.Service) *Handler {
|
||||||
|
return &Handler{
|
||||||
|
service: service,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) Health(e echo.Context) error {
|
||||||
|
return e.JSON(http.StatusOK, map[string]interface{}{"status": "ok"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) PatientsAnalytic(e echo.Context) error {
|
||||||
|
var req svc.ListPatientAnalyticRequest
|
||||||
|
|
||||||
|
richErr := richerror.New(richerror.Op("fetchingPatientList.PatientsAnalytic"))
|
||||||
|
|
||||||
|
if err := e.Bind(&req); err != nil {
|
||||||
|
richErr = richErr.WithErr(err)
|
||||||
|
richErr = richErr.WithKind(1)
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, richErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := h.service.List(e.Request().Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
richErr = richErr.WithErr(err)
|
||||||
|
richErr = richErr.WithKind(4)
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, richErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.JSON(http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) PatientsMapSummary(e echo.Context) error {
|
||||||
|
richErr := richerror.New(richerror.Op("fetchingPatientMapSummary.PatientsMapSummary"))
|
||||||
|
|
||||||
|
var req svc.GetPatientMapSummaryRequest
|
||||||
|
|
||||||
|
if err := e.Bind(&req); err != nil {
|
||||||
|
richErr = richErr.WithErr(err)
|
||||||
|
richErr = richErr.WithKind(1)
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, richErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, svcErr := h.service.GetMapSummary(e.Request().Context(), req)
|
||||||
|
if svcErr != nil {
|
||||||
|
richErr = richErr.WithErr(svcErr)
|
||||||
|
richErr = richErr.WithKind(4)
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, richErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.JSON(http.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package analytic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.gocasts.ir/ebhomengo/niki/patientapp/repository/mysql"
|
||||||
|
analytic2 "git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewPatientAnalyticRouter(s *echo.Group) {
|
||||||
|
|
||||||
|
mysqlRepo := mysql.NewPatientRepo()
|
||||||
|
//rpcRepo := grpc.NewPatientRepo()
|
||||||
|
|
||||||
|
analyticService := analytic2.NewPatientAnalyticService(mysqlRepo)
|
||||||
|
|
||||||
|
h := NewHandler(analyticService)
|
||||||
|
|
||||||
|
s.GET("/patients", h.PatientsAnalytic)
|
||||||
|
s.GET("/patients-summary", h.PatientsMapSummary)
|
||||||
|
s.GET("/health", h.Health)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
package analytic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.gocasts.ir/ebhomengo/niki/patientapp/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
HTTPServer *config.EchoServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer(server *config.EchoServer) *Server {
|
||||||
|
|
||||||
|
return &Server{
|
||||||
|
HTTPServer: server,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Server) Serve() error {
|
||||||
|
s.RegisterRoutes()
|
||||||
|
// Start server
|
||||||
|
return s.HTTPServer.Router.Start(fmt.Sprintf(":%d", s.HTTPServer.Config.Port))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Server) Stop(ctx context.Context) error {
|
||||||
|
return s.HTTPServer.Router.Shutdown(ctx)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Server) RegisterRoutes() {
|
||||||
|
|
||||||
|
v1 := s.HTTPServer.Router.Group("/v1")
|
||||||
|
{
|
||||||
|
// Analytic Group
|
||||||
|
analyticGroup := v1.Group("/analytic")
|
||||||
|
NewPatientAnalyticRouter(analyticGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic"
|
||||||
|
"git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AnalyticRepository struct{}
|
||||||
|
|
||||||
|
func NewPatientRepo() *AnalyticRepository {
|
||||||
|
|
||||||
|
return &AnalyticRepository{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *AnalyticRepository) GetPatients(ctx context.Context, f analytic.PatientFilter) ([]entity.Patient, error) {
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *AnalyticRepository) CountPatients(ctx context.Context, f analytic.PatientFilter) (int, error) {
|
||||||
|
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *AnalyticRepository) SummaryByCity(ctx context.Context, provinceID uint, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) {
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *AnalyticRepository) SummaryByProvince(ctx context.Context, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) {
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic"
|
||||||
|
"git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DataBase struct{}
|
||||||
|
|
||||||
|
func NewPatientRepo() *DataBase {
|
||||||
|
|
||||||
|
return &DataBase{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DataBase) GetPatients(ctx context.Context, f analytic.PatientFilter) ([]entity.Patient, error) {
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DataBase) CountPatients(ctx context.Context, f analytic.PatientFilter) (int, error) {
|
||||||
|
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DataBase) SummaryByCity(ctx context.Context, provinceID uint, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) {
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DataBase) SummaryByProvince(ctx context.Context, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) {
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
package analytic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/jalaali/go-jalaali"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func normalizeLimitOffset(limit, offset int) (int, int) {
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 50
|
||||||
|
}
|
||||||
|
if limit > 100 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
if offset < 0 {
|
||||||
|
offset = 0
|
||||||
|
}
|
||||||
|
return limit, offset
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert age range -> DOB range
|
||||||
|
func ageRangeToDOB(minAge, maxAge *int, now time.Time) (dobFrom, dobTo *string) {
|
||||||
|
if maxAge != nil {
|
||||||
|
t := now.AddDate(-(*maxAge + 1), 0, 1)
|
||||||
|
jy, jm, jd, err := jalaali.ToJalaali(t.Year(), t.Month(), t.Day())
|
||||||
|
if err != nil {
|
||||||
|
}
|
||||||
|
s := fmt.Sprintf("%04d/%02d/%02d", jy, jm, jd)
|
||||||
|
dobFrom = &s
|
||||||
|
}
|
||||||
|
|
||||||
|
if minAge != nil {
|
||||||
|
t := now.AddDate(-*minAge, 0, 0)
|
||||||
|
jy, jm, jd, err := jalaali.ToJalaali(t.Year(), t.Month(), t.Day())
|
||||||
|
if err != nil {
|
||||||
|
}
|
||||||
|
s := fmt.Sprintf("%04d/%02d/%02d", jy, jm, jd)
|
||||||
|
dobTo = &s
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
package analytic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ListPatientAnalyticRequest struct {
|
||||||
|
// All fields are optional
|
||||||
|
MinAge *int `query:"minAge,omitempty"`
|
||||||
|
MaxAge *int `query:"maxAge,omitempty"`
|
||||||
|
Sex *entity.Sex `query:"sex,omitempty"`
|
||||||
|
|
||||||
|
City *int64 `query:"city,omitempty"`
|
||||||
|
Province *int64 `query:"province,omitempty"`
|
||||||
|
|
||||||
|
Search *string `query:"search,omitempty"`
|
||||||
|
|
||||||
|
Pagination *Pagination `query:"pagination,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PatientAnalyticItem struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
FirstName string `json:"first_name"`
|
||||||
|
LastName string `json:"Last_name"`
|
||||||
|
DateOfBirth string `json:"dob,omitempty"`
|
||||||
|
Sex entity.Sex `json:"sex"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
Address entity.Address `json:"address"`
|
||||||
|
}
|
||||||
|
type PatientAnalyticResponse struct {
|
||||||
|
Items []PatientAnalyticItem `json:"items"`
|
||||||
|
Pagination *Pagination `json:"pagination"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToPatientResponse(patient entity.Patient) PatientAnalyticItem {
|
||||||
|
return PatientAnalyticItem{
|
||||||
|
ID: patient.ID,
|
||||||
|
FirstName: patient.FirstName,
|
||||||
|
LastName: patient.LastName,
|
||||||
|
DateOfBirth: patient.DateOfBirth,
|
||||||
|
Sex: patient.Sex,
|
||||||
|
Phone: patient.Phone,
|
||||||
|
Address: entity.Address{
|
||||||
|
ProvinceID: patient.Address.ProvinceID,
|
||||||
|
CityID: patient.Address.CityID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPatientMapSummaryRequest =========================== Map ==================================
|
||||||
|
type GetPatientMapSummaryRequest struct {
|
||||||
|
Level entity.MapLevel `query:"level"`
|
||||||
|
ParentID *int `query:"parentID"`
|
||||||
|
|
||||||
|
MinAge *int `query:"minAge,omitempty"`
|
||||||
|
MaxAge *int `query:"maxAge,omitempty"`
|
||||||
|
Sex *entity.Sex `query:"sex,omitempty"`
|
||||||
|
Search *string `query:"search,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetPatientMapSummaryResponse struct {
|
||||||
|
Level entity.MapLevel `json:"level"`
|
||||||
|
Items map[uint][]entity.MapSummaryItem `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pagination ================================ Pagination =============================
|
||||||
|
type Pagination struct {
|
||||||
|
Limit int `query:"limit,omitempty"`
|
||||||
|
Offset int `query:"offset,omitempty"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package analytic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PatientFilter struct {
|
||||||
|
DOBFrom *string // born after
|
||||||
|
DOBTo *string // born before
|
||||||
|
Sex *entity.Sex
|
||||||
|
|
||||||
|
City *int64
|
||||||
|
Province *int64
|
||||||
|
Country *int64
|
||||||
|
|
||||||
|
Search *string
|
||||||
|
|
||||||
|
Limit int
|
||||||
|
Offset int
|
||||||
|
}
|
||||||
|
|
||||||
|
type PatientMapFilter struct {
|
||||||
|
MinDOB *string
|
||||||
|
MaxDOB *string
|
||||||
|
Sex *entity.Sex
|
||||||
|
Search *string
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
package analytic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidProvinceID = errors.New("invalid province id")
|
||||||
|
ErrInvalidCountryID = errors.New("invalid country id")
|
||||||
|
ErrInvalidMapLevel = errors.New("invalid map level")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Repository interface {
|
||||||
|
GetPatients(ctx context.Context, f PatientFilter) ([]entity.Patient, error)
|
||||||
|
CountPatients(ctx context.Context, f PatientFilter) (int, error)
|
||||||
|
|
||||||
|
SummaryByCity(ctx context.Context, provinceID uint, f PatientMapFilter) (map[uint][]entity.MapSummaryItem, error)
|
||||||
|
SummaryByProvince(ctx context.Context, f PatientMapFilter) (map[uint][]entity.MapSummaryItem, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
repository Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPatientAnalyticService(repo Repository) Service {
|
||||||
|
return Service{
|
||||||
|
repository: repo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Service) List(ctx context.Context, req ListPatientAnalyticRequest) (PatientAnalyticResponse, error) {
|
||||||
|
|
||||||
|
limit, offset := normalizeLimitOffset(req.Pagination.Limit, req.Pagination.Offset)
|
||||||
|
|
||||||
|
// convert age range
|
||||||
|
dobFrom, dobTo := ageRangeToDOB(req.MinAge, req.MaxAge, time.Now())
|
||||||
|
|
||||||
|
filter := PatientFilter{
|
||||||
|
DOBFrom: dobFrom,
|
||||||
|
DOBTo: dobTo,
|
||||||
|
Sex: req.Sex,
|
||||||
|
City: req.City,
|
||||||
|
Province: req.Province,
|
||||||
|
Search: req.Search,
|
||||||
|
Limit: limit,
|
||||||
|
Offset: offset,
|
||||||
|
}
|
||||||
|
|
||||||
|
items, err := s.repository.GetPatients(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return PatientAnalyticResponse{}, fmt.Errorf("GetPatients: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
total, err := s.repository.CountPatients(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return PatientAnalyticResponse{}, fmt.Errorf("CountPatients: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mapping response
|
||||||
|
out := make([]PatientAnalyticItem, 0, len(items))
|
||||||
|
for _, value := range items {
|
||||||
|
out = append(out, ToPatientResponse(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
return PatientAnalyticResponse{
|
||||||
|
Items: out,
|
||||||
|
Pagination: &Pagination{
|
||||||
|
Limit: limit,
|
||||||
|
Offset: offset,
|
||||||
|
},
|
||||||
|
Total: total,
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Service) GetMapSummary(ctx context.Context, req GetPatientMapSummaryRequest) (GetPatientMapSummaryResponse, error) {
|
||||||
|
|
||||||
|
dobFrom, dobTo := ageRangeToDOB(req.MinAge, req.MaxAge, time.Now())
|
||||||
|
|
||||||
|
filter := PatientMapFilter{
|
||||||
|
MinDOB: dobFrom,
|
||||||
|
MaxDOB: dobTo,
|
||||||
|
Sex: req.Sex,
|
||||||
|
Search: req.Search,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch req.Level {
|
||||||
|
case entity.MapLevelCity:
|
||||||
|
if req.ParentID == nil || *req.ParentID <= 0 {
|
||||||
|
return GetPatientMapSummaryResponse{}, ErrInvalidProvinceID
|
||||||
|
}
|
||||||
|
|
||||||
|
items, err := s.repository.SummaryByCity(ctx, uint(*req.ParentID), filter)
|
||||||
|
if err != nil {
|
||||||
|
return GetPatientMapSummaryResponse{}, fmt.Errorf("SummaryByCity: %w", err)
|
||||||
|
}
|
||||||
|
return GetPatientMapSummaryResponse{Level: req.Level, Items: items}, nil
|
||||||
|
|
||||||
|
case entity.MapLevelProvince:
|
||||||
|
if req.ParentID == nil || *req.ParentID <= 0 {
|
||||||
|
return GetPatientMapSummaryResponse{}, ErrInvalidCountryID
|
||||||
|
}
|
||||||
|
|
||||||
|
items, err := s.repository.SummaryByProvince(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return GetPatientMapSummaryResponse{}, fmt.Errorf("SummaryByProvince: %w", err)
|
||||||
|
}
|
||||||
|
return GetPatientMapSummaryResponse{Level: req.Level, Items: items}, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return GetPatientMapSummaryResponse{}, ErrInvalidMapLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
type Address struct {
|
||||||
|
ID uint
|
||||||
|
PostalCode string
|
||||||
|
Address string
|
||||||
|
Name string
|
||||||
|
Lat float64
|
||||||
|
Lon float64
|
||||||
|
CityID uint
|
||||||
|
ProvinceID uint
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddressAggregated struct {
|
||||||
|
Address Address
|
||||||
|
Province Province
|
||||||
|
City City
|
||||||
|
}
|
||||||
|
|
||||||
|
type Province struct {
|
||||||
|
ID uint
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
type City struct {
|
||||||
|
ID uint
|
||||||
|
Name string
|
||||||
|
ProvinceID uint
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
type MapLevel string
|
||||||
|
|
||||||
|
const (
|
||||||
|
MapLevelCity MapLevel = "city"
|
||||||
|
MapLevelProvince MapLevel = "province"
|
||||||
|
MapLevelCountry MapLevel = "country"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MapSummaryItem struct {
|
||||||
|
LocationID int64
|
||||||
|
Name string
|
||||||
|
Count int
|
||||||
|
CentroidLat float64
|
||||||
|
CentroidLng float64
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
package entity
|
||||||
|
|
||||||
|
type Patient struct {
|
||||||
|
ID int64
|
||||||
|
FirstName string
|
||||||
|
LastName string
|
||||||
|
DateOfBirth string
|
||||||
|
Sex Sex
|
||||||
|
Phone string
|
||||||
|
Address Address
|
||||||
|
CaseStatus CaseStatus
|
||||||
|
ReferralSource ReferralSource
|
||||||
|
AssignedStaffId int64
|
||||||
|
StartDate string
|
||||||
|
EndDate string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sex ================================== Sex type ==========================================
|
||||||
|
type Sex string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SexUnknown Sex = "unknown"
|
||||||
|
SexMale Sex = "male"
|
||||||
|
SexFemale Sex = "female"
|
||||||
|
SexOther Sex = "other"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s Sex) SexValidation() bool {
|
||||||
|
switch s {
|
||||||
|
case SexUnknown, SexMale, SexFemale, SexOther:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaseStatus =================================== Case Status =======================================
|
||||||
|
type CaseStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Open CaseStatus = "open"
|
||||||
|
Close CaseStatus = "close"
|
||||||
|
InProgress CaseStatus = "inProgress"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s CaseStatus) CaseStatusValidation() bool {
|
||||||
|
switch s {
|
||||||
|
case Open, Close, InProgress:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReferralSource =================================== Referral source =======================================
|
||||||
|
type ReferralSource string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Hospital ReferralSource = "hospital"
|
||||||
|
Community ReferralSource = "community"
|
||||||
|
Other ReferralSource = "other"
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package date_parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseDate parses a date string in "YYYY-MM-DD" format and returns a time.Time object
|
||||||
|
func ParseDate(input string) (time.Time, error) {
|
||||||
|
const layout = "2006-01-02"
|
||||||
|
|
||||||
|
// Parse the input string
|
||||||
|
convertedDate, err := time.Parse(layout, input)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, fmt.Errorf("invalid date format: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return convertedDate, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 Amir Khazaie
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Jalaali
|
||||||
|
|
||||||
|
Golang implementation of [Jalaali JS](https://github.com/jalaali/jalaali-js) and [Jalaali Python](https://github.com/jalaali/jalaali-python) implementations of Jalaali (Jalali, Persian, Khayyami, Khorshidi, Shamsi) convertion to Gregorian calendar system and vice-versa.
|
||||||
|
|
||||||
|
This implementation is based on an [algorithm by Kazimierz M. Borkowski](http://www.astro.uni.torun.pl/~kb/Papers/EMP/PersianC-EMP.htm). Borkowski claims that this algorithm works correctly for 3000 years!
|
||||||
|
|
||||||
|
Documentation on API is available [here](https://pkg.go.dev/github.com/jalaali/go-jalaali) at Go official documentation site.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Use `go get` on this repository:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ go get -u github.com/jalaali/go-jalaali
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
* Wrapper around Golang [time package](https://golang.org/pkg/time):
|
||||||
|
* Call `Jalaali.Now()` to get instance of current time. You can use all function from `time` package with this wrapper.
|
||||||
|
* Call `Jalaali.From(t)` and pass a `time` instance to it. The you can work with it the same way you work with `time` package.
|
||||||
|
* Jalaali Formatting:
|
||||||
|
* Call `JFormat` method of a Jalaali instance and pass it the same formatting options that is used for Golang `time` package. The output will be in Jalaali date and use persian digits and words.
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
package jalaali
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
breaks = [...]int{-61, 9, 38, 199, 426, 686, 756, 818, 1111, 1181, 1210,
|
||||||
|
1635, 2060, 2097, 2192, 2262, 2324, 2394, 2456, 3178}
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToJalaali converts Gregorian to Jalaali date. Error is not nil if Jalaali
|
||||||
|
// year passed to function is not valid.
|
||||||
|
func ToJalaali(gregorianYear int, gregorianMonth time.Month, gregorianDay int) (int, Month, int, error) {
|
||||||
|
jy, jm, jd, err := d2j(g2d(gregorianYear, int(gregorianMonth), gregorianDay))
|
||||||
|
return jy, Month(jm), jd, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToGregorian converts Jalaali to Gregorian date. Error is not nil if Jalaali
|
||||||
|
// year passed to function is not valid.
|
||||||
|
func ToGregorian(jalaaliYear int, jalaaliMonth Month, jalaaliDay int) (int, time.Month, int, error) {
|
||||||
|
// validate the jalaali date using the utility function
|
||||||
|
if !IsValidDate(jalaaliYear, int(jalaaliMonth), jalaaliDay) {
|
||||||
|
return 0, 0, 0, fmt.Errorf("invalid jalaali date: year=%d, month=%d, day=%d", jalaaliYear, jalaaliMonth, jalaaliDay)
|
||||||
|
}
|
||||||
|
jdn, err := j2d(jalaaliYear, int(jalaaliMonth), jalaaliDay)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gy, gm, gd := d2g(jdn)
|
||||||
|
return gy, time.Month(gm), gd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func j2d(jy, jm, jd int) (jdn int, err error) {
|
||||||
|
_, gy, march, err := jalCal(jy)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return g2d(gy, 3, march) + (jm-1)*31 - div(jm, 7)*(jm-7) + jd - 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func d2j(jdn int) (int, int, int, error) {
|
||||||
|
gy, _, _ := d2g(jdn) // Calculate Gregorian year (gy).
|
||||||
|
jy := gy - 621
|
||||||
|
leap, _, march, err := jalCal(jy)
|
||||||
|
jdn1f := g2d(gy, 3, march)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find number of days that passed since 1 Farvardin.
|
||||||
|
k := jdn - jdn1f
|
||||||
|
if k >= 0 {
|
||||||
|
if k <= 185 {
|
||||||
|
// The first 6 months.
|
||||||
|
jm := 1 + div(k, 31)
|
||||||
|
jd := mod(k, 31) + 1
|
||||||
|
return jy, jm, jd, nil
|
||||||
|
}
|
||||||
|
// The remaining months.
|
||||||
|
k -= 186
|
||||||
|
} else {
|
||||||
|
// Previous Jalaali year.
|
||||||
|
jy--
|
||||||
|
k += 179
|
||||||
|
if leap == 1 {
|
||||||
|
k++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jm := 7 + div(k, 30)
|
||||||
|
jd := mod(k, 30) + 1
|
||||||
|
return jy, jm, jd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func jalCal(jy int) (int, int, int, error) {
|
||||||
|
bl, gy, leapJ, jp := len(breaks), jy+621, -14, breaks[0]
|
||||||
|
jump := 0
|
||||||
|
|
||||||
|
if jy < jp || jy >= breaks[bl-1] {
|
||||||
|
return 0, 0, 0, &ErrorInvalidYear{jy}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the limiting years for the Jalaali year jy.
|
||||||
|
for i := 1; i < bl; i++ {
|
||||||
|
jm := breaks[i]
|
||||||
|
jump = jm - jp
|
||||||
|
if jy < jm {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
leapJ += div(jump, 33)*8 + div(mod(jump, 33), 4)
|
||||||
|
jp = jm
|
||||||
|
}
|
||||||
|
n := jy - jp
|
||||||
|
|
||||||
|
// Find the number of leap years from AD 621 to the beginning
|
||||||
|
// of the current Jalaali year in the Persian calendar.
|
||||||
|
leapJ += div(n, 33)*8 + div(mod(n, 33)+3, 4)
|
||||||
|
if mod(jump, 33) == 4 && jump-n == 4 {
|
||||||
|
leapJ++
|
||||||
|
}
|
||||||
|
|
||||||
|
// And the same in the Gregorian calendar (until the year gy).
|
||||||
|
leapG := div(gy, 4) - div((div(gy, 100)+1)*3, 4) - 150
|
||||||
|
|
||||||
|
// Determine the Gregorian date of Farvardin the 1st.
|
||||||
|
march := 20 + leapJ - leapG
|
||||||
|
|
||||||
|
// Find how many years have passed since the last leap year.
|
||||||
|
if jump-n < 6 {
|
||||||
|
n -= jump + div(jump+4, 33)*33
|
||||||
|
}
|
||||||
|
leap := mod(mod(n+1, 33)-1, 4)
|
||||||
|
if leap == -1 {
|
||||||
|
leap = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
return leap, gy, march, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func g2d(gy, gm, gd int) int {
|
||||||
|
d := div((gy+div(gm-8, 6)+100100)*1461, 4) +
|
||||||
|
div(153*mod(gm+9, 12)+2, 5) +
|
||||||
|
gd - 34840408
|
||||||
|
d = d - div(div(gy+100100+div(gm-8, 6), 100)*3, 4) + 752
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func d2g(jdn int) (int, int, int) {
|
||||||
|
j := 4*jdn + 139361631
|
||||||
|
j = j + div(div(4*jdn+183187720, 146097)*3, 4)*4 - 3908
|
||||||
|
i := div(mod(j, 1461), 4)*5 + 308
|
||||||
|
gd := div(mod(i, 153), 5) + 1
|
||||||
|
gm := mod(div(i, 153), 12) + 1
|
||||||
|
gy := div(j, 1461) - 100100 + div(8-gm, 6)
|
||||||
|
return gy, gm, gd
|
||||||
|
}
|
||||||
|
|
||||||
|
func div(a, b int) int {
|
||||||
|
return a / b
|
||||||
|
}
|
||||||
|
|
||||||
|
func mod(a, b int) int {
|
||||||
|
return a % b
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package jalaali
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// ErrorNilReference is happening when a pointer is nil.
|
||||||
|
type ErrorNilReference struct{}
|
||||||
|
|
||||||
|
// ErrorInvalidYear is happening when year passed is is in proper range.
|
||||||
|
type ErrorInvalidYear struct {
|
||||||
|
year int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ErrorNilReference) Error() string {
|
||||||
|
return "jalaali: reference is nil"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ErrorInvalidYear) Error() string {
|
||||||
|
return fmt.Sprintf("jalaali: %v is invalid year", e.year)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,318 @@
|
||||||
|
package jalaali
|
||||||
|
|
||||||
|
const (
|
||||||
|
_ = iota
|
||||||
|
stdLongMonth = iota + stdNeedDate // "January"
|
||||||
|
stdMonth // "Jan"
|
||||||
|
stdNumMonth // "1"
|
||||||
|
stdZeroMonth // "01"
|
||||||
|
stdLongWeekDay // "Monday"
|
||||||
|
stdWeekDay // "Mon"
|
||||||
|
stdDay // "2"
|
||||||
|
stdUnderDay // "_2"
|
||||||
|
stdZeroDay // "02"
|
||||||
|
stdHour = iota + stdNeedClock // "15"
|
||||||
|
stdHour12 // "3"
|
||||||
|
stdZeroHour12 // "03"
|
||||||
|
stdMinute // "4"
|
||||||
|
stdZeroMinute // "04"
|
||||||
|
stdSecond // "5"
|
||||||
|
stdZeroSecond // "05"
|
||||||
|
stdLongYear = iota + stdNeedDate // "2006"
|
||||||
|
stdYear // "06"
|
||||||
|
stdPM = iota + stdNeedClock // "PM"
|
||||||
|
stdpm // "pm"
|
||||||
|
stdFracSecond0 // ".0", ".00", ... , trailing zeros included
|
||||||
|
stdFracSecond9 // ".9", ".99", ..., trailing zeros omitted
|
||||||
|
|
||||||
|
stdNeedDate = 1 << 8 // need month, day, year
|
||||||
|
stdNeedClock = 2 << 8 // need hour, minute, second
|
||||||
|
stdArgShift = 16 // extra argument in high bits, above low stdArgShift
|
||||||
|
stdMask = 1<<stdArgShift - 1 // mask out argument
|
||||||
|
)
|
||||||
|
|
||||||
|
// std0x records the std values for "01", "02", ..., "06".
|
||||||
|
var std0x = [...]int{stdZeroMonth, stdZeroDay, stdZeroHour12, stdZeroMinute, stdZeroSecond, stdYear}
|
||||||
|
|
||||||
|
// JFormat gets default Golang layout string and parse put Jalaali calender information
|
||||||
|
// into the final string and return it.
|
||||||
|
func (j Jalaali) JFormat(layout string) (string, error) {
|
||||||
|
const minBufSize = 64
|
||||||
|
|
||||||
|
bufSize := len(layout)
|
||||||
|
if bufSize < minBufSize { // minimum buffer size
|
||||||
|
bufSize = minBufSize
|
||||||
|
}
|
||||||
|
b := make([]byte, 0, len(layout))
|
||||||
|
|
||||||
|
b, err := j.jAppendFormat(b, layout)
|
||||||
|
return string(b), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// jAppendFormat is like JFormat but appends the textual
|
||||||
|
// representation to b and returns the extended buffer.
|
||||||
|
func (j Jalaali) jAppendFormat(b []byte, layout string) ([]byte, error) {
|
||||||
|
var (
|
||||||
|
year int = -1
|
||||||
|
month Month
|
||||||
|
day int
|
||||||
|
hour int = -1
|
||||||
|
min int
|
||||||
|
sec int
|
||||||
|
)
|
||||||
|
// Each iteration generates one std value.
|
||||||
|
for layout != "" {
|
||||||
|
prefix, std, suffix := nextStdChunk(layout)
|
||||||
|
if prefix != "" {
|
||||||
|
b = append(b, prefix...)
|
||||||
|
}
|
||||||
|
if std == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
layout = suffix
|
||||||
|
|
||||||
|
// Compute year, month, day if needed.
|
||||||
|
if year < 0 && std&stdNeedDate != 0 {
|
||||||
|
var err error
|
||||||
|
year, month, day, err = ToJalaali(j.Year(), j.Month(), j.Day())
|
||||||
|
if err != nil {
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute hour, minute, second if needed.
|
||||||
|
if hour < 0 && std&stdNeedClock != 0 {
|
||||||
|
hour, min, sec = j.Hour(), j.Minute(), j.Second()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch std & stdMask {
|
||||||
|
case stdYear:
|
||||||
|
y := year
|
||||||
|
if y < 0 {
|
||||||
|
y = -y
|
||||||
|
}
|
||||||
|
b = appendInt(b, y%100, 2)
|
||||||
|
case stdLongYear:
|
||||||
|
b = appendInt(b, year, 4)
|
||||||
|
case stdMonth, stdLongMonth:
|
||||||
|
m := month.String()
|
||||||
|
b = append(b, m...)
|
||||||
|
case stdNumMonth:
|
||||||
|
b = appendInt(b, int(month), 0)
|
||||||
|
case stdZeroMonth:
|
||||||
|
b = appendInt(b, int(month), 2)
|
||||||
|
case stdWeekDay, stdLongWeekDay:
|
||||||
|
s := Weekday((int(j.Weekday()) + 1) % 7).String()
|
||||||
|
b = append(b, s...)
|
||||||
|
case stdDay:
|
||||||
|
b = appendInt(b, day, 0)
|
||||||
|
case stdUnderDay:
|
||||||
|
if day < 10 {
|
||||||
|
b = append(b, ' ')
|
||||||
|
}
|
||||||
|
b = appendInt(b, day, 0)
|
||||||
|
case stdZeroDay:
|
||||||
|
b = appendInt(b, day, 2)
|
||||||
|
case stdHour:
|
||||||
|
b = appendInt(b, hour, 2)
|
||||||
|
case stdHour12:
|
||||||
|
// Noon is 12PM, midnight is 12AM.
|
||||||
|
hr := hour % 12
|
||||||
|
if hr == 0 {
|
||||||
|
hr = 12
|
||||||
|
}
|
||||||
|
b = appendInt(b, hr, 0)
|
||||||
|
case stdZeroHour12:
|
||||||
|
// Noon is 12PM, midnight is 12AM.
|
||||||
|
hr := hour % 12
|
||||||
|
if hr == 0 {
|
||||||
|
hr = 12
|
||||||
|
}
|
||||||
|
b = appendInt(b, hr, 2)
|
||||||
|
case stdMinute:
|
||||||
|
b = appendInt(b, min, 0)
|
||||||
|
case stdZeroMinute:
|
||||||
|
b = appendInt(b, min, 2)
|
||||||
|
case stdSecond:
|
||||||
|
b = appendInt(b, sec, 0)
|
||||||
|
case stdZeroSecond:
|
||||||
|
b = appendInt(b, sec, 2)
|
||||||
|
case stdPM, stdpm:
|
||||||
|
if hour >= 12 {
|
||||||
|
b = append(b, "بعدازظهر"...)
|
||||||
|
} else {
|
||||||
|
b = append(b, "قبلازظهر"...)
|
||||||
|
}
|
||||||
|
case stdFracSecond0, stdFracSecond9:
|
||||||
|
b = formatNano(b, uint(j.Nanosecond()), std>>stdArgShift, std&stdMask == stdFracSecond9)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// nextStdChunk finds the first occurrence of a std string in
|
||||||
|
// layout and returns the text before, the std string, and the text after.
|
||||||
|
func nextStdChunk(layout string) (prefix string, std int, suffix string) {
|
||||||
|
for i := 0; i < len(layout); i++ {
|
||||||
|
switch c := int(layout[i]); c {
|
||||||
|
case 'J': // January, Jan
|
||||||
|
if len(layout) >= i+3 && layout[i:i+3] == "Jan" {
|
||||||
|
if len(layout) >= i+7 && layout[i:i+7] == "January" {
|
||||||
|
return layout[0:i], stdLongMonth, layout[i+7:]
|
||||||
|
}
|
||||||
|
if !startsWithLowerCase(layout[i+3:]) {
|
||||||
|
return layout[0:i], stdMonth, layout[i+3:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'M': // Monday, Mon
|
||||||
|
if layout[i:i+3] == "Mon" {
|
||||||
|
if len(layout) >= i+6 && layout[i:i+6] == "Monday" {
|
||||||
|
return layout[0:i], stdLongWeekDay, layout[i+6:]
|
||||||
|
}
|
||||||
|
if !startsWithLowerCase(layout[i+3:]) {
|
||||||
|
return layout[0:i], stdWeekDay, layout[i+3:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case '0': // 01, 02, 03, 04, 05, 06
|
||||||
|
if len(layout) >= i+2 && '1' <= layout[i+1] && layout[i+1] <= '6' {
|
||||||
|
return layout[0:i], std0x[layout[i+1]-'1'], layout[i+2:]
|
||||||
|
}
|
||||||
|
|
||||||
|
case '1': // 15, 1
|
||||||
|
if len(layout) >= i+2 && layout[i+1] == '5' {
|
||||||
|
return layout[0:i], stdHour, layout[i+2:]
|
||||||
|
}
|
||||||
|
return layout[0:i], stdNumMonth, layout[i+1:]
|
||||||
|
|
||||||
|
case '2': // 2006, 2
|
||||||
|
if len(layout) >= i+4 && layout[i:i+4] == "2006" {
|
||||||
|
return layout[0:i], stdLongYear, layout[i+4:]
|
||||||
|
}
|
||||||
|
return layout[0:i], stdDay, layout[i+1:]
|
||||||
|
|
||||||
|
case '_': // _2, _2006
|
||||||
|
if len(layout) >= i+2 && layout[i+1] == '2' {
|
||||||
|
//_2006 is really a literal _, followed by stdLongYear
|
||||||
|
if len(layout) >= i+5 && layout[i+1:i+5] == "2006" {
|
||||||
|
return layout[0 : i+1], stdLongYear, layout[i+5:]
|
||||||
|
}
|
||||||
|
return layout[0:i], stdUnderDay, layout[i+2:]
|
||||||
|
}
|
||||||
|
|
||||||
|
case '3':
|
||||||
|
return layout[0:i], stdHour12, layout[i+1:]
|
||||||
|
|
||||||
|
case '4':
|
||||||
|
return layout[0:i], stdMinute, layout[i+1:]
|
||||||
|
|
||||||
|
case '5':
|
||||||
|
return layout[0:i], stdSecond, layout[i+1:]
|
||||||
|
|
||||||
|
case 'P': // PM
|
||||||
|
if len(layout) >= i+2 && layout[i+1] == 'M' {
|
||||||
|
return layout[0:i], stdPM, layout[i+2:]
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'p': // pm
|
||||||
|
if len(layout) >= i+2 && layout[i+1] == 'm' {
|
||||||
|
return layout[0:i], stdpm, layout[i+2:]
|
||||||
|
}
|
||||||
|
|
||||||
|
case '.': // .000 or .999 - repeated digits for fractional seconds.
|
||||||
|
if i+1 < len(layout) && (layout[i+1] == '0' || layout[i+1] == '9') {
|
||||||
|
ch := layout[i+1]
|
||||||
|
j := i + 1
|
||||||
|
for j < len(layout) && layout[j] == ch {
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
// String of digits must end here - only fractional second is all digits.
|
||||||
|
if !isDigit(layout, j) {
|
||||||
|
std := stdFracSecond0
|
||||||
|
if layout[i+1] == '9' {
|
||||||
|
std = stdFracSecond9
|
||||||
|
}
|
||||||
|
std |= (j - (i + 1)) << stdArgShift
|
||||||
|
return layout[0:i], std, layout[j:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return layout, 0, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// startsWithLowerCase reports whether the string has a lower-case letter at the beginning.
|
||||||
|
// Its purpose is to prevent matching strings like "Month" when looking for "Mon".
|
||||||
|
func startsWithLowerCase(str string) bool {
|
||||||
|
if len(str) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
c := str[0]
|
||||||
|
return 'a' <= c && c <= 'z'
|
||||||
|
}
|
||||||
|
|
||||||
|
// isDigit reports whether s[i] is in range and is a decimal digit.
|
||||||
|
func isDigit(s string, i int) bool {
|
||||||
|
if len(s) <= i {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
c := s[i]
|
||||||
|
return '0' <= c && c <= '9'
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendInt appends the decimal form of x to b and returns the result.
|
||||||
|
// If the decimal form (excluding sign) is shorter than width, the result is padded with leading 0's.
|
||||||
|
// Duplicates functionality in strconv, but avoids dependency.
|
||||||
|
func appendInt(b []byte, x int, width int) []byte {
|
||||||
|
u := uint(x)
|
||||||
|
if x < 0 {
|
||||||
|
b = append(b, '-')
|
||||||
|
u = uint(-x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assemble decimal in reverse order.
|
||||||
|
var buf [20]rune
|
||||||
|
i := len(buf)
|
||||||
|
for u >= 10 {
|
||||||
|
i--
|
||||||
|
q := u / 10
|
||||||
|
buf[i] = rune('۰' + u - q*10)
|
||||||
|
u = q
|
||||||
|
}
|
||||||
|
i--
|
||||||
|
buf[i] = rune('۰' + u)
|
||||||
|
|
||||||
|
// Add 0-padding.
|
||||||
|
for w := len(buf) - i; w < width; w++ {
|
||||||
|
b = append(b, []byte("۰")...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(b, []byte(string(buf[i:]))...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatNano appends a fractional second, as nanoseconds, to b
|
||||||
|
// and returns the result.
|
||||||
|
func formatNano(b []byte, nanosec uint, n int, trim bool) []byte {
|
||||||
|
u := nanosec
|
||||||
|
var buf [9]rune
|
||||||
|
for start := len(buf); start > 0; {
|
||||||
|
start--
|
||||||
|
buf[start] = rune(u%10 + '۰')
|
||||||
|
u /= 10
|
||||||
|
}
|
||||||
|
|
||||||
|
if n > 9 {
|
||||||
|
n = 9
|
||||||
|
}
|
||||||
|
if trim {
|
||||||
|
for n > 0 && buf[n-1] == '۰' {
|
||||||
|
n--
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b = append(b, '.')
|
||||||
|
return append(b, []byte(string(buf[:n]))...)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
module github.com/jalaali/go-jalaali
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
package jalaali
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A simple wrapper around Golang default time package. You have all the functionality of
|
||||||
|
// default time package and functionalities needed for Jalaali calender.
|
||||||
|
type Jalaali struct {
|
||||||
|
time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// From initialize new instance of Jalaali from a time instance.
|
||||||
|
func From(t time.Time) Jalaali {
|
||||||
|
return Jalaali{t}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now with return Jalaali instance of current time.
|
||||||
|
func Now() Jalaali {
|
||||||
|
return From(time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Month specifies a month of the year (Farvardin = 1, ...).
|
||||||
|
type Month int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Farvardin Month = 1 + iota
|
||||||
|
Ordibehesht
|
||||||
|
Khordad
|
||||||
|
Tir
|
||||||
|
Mordad
|
||||||
|
Shahrivar
|
||||||
|
Mehr
|
||||||
|
Aban
|
||||||
|
Azar
|
||||||
|
Dey
|
||||||
|
Bahman
|
||||||
|
Esfand
|
||||||
|
)
|
||||||
|
|
||||||
|
var months = []string{
|
||||||
|
"فروردین", "اردیبهشت", "خرداد",
|
||||||
|
"تیر", "مرداد", "شهریور",
|
||||||
|
"مهر", "آبان", "آذر",
|
||||||
|
"دی", "بهمن", "اسفند",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Month) String() string {
|
||||||
|
if Farvardin <= m && m <= Esfand {
|
||||||
|
return months[m-1]
|
||||||
|
}
|
||||||
|
return "%!Month(" + strconv.Itoa(int(m)) + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Weekday specifies a day of the week (Shanbe = 0, ...).
|
||||||
|
type Weekday int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Shanbe Weekday = iota
|
||||||
|
IekShanbe
|
||||||
|
DoShanbe
|
||||||
|
SeShanbe
|
||||||
|
ChaharShanbe
|
||||||
|
PanjShanbe
|
||||||
|
Jome
|
||||||
|
)
|
||||||
|
|
||||||
|
var days = []string{
|
||||||
|
"شنبه", "یکشنبه", "دوشنبه", "سهشنبه", "چهارشنبه", "پنجشنبه", "جمعه",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Weekday) String() string {
|
||||||
|
if Shanbe <= d && d <= Jome {
|
||||||
|
return days[d]
|
||||||
|
}
|
||||||
|
return "%!Weekday(" + strconv.Itoa(int(d)) + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,188 @@
|
||||||
|
package jalaali
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFromYMD(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
gy, gm, gd, jy, jm, jd int
|
||||||
|
}{
|
||||||
|
{1981, 8, 17, 1360, 5, 26},
|
||||||
|
{2013, 1, 10, 1391, 10, 21},
|
||||||
|
{2014, 8, 4, 1393, 5, 13},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
y, m, d, err := ToJalaali(test.gy, time.Month(test.gm), test.gd)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%v", err)
|
||||||
|
} else if y != test.jy || m != Month(test.jm) || d != test.jd {
|
||||||
|
t.Errorf("Expected %v/%v/%v got %v/%v%v.", test.jy, test.jm, test.jd, y, m, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToGregorian(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
jy, jm, jd, gy, gm, gd int
|
||||||
|
}{
|
||||||
|
{1360, 5, 26, 1981, 8, 17},
|
||||||
|
{1391, 10, 21, 2013, 1, 10},
|
||||||
|
{1393, 5, 13, 2014, 8, 4},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
y, m, d, err := ToGregorian(test.jy, Month(test.jm), test.jd)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%v", err)
|
||||||
|
} else if y != test.gy || m != time.Month(test.gm) || d != test.gd {
|
||||||
|
t.Errorf("Expected %v/%v/%v got %v/%v%v.", test.gy, test.gm, test.gd, y, m, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsValidDate(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
y, m, d int
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{-62, 12, 29, false},
|
||||||
|
{-61, 1, 1, true},
|
||||||
|
{3178, 1, 1, false},
|
||||||
|
{3177, 12, 29, true},
|
||||||
|
{1393, 0, 1, false},
|
||||||
|
{1393, 13, 1, false},
|
||||||
|
{1393, 1, 0, false},
|
||||||
|
{1393, 1, 32, false},
|
||||||
|
{1393, 1, 31, true},
|
||||||
|
{1393, 11, 31, false},
|
||||||
|
{1393, 11, 30, true},
|
||||||
|
{1393, 12, 30, false},
|
||||||
|
{1393, 12, 29, true},
|
||||||
|
{1395, 12, 30, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
valid := IsValidDate(test.y, test.m, test.d)
|
||||||
|
if valid != test.ok {
|
||||||
|
calculated, actual := "", " not"
|
||||||
|
if test.ok {
|
||||||
|
calculated, actual = " not", ""
|
||||||
|
}
|
||||||
|
t.Errorf("%v/%v/%v is%v valid date but considered%v valid.",
|
||||||
|
test.y, test.m, test.d, actual, calculated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsLeapYear(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
year int
|
||||||
|
leap bool
|
||||||
|
}{
|
||||||
|
{1393, false},
|
||||||
|
{1394, false},
|
||||||
|
{1395, true},
|
||||||
|
{1396, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
leap, err := IsLeapYear(test.year)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%v", err)
|
||||||
|
} else if leap != test.leap {
|
||||||
|
calculated, actual := "", " not"
|
||||||
|
if leap {
|
||||||
|
calculated, actual = " not", ""
|
||||||
|
}
|
||||||
|
t.Errorf("%v is%v leap but considered%v leap.", test.year, actual, calculated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMonthLength(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
y, m, ml int
|
||||||
|
}{
|
||||||
|
{1393, 1, 31},
|
||||||
|
{1393, 4, 31},
|
||||||
|
{1393, 6, 31},
|
||||||
|
{1393, 7, 30},
|
||||||
|
{1393, 10, 30},
|
||||||
|
{1393, 12, 29},
|
||||||
|
{1394, 12, 29},
|
||||||
|
{1395, 12, 30},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
calculated, err := MonthLength(test.y, test.m)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%v", err)
|
||||||
|
} else if calculated != test.ml {
|
||||||
|
t.Errorf("Length of %v/%v month is %v but considered %v.",
|
||||||
|
test.y, test.m, test.ml, calculated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJFormat(t *testing.T) {
|
||||||
|
iran, _ := time.LoadLocation("Asia/Tehran")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
time time.Time
|
||||||
|
format []string
|
||||||
|
result []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
time.Date(2001, 1, 1, 1, 1, 1, 1, iran),
|
||||||
|
[]string{
|
||||||
|
"2006 06", // Year formatting
|
||||||
|
"January Jan 1 01", // Month formatting
|
||||||
|
"Monday Mon 2 _2 02", // Day formatting
|
||||||
|
"15 3 03 4 04 5 05 PM pm", // Hour, Minute, Second formatting
|
||||||
|
".0 .00 .000 .000000 .000000000 .9 .99 .999 .999999 .999999999", // Nanosecond formatting
|
||||||
|
},
|
||||||
|
[]string{
|
||||||
|
"۱۳۷۹ ۷۹", // Year formatting
|
||||||
|
"دی دی ۱۰ ۱۰", // Month formatting
|
||||||
|
"دوشنبه دوشنبه ۱۲ ۱۲ ۱۲", // Day formatting
|
||||||
|
"۰۱ ۱ ۰۱ ۱ ۰۱ ۱ ۰۱ قبلازظهر قبلازظهر", // Hour, Minute, Second formatting
|
||||||
|
".۰ .۰۰ .۰۰۰ .۰۰۰۰۰۰ .۰۰۰۰۰۰۰۰۱ .۰۰۰۰۰۰۰۰۱", // Nanosecond formatting
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
time.Date(2001, 2, 3, 15, 17, 1, 999999999, iran),
|
||||||
|
[]string{
|
||||||
|
"2006 06", // Year formatting
|
||||||
|
"January Jan 1 01", // Month formatting
|
||||||
|
"Monday Mon 2 _2 02", // Day formatting
|
||||||
|
"15 3 03 4 04 5 05 PM pm", // Hour, Minute, Second formatting
|
||||||
|
".0 .00 .000 .000000 .000000000 .9 .99 .999 .999999 .999999999", // Nanosecond formatting
|
||||||
|
},
|
||||||
|
[]string{
|
||||||
|
"۱۳۷۹ ۷۹", // Year formatting
|
||||||
|
"بهمن بهمن ۱۱ ۱۱", // Month formatting
|
||||||
|
"شنبه شنبه ۱۵ ۱۵ ۱۵", // Day formatting
|
||||||
|
"۱۵ ۳ ۰۳ ۱۷ ۱۷ ۱ ۰۱ بعدازظهر بعدازظهر", // Hour, Minute, Second formatting
|
||||||
|
".۹ .۹۹ .۹۹۹ .۹۹۹۹۹۹ .۹۹۹۹۹۹۹۹۹ .۹ .۹۹ .۹۹۹ .۹۹۹۹۹۹ .۹۹۹۹۹۹۹۹۹", // Nanosecond formatting
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
j := From(test.time)
|
||||||
|
|
||||||
|
for f := range test.format {
|
||||||
|
result, err := j.JFormat(test.format[f])
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if result != test.result[f] {
|
||||||
|
t.Error("Bad formatting for test as index: ", i, "\nWanted: ", test.result[f], "\nGot: ", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
package jalaali
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
var enToFa = strings.NewReplacer(
|
||||||
|
"0", "۰",
|
||||||
|
"1", "۱",
|
||||||
|
"2", "۲",
|
||||||
|
"3", "۳",
|
||||||
|
"4", "۴",
|
||||||
|
"5", "۵",
|
||||||
|
"6", "۶",
|
||||||
|
"7", "۷",
|
||||||
|
"8", "۸",
|
||||||
|
"9", "۹",
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsValidDate take Jalaali date and return true if it is valid,
|
||||||
|
// otherwise false.
|
||||||
|
func IsValidDate(jy, jm, jd int) bool {
|
||||||
|
d, err := MonthLength(jy, jm)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return -61 <= jy && jy <= 3177 &&
|
||||||
|
1 <= jm && jm <= 12 &&
|
||||||
|
1 <= jd && jd <= d
|
||||||
|
}
|
||||||
|
|
||||||
|
// MonthLength take Jalaali date and return length of that specific
|
||||||
|
// month. Error is not nil if Jalaali year passed to function is not valid.
|
||||||
|
func MonthLength(jy, jm int) (int, error) {
|
||||||
|
if jm <= 6 {
|
||||||
|
return 31, nil
|
||||||
|
} else if jm <= 11 {
|
||||||
|
return 30, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
leap, err := IsLeapYear(jy)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else if leap {
|
||||||
|
return 30, nil
|
||||||
|
}
|
||||||
|
return 29, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLeapYear take a Jalaali year and return true if it is leap year. Error
|
||||||
|
// is not nil if Jalaali year passed to function is not valid.
|
||||||
|
func IsLeapYear(jy int) (bool, error) {
|
||||||
|
leap, _, _, err := jalCal(jy)
|
||||||
|
return leap == 0, err
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue