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

249 lines
7.3 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 completePeriods(datasetStart, datasetMax time.Time, location *time.Location) []Period {
if datasetMax.Before(datasetStart) {
return nil
}
localStart := datasetStart.In(location)
localMax := datasetMax.In(location)
datasetStartDay := time.Date(localStart.Year(), localStart.Month(), localStart.Day(), 0, 0, 0, 0, location)
datasetEnd := time.Date(localMax.Year(), localMax.Month(), localMax.Day(), 0, 0, 0, 0, location).AddDate(0, 0, 1)
periods := make([]Period, 0)
for dayStart := datasetEnd.AddDate(0, 0, -1); !dayStart.Before(datasetStartDay); dayStart = dayStart.AddDate(0, 0, -1) {
jy, jm, jd := gregorianToJalali(dayStart.Year(), int(dayStart.Month()), dayStart.Day())
periods = append(periods, newPeriod(
"d-"+dayStart.Format(time.DateOnly),
"DAY",
dayStart,
dayStart.AddDate(0, 0, 1),
fmt.Sprintf("روز %s %s %s", persianNumber(jd), jalaliMonthNames[jm-1], persianNumber(jy)),
false,
))
}
weekEnd := datasetEnd
for weekEnd.Weekday() != time.Saturday {
weekEnd = weekEnd.AddDate(0, 0, -1)
}
for weekStart := weekEnd.AddDate(0, 0, -7); !weekStart.Before(datasetStartDay); weekStart = weekStart.AddDate(0, 0, -7) {
weekEnd = weekStart.AddDate(0, 0, 7)
_, weekStartJM, weekStartJD := gregorianToJalali(weekStart.Year(), int(weekStart.Month()), weekStart.Day())
weekLastDay := weekEnd.AddDate(0, 0, -1)
weekEndJY, weekEndJM, weekEndJD := gregorianToJalali(weekLastDay.Year(), int(weekLastDay.Month()), weekLastDay.Day())
periods = append(periods, newPeriod(
"w-"+weekStart.Format(time.DateOnly),
"WEEK",
weekStart,
weekEnd,
fmt.Sprintf(
"هفته %s %s تا %s %s %s",
persianNumber(weekStartJD), jalaliMonthNames[weekStartJM-1],
persianNumber(weekEndJD), jalaliMonthNames[weekEndJM-1], persianNumber(weekEndJY),
),
false,
))
}
latestDay := datasetEnd.AddDate(0, 0, -1)
latestJY, latestJM, _ := gregorianToJalali(latestDay.Year(), int(latestDay.Month()), latestDay.Day())
monthYear, month := latestJY, latestJM
monthStart := jalaliDate(monthYear, month, 1, location)
nextYear, nextMonth := nextJalaliMonth(monthYear, month)
monthEnd := jalaliDate(nextYear, nextMonth, 1, location)
if monthEnd.After(datasetEnd) {
monthYear, month = previousJalaliMonth(monthYear, month)
}
for isDefault := true; ; isDefault = false {
monthStart = jalaliDate(monthYear, month, 1, location)
nextYear, nextMonth = nextJalaliMonth(monthYear, month)
monthEnd = jalaliDate(nextYear, nextMonth, 1, location)
if monthStart.Before(datasetStartDay) {
break
}
periods = append(periods, newPeriod(
fmt.Sprintf("jm-%04d-%02d", monthYear, month),
"JALALI_MONTH",
monthStart,
monthEnd,
fmt.Sprintf("%s %s", jalaliMonthNames[month-1], persianNumber(monthYear)),
isDefault,
))
monthYear, month = previousJalaliMonth(monthYear, month)
}
seasonYear := latestJY
seasonStartMonth := ((latestJM - 1) / 3 * 3) + 1
seasonStart, seasonEnd := seasonBoundaries(seasonYear, seasonStartMonth, location)
if seasonEnd.After(datasetEnd) {
seasonYear, seasonStartMonth = previousPersianSeason(seasonYear, seasonStartMonth)
}
for {
seasonStart, seasonEnd = seasonBoundaries(seasonYear, seasonStartMonth, location)
if seasonStart.Before(datasetStartDay) {
break
}
periods = append(periods, newPeriod(
fmt.Sprintf("ps-%04d-%02d", seasonYear, seasonStartMonth),
"PERSIAN_SEASON",
seasonStart,
seasonEnd,
fmt.Sprintf("%s %s", persianSeasonNames[(seasonStartMonth-1)/3], persianNumber(seasonYear)),
false,
))
seasonYear, seasonStartMonth = previousPersianSeason(seasonYear, seasonStartMonth)
}
return periods
}
func seasonBoundaries(year, startMonth int, location *time.Location) (time.Time, time.Time) {
endYear, endMonth := year, startMonth+3
if endMonth > 12 {
endYear++
endMonth -= 12
}
return jalaliDate(year, startMonth, 1, location), jalaliDate(endYear, endMonth, 1, location)
}
func previousPersianSeason(year, startMonth int) (int, int) {
startMonth -= 3
if startMonth < 1 {
return year - 1, startMonth + 12
}
return year, startMonth
}
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))
}