feat: expose complete Iranian reporting periods

This commit is contained in:
2026-08-20 20:47:14 +03:30
parent 630341f7be
commit f710762b90
3 changed files with 281 additions and 5 deletions

View File

@@ -0,0 +1,203 @@
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))
}