diff --git a/delivery/http_server/end2end/setup/docker.go b/delivery/http_server/end2end/setup/docker.go index 2eddecb0..fd3c718e 100644 --- a/delivery/http_server/end2end/setup/docker.go +++ b/delivery/http_server/end2end/setup/docker.go @@ -18,8 +18,8 @@ type TestContainer struct { dockerPool *dockertest.Pool // the connection pool to Docker. mariaResource *dockertest.Resource // MariaDB Docker container resource. redisResource *dockertest.Resource // Redis Docker container resource. - mariaDBConn *mysql.DB // Connection to the MariaDB database. - redisDBConn *redisadapter.Adapter // Connection to the Redis database. + mariaDBConn *mysql.DB // Connection to the MariaDB mysql. + redisDBConn *redisadapter.Adapter // Connection to the Redis mysql. containerExpiryInSeconds uint } @@ -158,7 +158,7 @@ func (t *TestContainer) Start() { return nil }); err != nil { - log.Fatalf("Could not connect to database: %s", err) + log.Fatalf("Could not connect to mysql: %s", err) } } diff --git a/main.go b/main.go index 0fccecb6..dba18f22 100644 --- a/main.go +++ b/main.go @@ -43,7 +43,7 @@ func Config() config.Config { } 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() if *migrate { migrator.New(migrator.Config{ diff --git a/patientapp/app.go b/patientapp/app.go index 29d637af..dce0558e 100644 --- a/patientapp/app.go +++ b/patientapp/app.go @@ -1 +1,37 @@ 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() +} diff --git a/patientapp/cmd/main.go b/patientapp/cmd/main.go new file mode 100644 index 00000000..0c28ac82 --- /dev/null +++ b/patientapp/cmd/main.go @@ -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() + +} diff --git a/patientapp/config/config.go b/patientapp/config/config.go new file mode 100644 index 00000000..de2c385c --- /dev/null +++ b/patientapp/config/config.go @@ -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 +} diff --git a/patientapp/delivery/http/analytic/.gitkeep b/patientapp/delivery/http/analytic/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/patientapp/delivery/http/analytic/handler.go b/patientapp/delivery/http/analytic/handler.go new file mode 100644 index 00000000..fbe7d3c4 --- /dev/null +++ b/patientapp/delivery/http/analytic/handler.go @@ -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) +} diff --git a/patientapp/delivery/http/analytic/router.go b/patientapp/delivery/http/analytic/router.go new file mode 100644 index 00000000..c6499428 --- /dev/null +++ b/patientapp/delivery/http/analytic/router.go @@ -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) + +} diff --git a/patientapp/delivery/http/analytic/server.go b/patientapp/delivery/http/analytic/server.go new file mode 100644 index 00000000..91ec8ba7 --- /dev/null +++ b/patientapp/delivery/http/analytic/server.go @@ -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) + } + +} diff --git a/patientapp/repository/database/.gitkeep b/patientapp/repository/database/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/patientapp/repository/grpc/analytic_repo.go b/patientapp/repository/grpc/analytic_repo.go new file mode 100644 index 00000000..fe598750 --- /dev/null +++ b/patientapp/repository/grpc/analytic_repo.go @@ -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 +} diff --git a/patientapp/repository/mysql/analytic_repo.go b/patientapp/repository/mysql/analytic_repo.go new file mode 100644 index 00000000..223080cf --- /dev/null +++ b/patientapp/repository/mysql/analytic_repo.go @@ -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 +} diff --git a/patientapp/service/analytic/.gitkeep b/patientapp/service/analytic/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/patientapp/service/analytic/helper.go b/patientapp/service/analytic/helper.go new file mode 100644 index 00000000..94b87fa5 --- /dev/null +++ b/patientapp/service/analytic/helper.go @@ -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 +} diff --git a/patientapp/service/analytic/param.go b/patientapp/service/analytic/param.go new file mode 100644 index 00000000..da14af37 --- /dev/null +++ b/patientapp/service/analytic/param.go @@ -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"` +} diff --git a/patientapp/service/analytic/patient_filter.go b/patientapp/service/analytic/patient_filter.go new file mode 100644 index 00000000..c7dd7695 --- /dev/null +++ b/patientapp/service/analytic/patient_filter.go @@ -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 +} diff --git a/patientapp/service/analytic/service.go b/patientapp/service/analytic/service.go new file mode 100644 index 00000000..518d8756 --- /dev/null +++ b/patientapp/service/analytic/service.go @@ -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 + } + +} diff --git a/patientapp/service/entity/address.go b/patientapp/service/entity/address.go new file mode 100644 index 00000000..193531a9 --- /dev/null +++ b/patientapp/service/entity/address.go @@ -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 +} diff --git a/patientapp/service/entity/map_summary.go b/patientapp/service/entity/map_summary.go new file mode 100644 index 00000000..fcfd1284 --- /dev/null +++ b/patientapp/service/entity/map_summary.go @@ -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 +} diff --git a/patientapp/service/entity/patient.go b/patientapp/service/entity/patient.go new file mode 100644 index 00000000..33857e26 --- /dev/null +++ b/patientapp/service/entity/patient.go @@ -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" +) diff --git a/pkg/date_parser/date_parser.go b/pkg/date_parser/date_parser.go new file mode 100644 index 00000000..5b89e6ff --- /dev/null +++ b/pkg/date_parser/date_parser.go @@ -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 +} diff --git a/vendor/github.com/jalaali/go-jalaali/LICENSE b/vendor/github.com/jalaali/go-jalaali/LICENSE new file mode 100644 index 00000000..1a6e1367 --- /dev/null +++ b/vendor/github.com/jalaali/go-jalaali/LICENSE @@ -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. diff --git a/vendor/github.com/jalaali/go-jalaali/README.md b/vendor/github.com/jalaali/go-jalaali/README.md new file mode 100644 index 00000000..e5f2b4ec --- /dev/null +++ b/vendor/github.com/jalaali/go-jalaali/README.md @@ -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. diff --git a/vendor/github.com/jalaali/go-jalaali/convertion.go b/vendor/github.com/jalaali/go-jalaali/convertion.go new file mode 100644 index 00000000..3163786c --- /dev/null +++ b/vendor/github.com/jalaali/go-jalaali/convertion.go @@ -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 +} diff --git a/vendor/github.com/jalaali/go-jalaali/errors.go b/vendor/github.com/jalaali/go-jalaali/errors.go new file mode 100644 index 00000000..59d33d01 --- /dev/null +++ b/vendor/github.com/jalaali/go-jalaali/errors.go @@ -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) +} diff --git a/vendor/github.com/jalaali/go-jalaali/format.go b/vendor/github.com/jalaali/go-jalaali/format.go new file mode 100644 index 00000000..459df940 --- /dev/null +++ b/vendor/github.com/jalaali/go-jalaali/format.go @@ -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<= 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]))...) +} diff --git a/vendor/github.com/jalaali/go-jalaali/go.mod b/vendor/github.com/jalaali/go-jalaali/go.mod new file mode 100644 index 00000000..b4d320f2 --- /dev/null +++ b/vendor/github.com/jalaali/go-jalaali/go.mod @@ -0,0 +1,3 @@ +module github.com/jalaali/go-jalaali + +go 1.13 diff --git a/vendor/github.com/jalaali/go-jalaali/jalaali.go b/vendor/github.com/jalaali/go-jalaali/jalaali.go new file mode 100644 index 00000000..622a642b --- /dev/null +++ b/vendor/github.com/jalaali/go-jalaali/jalaali.go @@ -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)) + ")" +} + diff --git a/vendor/github.com/jalaali/go-jalaali/jalaali_test.go b/vendor/github.com/jalaali/go-jalaali/jalaali_test.go new file mode 100644 index 00000000..6db3d615 --- /dev/null +++ b/vendor/github.com/jalaali/go-jalaali/jalaali_test.go @@ -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) + } + } + } +} diff --git a/vendor/github.com/jalaali/go-jalaali/utils.go b/vendor/github.com/jalaali/go-jalaali/utils.go new file mode 100644 index 00000000..c2279763 --- /dev/null +++ b/vendor/github.com/jalaali/go-jalaali/utils.go @@ -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 +} \ No newline at end of file