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:
hossein 2026-04-08 06:23:29 +00:00
commit 05b06df7eb
30 changed files with 1524 additions and 4 deletions

View File

@ -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)
} }
} }

View File

@ -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{

View File

@ -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()
}

26
patientapp/cmd/main.go Normal file
View File

@ -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()
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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"
)

View File

@ -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
}

21
vendor/github.com/jalaali/go-jalaali/LICENSE generated vendored Normal file
View File

@ -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.

23
vendor/github.com/jalaali/go-jalaali/README.md generated vendored Normal file
View File

@ -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.

147
vendor/github.com/jalaali/go-jalaali/convertion.go generated vendored Normal file
View File

@ -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
}

19
vendor/github.com/jalaali/go-jalaali/errors.go generated vendored Normal file
View File

@ -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)
}

318
vendor/github.com/jalaali/go-jalaali/format.go generated vendored Normal file
View File

@ -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]))...)
}

3
vendor/github.com/jalaali/go-jalaali/go.mod generated vendored Normal file
View File

@ -0,0 +1,3 @@
module github.com/jalaali/go-jalaali
go 1.13

79
vendor/github.com/jalaali/go-jalaali/jalaali.go generated vendored Normal file
View File

@ -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)) + ")"
}

188
vendor/github.com/jalaali/go-jalaali/jalaali_test.go generated vendored Normal file
View File

@ -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)
}
}
}
}

53
vendor/github.com/jalaali/go-jalaali/utils.go generated vendored Normal file
View File

@ -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
}