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.
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
2
main.go
2
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{
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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