-
Notifications
You must be signed in to change notification settings - Fork 2
/
Extra.elm
496 lines (357 loc) · 12.1 KB
/
Extra.elm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
module Time.Extra exposing
( Parts, partsToPosix
, posixToParts
, Interval(..)
, diff
, add, floor, ceiling
, range
, toOffset
)
{-|
# Create
@docs Parts, partsToPosix
# Convert
@docs posixToParts
# Intervals
@docs Interval
## Difference
@docs diff
## Shift
@docs add, floor, ceiling
## Lists
@docs range
# Time zone offset
@docs toOffset
-}
import Date exposing (Date)
import Time exposing (..)
dateToMillis : Date -> Int
dateToMillis date =
let
daysSinceEpoch =
(date |> Date.toRataDie) - 719163
in
daysSinceEpoch * 86400000
timeFromClock : Int -> Int -> Int -> Int -> Int
timeFromClock hour minute second millisecond =
hour * 3600000 + minute * 60000 + second * 1000 + millisecond
timeFromPosix : Zone -> Posix -> Int
timeFromPosix zone posix =
timeFromClock
(toHour zone posix)
(toMinute zone posix)
(toSecond zone posix)
(toMillis zone posix)
{-| What is the offset from UTC, in minutes, for this `Zone` at this
`Posix` time?
import Time exposing (Month(..))
import Time.Extra exposing (Parts, partsToPosix, toOffset)
toOffset nyc
(partsToPosix nyc (Parts 2018 Sep 26 10 30 0 0))
== -240
-- assuming `nyc` is a `Zone` for America/New_York
**Note:** It's possible to verify the example above by using time zone data
from the package [justinmimbs/timezone-data][tzdata] to define `nyc`:
import TimeZone
nyc =
TimeZone.america__new_york ()
[tzdata]: https://package.elm-lang.org/packages/justinmimbs/timezone-data/latest/
-}
toOffset : Zone -> Posix -> Int
toOffset zone posix =
let
localMillis =
(Date.fromPosix zone posix |> dateToMillis)
+ timeFromPosix zone posix
millis =
posixToMillis posix
in
(localMillis - millis) // 60000
posixFromDateTime : Zone -> Date -> Int -> Posix
posixFromDateTime zone date time =
-- find the local offset
let
millis =
(date |> dateToMillis) + time
offset0 =
millis |> millisToPosix |> toOffset zone
posix1 =
(millis - offset0 * 60000) |> millisToPosix
offset1 =
posix1 |> toOffset zone
in
if offset0 == offset1 then
posix1
else
-- local offset has changed within `offset0` time period (e.g. DST switch)
let
posix2 =
(millis - offset1 * 60000) |> millisToPosix
offset2 =
posix2 |> toOffset zone
in
if offset1 == offset2 then
posix2
else
-- `millis` is within the lost hour of a local switch
posix1
{-| Represents a time abstractly by describing its calendar date and clock
time. Given a time zone, this information can be converted to a `Posix`, i.e.
a concrete moment in time.
-}
type alias Parts =
{ year : Int
, month : Month
, day : Int
, hour : Int
, minute : Int
, second : Int
, millisecond : Int
}
{-| Create a `Posix` from a description of a time and a specific time zone.
import Time exposing (Month(..), utc)
import Time.Extra exposing (Parts, partsToPosix)
partsToPosix utc (Parts 2018 Sep 26 14 30 0 0)
Any out-of-range day or time values will be clamped within range.
partsToPosix utc (Parts 2018 Sep 31 24 60 -60 -500)
== partsToPosix utc (Parts 2018 Sep 30 23 59 0 0)
-}
partsToPosix : Zone -> Parts -> Posix
partsToPosix zone { year, month, day, hour, minute, second, millisecond } =
posixFromDateTime zone
(Date.fromCalendarDate year month day)
(timeFromClock
(hour |> clamp 0 23)
(minute |> clamp 0 59)
(second |> clamp 0 59)
(millisecond |> clamp 0 999)
)
{-| Convert a `Posix` to a description of a time in a specific time zone. This
is a convenience function for extracting parts of a time into a single record.
import Time exposing (Month(..), utc)
import Time.Extra exposing (Parts, partsToPosix, posixToParts)
posixToParts
utc
(partsToPosix utc (Parts 2018 Sep 26 14 30 0 0))
== { year = 2018
, month = Sep
, day = 26
, hour = 14
, minute = 30
, second = 0
, millisecond = 0
}
-}
posixToParts : Zone -> Posix -> Parts
posixToParts zone posix =
{ year = toYear zone posix
, month = toMonth zone posix
, day = toDay zone posix
, hour = toHour zone posix
, minute = toMinute zone posix
, second = toSecond zone posix
, millisecond = toMillis zone posix
}
{-| Represents an interval of time.
-}
type Interval
= Year
| Quarter
| Month
| Week
| Monday
| Tuesday
| Wednesday
| Thursday
| Friday
| Saturday
| Sunday
| Day
| Hour
| Minute
| Second
| Millisecond
{-| Shift a time into the past or future by adding a number of whole intervals.
import Time exposing (Month(..), utc)
import Time.Extra exposing (Interval(..), Parts, partsToPosix, add)
add Week 2 utc (partsToPosix utc (Parts 2018 Sep 1 11 55 0 0))
== partsToPosix utc (Parts 2018 Sep 15 11 55 0 0)
When adding `Month`, `Quarter`, or `Year` intervals, day values are clamped to
the end of the month if necessary.
add Month 1 utc (partsToPosix utc (Parts 2020 Jan 31 0 0 0 0))
== partsToPosix utc (Parts 2020 Feb 29 0 0 0 0)
-}
add : Interval -> Int -> Zone -> Posix -> Posix
add interval n zone posix =
case interval of
Millisecond ->
(posix |> posixToMillis) + n |> millisToPosix
Second ->
add Millisecond (n * 1000) zone posix
Minute ->
add Millisecond (n * 60000) zone posix
Hour ->
add Millisecond (n * 3600000) zone posix
Day ->
posixFromDateTime zone
(Date.fromPosix zone posix |> Date.add Date.Days n)
(timeFromPosix zone posix)
Month ->
posixFromDateTime zone
(Date.fromPosix zone posix |> Date.add Date.Months n)
(timeFromPosix zone posix)
Year ->
add Month (n * 12) zone posix
Quarter ->
add Month (n * 3) zone posix
Week ->
add Day (n * 7) zone posix
weekday ->
add Day (n * 7) zone posix
toFractionalDay : Zone -> Posix -> Float
toFractionalDay zone posix =
(timeFromPosix zone posix |> toFloat) / 86400000
toRataDieMoment : Zone -> Posix -> Float
toRataDieMoment zone posix =
(Date.fromPosix zone posix |> Date.toRataDie |> toFloat)
+ toFractionalDay zone posix
{-| The number of whole months between date and 0001-01-01 plus fraction
representing the current month. Only used for diffing months.
-}
toMonths : Zone -> Posix -> Float
toMonths zone posix =
let
wholeMonths =
(12 * (toYear zone posix - 1))
+ ((toMonth zone posix |> Date.monthToNumber) - 1)
|> toFloat
fractionalMonth =
((toDay zone posix |> toFloat) + toFractionalDay zone posix) / 100
in
wholeMonths + fractionalMonth
{-| Get the difference, as a number of whole intervals, between two times.
import Time exposing (Month(..), utc)
import Time.Extra exposing (Interval(..), Parts, partsToPosix, diff)
diff Month utc
(partsToPosix utc (Parts 2020 Jan 2 0 0 0 0))
(partsToPosix utc (Parts 2020 Apr 1 0 0 0 0))
== 2
-}
diff : Interval -> Zone -> Posix -> Posix -> Int
diff interval zone posix1 posix2 =
case interval of
Millisecond ->
(posix2 |> posixToMillis) - (posix1 |> posixToMillis)
Second ->
diff Millisecond zone posix1 posix2 // 1000
Minute ->
diff Millisecond zone posix1 posix2 // 60000
Hour ->
diff Millisecond zone posix1 posix2 // 3600000
Day ->
(posix2 |> toRataDieMoment zone) - (posix1 |> toRataDieMoment zone) |> truncate
Month ->
(posix2 |> toMonths zone) - (posix1 |> toMonths zone) |> truncate
Year ->
diff Month zone posix1 posix2 // 12
Quarter ->
diff Month zone posix1 posix2 // 3
Week ->
diff Day zone posix1 posix2 // 7
weekday ->
diff Week zone (floor weekday zone posix1) (floor weekday zone posix2)
{-| Round down a time to the beginning of the closest interval. The resulting
time will be less than or equal to the one provided.
import Time exposing (Month(..), utc)
import Time.Extra exposing (Interval(..), Parts, partsToPosix, floor)
floor Hour utc
(partsToPosix utc (Parts 1999 Dec 31 23 59 59 999))
== (partsToPosix utc (Parts 1999 Dec 31 23 0 0 0))
-}
floor : Interval -> Zone -> Posix -> Posix
floor interval zone posix =
case interval of
Millisecond ->
posix
Second ->
posixFromDateTime zone
(Date.fromPosix zone posix)
(timeFromClock (toHour zone posix) (toMinute zone posix) (toSecond zone posix) 0)
Minute ->
posixFromDateTime zone
(Date.fromPosix zone posix)
(timeFromClock (toHour zone posix) (toMinute zone posix) 0 0)
Hour ->
posixFromDateTime zone
(Date.fromPosix zone posix)
(timeFromClock (toHour zone posix) 0 0 0)
Day ->
floorDate Date.Day zone posix
Month ->
floorDate Date.Month zone posix
Year ->
floorDate Date.Year zone posix
Quarter ->
floorDate Date.Quarter zone posix
Week ->
floorDate Date.Week zone posix
Monday ->
floorDate Date.Monday zone posix
Tuesday ->
floorDate Date.Tuesday zone posix
Wednesday ->
floorDate Date.Wednesday zone posix
Thursday ->
floorDate Date.Thursday zone posix
Friday ->
floorDate Date.Friday zone posix
Saturday ->
floorDate Date.Saturday zone posix
Sunday ->
floorDate Date.Sunday zone posix
floorDate : Date.Interval -> Zone -> Posix -> Posix
floorDate dateInterval zone posix =
posixFromDateTime zone
(Date.fromPosix zone posix |> Date.floor dateInterval)
0
{-| Round up a time to the beginning of the closest interval. The resulting
time will be greater than or equal to the one provided.
import Time exposing (Month(..), utc)
import Time.Extra exposing (Interval(..), Parts, partsToPosix, ceiling)
ceiling Hour utc
(partsToPosix utc (Parts 1999 Dec 31 23 59 59 999))
== (partsToPosix utc (Parts 2000 Jan 1 0 0 0 0))
-}
ceiling : Interval -> Zone -> Posix -> Posix
ceiling interval zone posix =
let
floored =
posix |> floor interval zone
in
if floored == posix then
posix
else
floored |> add interval 1 zone
{-| Create a list of times, at rounded intervals, increasing by a step value,
between two times. The list will start on or after the first time, and end
before the second time.
import Time exposing (Month(..), utc)
import Time.Extra exposing (Interval(..), Parts, partsToPosix, range)
start = Parts 2020 Jan 1 12 0 0 0
until = Parts 2020 Jan 8 0 0 0 0
range Day 2 utc (partsToPosix utc start) (partsToPosix utc until)
== List.map (partsToPosix utc)
[ Parts 2020 Jan 2 0 0 0 0
, Parts 2020 Jan 4 0 0 0 0
, Parts 2020 Jan 6 0 0 0 0
]
-}
range : Interval -> Int -> Zone -> Posix -> Posix -> List Posix
range interval step zone start until =
rangeHelp interval (max 1 step) zone until [] (start |> ceiling interval zone)
rangeHelp : Interval -> Int -> Zone -> Posix -> List Posix -> Posix -> List Posix
rangeHelp interval step zone until revList current =
if posixToMillis current < posixToMillis until then
rangeHelp interval step zone until (current :: revList) (current |> add interval step zone)
else
List.reverse revList