Skip to content

Commit 0bf0a91

Browse files
committed
Refactor date conversion logic and update tests
Replaces previous Solar Hijri conversion logic with a more accurate algorithm, introducing new helper functions and error handling. Updates Kurdish calendar test cases to match corrected conversion results and fixes a typo in Sorani month names. Adds a new main.go example for converting Gregorian dates to Kurdish calendar dates.
1 parent 80d31a6 commit 0bf0a91

4 files changed

Lines changed: 220 additions & 99 deletions

File tree

cmd/main.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
"github.com/rojcode/kurdical"
8+
)
9+
10+
func main() {
11+
// Convert 24 Dec 2025 to Kurdish
12+
date := time.Date(2025, 10, 11, 0, 0, 0, 0, time.UTC)
13+
kurdish := kurdical.GregorianToKurdish(date, kurdical.Sorani, kurdical.MedianKingdom)
14+
15+
fmt.Printf("Gregorian: %s\n", date.Format("2006-01-02"))
16+
fmt.Printf("Kurdish (Median Kingdom, Sorani): %d-%d-%d %s\n", kurdish.Year, kurdish.Month, kurdish.Day, kurdish.MonthName)
17+
18+
// Also show Fall of Nineveh epoch
19+
kurdish2 := kurdical.GregorianToKurdish(date, kurdical.Kurmanji, kurdical.FallOfNineveh)
20+
fmt.Printf("Kurdish (Fall of Nineveh, Kurmanji): %d-%d-%d %s\n", kurdish2.Year, kurdish2.Month, kurdish2.Day, kurdish2.MonthName)
21+
}

conversions.go

Lines changed: 162 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,182 @@
11
package kurdical
22

