Files

291 lines
9.5 KiB
Go
Raw Permalink 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
}
// JalaliMonthBounds returns the Tehran-local start and inclusive end of the
// Jalali month containing at. It is shared by deterministic business rules.
func JalaliMonthBounds(at time.Time, location *time.Location) (time.Time, time.Time) {
local := at.In(location)
year, month, _ := gregorianToJalali(local.Year(), int(local.Month()), local.Day())
start := jalaliDate(year, month, 1, location)
nextYear, nextMonth := nextJalaliMonth(year, month)
return start, jalaliDate(nextYear, nextMonth, 1, location).AddDate(0, 0, -1)
}
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
}
// PrecedingPeriodLabel returns the exact Persian label for the complete period
// immediately before start. The label follows the same vocabulary as Periods.
func PrecedingPeriodLabel(periodType string, start time.Time) (string, bool) {
location, err := time.LoadLocation("Asia/Tehran")
if err != nil {
return "", false
}
localStart := start.In(location)
switch periodType {
case "DAY":
day := localStart.AddDate(0, 0, -1)
year, month, dayOfMonth := gregorianToJalali(day.Year(), int(day.Month()), day.Day())
return fmt.Sprintf("روز %s %s %s", persianNumber(dayOfMonth), jalaliMonthNames[month-1], persianNumber(year)), true
case "WEEK":
weekStart := localStart.AddDate(0, 0, -7)
weekLastDay := localStart.AddDate(0, 0, -1)
_, startMonth, startDay := gregorianToJalali(weekStart.Year(), int(weekStart.Month()), weekStart.Day())
endYear, endMonth, endDay := gregorianToJalali(weekLastDay.Year(), int(weekLastDay.Month()), weekLastDay.Day())
return fmt.Sprintf("هفته %s %s تا %s %s %s", persianNumber(startDay), jalaliMonthNames[startMonth-1], persianNumber(endDay), jalaliMonthNames[endMonth-1], persianNumber(endYear)), true
case "JALALI_MONTH":
year, month, _ := gregorianToJalali(localStart.Year(), int(localStart.Month()), localStart.Day())
year, month = previousJalaliMonth(year, month)
return fmt.Sprintf("%s %s", jalaliMonthNames[month-1], persianNumber(year)), true
case "PERSIAN_SEASON":
year, month, _ := gregorianToJalali(localStart.Year(), int(localStart.Month()), localStart.Day())
year, month = previousPersianSeason(year, ((month-1)/3*3)+1)
return fmt.Sprintf("%s %s", persianSeasonNames[(month-1)/3], persianNumber(year)), true
default:
return "", false
}
}
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))
}