forked from ebhomengo/niki
add echo web framework to patientapp domain
This commit is contained in:
parent
1403e2c927
commit
3259a7468e
|
|
@ -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,34 +1,37 @@
|
|||
package patientapp
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/config"
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/delivery/http/analytic"
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/repository/database"
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/repository/mysql"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type Application struct {
|
||||
Config Config
|
||||
HTTPServer *http.Server
|
||||
DB *database.DataBase
|
||||
Router http.Handler
|
||||
//Config Config
|
||||
HTTPServer *config.EchoServer
|
||||
DB *mysql.DataBase
|
||||
}
|
||||
|
||||
func Setup(config Config, server *http.Server, conn *database.DataBase) Application {
|
||||
handler := analytic.NewHandler()
|
||||
router := analytic.NewRouter(handler)
|
||||
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,
|
||||
//Config: config,
|
||||
HTTPServer: &server,
|
||||
DB: conn,
|
||||
Router: router,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Application) Start() {
|
||||
srv := &http.Server{Addr: a.HTTPServer.Addr}
|
||||
server := analytic.NewServer(srv, a.Router)
|
||||
func (a Application) Start() {
|
||||
|
||||
server := analytic.NewServer(a.HTTPServer)
|
||||
|
||||
_ = server.Serve()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
package patientapp
|
||||
|
||||
type Config struct {
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -1,13 +1,11 @@
|
|||
package analytic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/delivery/http/analytic/dto"
|
||||
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 {
|
||||
|
|
@ -20,136 +18,48 @@ func NewHandler(service svc.Service) *Handler {
|
|||
}
|
||||
}
|
||||
|
||||
func (h *Handler) Health(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
func (h *Handler) Health(e echo.Context) error {
|
||||
return e.JSON(http.StatusOK, map[string]interface{}{"status": "ok"})
|
||||
}
|
||||
|
||||
func (h *Handler) PatientsAnalytic(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
func (h *Handler) PatientsAnalytic(e echo.Context) error {
|
||||
var req svc.ListPatientAnalyticRequest
|
||||
|
||||
minAge, err := parseIntPtr(q, "minAge")
|
||||
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 {
|
||||
writeError(w, http.StatusBadRequest, "invalid minAge")
|
||||
return
|
||||
}
|
||||
maxAge, err := parseIntPtr(q, "maxAge")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid maxAge")
|
||||
return
|
||||
richErr = richErr.WithErr(err)
|
||||
richErr = richErr.WithKind(4)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, richErr)
|
||||
}
|
||||
|
||||
sex, err := parseSexPtr(q, "sex")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid sex")
|
||||
return
|
||||
}
|
||||
|
||||
provinceID, err := parseIntPtr(q, "provinceId")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid provinceId")
|
||||
return
|
||||
}
|
||||
cityID, err := parseIntPtr(q, "cityId")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid cityId")
|
||||
return
|
||||
}
|
||||
|
||||
search := strings.TrimSpace(q.Get("search"))
|
||||
var searchPtr *string
|
||||
if search != "" {
|
||||
searchPtr = &search
|
||||
}
|
||||
|
||||
limit, err := parseInt(q, "limit")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid limit")
|
||||
return
|
||||
}
|
||||
offset, err := parseInt(q, "offset")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid offset")
|
||||
return
|
||||
}
|
||||
|
||||
pId := int64(*provinceID)
|
||||
cId := int64(*cityID)
|
||||
|
||||
req := dto.ListPatientAnalyticRequest{
|
||||
MinAge: minAge,
|
||||
MaxAge: maxAge,
|
||||
Sex: sex,
|
||||
Province: &pId,
|
||||
City: &cId,
|
||||
Search: searchPtr,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
}
|
||||
|
||||
response, err := h.service.List(r.Context(), req)
|
||||
if err != nil {
|
||||
log.Println("List error:", err)
|
||||
writeError(w, http.StatusInternalServerError, fmt.Errorf("List error: %w", err).Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, response)
|
||||
return e.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func (h *Handler) PatientsMapSummary(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
func (h *Handler) PatientsMapSummary(e echo.Context) error {
|
||||
richErr := richerror.New(richerror.Op("fetchingPatientMapSummary.PatientsMapSummary"))
|
||||
|
||||
level, err := parseMapLevel(q, "level")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid level (city|province|country)")
|
||||
return
|
||||
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)
|
||||
}
|
||||
|
||||
parentID, err := parseIntPtr(q, "parentId")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid parentId")
|
||||
return
|
||||
}
|
||||
|
||||
minAge, err := parseIntPtr(q, "minAge")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid minAge")
|
||||
return
|
||||
}
|
||||
maxAge, err := parseIntPtr(q, "maxAge")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid maxAge")
|
||||
return
|
||||
}
|
||||
|
||||
sex, err := parseSexPtr(q, "sex")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid sex")
|
||||
return
|
||||
}
|
||||
|
||||
search := strings.TrimSpace(q.Get("search"))
|
||||
var searchPtr *string
|
||||
if search != "" {
|
||||
searchPtr = &search
|
||||
}
|
||||
|
||||
req := dto.GetPatientMapSummaryRequest{
|
||||
Level: level,
|
||||
ParentID: parentID,
|
||||
MinAge: minAge,
|
||||
MaxAge: maxAge,
|
||||
Sex: sex,
|
||||
Search: searchPtr,
|
||||
}
|
||||
|
||||
resp, svcErr := h.service.GetMapSummary(r.Context(), req)
|
||||
resp, svcErr := h.service.GetMapSummary(e.Request().Context(), req)
|
||||
if svcErr != nil {
|
||||
log.Println("GetMapSummary error:", svcErr)
|
||||
writeError(w, http.StatusInternalServerError, svcErr.Error())
|
||||
return
|
||||
richErr = richErr.WithErr(svcErr)
|
||||
richErr = richErr.WithKind(4)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, richErr)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
return e.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,70 +0,0 @@
|
|||
package analytic
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
|
||||
)
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, status int, msg string) {
|
||||
writeJSON(w, status, map[string]any{"error": msg})
|
||||
}
|
||||
|
||||
func parseInt(q urlValues, key string) (int, error) {
|
||||
val := strings.TrimSpace(q.Get(key))
|
||||
if val == "" {
|
||||
return 0, nil
|
||||
}
|
||||
return strconv.Atoi(val)
|
||||
}
|
||||
|
||||
type urlValues interface {
|
||||
Get(key string) string
|
||||
}
|
||||
|
||||
func parseIntPtr(q urlValues, key string) (*int, error) {
|
||||
val := strings.TrimSpace(q.Get(key))
|
||||
if val == "" {
|
||||
return nil, nil
|
||||
}
|
||||
i, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &i, nil
|
||||
}
|
||||
|
||||
func parseSexPtr(q urlValues, key string) (*entity.Sex, error) {
|
||||
val := strings.TrimSpace(q.Get(key))
|
||||
if val == "" {
|
||||
return nil, nil
|
||||
}
|
||||
s := entity.Sex(val)
|
||||
if !s.SexValidation() {
|
||||
return nil, strconv.ErrSyntax
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func parseMapLevel(q urlValues, key string) (entity.MapLevel, error) {
|
||||
val := strings.TrimSpace(q.Get(key))
|
||||
if val == "" {
|
||||
return "", strconv.ErrSyntax
|
||||
}
|
||||
l := entity.MapLevel(val)
|
||||
switch l {
|
||||
case entity.MapLevelCity, entity.MapLevelProvince, entity.MapLevelCountry:
|
||||
return l, nil
|
||||
default:
|
||||
return "", strconv.ErrSyntax
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,21 @@
|
|||
package analytic
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/repository/mysql"
|
||||
analytic2 "git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func NewRouter(h *Handler) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
func NewPatientAnalyticRouter(s *echo.Group) {
|
||||
|
||||
// Define Routes
|
||||
v1 := http.NewServeMux()
|
||||
v1.HandleFunc("GET /health", h.Health)
|
||||
v1.HandleFunc("GET /patients", h.PatientsAnalytic)
|
||||
v1.HandleFunc("GET /patients-summary", h.PatientsMapSummary)
|
||||
repo := mysql.NewPatientRepo()
|
||||
validator := analytic2.NewValidator()
|
||||
analyticService := analytic2.NewPatientAnalyticService(repo, *validator)
|
||||
|
||||
mux.Handle("/v1/", http.StripPrefix("/v1", v1))
|
||||
h := NewHandler(analyticService)
|
||||
|
||||
s.GET("/patients", h.PatientsAnalytic)
|
||||
s.GET("/patients-summary", h.PatientsMapSummary)
|
||||
s.GET("/health", h.Health)
|
||||
|
||||
return mux
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,41 @@
|
|||
package analytic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/config"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
HTTPServer *http.Server
|
||||
HTTPServer *config.EchoServer
|
||||
}
|
||||
|
||||
func NewServer(server *http.Server, router http.Handler) *Server {
|
||||
server.Handler = router
|
||||
func NewServer(server *config.EchoServer) *Server {
|
||||
|
||||
return &Server{
|
||||
HTTPServer: server,
|
||||
}
|
||||
}
|
||||
|
||||
func (s Server) Serve() error {
|
||||
s.RegisterRoutes()
|
||||
// Start server
|
||||
fmt.Printf("start sever on %s \n", s.HTTPServer.Addr)
|
||||
return s.HTTPServer.ListenAndServe()
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,303 +0,0 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic"
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
|
||||
errmsg "git.gocasts.ir/ebhomengo/niki/pkg/err_msg"
|
||||
richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error"
|
||||
"git.gocasts.ir/ebhomengo/niki/repository/mysql"
|
||||
)
|
||||
|
||||
type DataBase struct {
|
||||
conn *mysql.DB
|
||||
}
|
||||
|
||||
func NewPatientRepo(conn *mysql.DB) *DataBase {
|
||||
return &DataBase{
|
||||
conn: conn,
|
||||
}
|
||||
}
|
||||
|
||||
func (db *DataBase) GetPatients(ctx context.Context, f analytic.PatientFilter) ([]entity.Patient, error) {
|
||||
const op = "mysqlpatient.GetPatients"
|
||||
limit, offset := clampLimitOffset(f.Limit, f.Offset)
|
||||
|
||||
q := `
|
||||
SELECT
|
||||
p.id,
|
||||
p.first_name,
|
||||
p.last_name,
|
||||
p.date_of_birth,
|
||||
p.sex,
|
||||
p.phone,
|
||||
|
||||
p.country_id,
|
||||
p.province_id,
|
||||
p.city_id,
|
||||
p.postal_code,
|
||||
p.address_line1,
|
||||
p.address_line2,
|
||||
|
||||
p.case_status,
|
||||
p.referral_source,
|
||||
p.assigned_staff_id,
|
||||
p.start_date,
|
||||
p.end_date
|
||||
FROM patients p
|
||||
WHERE p.deleted_at IS NULL
|
||||
`
|
||||
|
||||
stmt, err := db.conn.PrepareStatement(ctx, mysql.StatementKeyCityGetProvinceIDByID, q)
|
||||
if err != nil {
|
||||
return nil, richerror.New(op).WithErr(err).
|
||||
WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected)
|
||||
}
|
||||
args := make([]any, 0)
|
||||
|
||||
// filters
|
||||
if f.Sex != nil {
|
||||
q += " AND p.sex = ?"
|
||||
args = append(args, string(*f.Sex))
|
||||
}
|
||||
if f.DOBFrom != nil {
|
||||
q += " AND p.date_of_birth >= ?"
|
||||
args = append(args, f.DOBFrom.Format("2006-01-02"))
|
||||
}
|
||||
if f.DOBTo != nil {
|
||||
q += " AND p.date_of_birth <= ?"
|
||||
args = append(args, f.DOBTo.Format("2006-01-02"))
|
||||
}
|
||||
if f.Country != nil {
|
||||
q += " AND p.country_id = ?"
|
||||
args = append(args, *f.Country)
|
||||
}
|
||||
if f.Province != nil {
|
||||
q += " AND p.province_id = ?"
|
||||
args = append(args, *f.Province)
|
||||
}
|
||||
if f.City != nil {
|
||||
q += " AND p.city_id = ?"
|
||||
args = append(args, *f.City)
|
||||
}
|
||||
|
||||
if f.Search != nil && strings.TrimSpace(*f.Search) != "" {
|
||||
term := "%" + strings.TrimSpace(*f.Search) + "%"
|
||||
q += " AND (p.first_name LIKE ? OR p.last_name LIKE ? OR p.phone LIKE ?)"
|
||||
args = append(args, term, term, term)
|
||||
}
|
||||
|
||||
q += " ORDER BY p.id DESC LIMIT ? OFFSET ?"
|
||||
args = append(args, limit, offset)
|
||||
|
||||
rows, err := stmt.QueryContext(ctx, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := make([]entity.Patient, 0, limit)
|
||||
|
||||
for rows.Next() {
|
||||
var p entity.Patient
|
||||
var dob sql.NullTime
|
||||
var provinceID, cityID uint
|
||||
|
||||
if err := rows.Scan(
|
||||
&p.ID,
|
||||
&p.FirstName,
|
||||
&p.LastName,
|
||||
&dob,
|
||||
&p.Sex,
|
||||
&p.Phone,
|
||||
|
||||
&provinceID,
|
||||
&cityID,
|
||||
&p.Address.PostalCode,
|
||||
|
||||
&p.CaseStatus,
|
||||
&p.ReferralSource,
|
||||
&p.AssignedStaffId,
|
||||
&p.StartDate,
|
||||
&p.EndDate,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if dob.Valid {
|
||||
t := dob.Time
|
||||
p.DateOfBirth = &t
|
||||
}
|
||||
|
||||
p.Address.ProvinceID = provinceID
|
||||
p.Address.CityID = cityID
|
||||
|
||||
out = append(out, p)
|
||||
}
|
||||
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (db *DataBase) CountPatients(ctx context.Context, f analytic.PatientFilter) (int, error) {
|
||||
const op = "mysqlpatient.CountPatients"
|
||||
q := `SELECT COUNT(*) FROM patients p WHERE p.deleted_at IS NULL`
|
||||
args := make([]any, 0)
|
||||
|
||||
stmt, err := db.conn.PrepareStatement(ctx, mysql.StatementKeyCityGetProvinceIDByID, q)
|
||||
if err != nil {
|
||||
return 0, richerror.New(op).WithErr(err).
|
||||
WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected)
|
||||
}
|
||||
|
||||
if f.Sex != nil {
|
||||
q += " AND p.sex = ?"
|
||||
args = append(args, string(*f.Sex))
|
||||
}
|
||||
if f.DOBFrom != nil {
|
||||
q += " AND p.date_of_birth >= ?"
|
||||
args = append(args, f.DOBFrom.Format("2006-01-02"))
|
||||
}
|
||||
if f.DOBTo != nil {
|
||||
q += " AND p.date_of_birth <= ?"
|
||||
args = append(args, f.DOBTo.Format("2006-01-02"))
|
||||
}
|
||||
if f.Country != nil {
|
||||
q += " AND p.country_id = ?"
|
||||
args = append(args, *f.Country)
|
||||
}
|
||||
if f.Province != nil {
|
||||
q += " AND p.province_id = ?"
|
||||
args = append(args, *f.Province)
|
||||
}
|
||||
if f.City != nil {
|
||||
q += " AND p.city_id = ?"
|
||||
args = append(args, *f.City)
|
||||
}
|
||||
|
||||
var total int
|
||||
|
||||
err = stmt.QueryRowContext(ctx, args...).Scan(&total)
|
||||
return total, err
|
||||
}
|
||||
|
||||
func (db *DataBase) SummaryByCity(ctx context.Context, provinceID int, f analytic.PatientMapFilter) ([]entity.MapSummaryItem, error) {
|
||||
const op = "mysqlpatient.SummaryByCity"
|
||||
q := `
|
||||
SELECT
|
||||
c.id AS location_id,
|
||||
c.name,
|
||||
c.centroid_lat,
|
||||
c.centroid_lng,
|
||||
COUNT(p.id) AS count
|
||||
FROM locations c
|
||||
JOIN patients p ON p.city_id = c.id
|
||||
WHERE c.level = 'city'
|
||||
AND c.parent_id = ?
|
||||
AND p.deleted_at IS NULL
|
||||
`
|
||||
|
||||
stmt, err := db.conn.PrepareStatement(ctx, mysql.StatementKeyCityGetProvinceIDByID, q)
|
||||
if err != nil {
|
||||
return nil, richerror.New(op).WithErr(err).
|
||||
WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected)
|
||||
}
|
||||
args := []any{provinceID}
|
||||
|
||||
if f.Sex != nil {
|
||||
q += " AND p.sex = ?"
|
||||
args = append(args, string(*f.Sex))
|
||||
}
|
||||
if f.MinDOB != nil {
|
||||
q += " AND p.date_of_birth >= ?"
|
||||
args = append(args, f.MinDOB.Format("2006-01-02"))
|
||||
}
|
||||
if f.MaxDOB != nil {
|
||||
q += " AND p.date_of_birth <= ?"
|
||||
args = append(args, f.MaxDOB.Format("2006-01-02"))
|
||||
}
|
||||
if f.Search != nil && strings.TrimSpace(*f.Search) != "" {
|
||||
term := "%" + strings.TrimSpace(*f.Search) + "%"
|
||||
q += " AND (p.first_name LIKE ? OR p.last_name LIKE ? OR p.phone LIKE ?)"
|
||||
args = append(args, term, term, term)
|
||||
}
|
||||
|
||||
q += ` GROUP BY c.id, c.name, c.centroid_lat, c.centroid_lng
|
||||
ORDER BY count DESC`
|
||||
|
||||
rows, err := stmt.QueryContext(ctx, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []entity.MapSummaryItem
|
||||
for rows.Next() {
|
||||
var it entity.MapSummaryItem
|
||||
if err := rows.Scan(&it.LocationID, &it.Name, &it.CentroidLat, &it.CentroidLng, &it.Count); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, it)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (db *DataBase) SummaryByProvince(ctx context.Context, countryID int, f analytic.PatientMapFilter) ([]entity.MapSummaryItem, error) {
|
||||
const op = "mysqlpatient.SummaryByProvince"
|
||||
|
||||
q := `
|
||||
SELECT
|
||||
prov.id AS location_id,
|
||||
prov.name,
|
||||
prov.centroid_lat,
|
||||
prov.centroid_lng,
|
||||
COUNT(p.id) AS count
|
||||
FROM patients p
|
||||
JOIN locations city ON city.id = p.city_id AND city.level = 'city'
|
||||
JOIN locations prov ON prov.id = city.parent_id AND prov.level = 'province'
|
||||
WHERE prov.parent_id = ?
|
||||
AND p.deleted_at IS NULL
|
||||
`
|
||||
|
||||
stmt, err := db.conn.PrepareStatement(ctx, mysql.StatementKeyCityGetProvinceIDByID, q)
|
||||
if err != nil {
|
||||
return nil, richerror.New(op).WithErr(err).
|
||||
WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected)
|
||||
}
|
||||
|
||||
args := []any{countryID}
|
||||
|
||||
if f.Sex != nil {
|
||||
q += " AND p.sex = ?"
|
||||
args = append(args, string(*f.Sex))
|
||||
}
|
||||
if f.MinDOB != nil {
|
||||
q += " AND p.date_of_birth >= ?"
|
||||
args = append(args, f.MinDOB.Format("2006-01-02"))
|
||||
}
|
||||
if f.MaxDOB != nil {
|
||||
q += " AND p.date_of_birth <= ?"
|
||||
args = append(args, f.MaxDOB.Format("2006-01-02"))
|
||||
}
|
||||
|
||||
q += ` GROUP BY prov.id, prov.name, prov.centroid_lat, prov.centroid_lng
|
||||
ORDER BY count DESC`
|
||||
|
||||
rows, err := stmt.QueryContext(ctx, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []entity.MapSummaryItem
|
||||
for rows.Next() {
|
||||
var it entity.MapSummaryItem
|
||||
if err := rows.Scan(&it.LocationID, &it.Name, &it.CentroidLat, &it.CentroidLng, &it.Count); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, it)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
package database
|
||||
|
||||
import "strings"
|
||||
|
||||
type whereClause struct {
|
||||
sql strings.Builder
|
||||
args []any
|
||||
}
|
||||
|
||||
func (w *whereClause) add(cond string, args ...any) {
|
||||
if w.sql.Len() == 0 {
|
||||
w.sql.WriteString(" WHERE ")
|
||||
} else {
|
||||
w.sql.WriteString(" AND ")
|
||||
}
|
||||
w.sql.WriteString(cond)
|
||||
w.args = append(w.args, args...)
|
||||
}
|
||||
|
||||
func (w *whereClause) String() string { return w.sql.String() }
|
||||
|
||||
func clampLimitOffset(limit, offset int) (int, int) {
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
return limit, offset
|
||||
}
|
||||
|
|
@ -0,0 +1,349 @@
|
|||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/service/analytic"
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
|
||||
)
|
||||
|
||||
type DataBase struct {
|
||||
//conn *mysql.DB
|
||||
patients []entity.Patient
|
||||
}
|
||||
|
||||
func NewPatientRepo( /*conn *mysql.DB*/) *DataBase {
|
||||
patients, err := LoadPatientsFromJSON("C:\\Users\\cafel\\Documents\\gocast\\eb\\patientapp\\repository\\mysql\\rawdata.json")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return nil
|
||||
}
|
||||
return &DataBase{
|
||||
//conn: conn,
|
||||
patients: patients,
|
||||
}
|
||||
}
|
||||
|
||||
func (db *DataBase) GetPatients(ctx context.Context, f analytic.PatientFilter) ([]entity.Patient, error) {
|
||||
|
||||
return db.patients, nil
|
||||
|
||||
}
|
||||
|
||||
func (db *DataBase) CountPatients(ctx context.Context, f analytic.PatientFilter) (int, error) {
|
||||
|
||||
return len(db.patients), nil
|
||||
}
|
||||
|
||||
func (db *DataBase) SummaryByCity(ctx context.Context, provinceID uint, f analytic.PatientMapFilter) ([]entity.MapSummaryItem, error) {
|
||||
|
||||
var out []entity.MapSummaryItem
|
||||
|
||||
for _, patient := range db.patients {
|
||||
if patient.Address.ProvinceID == provinceID {
|
||||
out = append(out, entity.MapSummaryItem{
|
||||
LocationID: int64(patient.Address.ID),
|
||||
Name: patient.Address.Name,
|
||||
CentroidLat: patient.Address.Lat,
|
||||
CentroidLng: patient.Address.Lon,
|
||||
})
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (db *DataBase) SummaryByProvince(ctx context.Context, f analytic.PatientMapFilter) (map[uint][]entity.MapSummaryItem, error) {
|
||||
result := make(map[uint][]entity.MapSummaryItem, 0)
|
||||
for _, patient := range db.patients {
|
||||
out := entity.MapSummaryItem{
|
||||
LocationID: int64(patient.Address.ID),
|
||||
Name: patient.Address.Name,
|
||||
CentroidLat: patient.Address.Lat,
|
||||
CentroidLng: patient.Address.Lon,
|
||||
}
|
||||
result[patient.Address.ProvinceID] = append(result[patient.Address.ProvinceID], out)
|
||||
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
//func (db *DataBase) GetPatients(ctx context.Context, f analytic.PatientFilter) ([]entity.Patient, error) {
|
||||
// const op = "mysqlpatient.GetPatients"
|
||||
// limit, offset := clampLimitOffset(f.Limit, f.Offset)
|
||||
//
|
||||
// q := `
|
||||
//SELECT
|
||||
// p.id,
|
||||
// p.first_name,
|
||||
// p.last_name,
|
||||
// p.date_of_birth,
|
||||
// p.sex,
|
||||
// p.phone,
|
||||
//
|
||||
// p.country_id,
|
||||
// p.province_id,
|
||||
// p.city_id,
|
||||
// p.postal_code,
|
||||
// p.address_line1,
|
||||
// p.address_line2,
|
||||
//
|
||||
// p.case_status,
|
||||
// p.referral_source,
|
||||
// p.assigned_staff_id,
|
||||
// p.start_date,
|
||||
// p.end_date
|
||||
//FROM patients p
|
||||
//WHERE p.deleted_at IS NULL
|
||||
//`
|
||||
//
|
||||
// stmt, err := db.conn.PrepareStatement(ctx, mysql.StatementKeyCityGetProvinceIDByID, q)
|
||||
// if err != nil {
|
||||
// return nil, richerror.New(op).WithErr(err).
|
||||
// WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected)
|
||||
// }
|
||||
// args := make([]any, 0)
|
||||
//
|
||||
// // filters
|
||||
// if f.Sex != nil {
|
||||
// q += " AND p.sex = ?"
|
||||
// args = append(args, string(*f.Sex))
|
||||
// }
|
||||
// if f.DOBFrom != nil {
|
||||
// q += " AND p.date_of_birth >= ?"
|
||||
// args = append(args, f.DOBFrom.Format("2006-01-02"))
|
||||
// }
|
||||
// if f.DOBTo != nil {
|
||||
// q += " AND p.date_of_birth <= ?"
|
||||
// args = append(args, f.DOBTo.Format("2006-01-02"))
|
||||
// }
|
||||
// if f.Country != nil {
|
||||
// q += " AND p.country_id = ?"
|
||||
// args = append(args, *f.Country)
|
||||
// }
|
||||
// if f.Province != nil {
|
||||
// q += " AND p.province_id = ?"
|
||||
// args = append(args, *f.Province)
|
||||
// }
|
||||
// if f.City != nil {
|
||||
// q += " AND p.city_id = ?"
|
||||
// args = append(args, *f.City)
|
||||
// }
|
||||
//
|
||||
// if f.Search != nil && strings.TrimSpace(*f.Search) != "" {
|
||||
// term := "%" + strings.TrimSpace(*f.Search) + "%"
|
||||
// q += " AND (p.first_name LIKE ? OR p.last_name LIKE ? OR p.phone LIKE ?)"
|
||||
// args = append(args, term, term, term)
|
||||
// }
|
||||
//
|
||||
// q += " ORDER BY p.id DESC LIMIT ? OFFSET ?"
|
||||
// args = append(args, limit, offset)
|
||||
//
|
||||
// rows, err := stmt.QueryContext(ctx, args...)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// defer rows.Close()
|
||||
//
|
||||
// out := make([]entity.Patient, 0, limit)
|
||||
//
|
||||
// for rows.Next() {
|
||||
// var p entity.Patient
|
||||
// var dob sql.NullTime
|
||||
// var provinceID, cityID uint
|
||||
//
|
||||
// if err := rows.Scan(
|
||||
// &p.ID,
|
||||
// &p.FirstName,
|
||||
// &p.LastName,
|
||||
// &dob,
|
||||
// &p.Sex,
|
||||
// &p.Phone,
|
||||
//
|
||||
// &provinceID,
|
||||
// &cityID,
|
||||
// &p.Address.PostalCode,
|
||||
//
|
||||
// &p.CaseStatus,
|
||||
// &p.ReferralSource,
|
||||
// &p.AssignedStaffId,
|
||||
// &p.StartDate,
|
||||
// &p.EndDate,
|
||||
// ); err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
//
|
||||
// if dob.Valid {
|
||||
// t := dob.Time
|
||||
// p.DateOfBirth = &t
|
||||
// }
|
||||
//
|
||||
// p.Address.ProvinceID = provinceID
|
||||
// p.Address.CityID = cityID
|
||||
//
|
||||
// out = append(out, p)
|
||||
// }
|
||||
//
|
||||
// return out, rows.Err()
|
||||
//}
|
||||
//
|
||||
//func (db *DataBase) CountPatients(ctx context.Context, f analytic.PatientFilter) (int, error) {
|
||||
// const op = "mysqlpatient.CountPatients"
|
||||
// q := `SELECT COUNT(*) FROM patients p WHERE p.deleted_at IS NULL`
|
||||
// args := make([]any, 0)
|
||||
//
|
||||
// stmt, err := db.conn.PrepareStatement(ctx, mysql.StatementKeyCityGetProvinceIDByID, q)
|
||||
// if err != nil {
|
||||
// return 0, richerror.New(op).WithErr(err).
|
||||
// WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected)
|
||||
// }
|
||||
//
|
||||
// if f.Sex != nil {
|
||||
// q += " AND p.sex = ?"
|
||||
// args = append(args, string(*f.Sex))
|
||||
// }
|
||||
// if f.DOBFrom != nil {
|
||||
// q += " AND p.date_of_birth >= ?"
|
||||
// args = append(args, f.DOBFrom.Format("2006-01-02"))
|
||||
// }
|
||||
// if f.DOBTo != nil {
|
||||
// q += " AND p.date_of_birth <= ?"
|
||||
// args = append(args, f.DOBTo.Format("2006-01-02"))
|
||||
// }
|
||||
// if f.Country != nil {
|
||||
// q += " AND p.country_id = ?"
|
||||
// args = append(args, *f.Country)
|
||||
// }
|
||||
// if f.Province != nil {
|
||||
// q += " AND p.province_id = ?"
|
||||
// args = append(args, *f.Province)
|
||||
// }
|
||||
// if f.City != nil {
|
||||
// q += " AND p.city_id = ?"
|
||||
// args = append(args, *f.City)
|
||||
// }
|
||||
//
|
||||
// var total int
|
||||
//
|
||||
// err = stmt.QueryRowContext(ctx, args...).Scan(&total)
|
||||
// return total, err
|
||||
//}
|
||||
//
|
||||
//func (db *DataBase) SummaryByCity(ctx context.Context, provinceID int, f analytic.PatientMapFilter) ([]entity.MapSummaryItem, error) {
|
||||
// const op = "mysqlpatient.SummaryByCity"
|
||||
// q := `
|
||||
//SELECT
|
||||
// c.id AS location_id,
|
||||
// c.name,
|
||||
// c.centroid_lat,
|
||||
// c.centroid_lng,
|
||||
// COUNT(p.id) AS count
|
||||
//FROM locations c
|
||||
//JOIN patients p ON p.city_id = c.id
|
||||
//WHERE c.level = 'city'
|
||||
// AND c.parent_id = ?
|
||||
// AND p.deleted_at IS NULL
|
||||
//`
|
||||
//
|
||||
// stmt, err := db.conn.PrepareStatement(ctx, mysql.StatementKeyCityGetProvinceIDByID, q)
|
||||
// if err != nil {
|
||||
// return nil, richerror.New(op).WithErr(err).
|
||||
// WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected)
|
||||
// }
|
||||
// args := []any{provinceID}
|
||||
//
|
||||
// if f.Sex != nil {
|
||||
// q += " AND p.sex = ?"
|
||||
// args = append(args, string(*f.Sex))
|
||||
// }
|
||||
// if f.MinDOB != nil {
|
||||
// q += " AND p.date_of_birth >= ?"
|
||||
// args = append(args, f.MinDOB.Format("2006-01-02"))
|
||||
// }
|
||||
// if f.MaxDOB != nil {
|
||||
// q += " AND p.date_of_birth <= ?"
|
||||
// args = append(args, f.MaxDOB.Format("2006-01-02"))
|
||||
// }
|
||||
// if f.Search != nil && strings.TrimSpace(*f.Search) != "" {
|
||||
// term := "%" + strings.TrimSpace(*f.Search) + "%"
|
||||
// q += " AND (p.first_name LIKE ? OR p.last_name LIKE ? OR p.phone LIKE ?)"
|
||||
// args = append(args, term, term, term)
|
||||
// }
|
||||
//
|
||||
// q += ` GROUP BY c.id, c.name, c.centroid_lat, c.centroid_lng
|
||||
// ORDER BY count DESC`
|
||||
//
|
||||
// rows, err := stmt.QueryContext(ctx, args...)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// defer rows.Close()
|
||||
//
|
||||
// var out []entity.MapSummaryItem
|
||||
// for rows.Next() {
|
||||
// var it entity.MapSummaryItem
|
||||
// if err := rows.Scan(&it.LocationID, &it.Name, &it.CentroidLat, &it.CentroidLng, &it.Count); err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// out = append(out, it)
|
||||
// }
|
||||
// return out, rows.Err()
|
||||
//}
|
||||
//
|
||||
//func (db *DataBase) SummaryByProvince(ctx context.Context, countryID int, f analytic.PatientMapFilter) ([]entity.MapSummaryItem, error) {
|
||||
// const op = "mysqlpatient.SummaryByProvince"
|
||||
//
|
||||
// q := `
|
||||
//SELECT
|
||||
// prov.id AS location_id,
|
||||
// prov.name,
|
||||
// prov.centroid_lat,
|
||||
// prov.centroid_lng,
|
||||
// COUNT(p.id) AS count
|
||||
//FROM patients p
|
||||
//JOIN locations city ON city.id = p.city_id AND city.level = 'city'
|
||||
//JOIN locations prov ON prov.id = city.parent_id AND prov.level = 'province'
|
||||
//WHERE prov.parent_id = ?
|
||||
// AND p.deleted_at IS NULL
|
||||
//`
|
||||
//
|
||||
// stmt, err := db.conn.PrepareStatement(ctx, mysql.StatementKeyCityGetProvinceIDByID, q)
|
||||
// if err != nil {
|
||||
// return nil, richerror.New(op).WithErr(err).
|
||||
// WithMessage(errmsg.ErrorMsgCantPrepareStatement).WithKind(richerror.KindUnexpected)
|
||||
// }
|
||||
//
|
||||
// args := []any{countryID}
|
||||
//
|
||||
// if f.Sex != nil {
|
||||
// q += " AND p.sex = ?"
|
||||
// args = append(args, string(*f.Sex))
|
||||
// }
|
||||
// if f.MinDOB != nil {
|
||||
// q += " AND p.date_of_birth >= ?"
|
||||
// args = append(args, f.MinDOB.Format("2006-01-02"))
|
||||
// }
|
||||
// if f.MaxDOB != nil {
|
||||
// q += " AND p.date_of_birth <= ?"
|
||||
// args = append(args, f.MaxDOB.Format("2006-01-02"))
|
||||
// }
|
||||
//
|
||||
// q += ` GROUP BY prov.id, prov.name, prov.centroid_lat, prov.centroid_lng
|
||||
// ORDER BY count DESC`
|
||||
//
|
||||
// rows, err := stmt.QueryContext(ctx, args...)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// defer rows.Close()
|
||||
//
|
||||
// var out []entity.MapSummaryItem
|
||||
// for rows.Next() {
|
||||
// var it entity.MapSummaryItem
|
||||
// if err := rows.Scan(&it.LocationID, &it.Name, &it.CentroidLat, &it.CentroidLng, &it.Count); err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// out = append(out, it)
|
||||
// }
|
||||
// return out, rows.Err()
|
||||
//}
|
||||
|
|
@ -7,4 +7,4 @@ postgres_db:
|
|||
db_name: patient_db
|
||||
ssl_mode: disable
|
||||
|
||||
path_of_migration: ./patientapp/repository/database/migrations
|
||||
path_of_migration: ./patientapp/repository/mysql/migrations
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package mysql
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
|
||||
)
|
||||
|
||||
type whereClause struct {
|
||||
sql strings.Builder
|
||||
args []any
|
||||
}
|
||||
|
||||
func (w *whereClause) add(cond string, args ...any) {
|
||||
if w.sql.Len() == 0 {
|
||||
w.sql.WriteString(" WHERE ")
|
||||
} else {
|
||||
w.sql.WriteString(" AND ")
|
||||
}
|
||||
w.sql.WriteString(cond)
|
||||
w.args = append(w.args, args...)
|
||||
}
|
||||
|
||||
func (w *whereClause) String() string { return w.sql.String() }
|
||||
|
||||
func clampLimitOffset(limit, offset int) (int, int) {
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
return limit, offset
|
||||
}
|
||||
|
||||
func LoadPatientsFromJSON(filePath string) ([]entity.Patient, error) {
|
||||
// Read the file
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
|
||||
// Unmarshal into slice of Patient
|
||||
var patients []entity.Patient
|
||||
if err := json.Unmarshal(data, &patients); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
|
||||
}
|
||||
|
||||
return patients, nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,6 +1,10 @@
|
|||
package analytic
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/jalaali/go-jalaali"
|
||||
"time"
|
||||
)
|
||||
|
||||
func normalizeLimitOffset(limit, offset int) (int, int) {
|
||||
if limit <= 0 {
|
||||
|
|
@ -16,17 +20,24 @@ func normalizeLimitOffset(limit, offset int) (int, int) {
|
|||
}
|
||||
|
||||
// convert age range -> DOB range
|
||||
func ageRangeToDOB(minAge, maxAge *int, now time.Time) (dobFrom, dobTo *time.Time) {
|
||||
// maxAge => DOB >= now-(maxAge+1y)+1day
|
||||
// eg: now is : 3/31/2026; maxAge=30 => (now - 31)= 3/31/1994 => +=1
|
||||
func ageRangeToDOB(minAge, maxAge *int, now time.Time) (dobFrom, dobTo *string) {
|
||||
if maxAge != nil {
|
||||
t := now.AddDate(-*maxAge-1, 0, 0).Add(24 * time.Hour)
|
||||
dobFrom = &t
|
||||
t := now.AddDate(-(*maxAge + 1), 0, 1)
|
||||
jy, jm, jd, err := jalaali.ToJalaali(t.Year(), t.Month(), t.Day())
|
||||
if err != nil {
|
||||
}
|
||||
// minAge => DOB <= now-minAge
|
||||
s := fmt.Sprintf("%04d/%02d/%02d", jy, jm, jd)
|
||||
dobFrom = &s
|
||||
}
|
||||
|
||||
if minAge != nil {
|
||||
t := now.AddDate(-*minAge, 0, 0)
|
||||
dobTo = &t
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
package dto
|
||||
package analytic
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
|
||||
)
|
||||
|
||||
|
|
@ -25,7 +23,7 @@ type PatientAnalyticItem struct {
|
|||
ID int64 `json:"id"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"Last_name"`
|
||||
DateOfBirth *time.Time `json:"dob,omitempty"`
|
||||
DateOfBirth string `json:"dob,omitempty"`
|
||||
Sex entity.Sex `json:"sex"`
|
||||
Phone string `json:"phone"`
|
||||
Address entity.Address `json:"address"`
|
||||
|
|
@ -1,14 +1,12 @@
|
|||
package analytic
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
|
||||
)
|
||||
|
||||
type PatientFilter struct {
|
||||
DOBFrom *time.Time // born after
|
||||
DOBTo *time.Time // born before
|
||||
DOBFrom *string // born after
|
||||
DOBTo *string // born before
|
||||
Sex *entity.Sex
|
||||
|
||||
City *int64
|
||||
|
|
@ -22,8 +20,8 @@ type PatientFilter struct {
|
|||
}
|
||||
|
||||
type PatientMapFilter struct {
|
||||
MinDOB *time.Time
|
||||
MaxDOB *time.Time
|
||||
MinDOB *string
|
||||
MaxDOB *string
|
||||
Sex *entity.Sex
|
||||
Search *string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
analytic2 "git.gocasts.ir/ebhomengo/niki/patientapp/delivery/http/analytic/dto"
|
||||
"git.gocasts.ir/ebhomengo/niki/patientapp/service/entity"
|
||||
)
|
||||
|
||||
|
|
@ -22,7 +21,6 @@ type Repository interface {
|
|||
|
||||
SummaryByCity(ctx context.Context, provinceID int, f PatientMapFilter) ([]entity.MapSummaryItem, error)
|
||||
SummaryByProvince(ctx context.Context, countryID int, f PatientMapFilter) ([]entity.MapSummaryItem, error)
|
||||
SummaryByCountry(ctx context.Context, f PatientMapFilter) ([]entity.MapSummaryItem, error)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
|
|
@ -37,7 +35,7 @@ func NewPatientAnalyticService(repo Repository, validator Validator) Service {
|
|||
}
|
||||
}
|
||||
|
||||
func (s Service) List(ctx context.Context, req analytic2.ListPatientAnalyticRequest) (analytic2.PatientAnalyticResponse, error) {
|
||||
func (s Service) List(ctx context.Context, req ListPatientAnalyticRequest) (PatientAnalyticResponse, error) {
|
||||
|
||||
limit, offset := normalizeLimitOffset(req.Limit, req.Offset)
|
||||
|
||||
|
|
@ -57,21 +55,21 @@ func (s Service) List(ctx context.Context, req analytic2.ListPatientAnalyticRequ
|
|||
|
||||
items, err := s.repository.GetPatients(ctx, filter)
|
||||
if err != nil {
|
||||
return analytic2.PatientAnalyticResponse{}, fmt.Errorf("GetPatients: %w", err)
|
||||
return PatientAnalyticResponse{}, fmt.Errorf("GetPatients: %w", err)
|
||||
}
|
||||
|
||||
total, err := s.repository.CountPatients(ctx, filter)
|
||||
if err != nil {
|
||||
return analytic2.PatientAnalyticResponse{}, fmt.Errorf("CountPatients: %w", err)
|
||||
return PatientAnalyticResponse{}, fmt.Errorf("CountPatients: %w", err)
|
||||
}
|
||||
|
||||
// mapping response
|
||||
out := make([]analytic2.PatientAnalyticItem, 0, len(items))
|
||||
out := make([]PatientAnalyticItem, 0, len(items))
|
||||
for _, value := range items {
|
||||
out = append(out, analytic2.ToPatientResponse(value))
|
||||
out = append(out, ToPatientResponse(value))
|
||||
}
|
||||
|
||||
return analytic2.PatientAnalyticResponse{
|
||||
return PatientAnalyticResponse{
|
||||
Items: out,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
|
|
@ -80,7 +78,7 @@ func (s Service) List(ctx context.Context, req analytic2.ListPatientAnalyticRequ
|
|||
|
||||
}
|
||||
|
||||
func (s Service) GetMapSummary(ctx context.Context, req analytic2.GetPatientMapSummaryRequest) (analytic2.GetPatientMapSummaryResponse, error) {
|
||||
func (s Service) GetMapSummary(ctx context.Context, req GetPatientMapSummaryRequest) (GetPatientMapSummaryResponse, error) {
|
||||
|
||||
dobFrom, dobTo := ageRangeToDOB(req.MinAge, req.MaxAge, time.Now())
|
||||
|
||||
|
|
@ -94,35 +92,28 @@ func (s Service) GetMapSummary(ctx context.Context, req analytic2.GetPatientMapS
|
|||
switch req.Level {
|
||||
case entity.MapLevelCity:
|
||||
if req.ParentID == nil || *req.ParentID <= 0 {
|
||||
return analytic2.GetPatientMapSummaryResponse{}, ErrInvalidProvinceID
|
||||
return GetPatientMapSummaryResponse{}, ErrInvalidProvinceID
|
||||
}
|
||||
|
||||
items, err := s.repository.SummaryByCity(ctx, *req.ParentID, filter)
|
||||
if err != nil {
|
||||
return analytic2.GetPatientMapSummaryResponse{}, fmt.Errorf("SummaryByCity: %w", err)
|
||||
return GetPatientMapSummaryResponse{}, fmt.Errorf("SummaryByCity: %w", err)
|
||||
}
|
||||
return analytic2.GetPatientMapSummaryResponse{Level: req.Level, Items: items}, nil
|
||||
return GetPatientMapSummaryResponse{Level: req.Level, Items: items}, nil
|
||||
|
||||
case entity.MapLevelProvince:
|
||||
if req.ParentID == nil || *req.ParentID <= 0 {
|
||||
return analytic2.GetPatientMapSummaryResponse{}, ErrInvalidCountryID
|
||||
return GetPatientMapSummaryResponse{}, ErrInvalidCountryID
|
||||
}
|
||||
|
||||
items, err := s.repository.SummaryByProvince(ctx, *req.ParentID, filter)
|
||||
if err != nil {
|
||||
return analytic2.GetPatientMapSummaryResponse{}, fmt.Errorf("SummaryByProvince: %w", err)
|
||||
return GetPatientMapSummaryResponse{}, fmt.Errorf("SummaryByProvince: %w", err)
|
||||
}
|
||||
return analytic2.GetPatientMapSummaryResponse{Level: req.Level, Items: items}, nil
|
||||
|
||||
case entity.MapLevelCountry:
|
||||
items, err := s.repository.SummaryByCountry(ctx, filter)
|
||||
if err != nil {
|
||||
return analytic2.GetPatientMapSummaryResponse{}, fmt.Errorf("SummaryByCountry: %w", err)
|
||||
}
|
||||
return analytic2.GetPatientMapSummaryResponse{Level: req.Level, Items: items}, nil
|
||||
return GetPatientMapSummaryResponse{Level: req.Level, Items: items}, nil
|
||||
|
||||
default:
|
||||
return analytic2.GetPatientMapSummaryResponse{}, ErrInvalidMapLevel
|
||||
return GetPatientMapSummaryResponse{}, ErrInvalidMapLevel
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,3 +2,7 @@ package analytic
|
|||
|
||||
type Validator struct {
|
||||
}
|
||||
|
||||
func NewValidator() *Validator {
|
||||
return &Validator{}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
package entity
|
||||
|
||||
import "time"
|
||||
|
||||
type Patient struct {
|
||||
ID int64
|
||||
FirstName string
|
||||
LastName string
|
||||
DateOfBirth *time.Time
|
||||
DateOfBirth string
|
||||
Sex Sex
|
||||
Phone string
|
||||
Address Address
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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