21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
|
# File 'lib/jade/stdlib/calendar.rb', line 21
def code
<<~JADE
module Calendar exposing (
Date,
Month(..),
Unit(..),
Weekday(..),
add,
day,
diff,
from_calendar_date,
from_iso_string,
from_rata_die,
month,
month_from_int,
month_to_int,
to_iso_string,
to_rata_die,
today,
weekday,
weekday_from_int,
weekday_to_int,
year,
)
import Decode exposing (Decodable, Decoder, Value)
import Encode exposing (Encodable)
type Month
= Jan
| Feb
| Mar
| Apr
| May
| Jun
| Jul
| Aug
| Sep
| Oct
| Nov
| Dec
type Weekday
= Mon
| Tue
| Wed
| Thu
| Fri
| Sat
| Sun
type Unit
= Years
| Months
| Weeks
| Days
struct Date = {
year: Int,
month: Month,
day: Int
}
uses Jade::Calendar::Runtime with
today_raw : Task({ year: Int, month: Int, day: Int }, Never)
end
def today -> Task(Date, Never)
raw <- today_raw()
Task.succeed(Date(raw.year, month_from_int(raw.month), raw.day))
end
def from_calendar_date(y: Int, m: Month, d: Int) -> Date
Date(y, m, d)
end
def year(d: Date) -> Int
d.year
end
def month(d: Date) -> Month
d.month
end
def day(d: Date) -> Int
d.day
end
def month_to_int(m: Month) -> Int
case m
in Jan then 1
in Feb then 2
in Mar then 3
in Apr then 4
in May then 5
in Jun then 6
in Jul then 7
in Aug then 8
in Sep then 9
in Oct then 10
in Nov then 11
in Dec then 12
end
end
def month_from_int(i: Int) -> Month
case i
in 1 then Jan
in 2 then Feb
in 3 then Mar
in 4 then Apr
in 5 then May
in 6 then Jun
in 7 then Jul
in 8 then Aug
in 9 then Sep
in 10 then Oct
in 11 then Nov
in 12 then Dec
else Jan
end
end
def weekday_to_int(w: Weekday) -> Int
case w
in Mon then 1
in Tue then 2
in Wed then 3
in Thu then 4
in Fri then 5
in Sat then 6
in Sun then 7
end
end
def weekday_from_int(i: Int) -> Weekday
case i
in 1 then Mon
in 2 then Tue
in 3 then Wed
in 4 then Thu
in 5 then Fri
in 6 then Sat
in 7 then Sun
else Mon
end
end
def days_in_month(y: Int, m: Month) -> Int
case m
in Jan then 31
in Feb then leap?(y) ? 29 : 28
in Mar then 31
in Apr then 30
in May then 31
in Jun then 30
in Jul then 31
in Aug then 31
in Sep then 30
in Oct then 31
in Nov then 30
in Dec then 31
end
end
def leap?(y: Int) -> Bool
(mod(y, 4) == 0) && ((mod(y, 100) != 0) || (mod(y, 400) == 0))
end
def days_before_year(y: Int) -> Int
n = y - 1
n * 365 + n / 4 - n / 100 + n / 400
end
def days_before_month(y: Int, m: Month) -> Int
mi = month_to_int(m)
base = (case mi
in 1 then 0
in 2 then 31
in 3 then 59
in 4 then 90
in 5 then 120
in 6 then 151
in 7 then 181
in 8 then 212
in 9 then 243
in 10 then 273
in 11 then 304
in 12 then 334
else 0
end)
mi > 2 && leap?(y) ? base + 1 : base
end
def to_rata_die(d: Date) -> Int
days_before_year(d.year) + days_before_month(d.year, d.month) + d.day
end
def weekday(d: Date) -> Weekday
weekday_from_int(mod(to_rata_die(d) - 1, 7) + 1)
end
def pad_to(s: String, width: Int) -> String
len = String.length(s)
len < width ? String.repeat("0", width - len) ++ s : s
end
def to_iso_string(d: Date) -> String
pad_to(String.from_int(d.year), 4)
++ "-"
++ pad_to(String.from_int(month_to_int(d.month)), 2)
++ "-"
++ pad_to(String.from_int(d.day), 2)
end
def from_iso_string(s: String) -> Result(Date, String)
case String.split(s, "-")
in [y, m, day_str]
case (String.to_int(y), String.to_int(m), String.to_int(day_str))
in (Just(yi), Just(mi), Just(di))
Ok(from_calendar_date(yi, month_from_int(mi), di))
else Err("invalid ISO date: " ++ s)
end
else Err("invalid ISO date: " ++ s)
end
end
def add(d: Date, unit: Unit, n: Int) -> Date
case unit
in Days then add_days(d, n)
in Weeks then add_days(d, n * 7)
in Months then add_months(d, n)
in Years then add_months(d, n * 12)
end
end
def add_days(d: Date, n: Int) -> Date
from_rata_die(to_rata_die(d) + n)
end
def add_months(d: Date, n: Int) -> Date
total = month_to_int(d.month) - 1 + n
new_year = d.year + total / 12
new_month = month_from_int(mod(total, 12) + 1)
from_calendar_date(
new_year,
new_month,
min(d.day, days_in_month(new_year, new_month)),
)
end
def min(a: Int, b: Int) -> Int
a < b ? a : b
end
def diff(a: Date, b: Date, unit: Unit) -> Int
case unit
in Days then to_rata_die(b) - to_rata_die(a)
in Weeks then (to_rata_die(b) - to_rata_die(a)) / 7
in Months then calendar_months(a, b)
in Years then calendar_months(a, b) / 12
end
end
def calendar_months(a: Date, b: Date) -> Int
raw = (b.year - a.year) * 12 + (month_to_int(b.month) - month_to_int(a.month))
raw > 0 && b.day < a.day ? raw - 1 : raw < 0 && b.day > a.day ? raw + 1 : raw
end
def from_rata_die(rd: Int) -> Date
y = bump_year(rd, (rd - 1) / 366 + 1)
doy = rd - days_before_year(y)
case search_month(y, doy, 12)
in (m, d) then from_calendar_date(y, m, d)
end
end
def bump_year(rd: Int, y: Int) -> Int
days_before_year(y + 1) < rd ? bump_year(rd, y + 1) : y
end
def search_month(y: Int, doy: Int, mi: Int) -> (Month, Int)
m = month_from_int(mi)
offset = days_before_month(y, m)
doy > offset ? (m, doy - offset) : search_month(y, doy, mi - 1)
end
def compare_month(a: Month, b: Month) -> Ordering
compare(month_to_int(a), month_to_int(b))
end
def month_eq(a: Month, b: Month) -> Bool
month_to_int(a) == month_to_int(b)
end
implements Comparable(Month) with
compare: compare_month
end
implements Eq(Month) with
(==): month_eq
end
def compare_date(a: Date, b: Date) -> Ordering
case compare(a.year, b.year)
in EQ
case compare_month(a.month, b.month)
in EQ then compare(a.day, b.day)
in o then o
end
in o then o
end
end
def date_eq(a: Date, b: Date) -> Bool
a.year == b.year && a.month == b.month && a.day == b.day
end
implements Comparable(Date) with
compare: compare_date
end
implements Eq(Date) with
(==): date_eq
end
def parse_date(s: String) -> Decoder(Date)
Decode.from_result(from_iso_string(s))
end
implements Decodable(Date) with
decoder: -> { Decode.string |> Decode.and_then(parse_date) }
end
implements Encodable(Date) with
encoder: (d) -> { Encode.string(to_iso_string(d)) }
end
JADE
end
|