- Fix Jalali/Gregorian/Hijri day-of-week startOffset formulas (dow returns 0=Sat, not 0=Sun as the old comment claimed) - Fix calendar and export month picker navigation jumping 2 months (navMonth buttons already have pre-computed values; handlers were decrementing/incrementing them again) - Fix GetOrCreateUser/GetOrCreateDay race condition with INSERT OR IGNORE - Add month/day bounds validation in handleEditMsg to prevent panic
343 lines
8.1 KiB
Go
343 lines
8.1 KiB
Go
package bot
|
|
|
|
import "fmt"
|
|
|
|
var gregMonthDays = []int{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
|
|
var jalaliMonthDays = []int{31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29}
|
|
|
|
var gregMonthNames = []string{
|
|
"January", "February", "March", "April", "May", "June",
|
|
"July", "August", "September", "October", "November", "December",
|
|
}
|
|
var jalaliMonthNames = []string{
|
|
"Farvardin", "Ordibehesht", "Khordad", "Tir", "Mordad", "Shahrivar",
|
|
"Mehr", "Aban", "Azar", "Dey", "Bahman", "Esfand",
|
|
}
|
|
var hijriMonthNames = []string{
|
|
"Muharram", "Safar", "Rabi' I", "Rabi' II", "Jumada I", "Jumada II",
|
|
"Rajab", "Sha'ban", "Ramadan", "Shawwal", "Dhu al-Qa'dah", "Dhu al-Hijjah",
|
|
}
|
|
|
|
func isGregorianLeap(year int) bool {
|
|
return year%4 == 0 && (year%100 != 0 || year%400 == 0)
|
|
}
|
|
|
|
func isJalaliLeap(year int) bool {
|
|
r := year % 33
|
|
return r == 1 || r == 5 || r == 9 || r == 13 || r == 17 || r == 22 || r == 26 || r == 30
|
|
}
|
|
|
|
func monthLenGreg(year, month int) int {
|
|
if month == 2 && isGregorianLeap(year) {
|
|
return 29
|
|
}
|
|
return gregMonthDays[month-1]
|
|
}
|
|
|
|
func monthLenJalali(year, month int) int {
|
|
if month == 12 && isJalaliLeap(year) {
|
|
return 30
|
|
}
|
|
return jalaliMonthDays[month-1]
|
|
}
|
|
|
|
func isHijriLeap(year int) bool {
|
|
switch year % 30 {
|
|
case 2, 5, 7, 10, 13, 16, 18, 21, 24, 27, 29:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func monthLenHijri(year, month int) int {
|
|
if month == 12 && isHijriLeap(year) {
|
|
return 30
|
|
}
|
|
if month%2 == 1 {
|
|
return 30
|
|
}
|
|
return 29
|
|
}
|
|
|
|
// JDN for March 21, 622 CE (Jalali epoch: 1 Farvardin 1)
|
|
const jalaliEpochJDN = 1948320
|
|
|
|
func gregorianToJDN(gy, gm, gd int) int {
|
|
a := (14 - gm) / 12
|
|
y := gy + 4800 - a
|
|
m := gm + 12*a - 3
|
|
return gd + (153*m+2)/5 + 365*y + y/4 - y/100 + y/400 - 32045
|
|
}
|
|
|
|
func jdnToGregorian(jdn int) (int, int, int) {
|
|
f := jdn + 1401 + ((4*jdn+274277)/146097)*3/4 - 38
|
|
e := 4*f + 3
|
|
g := (e % 1461) / 4
|
|
h := 5*g + 2
|
|
day := (h%153)/5 + 1
|
|
month := ((h/153 + 2) % 12) + 1
|
|
year := e/1461 - 4716 + (14-month)/12
|
|
return year, month, day
|
|
}
|
|
|
|
func gregorianToJalali(gy, gm, gd int) (int, int, int) {
|
|
jdn := gregorianToJDN(gy, gm, gd)
|
|
days := jdn - jalaliEpochJDN
|
|
if days < 0 {
|
|
return 0, 0, 0
|
|
}
|
|
|
|
jy := 1
|
|
for {
|
|
yearDays := 365
|
|
if isJalaliLeap(jy) {
|
|
yearDays = 366
|
|
}
|
|
if days < yearDays {
|
|
break
|
|
}
|
|
days -= yearDays
|
|
jy++
|
|
}
|
|
|
|
jm := 1
|
|
for mm := 0; mm < 12; mm++ {
|
|
md := monthLenJalali(jy, mm+1)
|
|
if days < md {
|
|
break
|
|
}
|
|
days -= md
|
|
jm++
|
|
}
|
|
jd := days + 1
|
|
return jy, jm, jd
|
|
}
|
|
|
|
func jalaliToGregorian(jy, jm, jd int) (int, int, int) {
|
|
days := 0
|
|
for y := 1; y < jy; y++ {
|
|
if isJalaliLeap(y) {
|
|
days += 366
|
|
} else {
|
|
days += 365
|
|
}
|
|
}
|
|
for m := 1; m < jm; m++ {
|
|
days += monthLenJalali(jy, m)
|
|
}
|
|
days += jd - 1
|
|
jdn := jalaliEpochJDN + days
|
|
return jdnToGregorian(jdn)
|
|
}
|
|
|
|
func hijriToGregorian(hy, hm, hd int) (int, int, int) {
|
|
days := (hy-1)*354 + (hy-1)*11/30
|
|
for m := 1; m < hm; m++ {
|
|
days += monthLenHijri(hy, m)
|
|
}
|
|
days += hd - 1
|
|
|
|
jdn := 1948440 + days
|
|
|
|
f := jdn + 1401 + ((4*jdn+274277)/146097)*3/4 - 38
|
|
e := 4*f + 3
|
|
g := (e % 1461) / 4
|
|
h := 5*g + 2
|
|
day := (h%153)/5 + 1
|
|
month := ((h/153 + 2) % 12) + 1
|
|
year := e/1461 - 4716 + (14-month)/12
|
|
return year, month, day
|
|
}
|
|
|
|
func gregorianToHijri(gy, gm, gd int) (int, int, int) {
|
|
jdn := gregorianToJDN(gy, gm, gd)
|
|
days := jdn - 1948440
|
|
|
|
hy := 1
|
|
for {
|
|
yearDays := 354
|
|
if isHijriLeap(hy) {
|
|
yearDays = 355
|
|
}
|
|
if days < yearDays {
|
|
break
|
|
}
|
|
days -= yearDays
|
|
hy++
|
|
}
|
|
|
|
hm := 1
|
|
for mm := 1; mm <= 12; mm++ {
|
|
md := monthLenHijri(hy, mm)
|
|
if days < md {
|
|
break
|
|
}
|
|
days -= md
|
|
hm++
|
|
}
|
|
hd := days + 1
|
|
return hy, hm, hd
|
|
}
|
|
|
|
func dayOfWeekGregorian(gy, gm, gd int) int {
|
|
// Zeller-like: returns 0=Sat,1=Sun,2=Mon,3=Tue,4=Wed,5=Thu,6=Fri
|
|
if gm < 3 {
|
|
gm += 12
|
|
gy--
|
|
}
|
|
return (gd + (13*(gm+1))/5 + gy + gy/4 - gy/100 + gy/400) % 7
|
|
}
|
|
|
|
type calDay struct {
|
|
dayNum int
|
|
date string // YYYY-MM-DD gregorian key
|
|
}
|
|
|
|
type calMonth struct {
|
|
title string
|
|
weekDays []string
|
|
days []calDay
|
|
}
|
|
|
|
func buildCalendarMonth(cal string, year, month int) calMonth {
|
|
var title string
|
|
var weekDays []string
|
|
var totalDays int
|
|
|
|
if cal == "jalali" {
|
|
title = fmt.Sprintf("%s %d", jalaliMonthNames[month-1], year)
|
|
weekDays = []string{"Sh", "Ye", "Do", "Se", "Ch", "Pa", "Jo"}
|
|
totalDays = monthLenJalali(year, month)
|
|
|
|
gy, gm, gd := jalaliToGregorian(year, month, 1)
|
|
dow := dayOfWeekGregorian(gy, gm, gd)
|
|
startOffset := dow
|
|
|
|
var days []calDay
|
|
for i := 0; i < startOffset; i++ {
|
|
days = append(days, calDay{dayNum: 0, date: ""})
|
|
}
|
|
for d := 1; d <= totalDays; d++ {
|
|
gy, gm, gd = jalaliToGregorian(year, month, d)
|
|
days = append(days, calDay{
|
|
dayNum: d,
|
|
date: fmt.Sprintf("%04d-%02d-%02d", gy, gm, gd),
|
|
})
|
|
}
|
|
return calMonth{title: title, weekDays: weekDays, days: days}
|
|
}
|
|
|
|
if cal == "hijri" {
|
|
title = fmt.Sprintf("%s %d", hijriMonthNames[month-1], year)
|
|
weekDays = []string{"Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"}
|
|
totalDays = monthLenHijri(year, month)
|
|
|
|
gy, gm, gd := hijriToGregorian(year, month, 1)
|
|
dow := dayOfWeekGregorian(gy, gm, gd)
|
|
startOffset := (dow + 5) % 7
|
|
|
|
var days []calDay
|
|
for i := 0; i < startOffset; i++ {
|
|
days = append(days, calDay{dayNum: 0, date: ""})
|
|
}
|
|
for d := 1; d <= totalDays; d++ {
|
|
gy, gm, gd = hijriToGregorian(year, month, d)
|
|
days = append(days, calDay{
|
|
dayNum: d,
|
|
date: fmt.Sprintf("%04d-%02d-%02d", gy, gm, gd),
|
|
})
|
|
}
|
|
return calMonth{title: title, weekDays: weekDays, days: days}
|
|
}
|
|
|
|
title = fmt.Sprintf("%s %d", gregMonthNames[month-1], year)
|
|
weekDays = []string{"Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"}
|
|
totalDays = monthLenGreg(year, month)
|
|
|
|
dow := dayOfWeekGregorian(year, month, 1)
|
|
startOffset := (dow + 5) % 7
|
|
|
|
var days []calDay
|
|
for i := 0; i < startOffset; i++ {
|
|
days = append(days, calDay{dayNum: 0, date: ""})
|
|
}
|
|
for d := 1; d <= totalDays; d++ {
|
|
days = append(days, calDay{
|
|
dayNum: d,
|
|
date: fmt.Sprintf("%04d-%02d-%02d", year, month, d),
|
|
})
|
|
}
|
|
return calMonth{title: title, weekDays: weekDays, days: days}
|
|
}
|
|
|
|
func parseGregorianDateKey(key string) (int, int, int) {
|
|
var y, m, d int
|
|
fmt.Sscanf(key, "%04d-%02d-%02d", &y, &m, &d)
|
|
return y, m, d
|
|
}
|
|
|
|
func formatDateForCalendar(date, calType string) string {
|
|
y, m, d := parseGregorianDateKey(date)
|
|
switch calType {
|
|
case "jalali":
|
|
jy, jm, jd := gregorianToJalali(y, m, d)
|
|
return fmt.Sprintf("%04d-%02d-%02d", jy, jm, jd)
|
|
case "hijri":
|
|
hy, hm, hd := gregorianToHijri(y, m, d)
|
|
return fmt.Sprintf("%04d-%02d-%02d", hy, hm, hd)
|
|
default:
|
|
return date
|
|
}
|
|
}
|
|
|
|
// userDateToGregorian converts a YYYY-MM-DD string in the user's calendar
|
|
// to the corresponding Gregorian YYYY-MM-DD string.
|
|
func userDateToGregorian(dateStr, cal string) string {
|
|
y, m, d := parseGregorianDateKey(dateStr)
|
|
switch cal {
|
|
case "jalali":
|
|
y, m, d = jalaliToGregorian(y, m, d)
|
|
case "hijri":
|
|
y, m, d = hijriToGregorian(y, m, d)
|
|
}
|
|
return fmt.Sprintf("%04d-%02d-%02d", y, m, d)
|
|
}
|
|
|
|
// userYearMonthToGregorian converts a (year, month) in the user's calendar
|
|
// to the corresponding Gregorian (year, month). The day is assumed to be 1.
|
|
func userYearMonthToGregorian(year, month int, cal string) (int, int) {
|
|
switch cal {
|
|
case "jalali":
|
|
gy, gm, _ := jalaliToGregorian(year, month, 1)
|
|
return gy, gm
|
|
case "hijri":
|
|
gy, gm, _ := hijriToGregorian(year, month, 1)
|
|
return gy, gm
|
|
}
|
|
return year, month
|
|
}
|
|
|
|
// monthGregorianRange returns the Gregorian start and end date strings
|
|
// for a given calendar month (cal=gregorian|jalali|hijri).
|
|
func monthGregorianRange(cal string, year, month int) (start, end string) {
|
|
switch cal {
|
|
case "jalali":
|
|
gy, gm, gd := jalaliToGregorian(year, month, 1)
|
|
start = fmt.Sprintf("%04d-%02d-%02d", gy, gm, gd)
|
|
lastDay := monthLenJalali(year, month)
|
|
gy, gm, gd = jalaliToGregorian(year, month, lastDay)
|
|
end = fmt.Sprintf("%04d-%02d-%02d", gy, gm, gd)
|
|
case "hijri":
|
|
gy, gm, gd := hijriToGregorian(year, month, 1)
|
|
start = fmt.Sprintf("%04d-%02d-%02d", gy, gm, gd)
|
|
lastDay := monthLenHijri(year, month)
|
|
gy, gm, gd = hijriToGregorian(year, month, lastDay)
|
|
end = fmt.Sprintf("%04d-%02d-%02d", gy, gm, gd)
|
|
default:
|
|
start = fmt.Sprintf("%04d-%02d-%02d", year, month, 1)
|
|
lastDay := monthLenGreg(year, month)
|
|
end = fmt.Sprintf("%04d-%02d-%02d", year, month, lastDay)
|
|
}
|
|
return
|
|
}
|