Files
zarinpal-analytics-backend/internal/analytics/periods.go

204 lines
6.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package analytics
import (
"fmt"
"strings"
"time"
)
var jalaliBreaks = [...]int{-61, 9, 38, 199, 426, 686, 756, 818, 1111, 1181, 1210, 1635, 2060, 2097, 2192, 2262, 2324, 2394, 2456, 3178}
var jalaliMonthNames = [...]string{
"فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور",
"مهر", "آبان", "آذر", "دی", "بهمن", "اسفند",
}
var persianSeasonNames = [...]string{"بهار", "تابستان", "پاییز", "زمستان"}
type jalaliCalculation struct {
leap int
gy int
march int
}
func jalaliCalendar(jy int) jalaliCalculation {
gy := jy + 621
leapJ := -14
jp := jalaliBreaks[0]
jump := 0
for index := 1; index < len(jalaliBreaks); index++ {
jm := jalaliBreaks[index]
jump = jm - jp
if jy < jm {
break
}
leapJ += jump/33*8 + (jump%33)/4
jp = jm
}
n := jy - jp
leapJ += n/33*8 + (n%33+3)/4
if jump%33 == 4 && jump-n == 4 {
leapJ++
}
leapG := gy/4 - ((gy/100+1)*3)/4 - 150
march := 20 + leapJ - leapG
if jump-n < 6 {
n = n - jump + (jump+4)/33*33
}
leap := ((n+1)%33 - 1) % 4
if leap == -1 {
leap = 4
}
return jalaliCalculation{leap: leap, gy: gy, march: march}
}
func gregorianDayNumber(gy, gm, gd int) int {
d := ((gy+(gm-8)/6+100100)*1461)/4 + (153*((gm+9)%12)+2)/5 + gd - 34840408
d = d - (((gy+100100+(gm-8)/6)/100)*3)/4 + 752
return d
}
func gregorianFromDayNumber(dayNumber int) (int, int, int) {
j := 4*dayNumber + 139361631
j = j + (((4*dayNumber+183187720)/146097)*3/4)*4 - 3908
i := ((j % 1461) / 4 * 5) + 308
gd := (i%153)/5 + 1
gm := (i/153)%12 + 1
gy := j/1461 - 100100 + (8-gm)/6
return gy, gm, gd
}
func jalaliDayNumber(jy, jm, jd int) int {
calculation := jalaliCalendar(jy)
return gregorianDayNumber(calculation.gy, 3, calculation.march) + (jm-1)*31 - (jm/7)*(jm-7) + jd - 1
}
func jalaliToGregorian(jy, jm, jd int) (int, int, int) {
return gregorianFromDayNumber(jalaliDayNumber(jy, jm, jd))
}
func gregorianToJalali(gy, gm, gd int) (int, int, int) {
jy := gy - 621
calculation := jalaliCalendar(jy)
dayNumber := gregorianDayNumber(gy, gm, gd)
firstFarvardin := gregorianDayNumber(gy, 3, calculation.march)
k := dayNumber - firstFarvardin
if k >= 0 {
if k <= 185 {
return jy, 1 + k/31, k%31 + 1
}
k -= 186
} else {
jy--
k += 179
if calculation.leap == 1 {
k++
}
}
return jy, 7 + k/30, k%30 + 1
}
func jalaliDate(jy, jm, jd int, location *time.Location) time.Time {
gy, gm, gd := jalaliToGregorian(jy, jm, jd)
return time.Date(gy, time.Month(gm), gd, 0, 0, 0, 0, location)
}
func previousJalaliMonth(jy, jm int) (int, int) {
jm--
if jm == 0 {
return jy - 1, 12
}
return jy, jm
}
func nextJalaliMonth(jy, jm int) (int, int) {
jm++
if jm == 13 {
return jy + 1, 1
}
return jy, jm
}
func latestCompletePeriods(datasetStart, datasetMax time.Time, location *time.Location) []Period {
localStart := datasetStart.In(location)
localMax := datasetMax.In(location)
datasetStartDay := time.Date(localStart.Year(), localStart.Month(), localStart.Day(), 0, 0, 0, 0, location)
dayStart := time.Date(localMax.Year(), localMax.Month(), localMax.Day(), 0, 0, 0, 0, location)
datasetEnd := dayStart.AddDate(0, 0, 1)
weekEnd := datasetEnd
for weekEnd.Weekday() != time.Saturday {
weekEnd = weekEnd.AddDate(0, 0, -1)
}
weekStart := weekEnd.AddDate(0, 0, -7)
jy, jm, _ := gregorianToJalali(localMax.Year(), int(localMax.Month()), localMax.Day())
monthStart := jalaliDate(jy, jm, 1, location)
nextYear, nextMonth := nextJalaliMonth(jy, jm)
monthEnd := jalaliDate(nextYear, nextMonth, 1, location)
if monthEnd.After(datasetEnd) {
jy, jm = previousJalaliMonth(jy, jm)
monthStart = jalaliDate(jy, jm, 1, location)
nextYear, nextMonth = nextJalaliMonth(jy, jm)
monthEnd = jalaliDate(nextYear, nextMonth, 1, location)
}
seasonStartMonth := ((jm-1)/3)*3 + 1
seasonYear := jy
seasonStart := jalaliDate(seasonYear, seasonStartMonth, 1, location)
seasonEndYear, seasonEndMonth := seasonYear, seasonStartMonth+3
if seasonEndMonth > 12 {
seasonEndYear++
seasonEndMonth -= 12
}
seasonEnd := jalaliDate(seasonEndYear, seasonEndMonth, 1, location)
if seasonEnd.After(datasetEnd) {
seasonStartMonth -= 3
if seasonStartMonth < 1 {
seasonYear--
seasonStartMonth += 12
}
seasonStart = jalaliDate(seasonYear, seasonStartMonth, 1, location)
seasonEndYear, seasonEndMonth = seasonYear, seasonStartMonth+3
if seasonEndMonth > 12 {
seasonEndYear++
seasonEndMonth -= 12
}
seasonEnd = jalaliDate(seasonEndYear, seasonEndMonth, 1, location)
}
dayJY, dayJM, dayJD := gregorianToJalali(dayStart.Year(), int(dayStart.Month()), dayStart.Day())
weekEndJY, weekEndJM, weekEndJD := gregorianToJalali(weekEnd.AddDate(0, 0, -1).Year(), int(weekEnd.AddDate(0, 0, -1).Month()), weekEnd.AddDate(0, 0, -1).Day())
weekStartJY, weekStartJM, weekStartJD := gregorianToJalali(weekStart.Year(), int(weekStart.Month()), weekStart.Day())
periods := []Period{
newPeriod("d-"+dayStart.Format(time.DateOnly), "DAY", dayStart, datasetEnd,
fmt.Sprintf("روز %s %s %s", persianNumber(dayJD), jalaliMonthNames[dayJM-1], persianNumber(dayJY)), false),
newPeriod("w-"+weekStart.Format(time.DateOnly), "WEEK", weekStart, weekEnd,
fmt.Sprintf("هفته %s %s تا %s %s", persianNumber(weekStartJD), jalaliMonthNames[weekStartJM-1], persianNumber(weekEndJD), jalaliMonthNames[weekEndJM-1]), false),
newPeriod(fmt.Sprintf("jm-%04d-%02d", jy, jm), "JALALI_MONTH", monthStart, monthEnd,
fmt.Sprintf("%s %s", jalaliMonthNames[jm-1], persianNumber(jy)), true),
newPeriod(fmt.Sprintf("ps-%04d-%02d", seasonYear, seasonStartMonth), "PERSIAN_SEASON", seasonStart, seasonEnd,
fmt.Sprintf("%s %s", persianSeasonNames[(seasonStartMonth-1)/3], persianNumber(seasonYear)), false),
}
for index := range periods {
periods[index].IsComplete = !periods[index].LocalStartAt.Before(datasetStartDay)
}
_ = weekEndJY
_ = weekStartJY
return periods
}
func newPeriod(id, kind string, localStart, localEnd time.Time, label string, isDefault bool) Period {
return Period{
PeriodID: id, PeriodType: kind, StartAt: localStart.UTC(), EndAt: localEnd.UTC(),
LocalStartAt: localStart, LocalEndAt: localEnd, Timezone: "Asia/Tehran",
JalaliLabelFA: label, IsComplete: true, IsDefault: isDefault,
}
}
func persianNumber(value int) string {
replacer := strings.NewReplacer("0", "۰", "1", "۱", "2", "۲", "3", "۳", "4", "۴", "5", "۵", "6", "۶", "7", "۷", "8", "۸", "9", "۹")
return replacer.Replace(fmt.Sprint(value))
}