Files
worktimeBot/internal/bot/dateutil.go
db123 9d123316e6 feat: Hijri calendar, virtual midnight crossover, overlap rejection, WAL mode
- Hijri (قمری) calendar added as 3rd calendar option with tabular conversion
- Calendar type select menu (like accent) with gregorian/jalali/hijri
- Calendar type applied to exports and reports
- Midnight crossover: virtual splits at computation time (no synthetic DB events)
  - Handles both UTC and local timezone boundaries
  - Timezone changes handled naturally (recomputed on-the-fly)
- Rate limiting per user ID instead of chat ID
- WAL mode enabled for SQLite
- Smart reply keyboard updates after every state change
- Overlapping time edits rejected (must preserve in/out alternation)
- Single event deletion warns if timeline needs repair
- Day off no longer prevents clock in/out
- Accent colors shown with descriptive names in select menu
- Delete button label changed to DEL for visual distinction
2026-06-24 09:39:15 +03:30

287 lines
6.0 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
}
func gregorianToJalali(gy, gm, gd int) (int, int, int) {
gdm := []int{0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}
gy2 := gy - 1
gd2 := gdm[gm-1] + gd
if gm > 2 && isGregorianLeap(gy) {
gd2++
}
gd2 += gy2*365 + gy2/4 - gy2/100 + gy2/400
gd2 -= 226899
jy := 979
for {
jyDays := 365
if isJalaliLeap(jy) {
jyDays = 366
}
if gd2 < jyDays {
break
}
gd2 -= jyDays
jy++
}
jm := 1
for _, d := range jalaliMonthDays {
if gd2 < d {
break
}
gd2 -= d
jm++
}
return jy, jm, gd2 + 1
}
func jalaliToGregorian(jy, jm, jd int) (int, int, int) {
jy2 := jy - 979
days := jy2 * 365
for y := 979; y < jy; y++ {
if isJalaliLeap(y) {
days++
}
}
jdm := []int{0, 31, 62, 93, 124, 155, 186, 216, 246, 276, 306, 336}
days += jdm[jm-1] + jd - 1
days += 226899
gy := 1
for {
gyDays := 365
if isGregorianLeap(gy) {
gyDays = 366
}
if days < gyDays {
break
}
days -= gyDays
gy++
}
gm := 1
for i, d := range gregMonthDays {
if gm == 2 && isGregorianLeap(gy) {
d = 29
}
if days < d {
break
}
days -= d
gm = i + 2
}
return gy, gm, days + 1
}
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) {
a := (14 - gm) / 12
y := gy + 4800 - a
m := gm + 12*a - 3
jdn := gd + (153*m+2)/5 + 365*y + y/4 - y/100 + y/400 - 32045
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=Sun,1=Mon,...,6=Sat
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)
_ = dow
var days []calDay
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 + 6) % 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)
// Convert Sunday=0 to Monday=0 alignment
startOffset := (dow + 6) % 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 gregorianDateKey(year, month, day int) string {
return fmt.Sprintf("%04d-%02d-%02d", year, month, day)
}
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
}