249 lines
7.3 KiB
Go
249 lines
7.3 KiB
Go
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)
|
||
weekStartJY, 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 %s",
|
||
persianNumber(weekStartJD), jalaliMonthNames[weekStartJM-1], persianNumber(weekStartJY),
|
||
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))
|
||
}
|