3-
// julianDayNumber calculates the Julian Day Number for a Gregorian date.
4-
func julianDayNumber(year, month, day int) int {
5-
a := (14 - month) / 12
6-
y := year + 4800 - a
7-
m := month + 12*a - 3
8-
return day + (153*m+2)/5 + 365*y + y/4 - y/100 + y/400 - 32045
3+
import (
4+
"fmt"
5+
"time"
6+
)
7+
8+
var (
9+
breaks = [...]int{-61, 9, 38, 199, 426, 686, 756, 818, 1111, 1181, 1210,
10+
1635, 2060, 2097, 2192, 2262, 2324, 2394, 2456, 3178}
11+
)
12+
13+
// gregorianToSolarHijri converts Gregorian date to Solar Hijri.
14+
func gregorianToSolarHijri(gYear, gMonth, gDay int) (sYear, sMonth, sDay int) {
15+
jy, jm, jd, err := toJalaali(gYear, time.Month(gMonth), gDay)
16+
if err != nil {
17+
// For invalid dates, return 0
18+
return 0, 0, 0
19+
}
20+
return jy, int(jm), jd
921
}
1022

11-
// jdnToGregorian converts Julian Day Number to Gregorian date.
12-
func jdnToGregorian(jdn int) (year, month, day int) {
13-
l := jdn + 68569
14-
n := (4 * l) / 146097
15-
l = l - (146097*n+3)/4
16-
i := (4000 * (l + 1)) / 1461001
17-
l = l - (1461*i)/4 + 31
18-
j := (80 * l) / 2447
19-
day = l - (2447*j)/80
20-
l = j / 11
21-
month = j + 2 - 12*l
22-
year = 100*(n-49) + i + l
23-
return
23+
// solarHijriToGregorian converts Solar Hijri date to Gregorian.
24+
func solarHijriToGregorian(sYear, sMonth, sDay int) (gYear, gMonth, gDay int) {
25+
gy, gm, gd, err := toGregorian(sYear, Month(sMonth), sDay)
26+
if err != nil {
27+
return 0, 0, 0
28+
}
29+
return gy, int(gm), gd
2430
}
2531

26-
// isSolarHijriLeap determines if a Solar Hijri year is leap.
27-
func isSolarHijriLeap(year int) bool {
28-
return (year+1)%4 == 0
32+
// Month represents a month of the year.
33+
type Month int
34+
35+
const (
36+
Farvardin Month = 1 + iota
37+
Ordibehesht
38+
Khordad
39+
Tir
40+
Mordad
41+
Shahrivar
42+
Mehr
43+
Aban
44+
Azar
45+
Dey
46+
Bahman
47+
Esfand
48+
)
49+
50+
// toJalaali converts Gregorian to Jalaali date.
51+
func toJalaali(gregorianYear int, gregorianMonth time.Month, gregorianDay int) (int, Month, int, error) {
52+
jy, jm, jd, err := d2j(g2d(gregorianYear, int(gregorianMonth), gregorianDay))
53+
return jy, Month(jm), jd, err
2954
}
3055

31-
// gregorianToSolarHijri converts Gregorian date to Solar Hijri.
32-
func gregorianToSolarHijri(gYear, gMonth, gDay int) (sYear, sMonth, sDay int) {
33-
gJDN := julianDayNumber(gYear, gMonth, gDay)
34-
sJDN := gJDN - 1948087
35-
sYear = 1
36-
for {
37-
daysInYear := 365
38-
if isSolarHijriLeap(sYear) {
39-
daysInYear = 366
56+
// toGregorian converts Jalaali to Gregorian date.
57+
func toGregorian(jalaaliYear int, jalaaliMonth Month, jalaaliDay int) (int, time.Month, int, error) {
58+
jdn, err := j2d(jalaaliYear, int(jalaaliMonth), jalaaliDay)
59+
if err != nil {
60+
return 0, 0, 0, err
61+
}
62+
gy, gm, gd := d2g(jdn)
63+
return gy, time.Month(gm), gd, nil
64+
}
65+
66+
func j2d(jy, jm, jd int) (jdn int, err error) {
67+
_, gy, march, err := jalCal(jy)
68+
if err != nil {
69+
return 0, err
70+
}
71+
return g2d(gy, 3, march) + (jm-1)*31 - div(jm, 7)*(jm-7) + jd - 1, nil
72+
}
73+
74+
func d2j(jdn int) (int, int, int, error) {
75+
gy, _, _ := d2g(jdn)
76+
jy := gy - 621
77+
leap, _, march, err := jalCal(jy)
78+
jdn1f := g2d(gy, 3, march)
79+
80+
if err != nil {
81+
return 0, 0, 0, err
82+
}
83+
84+
k := jdn - jdn1f
85+
if k >= 0 {
86+
if k <= 185 {
87+
jm := 1 + div(k, 31)
88+
jd := mod(k, 31) + 1
89+
return jy, jm, jd, nil
4090
}
41-
if sJDN <= daysInYear {
42-
break
91+
k -= 186
92+
} else {
93+
jy--
94+
k += 179
95+
if leap == 1 {
96+
k++
4397
}
44-
sJDN -= daysInYear
45-
sYear++
4698
}
47-
monthDays := []int{31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29}
48-
if isSolarHijriLeap(sYear) {
49-
monthDays[11] = 30
99+
jm := 7 + div(k, 30)
100+
jd := mod(k, 30) + 1
101+
return jy, jm, jd, nil
102+
}
103+
104+
func jalCal(jy int) (int, int, int, error) {
105+
bl, gy, leapJ, jp := len(breaks), jy+621, -14, breaks[0]
106+
jump := 0
107+
108+
if jy < jp || jy >= breaks[bl-1] {
109+
return 0, 0, 0, &ErrorInvalidYear{jy}
50110
}
51-
sMonth = 1
52-
for sMonth <= 12 {
53-
if sJDN <= monthDays[sMonth-1] {
54-
sDay = sJDN
111+
112+
for i := 1; i < bl; i++ {
113+
jm := breaks[i]
114+
jump = jm - jp
115+
if jy < jm {
55116
break
56117
}
57-
sJDN -= monthDays[sMonth-1]
58-
sMonth++
118+
leapJ += div(jump, 33)*8 + div(mod(jump, 33), 4)
119+
jp = jm
59120
}
60-
return
61-
}
121+
n := jy - jp
62122

63-
// solarHijriToGregorian converts Solar Hijri date to Gregorian.
64-
func solarHijriToGregorian(sYear, sMonth, sDay int) (gYear, gMonth, gDay int) {
65-
days := 0
66-
for y := 1; y < sYear; y++ {
67-
days += 365
68-
if isSolarHijriLeap(y) {
69-
days++
70-
}
123+
leapJ += div(n, 33)*8 + div(mod(n, 33)+3, 4)
124+
if mod(jump, 33) == 4 && jump-n == 4 {
125+
leapJ++
71126
}
72-
monthDays := []int{31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29}
73-
if isSolarHijriLeap(sYear) {
74-
monthDays[11] = 30
127+
128+
leapG := div(gy, 4) - div((div(gy, 100)+1)*3, 4) - 150
129+
130+
march := 20 + leapJ - leapG
131+
132+
if jump-n < 6 {
133+
n -= jump + div(jump+4, 33)*33
75134
}
76-
for m := 1; m < sMonth; m++ {
77-
days += monthDays[m-1]
135+
leap := mod(mod(n+1, 33)-1, 4)
136+
if leap == -1 {
137+
leap = 4
78138
}
79-
days += sDay - 1
80-
jdn := 1948087 + days
81-
gYear, gMonth, gDay = jdnToGregorian(jdn)
82-
return
139+
140+
return leap, gy, march, nil
141+
}
142+
143+
func g2d(gy, gm, gd int) int {
144+
d := div((gy+div(gm-8, 6)+100100)*1461, 4) +
145+
div(153*mod(gm+9, 12)+2, 5) +
146+
gd - 34840408
147+
d = d - div(div(gy+100100+div(gm-8, 6), 100)*3, 4) + 752
148+
return d
149+
}
150+
151+
func d2g(jdn int) (int, int, int) {
152+
j := 4*jdn + 139361631
153+
j = j + div(div(4*jdn+183187720, 146097)*3, 4)*4 - 3908
154+
i := div(mod(j, 1461), 4)*5 + 308
155+
gd := div(mod(i, 153), 5) + 1
156+
gm := mod(div(i, 153), 12) + 1
157+
gy := div(j, 1461) - 100100 + div(8-gm, 6)
158+
return gy, gm, gd
159+
}
160+
161+
func div(a, b int) int {
162+
return a / b
163+
}
164+
165+
func mod(a, b int) int {
166+
return a % b
167+
}
168+
169+
// ErrorInvalidYear represents an error for invalid year.
170+
type ErrorInvalidYear struct {
171+
Year int
172+
}
173+
174+
func (e *ErrorInvalidYear) Error() string {
175+
return fmt.Sprintf("invalid year: %d", e.Year)
176+
}
177+
178+
// isSolarHijriLeap determines if a Solar Hijri year is leap.
179+
func isSolarHijriLeap(year int) bool {
180+
leap, _, _, err := jalCal(year)
181+
return err == nil && leap == 1
83182
}

dialects.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ var monthNames = map[Dialect][]string{
4949
"گولان",
5050
"زه‌ردان",
5151
"په‌رپه‌ر",
52-
"گ‌لاویژ",
52+
"گەلاویژ",
5353
"نوخشان",
5454
"به‌ران",
5555
"خه‌زان",

0 commit comments

Comments
 (0)