jalalix 0.1.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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE +21 -0
- data/README.md +52 -0
- data/lib/jalalix/jalalix.rb +795 -0
- data/lib/jalalix/version.rb +5 -0
- data/lib/jalalix.rb +6 -0
- metadata +47 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c9eb41b1bcee603c51d99a5e359d971ca3024fee1f6e451040c8fe19af8a54f9
|
|
4
|
+
data.tar.gz: 50db7117c64b1899dcca2ad8f2804ee7719e8c31bf5eab8e1b040194bd75c0ab
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b3de3e6f9ada868708fdff2417c9715aa9731c67677ae43d792da41cffbc902c1188848836b7ca339bdb351eb5c481a4154eaeeb22305b2d2d64e8d4deb1977f
|
|
7
|
+
data.tar.gz: a25f3c0549d4a09d579cabbaaa33b0fc4469ecb1597da8d7d69673a9048ccdbcf576488a4157784f68a38b5534c49edd171581223e23b5c4a15b77037c5ee6dc
|
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) Hossein Maklavani
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# JalaliX
|
|
2
|
+
|
|
3
|
+
Jalali (Persian / Shamsi) calendar for Ruby. Pure Ruby 3.0+, no runtime gems (timezone via stdlib `ENV["TZ"]` / zoneinfo, with embedded offsets for `UTC` and `Asia/Tehran`).
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
gem install jalalix
|
|
7
|
+
# or: bundle add jalalix
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
require "jalalix"
|
|
12
|
+
|
|
13
|
+
date = JalaliX.create("2025-02-25")
|
|
14
|
+
|
|
15
|
+
date.format("YYYY/MM/DD") # 1403/12/07
|
|
16
|
+
date.format("DD MMM YYYY", persianDigits: true) # ۰۷ اسفند ۱۴۰۳
|
|
17
|
+
|
|
18
|
+
JalaliX.parseJalali("1403/12/07").toISODate # 2025-02-25
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`toISODate` uses wall-calendar getters in the active timezone.
|
|
22
|
+
|
|
23
|
+
## Months
|
|
24
|
+
|
|
25
|
+
- `getMonth()` -> 1..12 (Farvardin = 1)
|
|
26
|
+
- `setMonth(m)` / `setFullYear(y, m)` -> 0..11, same as JS `Date` (Farvardin = 0)
|
|
27
|
+
|
|
28
|
+
## Timezone
|
|
29
|
+
|
|
30
|
+
Internal storage is a **UTC Unix millisecond instant** plus an IANA timezone id. `setTimezone` is **instant-preserving**.
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
d = JalaliX.create(1_740_430_800_000)
|
|
34
|
+
tehran = d.setTimezone("Asia/Tehran")
|
|
35
|
+
tehran.format("YYYY/MM/DD") # 1403/12/07
|
|
36
|
+
tehran.format("HH:mm:ss") # 00:30:00
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- **Aliases:** `utc` / `gmt` -> `UTC`; `local` / `default` / `system` -> host zone when detectable.
|
|
40
|
+
- **Invalid zone** -> `ArgumentError`.
|
|
41
|
+
- Offsets use system zoneinfo through `ENV["TZ"]` when available; `Asia/Tehran` (+03:30 since 2022) and `UTC` are also embedded so golden parity works without TZInfo.
|
|
42
|
+
|
|
43
|
+
## Test
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
rake test
|
|
47
|
+
# or: ruby -Ilib:test test/test_golden.rb
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## License
|
|
51
|
+
|
|
52
|
+
MIT
|
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require "date"
|
|
5
|
+
|
|
6
|
+
# JalaliX — Jalali (Persian / Shamsi) calendar for Ruby.
|
|
7
|
+
# Pure Ruby port of @jalalix/typescript — same algorithm and camelCase API.
|
|
8
|
+
# Timezone: system zoneinfo via ENV["TZ"] when available; embedded fixed
|
|
9
|
+
# offsets for golden zones (UTC, Asia/Tehran +03:30 since 2022) as fallback.
|
|
10
|
+
class JalaliX
|
|
11
|
+
PERSIAN_NUMBERS = %w[۰ ۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹].freeze
|
|
12
|
+
|
|
13
|
+
# Fixed offsets (seconds east of UTC) for zones needed without zoneinfo.
|
|
14
|
+
# Asia/Tehran abolished DST in 2022 -> permanent +03:30.
|
|
15
|
+
EMBEDDED_OFFSETS = {
|
|
16
|
+
"UTC" => 0,
|
|
17
|
+
"Etc/UTC" => 0,
|
|
18
|
+
"GMT" => 0,
|
|
19
|
+
"Asia/Tehran" => 3 * 3600 + 30 * 60,
|
|
20
|
+
"Asia/Tokyo" => 9 * 3600
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
WallParts = Struct.new(:year, :month, :day, :hour, :minute, :second, :ms, keyword_init: true)
|
|
24
|
+
|
|
25
|
+
attr_reader :_ms, :_timezone
|
|
26
|
+
|
|
27
|
+
def initialize(ms = nil)
|
|
28
|
+
@_ms = ms.nil? ? (Time.now.to_f * 1000).to_i : ms.to_i
|
|
29
|
+
@_timezone = "UTC"
|
|
30
|
+
@_jalali = {
|
|
31
|
+
year: 1348, month: 10, day: 11,
|
|
32
|
+
hours: 0, min: 0, sec: 0, ms: 0, isLeapYear: false
|
|
33
|
+
}
|
|
34
|
+
@_is_converted = false
|
|
35
|
+
@_leap_year_division = 0.24219858156
|
|
36
|
+
@_first_day_of_week = 6
|
|
37
|
+
@_date_length = 86_400_000
|
|
38
|
+
@_week_days_names = %w[شنبه یکشنبه دوشنبه سه\ شنبه چهار\ شنبه پنج\ شنبه جمعه]
|
|
39
|
+
@_week_days_letters = %w[ش ی د س چ پ ج]
|
|
40
|
+
@_week_days_gregorian_names = %w[Sunday Monday Tuesday Wednesday Thursday Friday Saturday]
|
|
41
|
+
@_week_days_gregorian_letters = %w[S M T W T F S]
|
|
42
|
+
@_months_names = %w[فروردین اردیبهشت خرداد تیر مرداد شهریور مهر آبان آذر دی بهمن اسفند]
|
|
43
|
+
@_months_gregorian_names = %w[January February March April May June July August September October November December]
|
|
44
|
+
@_am = "ق.ظ."
|
|
45
|
+
@_pm = "ب.ظ."
|
|
46
|
+
@_am_gregorian = "AM"
|
|
47
|
+
@_pm_gregorian = "PM"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.host_timezone
|
|
51
|
+
tz = ENV["TZ"]
|
|
52
|
+
return tz if tz && !tz.empty?
|
|
53
|
+
|
|
54
|
+
# Try local Time zone name
|
|
55
|
+
begin
|
|
56
|
+
name = Time.now.zone
|
|
57
|
+
return "UTC" if name.nil? || name.empty?
|
|
58
|
+
rescue StandardError
|
|
59
|
+
end
|
|
60
|
+
"UTC"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.zone_known?(name)
|
|
64
|
+
return true if EMBEDDED_OFFSETS.key?(name)
|
|
65
|
+
return true if name == "UTC"
|
|
66
|
+
# Probe system zoneinfo via ENV TZ
|
|
67
|
+
old = ENV["TZ"]
|
|
68
|
+
begin
|
|
69
|
+
ENV["TZ"] = name
|
|
70
|
+
Time.now
|
|
71
|
+
true
|
|
72
|
+
rescue StandardError
|
|
73
|
+
false
|
|
74
|
+
ensure
|
|
75
|
+
if old.nil?
|
|
76
|
+
ENV.delete("TZ")
|
|
77
|
+
else
|
|
78
|
+
ENV["TZ"] = old
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Offset seconds east of UTC for this instant in the given IANA zone.
|
|
84
|
+
def self.offset_seconds_at(ms, zone)
|
|
85
|
+
return 0 if zone.nil? || zone.empty? || zone.upcase == "UTC" || zone == "Etc/UTC" || zone.upcase == "GMT"
|
|
86
|
+
|
|
87
|
+
if EMBEDDED_OFFSETS.key?(zone) && (zone == "Asia/Tehran" || zone.start_with?("Etc/") || zone == "Asia/Tokyo")
|
|
88
|
+
# Prefer system zoneinfo when present; fall back to embedded.
|
|
89
|
+
sys = system_offset_seconds(ms, zone)
|
|
90
|
+
return sys unless sys.nil?
|
|
91
|
+
return EMBEDDED_OFFSETS[zone]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
sys = system_offset_seconds(ms, zone)
|
|
95
|
+
return sys unless sys.nil?
|
|
96
|
+
|
|
97
|
+
emb = EMBEDDED_OFFSETS[zone]
|
|
98
|
+
return emb unless emb.nil?
|
|
99
|
+
|
|
100
|
+
raise ArgumentError, "Invalid IANA timezone: #{zone}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self.system_offset_seconds(ms, zone)
|
|
104
|
+
old = ENV["TZ"]
|
|
105
|
+
begin
|
|
106
|
+
ENV["TZ"] = zone
|
|
107
|
+
t = Time.at(ms / 1000.0)
|
|
108
|
+
# Time#utc_offset is seconds east of UTC
|
|
109
|
+
t.utc_offset
|
|
110
|
+
rescue StandardError
|
|
111
|
+
nil
|
|
112
|
+
ensure
|
|
113
|
+
if old.nil?
|
|
114
|
+
ENV.delete("TZ")
|
|
115
|
+
else
|
|
116
|
+
ENV["TZ"] = old
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self.wall_from_ms(ms, zone)
|
|
122
|
+
off = offset_seconds_at(ms, zone)
|
|
123
|
+
# Civil time as if UTC
|
|
124
|
+
wall_ms = ms + off * 1000
|
|
125
|
+
sec = wall_ms.div(1000)
|
|
126
|
+
milli = wall_ms - sec * 1000
|
|
127
|
+
if milli.negative?
|
|
128
|
+
sec -= 1
|
|
129
|
+
milli += 1000
|
|
130
|
+
end
|
|
131
|
+
t = Time.at(sec).utc
|
|
132
|
+
WallParts.new(
|
|
133
|
+
year: t.year, month: t.month, day: t.day,
|
|
134
|
+
hour: t.hour, minute: t.min, second: t.sec,
|
|
135
|
+
ms: milli
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Map wall clock in zone -> UTC ms (iterative for DST).
|
|
140
|
+
def self.wall_to_utc(zone, y, m, d, h, mi, s, ms)
|
|
141
|
+
want = Time.utc(y, m, d, h, mi, s).to_i * 1000
|
|
142
|
+
utc = want
|
|
143
|
+
14.times do
|
|
144
|
+
parts = wall_from_ms(utc, zone)
|
|
145
|
+
got = Time.utc(parts.year, parts.month, parts.day, parts.hour, parts.minute, parts.second).to_i * 1000
|
|
146
|
+
diff = want - got
|
|
147
|
+
break if diff.zero?
|
|
148
|
+
utc += diff
|
|
149
|
+
end
|
|
150
|
+
(utc.div(1000) * 1000) + (((ms % 1000) + 1000) % 1000)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def get_wall_parts
|
|
154
|
+
self.class.wall_from_ms(@_ms, @_timezone)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def wall_to_utc(y, m, d, h, mi, s, ms)
|
|
158
|
+
self.class.wall_to_utc(@_timezone, y, m, d, h, mi, s, ms)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def to_jalali
|
|
162
|
+
unless @_is_converted
|
|
163
|
+
wall = get_wall_parts
|
|
164
|
+
civil_day = Time.utc(wall.year, wall.month, wall.day).to_i / (@_date_length / 1000)
|
|
165
|
+
base_day = Time.utc(1970, 1, 1).to_i / (@_date_length / 1000)
|
|
166
|
+
diff_days = civil_day - base_day
|
|
167
|
+
|
|
168
|
+
@_jalali[:year] = 1348
|
|
169
|
+
@_jalali[:month] = 10
|
|
170
|
+
@_jalali[:day] = 11
|
|
171
|
+
|
|
172
|
+
if diff_days.positive?
|
|
173
|
+
diff_days.times do
|
|
174
|
+
@_jalali[:day] += 1
|
|
175
|
+
if @_jalali[:month] < 7 && @_jalali[:day] > 31
|
|
176
|
+
@_jalali[:month] += 1
|
|
177
|
+
@_jalali[:day] = 1
|
|
178
|
+
elsif @_jalali[:month] > 6 && @_jalali[:month] < 12 && @_jalali[:day] > 30
|
|
179
|
+
@_jalali[:month] += 1
|
|
180
|
+
@_jalali[:day] = 1
|
|
181
|
+
elsif @_jalali[:month] == 12
|
|
182
|
+
leap = check_leap_year(@_jalali[:year])
|
|
183
|
+
if leap && @_jalali[:day] > 30
|
|
184
|
+
@_jalali[:year] += 1
|
|
185
|
+
@_jalali[:month] = 1
|
|
186
|
+
@_jalali[:day] = 1
|
|
187
|
+
elsif !leap && @_jalali[:day] > 29
|
|
188
|
+
@_jalali[:year] += 1
|
|
189
|
+
@_jalali[:month] = 1
|
|
190
|
+
@_jalali[:day] = 1
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
elsif diff_days.negative?
|
|
195
|
+
(-diff_days).times do
|
|
196
|
+
@_jalali[:day] -= 1
|
|
197
|
+
next unless @_jalali[:day] < 1
|
|
198
|
+
|
|
199
|
+
if @_jalali[:month] == 1
|
|
200
|
+
@_jalali[:year] -= 1
|
|
201
|
+
@_jalali[:month] = 12
|
|
202
|
+
@_jalali[:day] = check_leap_year(@_jalali[:year]) ? 30 : 29
|
|
203
|
+
elsif @_jalali[:month] < 8
|
|
204
|
+
@_jalali[:month] -= 1
|
|
205
|
+
@_jalali[:day] = 31
|
|
206
|
+
elsif @_jalali[:month] < 13
|
|
207
|
+
@_jalali[:month] -= 1
|
|
208
|
+
@_jalali[:day] = 30
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
@_jalali[:hours] = wall.hour
|
|
214
|
+
@_jalali[:min] = wall.minute
|
|
215
|
+
@_jalali[:sec] = wall.second
|
|
216
|
+
@_jalali[:ms] = wall.ms
|
|
217
|
+
@_jalali[:isLeapYear] = check_leap_year(@_jalali[:year])
|
|
218
|
+
@_is_converted = true
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
wall = get_wall_parts
|
|
222
|
+
@_jalali[:hours] = wall.hour
|
|
223
|
+
@_jalali[:min] = wall.minute
|
|
224
|
+
@_jalali[:sec] = wall.second
|
|
225
|
+
@_jalali[:ms] = wall.ms
|
|
226
|
+
@_jalali
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def to_gregorian(j_date)
|
|
230
|
+
wall = get_wall_parts
|
|
231
|
+
jalali = {
|
|
232
|
+
year: 1348, month: 10, day: 11,
|
|
233
|
+
hours: j_date[:hours] || j_date["hours"] || wall.hour,
|
|
234
|
+
min: j_date[:min] || j_date["min"] || wall.minute,
|
|
235
|
+
sec: j_date[:sec] || j_date["sec"] || wall.second,
|
|
236
|
+
ms: j_date[:ms] || j_date["ms"] || wall.ms,
|
|
237
|
+
isLeapYear: false
|
|
238
|
+
}
|
|
239
|
+
jy = j_date[:year] || j_date["year"]
|
|
240
|
+
jm = j_date[:month] || j_date["month"]
|
|
241
|
+
jd = j_date[:day] || j_date["day"]
|
|
242
|
+
hours = (j_date.key?(:hours) || j_date.key?("hours")) ? (j_date[:hours] || j_date["hours"]) : wall.hour
|
|
243
|
+
min = (j_date.key?(:min) || j_date.key?("min")) ? (j_date[:min] || j_date["min"]) : wall.minute
|
|
244
|
+
sec = (j_date.key?(:sec) || j_date.key?("sec")) ? (j_date[:sec] || j_date["sec"]) : wall.second
|
|
245
|
+
ms = (j_date.key?(:ms) || j_date.key?("ms")) ? (j_date[:ms] || j_date["ms"]) : wall.ms
|
|
246
|
+
|
|
247
|
+
diff_days = 0
|
|
248
|
+
after = jy > 1348 || (jy == 1348 && jm >= 10) || (jy == 1348 && jm == 10 && jd >= 11)
|
|
249
|
+
before = jy < 1348 || (jy == 1348 && jm < 10) || (jy == 1348 && jm == 10 && jd < 11)
|
|
250
|
+
|
|
251
|
+
if after
|
|
252
|
+
loop do
|
|
253
|
+
break if jalali[:year] == jy && jalali[:month] == jm && jalali[:day] == jd
|
|
254
|
+
|
|
255
|
+
diff_days += 1
|
|
256
|
+
jalali[:day] += 1
|
|
257
|
+
if jalali[:month] < 7 && jalali[:day] > 31
|
|
258
|
+
jalali[:month] += 1
|
|
259
|
+
jalali[:day] = 1
|
|
260
|
+
elsif jalali[:month] > 6 && jalali[:month] < 12 && jalali[:day] > 30
|
|
261
|
+
jalali[:month] += 1
|
|
262
|
+
jalali[:day] = 1
|
|
263
|
+
elsif jalali[:month] == 12
|
|
264
|
+
leap = check_leap_year(jalali[:year])
|
|
265
|
+
if leap && jalali[:day] > 30
|
|
266
|
+
jalali[:year] += 1
|
|
267
|
+
jalali[:month] = 1
|
|
268
|
+
jalali[:day] = 1
|
|
269
|
+
elsif !leap && jalali[:day] > 29
|
|
270
|
+
jalali[:year] += 1
|
|
271
|
+
jalali[:month] = 1
|
|
272
|
+
jalali[:day] = 1
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
elsif before
|
|
277
|
+
loop do
|
|
278
|
+
break if jalali[:year] == jy && jalali[:month] == jm && jalali[:day] == jd
|
|
279
|
+
|
|
280
|
+
diff_days -= 1
|
|
281
|
+
jalali[:day] -= 1
|
|
282
|
+
next unless jalali[:day] < 1
|
|
283
|
+
|
|
284
|
+
if jalali[:month] == 1
|
|
285
|
+
jalali[:year] -= 1
|
|
286
|
+
jalali[:month] = 12
|
|
287
|
+
jalali[:day] = check_leap_year(jalali[:year]) ? 30 : 29
|
|
288
|
+
elsif jalali[:month] < 8
|
|
289
|
+
jalali[:month] -= 1
|
|
290
|
+
jalali[:day] = 31
|
|
291
|
+
elsif jalali[:month] < 13
|
|
292
|
+
jalali[:month] -= 1
|
|
293
|
+
jalali[:day] = 30
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
wall_as_utc = diff_days * @_date_length + (hours * 3600 + min * 60 + sec) * 1000 + ms
|
|
299
|
+
civil = Time.at(wall_as_utc / 1000.0).utc
|
|
300
|
+
wall_to_utc(civil.year, civil.month, civil.day, civil.hour, civil.min, civil.sec, civil.usec / 1000)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def check_leap_year(year)
|
|
304
|
+
return false unless year.positive?
|
|
305
|
+
|
|
306
|
+
check_year = (year + 2346) * @_leap_year_division
|
|
307
|
+
check_year - check_year.to_i < @_leap_year_division
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def reset
|
|
311
|
+
@_is_converted = false
|
|
312
|
+
@_jalali[:year] = 1348
|
|
313
|
+
@_jalali[:month] = 10
|
|
314
|
+
@_jalali[:day] = 11
|
|
315
|
+
to_jalali
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def add_zero(number)
|
|
319
|
+
"#{number > -1 && number < 10 ? '0' : ''}#{number}"
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def get_wall_weekday
|
|
323
|
+
wall = get_wall_parts
|
|
324
|
+
Time.utc(wall.year, wall.month, wall.day).wday
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def get_utc_timezone
|
|
328
|
+
wall = get_wall_parts
|
|
329
|
+
as_utc = Time.utc(wall.year, wall.month, wall.day, wall.hour, wall.minute, wall.second).to_i * 1000 + wall.ms
|
|
330
|
+
offset_min = ((as_utc - @_ms) / 60_000.0).round
|
|
331
|
+
sign = offset_min >= 0 ? "+" : "-"
|
|
332
|
+
abs = offset_min.abs
|
|
333
|
+
" GMT#{sign}#{add_zero(abs / 60)}#{add_zero(abs % 60)}"
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def normalize_date(days)
|
|
337
|
+
setTime(@_ms + days * @_date_length)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def normalize_jalali(j_date, year, month, day)
|
|
341
|
+
if month < 1
|
|
342
|
+
year_diff = ((1 - month) / 12.0).ceil
|
|
343
|
+
year -= year_diff
|
|
344
|
+
month = 12 * year_diff + month
|
|
345
|
+
elsif month > 12
|
|
346
|
+
year_diff = ((month - 1) / 12.0).floor
|
|
347
|
+
year += year_diff
|
|
348
|
+
month -= 12 * year_diff
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
if month > 6 && month < 12 && day > 30
|
|
352
|
+
day = 30
|
|
353
|
+
elsif month == 12
|
|
354
|
+
leap = check_leap_year(year)
|
|
355
|
+
if leap && day > 30
|
|
356
|
+
day = 30
|
|
357
|
+
elsif !leap && day > 29
|
|
358
|
+
day = 29
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
merged = j_date.merge(year: year, month: month, day: day)
|
|
363
|
+
setTime(to_gregorian(merged))
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def normalize_timezone(timezone)
|
|
367
|
+
return @_timezone if timezone.nil?
|
|
368
|
+
|
|
369
|
+
trimmed = timezone.to_s.strip
|
|
370
|
+
raise ArgumentError, "Invalid IANA timezone: empty" if trimmed.empty?
|
|
371
|
+
|
|
372
|
+
lowered = trimmed.downcase
|
|
373
|
+
return self.class.host_timezone if %w[default local system].include?(lowered)
|
|
374
|
+
return "UTC" if %w[utc gmt].include?(lowered)
|
|
375
|
+
|
|
376
|
+
# Validate
|
|
377
|
+
begin
|
|
378
|
+
self.class.offset_seconds_at(@_ms, trimmed)
|
|
379
|
+
rescue ArgumentError
|
|
380
|
+
raise ArgumentError, "Invalid IANA timezone: #{trimmed}"
|
|
381
|
+
end
|
|
382
|
+
trimmed
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def toString
|
|
386
|
+
j = to_jalali
|
|
387
|
+
"#{week_day_name} #{add_zero(j[:day])} #{@_months_names[j[:month] - 1]} #{add_zero(j[:year])} #{j[:hours]}:#{add_zero(j[:min])}:#{add_zero(j[:sec])}#{get_utc_timezone}"
|
|
388
|
+
end
|
|
389
|
+
alias to_s toString
|
|
390
|
+
|
|
391
|
+
def toDateString
|
|
392
|
+
j = to_jalali
|
|
393
|
+
"#{week_day_name} #{add_zero(j[:day])} #{@_months_names[j[:month] - 1]} #{add_zero(j[:year])}"
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def toLocaleDateString
|
|
397
|
+
j = to_jalali
|
|
398
|
+
"#{add_zero(j[:year])}/#{add_zero(j[:month])}/#{add_zero(j[:day])}"
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def toLocaleTimeString
|
|
402
|
+
j = to_jalali
|
|
403
|
+
"#{j[:hours] % 12}:#{add_zero(j[:min])}:#{add_zero(j[:sec])} #{getMeridiem}"
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def getFullYear
|
|
407
|
+
to_jalali[:year]
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def getMonth
|
|
411
|
+
to_jalali[:month]
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def getDate
|
|
415
|
+
to_jalali[:day]
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def getDay
|
|
419
|
+
(get_wall_weekday - @_first_day_of_week + 7 + 1) % 7
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def getHours
|
|
423
|
+
get_wall_parts.hour
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def getMinutes
|
|
427
|
+
get_wall_parts.minute
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def getSeconds
|
|
431
|
+
get_wall_parts.second
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def getMilliseconds
|
|
435
|
+
((@_ms % 1000) + 1000) % 1000
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def getTime
|
|
439
|
+
@_ms
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def setTime(time)
|
|
443
|
+
@_ms = time.to_i
|
|
444
|
+
reset
|
|
445
|
+
getTime
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def setMilliseconds(ms)
|
|
449
|
+
wall = get_wall_parts
|
|
450
|
+
setTime(wall_to_utc(wall.year, wall.month, wall.day, wall.hour, wall.minute, wall.second, ms))
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def setSeconds(sec, ms = nil)
|
|
454
|
+
wall = get_wall_parts
|
|
455
|
+
setTime(wall_to_utc(wall.year, wall.month, wall.day, wall.hour, wall.minute, sec, ms.nil? ? wall.ms : ms))
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def setMinutes(min, sec = nil, ms = nil)
|
|
459
|
+
wall = get_wall_parts
|
|
460
|
+
setTime(wall_to_utc(wall.year, wall.month, wall.day, wall.hour, min, sec.nil? ? wall.second : sec, ms.nil? ? wall.ms : ms))
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def setHours(hours, min = nil, sec = nil, ms = nil)
|
|
464
|
+
wall = get_wall_parts
|
|
465
|
+
setTime(wall_to_utc(wall.year, wall.month, wall.day, hours, min.nil? ? wall.minute : min, sec.nil? ? wall.second : sec, ms.nil? ? wall.ms : ms))
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def setDate(date)
|
|
469
|
+
j = to_jalali
|
|
470
|
+
normalize_date(date - j[:day])
|
|
471
|
+
getTime
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def setMonth(month, date = nil)
|
|
475
|
+
j = to_jalali
|
|
476
|
+
normalize_jalali(j, j[:year], month + 1, j[:day])
|
|
477
|
+
reset
|
|
478
|
+
unless date.nil?
|
|
479
|
+
j = to_jalali
|
|
480
|
+
normalize_date(date - j[:day])
|
|
481
|
+
reset
|
|
482
|
+
end
|
|
483
|
+
getTime
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def setFullYear(year, month = nil, date = nil)
|
|
487
|
+
j = to_jalali
|
|
488
|
+
normalize_jalali(j, year, month.nil? ? j[:month] : month + 1, j[:day])
|
|
489
|
+
reset
|
|
490
|
+
unless date.nil?
|
|
491
|
+
j = to_jalali
|
|
492
|
+
normalize_date(date - j[:day])
|
|
493
|
+
reset
|
|
494
|
+
end
|
|
495
|
+
getTime
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def self.create(date, options = nil)
|
|
499
|
+
ms = parse_input(date)
|
|
500
|
+
output = new(ms)
|
|
501
|
+
if options.is_a?(Hash) && (options[:timezone] || options["timezone"])
|
|
502
|
+
output = output.setTimezone(options[:timezone] || options["timezone"])
|
|
503
|
+
end
|
|
504
|
+
output
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def self.parse_input(value)
|
|
508
|
+
case value
|
|
509
|
+
when JalaliX then value.getTime
|
|
510
|
+
when Integer, Float then value.to_i
|
|
511
|
+
when Time then (value.to_f * 1000).to_i
|
|
512
|
+
when Date then Time.utc(value.year, value.month, value.day).to_i * 1000
|
|
513
|
+
when String then parse_iso_string(value)
|
|
514
|
+
else
|
|
515
|
+
raise TypeError, "JalaliX.create: unsupported date type #{value.class}"
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def self.parse_iso_string(value)
|
|
520
|
+
s = value.strip
|
|
521
|
+
if s.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
522
|
+
y, m, d = s.split("-").map(&:to_i)
|
|
523
|
+
return Time.utc(y, m, d).to_i * 1000
|
|
524
|
+
end
|
|
525
|
+
t = Time.parse(s)
|
|
526
|
+
(t.to_f * 1000).to_i
|
|
527
|
+
rescue StandardError
|
|
528
|
+
raise ArgumentError, "JalaliX.create: invalid date string #{value.inspect}"
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def self.parseJalali(value, options = nil)
|
|
532
|
+
normalized = fromPersian(value.to_s).strip
|
|
533
|
+
match = normalized.match(/\A(\d{4})[\/-](\d{1,2})[\/-](\d{1,2})(?:[ T](\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?\z/)
|
|
534
|
+
raise ArgumentError, "JalaliX.parseJalali: invalid Jalali date \"#{value}\"" unless match
|
|
535
|
+
|
|
536
|
+
year = match[1].to_i
|
|
537
|
+
month = match[2].to_i
|
|
538
|
+
day = match[3].to_i
|
|
539
|
+
hours = (match[4] || 0).to_i
|
|
540
|
+
minutes = (match[5] || 0).to_i
|
|
541
|
+
seconds = (match[6] || 0).to_i
|
|
542
|
+
raise ArgumentError, "JalaliX.parseJalali: invalid Jalali date \"#{value}\"" if month < 1 || month > 12 || day < 1 || day > 31
|
|
543
|
+
|
|
544
|
+
output = create(Time.now, options)
|
|
545
|
+
output.setFullYear(year, month - 1, day)
|
|
546
|
+
output.setHours(hours, minutes, seconds, 0)
|
|
547
|
+
output
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def self.isValidDate(date)
|
|
551
|
+
date.is_a?(JalaliX) || date.is_a?(Time) || date.is_a?(Date)
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def self.toPersian(str)
|
|
555
|
+
out = str.dup
|
|
556
|
+
10.times { |i| out = out.gsub(i.to_s, PERSIAN_NUMBERS[i]) }
|
|
557
|
+
out
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def self.fromPersian(str)
|
|
561
|
+
out = str.dup
|
|
562
|
+
10.times { |i| out = out.gsub(PERSIAN_NUMBERS[i], i.to_s) }
|
|
563
|
+
out
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def self.compare(date, comparing)
|
|
567
|
+
date.getTime < comparing.getTime
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
def clone
|
|
571
|
+
output = JalaliX.new(@_ms)
|
|
572
|
+
output.instance_variable_set(:@_timezone, @_timezone)
|
|
573
|
+
output
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def getTimezone
|
|
577
|
+
@_timezone
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def getWeekOfYear
|
|
581
|
+
day_of_year = getDayOfYear
|
|
582
|
+
day_of_week = JalaliX.new(@_ms).tap { |d| d.instance_variable_set(:@_timezone, @_timezone) }.getStartOfYear.getDayISO
|
|
583
|
+
((day_of_year + day_of_week - 1) / 7.0).ceil
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
def getDayOfYear
|
|
587
|
+
j = to_jalali
|
|
588
|
+
if j[:month] < 7
|
|
589
|
+
j[:day] + (j[:month] - 1) * 31
|
|
590
|
+
elsif j[:month] < 12
|
|
591
|
+
j[:day] + 186 + (j[:month] - 7) * 30
|
|
592
|
+
else
|
|
593
|
+
j[:day] + 336
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
def getDayISO
|
|
598
|
+
((getDay + 6) % 7) + 1
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def getHoursISO
|
|
602
|
+
hours = getHours
|
|
603
|
+
hours > 12 ? hours - 12 : hours
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
def getMeridiem
|
|
607
|
+
j = to_jalali
|
|
608
|
+
j[:hours] > 12 ? @_pm : @_am
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
def getStartOfYear
|
|
612
|
+
addDays(-1 * getDayOfYear + 1).getStartOfDay
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
def getStartOfMonth
|
|
616
|
+
addDays(-1 * getDate + 1).getStartOfDay
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def getStartOfWeek
|
|
620
|
+
addDays(-1 * getDayISO + 1).getStartOfDay
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
def getStartOfDay
|
|
624
|
+
setHours(0, 0, 0, 0)
|
|
625
|
+
self
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
def getEndOfYear
|
|
629
|
+
j = to_jalali
|
|
630
|
+
last = j[:isLeapYear] ? 366 : 365
|
|
631
|
+
addDays(last - getDayOfYear).getEndOfDay
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
def getEndOfMonth
|
|
635
|
+
j = to_jalali
|
|
636
|
+
last = 31
|
|
637
|
+
if j[:month] > 6 && j[:month] < 12
|
|
638
|
+
last = 30
|
|
639
|
+
elsif j[:month] == 12
|
|
640
|
+
last = j[:isLeapYear] ? 30 : 29
|
|
641
|
+
end
|
|
642
|
+
addDays(last - getDate).getEndOfDay
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
def getEndOfWeek
|
|
646
|
+
addDays(7 - getDayISO).getEndOfDay
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
def getEndOfDay
|
|
650
|
+
setHours(23, 59, 59, 999)
|
|
651
|
+
self
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
def setTimezone(timezone)
|
|
655
|
+
output = JalaliX.new(@_ms)
|
|
656
|
+
output.instance_variable_set(:@_timezone, normalize_timezone(timezone))
|
|
657
|
+
output
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
def addYears(year)
|
|
661
|
+
j = to_jalali
|
|
662
|
+
normalize_jalali(j, j[:year] + year, j[:month], j[:day])
|
|
663
|
+
reset
|
|
664
|
+
self
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
def addMonths(month)
|
|
668
|
+
j = to_jalali
|
|
669
|
+
normalize_jalali(j, j[:year], j[:month] + month, j[:day])
|
|
670
|
+
reset
|
|
671
|
+
self
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
def addWeeks(weeks)
|
|
675
|
+
normalize_date(weeks * 7)
|
|
676
|
+
reset
|
|
677
|
+
self
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
def addDays(days)
|
|
681
|
+
normalize_date(days)
|
|
682
|
+
reset
|
|
683
|
+
self
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
def toISODate(calendar = "gregorian")
|
|
687
|
+
return format("YYYY-MM-DD") if calendar.to_s == "jalali"
|
|
688
|
+
|
|
689
|
+
wall = get_wall_parts
|
|
690
|
+
"#{wall.year}-#{add_zero(wall.month)}-#{add_zero(wall.day)}"
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
def format(str, persian_or_options = nil, gregorian = nil)
|
|
694
|
+
persian = false
|
|
695
|
+
use_gregorian = false
|
|
696
|
+
if persian_or_options.is_a?(Hash)
|
|
697
|
+
persian = !!(persian_or_options[:persianDigits] || persian_or_options["persianDigits"])
|
|
698
|
+
cal = persian_or_options[:calendar] || persian_or_options["calendar"]
|
|
699
|
+
use_gregorian = cal.to_s == "gregorian"
|
|
700
|
+
else
|
|
701
|
+
persian = !!persian_or_options
|
|
702
|
+
use_gregorian = !!gregorian
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
placeholders = []
|
|
706
|
+
ph = lambda do |val|
|
|
707
|
+
i = placeholders.length
|
|
708
|
+
placeholders << val
|
|
709
|
+
"\uE000#{i}\uE001"
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
output = str.dup
|
|
713
|
+
wall = get_wall_parts
|
|
714
|
+
year = use_gregorian ? wall.year : getFullYear
|
|
715
|
+
month = use_gregorian ? wall.month : getMonth
|
|
716
|
+
week_of_year = use_gregorian ? gregorian_week_of_year : getWeekOfYear
|
|
717
|
+
day_of_year = use_gregorian ? gregorian_day_of_year : getDayOfYear
|
|
718
|
+
date = use_gregorian ? wall.day : getDate
|
|
719
|
+
month_name = use_gregorian ? @_months_gregorian_names[month - 1] : @_months_names[month - 1]
|
|
720
|
+
week_day_name_val = use_gregorian ? @_week_days_gregorian_names[get_wall_weekday] : week_day_name
|
|
721
|
+
week_day_letter = use_gregorian ? @_week_days_gregorian_letters[get_wall_weekday] : @_week_days_letters[(getDay + @_first_day_of_week) % 7]
|
|
722
|
+
meridiem = use_gregorian ? (getHours >= 12 ? @_pm_gregorian : @_am_gregorian) : getMeridiem
|
|
723
|
+
year_str = year.to_s
|
|
724
|
+
|
|
725
|
+
{
|
|
726
|
+
"YYYY" => year_str,
|
|
727
|
+
"YYY" => year_str[-3..],
|
|
728
|
+
"YY" => year_str[-2..],
|
|
729
|
+
"Y" => year_str[-1],
|
|
730
|
+
"MMM" => month_name,
|
|
731
|
+
"MM" => add_zero(month),
|
|
732
|
+
"M" => month.to_s,
|
|
733
|
+
"W" => add_zero(week_of_year),
|
|
734
|
+
"w" => week_of_year.to_s,
|
|
735
|
+
"DW" => getDayISO.to_s,
|
|
736
|
+
"dw" => (use_gregorian ? (get_wall_weekday.zero? ? 7 : get_wall_weekday) : getDay).to_s,
|
|
737
|
+
"WN" => week_day_name_val,
|
|
738
|
+
"wn" => week_day_name_val,
|
|
739
|
+
"WL" => week_day_letter,
|
|
740
|
+
"wl" => week_day_letter,
|
|
741
|
+
"DDDD" => add_zero(day_of_year),
|
|
742
|
+
"DDD" => day_of_year.to_s,
|
|
743
|
+
"DD" => add_zero(date),
|
|
744
|
+
"D" => date.to_s
|
|
745
|
+
}.each do |token, val|
|
|
746
|
+
output.gsub!(token, ph.call(val)) if output.include?(token)
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
hours = getHours
|
|
750
|
+
output.gsub!("HH", ph.call(add_zero(hours))) if output.include?("HH")
|
|
751
|
+
output.gsub!("H", ph.call(hours.to_s)) if output.include?("H")
|
|
752
|
+
hours_iso = hours > 12 ? hours - 12 : (hours.zero? ? 12 : hours)
|
|
753
|
+
output.gsub!("hh", ph.call(add_zero(hours_iso))) if output.include?("hh")
|
|
754
|
+
output.gsub!("h", ph.call(hours_iso.to_s)) if output.include?("h")
|
|
755
|
+
output.gsub!("mm", ph.call(add_zero(getMinutes))) if output.include?("mm")
|
|
756
|
+
output.gsub!("m", ph.call(getMinutes.to_s)) if output.include?("m")
|
|
757
|
+
output.gsub!("ss", ph.call(add_zero(getSeconds))) if output.include?("ss")
|
|
758
|
+
output.gsub!("s", ph.call(getSeconds.to_s)) if output.include?("s")
|
|
759
|
+
output.gsub!("sss", ph.call(getMilliseconds.to_s)) if output.include?("sss")
|
|
760
|
+
output.gsub!("SSS", ph.call(getMilliseconds.to_s)) if output.include?("SSS")
|
|
761
|
+
output.gsub!("A", ph.call(meridiem)) if output.include?("A")
|
|
762
|
+
output.gsub!("a", ph.call(meridiem)) if output.include?("a")
|
|
763
|
+
output.gsub!("X", ph.call((@_ms / 1000).floor.to_s)) if output.include?("X")
|
|
764
|
+
output.gsub!("x", ph.call(@_ms.to_s)) if output.include?("x")
|
|
765
|
+
|
|
766
|
+
placeholders.each_with_index do |val, i|
|
|
767
|
+
output.gsub!("\uE000#{i}\uE001", val)
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
persian ? self.class.toPersian(output) : output
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
private
|
|
774
|
+
|
|
775
|
+
def week_day_name
|
|
776
|
+
@_week_days_names[(getDay + @_first_day_of_week) % 7]
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
def gregorian_day_of_year
|
|
780
|
+
wall = get_wall_parts
|
|
781
|
+
start = Time.utc(wall.year, 1, 1) - 86_400
|
|
782
|
+
civil = Time.utc(wall.year, wall.month, wall.day)
|
|
783
|
+
((civil - start) / 86_400).round
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
def gregorian_week_of_year
|
|
787
|
+
wall = get_wall_parts
|
|
788
|
+
d = Time.utc(wall.year, wall.month, wall.day)
|
|
789
|
+
dow = d.wday
|
|
790
|
+
dow = 7 if dow.zero?
|
|
791
|
+
d += (4 - dow) * 86_400
|
|
792
|
+
year_start = Time.utc(d.year, 1, 1)
|
|
793
|
+
((((d - year_start) / 86_400) + 1) / 7.0).ceil
|
|
794
|
+
end
|
|
795
|
+
end
|
data/lib/jalalix.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: jalalix
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Hossein Maklavani
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Pure Ruby port of JalaliX — same algorithm and camelCase API as the TypeScript
|
|
13
|
+
package.
|
|
14
|
+
email:
|
|
15
|
+
- hossein@maklavani.dev
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- CHANGELOG.md
|
|
21
|
+
- LICENSE
|
|
22
|
+
- README.md
|
|
23
|
+
- lib/jalalix.rb
|
|
24
|
+
- lib/jalalix/jalalix.rb
|
|
25
|
+
- lib/jalalix/version.rb
|
|
26
|
+
homepage: https://jalalix.com
|
|
27
|
+
licenses:
|
|
28
|
+
- MIT
|
|
29
|
+
metadata: {}
|
|
30
|
+
rdoc_options: []
|
|
31
|
+
require_paths:
|
|
32
|
+
- lib
|
|
33
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
34
|
+
requirements:
|
|
35
|
+
- - ">="
|
|
36
|
+
- !ruby/object:Gem::Version
|
|
37
|
+
version: 3.0.0
|
|
38
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '0'
|
|
43
|
+
requirements: []
|
|
44
|
+
rubygems_version: 4.0.23
|
|
45
|
+
specification_version: 4
|
|
46
|
+
summary: Jalali (Persian / Shamsi) calendar for Ruby
|
|
47
|
+
test_files: []
|