kurdish_date 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e3abb81f51a490c36e4031b79fe07736ccdaecbd511720aeed2d5ba2aeb6219f
4
+ data.tar.gz: 706e4ace385092932d785148801aa33cef16ccae41b9027f86898582476f3e01
5
+ SHA512:
6
+ metadata.gz: 0e151ba85cc7911ead063325d308b8e583000ddfeedbaa9b5f5f48f3ca6f083b2bbaeb8f6ea02de76de449a6c1cb1f3e2eb1c217d3ed2092cf19ddf0c3fc75a1
7
+ data.tar.gz: c3467a4f83a69a9437bd0caf522100dea22754d2be70526ad757a0e11228d3660f9d9196c0efb93a020732c808ec1a32749b77615504d363df387d799f9cb07d
data/CHANGELOG.md ADDED
@@ -0,0 +1,50 @@
1
+ # Changelog
2
+
3
+ All notable changes to `kurdish_date` will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.2.0] - 2026-08-25
11
+
12
+ ### Added
13
+ - **Two scripts**: `KurdishDate.script` / `KurdishDate.script=` to pick
14
+ the default output script (`:latin` or `:sorani`).
15
+ - All month and weekday names available in both Latin (Hawar-style
16
+ transliteration) and Sorani (native Arabic-based) scripts.
17
+ - `Locale.month_name(month, script)` and
18
+ `Locale.weekday_name(weekday_index, script)` helpers.
19
+ - New `strftime` tokens: `%BB`, `%AA`, `%aa` — always render in
20
+ Sorani, regardless of the default script, so a single format string
21
+ can mix scripts (e.g. `"%A، %-d %BB %Y"`).
22
+ - `#weekday_name(script = nil)` and `#month_name(script = nil)` accept
23
+ an explicit script per call.
24
+ - `Locale` module reorganised: `MONTHS` and `WEEKDAYS` are now
25
+ hashes keyed by `:latin` / `:sorani`.
26
+ - Corrected spellings: `Şemme`, `Yekşemme`, `Duşemme`, `Seyşemme`,
27
+ `Çwarşemme`, `Pêncşemme`, `Heynî`; `Xezelwer`, `Pûşper`.
28
+
29
+ ## [0.1.0] - 2026-08-25
30
+
31
+ ### Added
32
+ - Initial release.
33
+ - `KurdishDate::KurdishDate.from_gregorian(date)` — convert a Gregorian
34
+ `Date` / `DateTime` / `Time` to a Kurdish Madhi date.
35
+ - `KurdishDate::KurdishDate.from_kurdish(year, month, day)` — build a
36
+ Kurdish date directly.
37
+ - `KurdishDate::KurdishDate.today` / `.now` — current date in the Kurdish
38
+ calendar.
39
+ - `#to_gregorian` / `#to_date` / `#to_time` / `#to_datetime` — convert
40
+ back to a Ruby date / time.
41
+ - `#strftime` with Kurdish month / weekday names, supporting
42
+ `%Y`, `%y`, `%m`, `%-m`, `%d`, `%-d`, `%B`, `%A`, `%a`.
43
+ - `KurdishDate::Locale::MONTHS` and `KurdishDate::Locale::WEEKDAYS` —
44
+ Central Kurdish (Sorani) month and weekday names.
45
+ - Borkowski-based astronomical conversion, accurate to within one day
46
+ over a window of roughly 5,000 years.
47
+ - Minitest test suite (39 tests, 61 assertions).
48
+
49
+ [Unreleased]: https://github.com/rojcode/kurdish_date/compare/v0.1.0...HEAD
50
+ [0.1.0]: https://github.com/rojcode/kurdish_date/releases/tag/v0.1.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kurdish_date contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,183 @@
1
+ # kurdish_date
2
+
3
+ A Ruby library for working with the **Kurdish (Sorani) calendar**, also
4
+ known as the Kurdish Madhi (Median) calendar.
5
+
6
+ The day-by-day structure is the same as the Solar Hijri (Jalali) calendar
7
+ used in Iran — six 31-day months, five 30-day months, and a final 29-day
8
+ month (30 in a leap year). The year numbering is the **Madhi era**, which
9
+ counts from the traditional founding of the Median kingdom by Diako
10
+ around 700 BCE. The offset from the Solar Hijri year is fixed at **+1321**,
11
+ so the Hijri-Shamsi year 1405 corresponds to the Kurdish Madhi year 2726.
12
+
13
+ The conversion algorithm is the [Borkowski approximation][borkowski] —
14
+ the same approach used by .NET `PersianCalendar`, the Java
15
+ `PersianDate` library, and `jalaali-js`. It is astronomically accurate to
16
+ within one day over a window of roughly 5,000 years.
17
+
18
+ [borkowski]: http://www.astro.uni.torun.pl/~kb/Papers/EMP/PersianC-EMP.htm
19
+
20
+ ## Installation
21
+
22
+ Add to your `Gemfile`:
23
+
24
+ ```ruby
25
+ gem "kurdish_date"
26
+ ```
27
+
28
+ Or install directly:
29
+
30
+ ```bash
31
+ gem install kurdish_date
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ```ruby
37
+ require "kurdish_date"
38
+ require "date"
39
+
40
+ # Convert a Gregorian date into the Kurdish (Madhi) calendar.
41
+ KurdishDate::KurdishDate.from_gregorian(Date.new(2024, 3, 20))
42
+ # => #<KurdishDate 2724-01-01 (Çwarşemme)>
43
+
44
+ # Convert a Kurdish date back to a Gregorian Date.
45
+ KurdishDate::KurdishDate.from_kurdish(2724, 1, 1).to_gregorian
46
+ # => #<Date: 2024-03-20 ...>
47
+
48
+ # Today, in the Kurdish calendar.
49
+ KurdishDate::KurdishDate.today
50
+ # => #<KurdishDate ...>
51
+
52
+ # Format with strftime (subset of strftime tokens).
53
+ d = KurdishDate::KurdishDate.from_kurdish(2724, 3, 9)
54
+ d.strftime("%Y-%m-%d") # => "2724-03-09"
55
+ d.strftime("%-d %B %Y") # => "9 Cozerdan 2724" (Latin, the default)
56
+ ```
57
+
58
+ ### Two scripts: Latin and Sorani
59
+
60
+ Month and weekday names are available in two scripts:
61
+
62
+ * **Latin** — Hawar-style transliteration (`Xakelêwe`, `Çwarşemme`).
63
+ * **Sorani** — the native Arabic-based Sorani script
64
+ (`خاکەلێوە`, `چوارشەممە`).
65
+
66
+ Set the default script globally:
67
+
68
+ ```ruby
69
+ KurdishDate::KurdishDate.script = :sorani
70
+
71
+ d = KurdishDate::KurdishDate.from_gregorian(Date.new(2024, 3, 20))
72
+ d.month_name # => "خاکەلێوە"
73
+ d.weekday_name # => "چوارشەممە"
74
+ d.strftime("%-d %B %Y") # => "1 خاکەلێوە 2724"
75
+ ```
76
+
77
+ Or pass the script per call:
78
+
79
+ ```ruby
80
+ d.month_name(:latin) # => "Xakelêwe"
81
+ d.month_name(:sorani) # => "خاکەلێوە"
82
+ ```
83
+
84
+ Or mix scripts in a single format string using the `BB` / `AA` / `aa`
85
+ tokens (always Sorani, regardless of the default):
86
+
87
+ ```ruby
88
+ d.strftime("%A، %-d %BB %Y") # => "چوارشەممە، 1 خاکەلێوە 2724"
89
+ ```
90
+
91
+ ## API
92
+
93
+ ### Factory methods
94
+
95
+ | Method | Description |
96
+ | --- | --- |
97
+ | `KurdishDate.from_gregorian(date)` | Build from a `Date` / `DateTime` / `Time`. |
98
+ | `KurdishDate.from_kurdish(year, month, day)` | Build from a Kurdish date. |
99
+ | `KurdishDate.today` | Today, in the Kurdish calendar. |
100
+ | `KurdishDate.now` | Now, in the Kurdish calendar. |
101
+
102
+ ### Instance methods
103
+
104
+ | Method | Description |
105
+ | --- | --- |
106
+ | `#year`, `#month`, `#day` | Kurdish Madhi year / month / day. |
107
+ | `#to_gregorian` | Convert back to a `Date`. |
108
+ | `#to_date`, `#to_time`, `#to_datetime` | Convenience conversions. |
109
+ | `#weekday_name(script = nil)` | Kurdish weekday, e.g. `"Çwarşemme"` or `"چوارشەممە"`. |
110
+ | `#month_name(script = nil)` | Kurdish month, e.g. `"Gulan"` or `"گوڵان"`. |
111
+ | `#leap?` | True if the underlying Solar Hijri year is a leap year. |
112
+ | `#month_days` | Number of days in this month. |
113
+ | `#strftime(format, script: nil)` | Format with the tokens listed below. |
114
+
115
+ ### `strftime` tokens
116
+
117
+ | Token | Meaning | Script |
118
+ | --- | --- | --- |
119
+ | `%Y` | 4-digit year | — |
120
+ | `%y` | 2-digit year | — |
121
+ | `%m` | Zero-padded month | — |
122
+ | `%-m` | Unpadded month | — |
123
+ | `%d` | Zero-padded day | — |
124
+ | `%-d` | Unpadded day | — |
125
+ | `%B` | Full month name | default script |
126
+ | `%BB` | Full month name | **Sorani** (always) |
127
+ | `%b` | Abbreviated month name | default script |
128
+ | `%A` | Full weekday name | default script |
129
+ | `%AA` | Full weekday name | **Sorani** (always) |
130
+ | `%a` | Abbreviated weekday name | default script |
131
+ | `%aa` | Abbreviated weekday name | **Sorani** (always) |
132
+
133
+ ### Calendar predicates
134
+
135
+ | Method | Description |
136
+ | --- | --- |
137
+ | `KurdishDate.leap?(solar_hijri_year)` | Leap-year check (Solar Hijri year, not Madhi). |
138
+ | `KurdishDate.month_days(solar_hijri_year, month)` | Days in the given month. |
139
+ | `KurdishDate.script` / `KurdishDate.script=` | Default script (`:latin` or `:sorani`). |
140
+
141
+ ### `Locale` module
142
+
143
+ ```ruby
144
+ KurdishDate::Locale::MONTHS[:latin] # 12 Latin month names
145
+ KurdishDate::Locale::MONTHS[:sorani] # 12 Sorani month names
146
+ KurdishDate::Locale::WEEKDAYS[:latin] # 7 Latin weekday names (Sat..Fri)
147
+ KurdishDate::Locale::WEEKDAYS[:sorani] # 7 Sorani weekday names
148
+ KurdishDate::Locale.month_name(1, :sorani) # => "خاکەلێوە"
149
+ KurdishDate::Locale.weekday_name(0, :latin) # => "Şemme"
150
+ ```
151
+
152
+ ## Kurdish month names
153
+
154
+ | # | Latin | Sorani | Persian equivalent |
155
+ | - | - | - | - |
156
+ | 1 | Xakelêwe | خاکەلێوە | Farvardin |
157
+ | 2 | Gulan | گوڵان | Ordibehesht |
158
+ | 3 | Cozerdan | جۆزەردان | Khordad |
159
+ | 4 | Pûşper | پووشپەڕ | Tir |
160
+ | 5 | Gelawêj | گەلاوێژ | Mordad |
161
+ | 6 | Xermanan | خەرمانان | Shahrivar |
162
+ | 7 | Rezber | ڕەزبەر | Mehr |
163
+ | 8 | Xezelwer | خەزەڵوەر | Aban |
164
+ | 9 | Sermawêz | سەرماوەز | Azar |
165
+ | 10 | Befranbar | بەفرانبار | Dey |
166
+ | 11 | Rêbendan | ڕێبەندان | Bahman |
167
+ | 12 | Reşemê | ڕەشەمێ | Esfand |
168
+
169
+ The Kurdish week starts on **Saturday**.
170
+
171
+ ## Development
172
+
173
+ ```bash
174
+ bundle install
175
+ rake test
176
+ ```
177
+
178
+ The test suite uses Minitest (stdlib). An RSpec suite is also included in
179
+ `spec/` for projects that prefer RSpec; install RSpec separately to use it.
180
+
181
+ ## License
182
+
183
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,86 @@
1
+ module KurdishDate
2
+ # Locale data for the Kurdish (Sorani) calendar.
3
+ #
4
+ # The Kurdish (Sorani) calendar is the same astronomical calendar as
5
+ # the Solar Hijri (Jalali) calendar used in Iran. This module holds
6
+ # the localised month and weekday names in Central Kurdish.
7
+ #
8
+ # Two scripts are provided for each entry:
9
+ #
10
+ # * `:latin` — the standard transliteration (Hawar-style) used in
11
+ # everyday Latin-script Kurdish writing.
12
+ # * `:sorani` — the native Sorani script (Arabic-based), which is the
13
+ # standard orthography in Iraqi Kurdistan and the Iranian Kurdish
14
+ # regions for daily, literary, and official use.
15
+ module Locale
16
+ # Kurdish month names. The first six months have 31 days, the next
17
+ # five have 30 days, and the 12th month has 29 (or 30 in a leap year).
18
+ MONTHS = {
19
+ latin: [
20
+ "Xakelêwe", # 1
21
+ "Gulan", # 2
22
+ "Cozerdan", # 3
23
+ "Pûşper", # 4
24
+ "Gelawêj", # 5
25
+ "Xermanan", # 6
26
+ "Rezber", # 7
27
+ "Xezelwer", # 8
28
+ "Sermawez", # 9
29
+ "Befranbar", # 10
30
+ "Rêbendan", # 11
31
+ "Reşemê" # 12
32
+ ].freeze,
33
+ sorani: [
34
+ "خاکەلێوە", # 1
35
+ "گوڵان", # 2
36
+ "جۆزەردان", # 3
37
+ "پووشپەڕ", # 4
38
+ "گەلاوێژ", # 5
39
+ "خەرمانان", # 6
40
+ "ڕەزبەر", # 7
41
+ "خەزەڵوەر", # 8
42
+ "سەرماوەز", # 9
43
+ "بەفرانبار", # 10
44
+ "ڕێبەندان", # 11
45
+ "ڕەشەمێ" # 12
46
+ ].freeze
47
+ }.freeze
48
+
49
+ # Weekday names. The Kurdish week starts on Saturday.
50
+ WEEKDAYS = {
51
+ latin: [
52
+ "Şemme", # 0 — Saturday — شەممە
53
+ "Yekşemme", # 1 — Sunday — یەکشەممە
54
+ "Duşemme", # 2 — Monday — دووشەممە
55
+ "Seyşemme", # 3 — Tuesday — سێشەممە
56
+ "Çwarşemme", # 4 — Wednesday — چوارشەممە
57
+ "Pêncşemme", # 5 — Thursday — پێنجشەممە
58
+ "Heynî" # 6 — Friday — هەینی
59
+ ].freeze,
60
+ sorani: [
61
+ "شەممە", # 0 — Saturday
62
+ "یەکشەممە", # 1 — Sunday
63
+ "دووشەممە", # 2 — Monday
64
+ "سێشەممە", # 3 — Tuesday
65
+ "چوارشەممە", # 4 — Wednesday
66
+ "پێنجشەممە", # 5 — Thursday
67
+ "هەینی" # 6 — Friday
68
+ ].freeze
69
+ }.freeze
70
+
71
+ # Default script when none is specified.
72
+ DEFAULT_SCRIPT = :latin
73
+
74
+ # Returns the month name for the given 1-based month number, in the
75
+ # requested script (:latin or :sorani).
76
+ def self.month_name(month, script = DEFAULT_SCRIPT)
77
+ MONTHS.fetch(script)[month - 1]
78
+ end
79
+
80
+ # Returns the weekday name for the given 0-based weekday index
81
+ # (Saturday = 0), in the requested script (:latin or :sorani).
82
+ def self.weekday_name(weekday_index, script = DEFAULT_SCRIPT)
83
+ WEEKDAYS.fetch(script)[weekday_index]
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,3 @@
1
+ module KurdishDate
2
+ VERSION = "0.2.0"
3
+ end
@@ -0,0 +1,272 @@
1
+ require "date"
2
+ require "kurdish_date/version"
3
+ require "kurdish_date/locale"
4
+
5
+ module KurdishDate
6
+ class Error < StandardError; end
7
+ class InvalidDateError < Error; end
8
+
9
+ # Represents a date in the Kurdish (Sorani) calendar.
10
+ #
11
+ # The Kurdish (Sorani) calendar uses the same astronomical year
12
+ # structure as the Solar Hijri (Jalali) calendar used in Iran — the
13
+ # first six months have 31 days, the next five 30 days, and the
14
+ # final month (Esme Xakêle / Reşemê / Esfand) has 29 days in a
15
+ # common year and 30 in a leap year.
16
+ #
17
+ # The year numbering is the **Madhi (Kurdish Median) era**, counting
18
+ # from the traditional founding of the Median kingdom by Diako
19
+ # around 700 BCE. The offset is fixed at +1321 years from the
20
+ # Solar Hijri (Jalali) year, so a Hijri-Shamsi year of 1405
21
+ # corresponds to the Kurdish Madhi year of 2726.
22
+ #
23
+ # The day-by-day conversion uses Borkowski's analytic approximation
24
+ # (the same algorithm used by .NET PersianCalendar, jalaali-js, and
25
+ # the Java PersianDate library), accurate to within one day over a
26
+ # window of roughly 5,000 years.
27
+ #
28
+ # Month and weekday names are available in two scripts:
29
+ #
30
+ # * `:latin` — Hawar-style Latin transliteration
31
+ # * `:sorani` — the native Arabic-based Sorani script
32
+ #
33
+ # Use {KurdishDate.script=}, {KurdishDate.script}, or the per-call
34
+ # `:script` option on the formatting methods to choose.
35
+ class KurdishDate
36
+ MONTHS = Locale::MONTHS
37
+ WEEKDAYS = Locale::WEEKDAYS
38
+
39
+ # Ruby's Date#wday: 0=Sun, 1=Mon, ..., 5=Fri, 6=Sat.
40
+ KURDISH_WEEK_START = 6 # Saturday
41
+
42
+ # Borkowski's reference constants. The Solar Hijri epoch (1 Farwardin
43
+ # 1 SH) corresponds to JDN 2,121,446.
44
+ CYCLE_DAYS = 1_029_983 # days in a 2820-year super-cycle
45
+ CYCLE_YEARS = 2_820
46
+ PERSIAN_EPOCH = 2_121_446
47
+ YEAR_LENGTH = 365.24219858156028
48
+ LEAP_THRESHOLD = 0.24219858156028
49
+
50
+ # Offset from Solar Hijri year to Kurdish Madhi (Diako) year.
51
+ MADHI_OFFSET = 1321
52
+
53
+ @script = Locale::DEFAULT_SCRIPT
54
+
55
+ class << self
56
+ # Default script (:latin or :sorani) used by formatting helpers
57
+ # when no script is passed explicitly.
58
+ attr_accessor :script
59
+ end
60
+
61
+ attr_reader :year, :month, :day
62
+
63
+ def initialize(year, month, day)
64
+ unless year.is_a?(Integer) && month.is_a?(Integer) && day.is_a?(Integer)
65
+ raise InvalidDateError, "year, month and day must be Integers"
66
+ end
67
+
68
+ unless month.between?(1, 12)
69
+ raise InvalidDateError, "month must be between 1 and 12, got #{month}"
70
+ end
71
+
72
+ # Calendar rules (leap year, month lengths) are defined on the
73
+ # Solar Hijri year; the Madhi year differs by a fixed offset.
74
+ solar_year = year - MADHI_OFFSET
75
+ max_day = self.class.month_days(solar_year, month)
76
+ unless day.between?(1, max_day)
77
+ raise InvalidDateError,
78
+ "day must be between 1 and #{max_day} for month #{month} of year #{year}, got #{day}"
79
+ end
80
+
81
+ @year = year
82
+ @month = month
83
+ @day = day
84
+ end
85
+
86
+ # ---- factory methods ------------------------------------------------
87
+
88
+ # Build a KurdishDate from a Gregorian date (Date / DateTime / Time).
89
+ def self.from_gregorian(date)
90
+ jd = date.respond_to?(:jd) ? date.jd : ::Date.parse(date.to_s).jd
91
+ jy, jm, jd2 = jalali_from_jd(jd)
92
+ new(jy + MADHI_OFFSET, jm, jd2)
93
+ end
94
+
95
+ # Build a KurdishDate from a Kurdish Madhi (year, month, day).
96
+ def self.from_kurdish(year, month, day)
97
+ new(year, month, day)
98
+ end
99
+
100
+ # Today in the Kurdish calendar.
101
+ def self.today
102
+ from_gregorian(::Date.today)
103
+ end
104
+
105
+ # Now in the Kurdish calendar.
106
+ def self.now
107
+ from_gregorian(::DateTime.now)
108
+ end
109
+
110
+ # ---- accessors ------------------------------------------------------
111
+
112
+ def to_gregorian
113
+ jd = self.class.jd_from_jalali(@year - MADHI_OFFSET, @month, @day)
114
+ ::Date.jd(jd)
115
+ end
116
+
117
+ def to_date
118
+ to_gregorian
119
+ end
120
+
121
+ def to_time
122
+ to_gregorian.to_time
123
+ end
124
+
125
+ def to_datetime
126
+ jd = self.class.jd_from_jalali(@year - MADHI_OFFSET, @month, @day)
127
+ ::DateTime.jd(jd)
128
+ end
129
+
130
+ # Returns the Kurdish weekday index, 0-based, starting at Saturday.
131
+ def weekday_index
132
+ ((to_gregorian.wday - KURDISH_WEEK_START) % 7)
133
+ end
134
+
135
+ # Full Kurdish weekday name, e.g. "Şemme" (Latin) or
136
+ # "شەممە" (Sorani). `script` defaults to the class-level setting.
137
+ def weekday_name(script = nil)
138
+ Locale.weekday_name(weekday_index, script || self.class.script)
139
+ end
140
+
141
+ # Full Kurdish month name, e.g. "Gulan" (Latin) or
142
+ # "گوڵان" (Sorani). `script` defaults to the class-level setting.
143
+ def month_name(script = nil)
144
+ Locale.month_name(@month, script || self.class.script)
145
+ end
146
+
147
+ def leap?
148
+ self.class.leap?(@year - MADHI_OFFSET)
149
+ end
150
+
151
+ def month_days
152
+ self.class.month_days(@year - MADHI_OFFSET, @month)
153
+ end
154
+
155
+ # ---- formatting -----------------------------------------------------
156
+
157
+ # Format the date. Supported tokens:
158
+ # %Y 4-digit year %y 2-digit year
159
+ # %m zero-padded month %-m unpadded month
160
+ # %d zero-padded day %-d unpadded day
161
+ # %B full month name (Latin) %BB full month name (Sorani)
162
+ # %b abbreviated month name (Latin)
163
+ # %A full weekday name (Latin) %AA full weekday name (Sorani)
164
+ # %a abbreviated weekday name (Latin) %aa abbreviated weekday name (Sorani)
165
+ #
166
+ # The single-letter tokens (%B / %A / %a / %b) use the default
167
+ # script set by `KurdishDate.script=`. The double-letter tokens
168
+ # (%BB / %AA / %aa) always use Sorani so a mixed-language format
169
+ # string is possible.
170
+ def strftime(format = "%Y-%m-%d", script: nil)
171
+ script = (script || self.class.script).to_sym
172
+ format.gsub(/%-?Y|%-?y|%-?m|%-?d|%BB|%AA|%aa|%B|%b|%A|%a/) do |token|
173
+ case token
174
+ when "%Y" then @year.to_s.rjust(4, "0")
175
+ when "%y" then (@year % 100).to_s.rjust(2, "0")
176
+ when "%m" then @month.to_s.rjust(2, "0")
177
+ when "%-m" then @month.to_s
178
+ when "%d" then @day.to_s.rjust(2, "0")
179
+ when "%-d" then @day.to_s
180
+ when "%BB" then Locale.month_name(@month, :sorani)
181
+ when "%AA" then Locale.weekday_name(weekday_index, :sorani)
182
+ when "%aa" then Locale.weekday_name(weekday_index, :sorani)[0, 3]
183
+ when "%B" then Locale.month_name(@month, script)
184
+ when "%b" then Locale.month_name(@month, script)[0, 3]
185
+ when "%A" then Locale.weekday_name(weekday_index, script)
186
+ when "%a" then Locale.weekday_name(weekday_index, script)[0, 3]
187
+ end
188
+ end
189
+ end
190
+
191
+ def to_s(format = "%Y-%m-%d", **opts)
192
+ strftime(format, **opts)
193
+ end
194
+
195
+ def inspect
196
+ "#<KurdishDate #{strftime("%Y-%m-%d")} (#{weekday_name})>"
197
+ end
198
+
199
+ def ==(other)
200
+ other.is_a?(KurdishDate) &&
201
+ year == other.year && month == other.month && day == other.day
202
+ end
203
+ alias eql? ==
204
+
205
+ def hash
206
+ [@year, @month, @day].hash
207
+ end
208
+
209
+ def to_a
210
+ [@year, @month, @day]
211
+ end
212
+ alias deconstruct to_a
213
+
214
+ # ---- calendar predicates -------------------------------------------
215
+
216
+ # Note: leap year is a property of the *solar* year, which is shared
217
+ # between the Solar Hijri and Kurdish Madhi calendars.
218
+ def self.leap?(solar_hijri_year)
219
+ ((solar_hijri_year + 2346) * LEAP_THRESHOLD) % 1 < LEAP_THRESHOLD
220
+ end
221
+
222
+ def self.month_days(solar_hijri_year, month)
223
+ return 31 if month.between?(1, 6)
224
+ return 30 if month.between?(7, 11)
225
+ leap?(solar_hijri_year) ? 30 : 29
226
+ end
227
+
228
+ # ---- internal: JDN <-> Solar-Hijri (Borkowski) ---------------------
229
+ # All internal conversions operate on Solar Hijri year numbers.
230
+ # The Madhi offset is applied at the public API boundary.
231
+
232
+ # JDN -> (Solar Hijri year, month, day). Borkowski's approximation.
233
+ def self.jalali_from_jd(jd)
234
+ jd = jd.to_i
235
+ offset = jd - PERSIAN_EPOCH
236
+ cycle_no = offset / CYCLE_DAYS
237
+ cycle_no -= 1 if offset < 0
238
+ cycle_start = PERSIAN_EPOCH + cycle_no * CYCLE_DAYS
239
+ yc = ((jd - cycle_start) / YEAR_LENGTH).floor
240
+ year = yc + 475 + cycle_no * CYCLE_YEARS
241
+ lll = PERSIAN_EPOCH + cycle_no * CYCLE_DAYS + (yc * YEAR_LENGTH).floor
242
+ day = jd - lll + 1
243
+ if day > (leap?(year) ? 366 : 365)
244
+ year += 1
245
+ day = 1
246
+ end
247
+ month = 1
248
+ d = day
249
+ while month <= 12 && d > month_days(year, month)
250
+ d -= month_days(year, month)
251
+ month += 1
252
+ end
253
+ [year, month, d]
254
+ end
255
+
256
+ # (Solar Hijri year, month, day) -> JDN. Borkowski's reverse.
257
+ def self.jd_from_jalali(year, month, day)
258
+ era = (year - 475) / CYCLE_YEARS
259
+ era -= 1 if (year - 475) < 0
260
+ y_c = (year - 475) - era * CYCLE_YEARS
261
+ first_d = PERSIAN_EPOCH + era * CYCLE_DAYS + (y_c * YEAR_LENGTH).floor
262
+ first_d + (day_of_year(year, month, day) - 1)
263
+ end
264
+
265
+ # Day-of-year in the Solar Hijri / Kurdish calendar.
266
+ def self.day_of_year(year, month, day)
267
+ sum = 0
268
+ (1...month).each { |m| sum += month_days(year, m) }
269
+ sum + day
270
+ end
271
+ end
272
+ end
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kurdish_date
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - RojCode
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-08-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '13.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '13.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.12'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.12'
55
+ description: A Ruby library for working with the Kurdish (Sorani) calendar. Provides
56
+ conversion to/from the Gregorian calendar, Kurdish month and weekday names, and
57
+ formatting helpers for Central Kurdish (Sorani).
58
+ email:
59
+ - rojcode@example.com
60
+ executables: []
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - CHANGELOG.md
65
+ - LICENSE
66
+ - README.md
67
+ - lib/kurdish_date.rb
68
+ - lib/kurdish_date/locale.rb
69
+ - lib/kurdish_date/version.rb
70
+ homepage: https://github.com/rojcode/kurdish_date
71
+ licenses:
72
+ - MIT
73
+ metadata:
74
+ homepage_uri: https://github.com/rojcode/kurdish_date
75
+ source_code_uri: https://github.com/rojcode/kurdish_date
76
+ changelog_uri: https://github.com/rojcode/kurdish_date/blob/main/CHANGELOG.md
77
+ bug_tracker_uri: https://github.com/rojcode/kurdish_date/issues
78
+ rubygems_mfa_required: 'true'
79
+ post_install_message:
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: 2.7.0
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubygems_version: 3.0.3.1
95
+ signing_key:
96
+ specification_version: 4
97
+ summary: Kurdish (Sorani) calendar — convert Gregorian dates to/from Kurdish dates.
98
+ test_files: []