2
MS_PER_MINUTE = 60 * 1000
3
MS_PER_DAY = 24 * 60 * MS_PER_MINUTE
5
# Copy the UTC face of a `Date` object to a new object's local face, or vice versa.
7
# The special cases ensure that any year is treated as non-rolling; e.g. 99 is
8
# 0099 and not 1999. (The fact that the shape of a year is congruent modulo 400
9
# years is used to help minimize the necessary changes.)
11
# Returns a `Date` object with its UTC face (the face returned by `.getUTC*()`)
12
# set to the local face (the face returned by the local methods `.get*()`
13
# corresponding to `.getUTC*()`) suggested by the `Date` `from`.
15
# The conversion is lossless in this direction.
16
dateLocalToUtc = (from) ->
17
y = from.getFullYear()
21
min = from.getMinutes()
23
ms = from.getMilliseconds()
24
to = new Date Date.UTC y, mo, d, h, min, s, ms
25
if to.getUTCFullYear() isnt y
26
to.setTime Date.UTC y + 400, mo, d, h, min, s, ms
30
# Returns a `Date` object with its local face set to the UTC face suggested by
31
# of the `Date` object `from`.
33
# This conversion is generally lossy if the face falls within a daylight/summer
34
# time ambiguity; the actual results are dependent on the JavaScript
36
dateUtcToLocalRaw = (from) ->
37
y = from.getUTCFullYear()
38
mo = from.getUTCMonth()
40
h = from.getUTCHours()
41
min = from.getUTCMinutes()
42
s = from.getUTCSeconds()
43
ms = from.getUTCMilliseconds()
44
to = new Date y, mo, d, h, min, s, ms
45
if to.getFullYear() isnt y
46
to = new Date y + 400, mo, d, h, min, s, ms
50
# `faceMs` is an interpretation of an ms date as if the epoch were not
51
# strictly UTC but entirely timezone-ignorant instead (i.e., the number of ms
52
# since `1970-01-01T00:00:00` with no specified timezone). If a `Date` object
53
# is set to `faceMs`, the intended face appears on its UTC face.
55
# Returns a `Date` object with the local face set as suggested by `faceMs`.
57
# This conversion is generally lossy if the face falls within a daylight/summer
58
# time ambiguity; the actual results are dependent on the JavaScript
60
faceMsToLocalRaw = (faceMs) -> dateUtcToLocalRaw new Date faceMs
63
# Returns a `Date` object with its local face set to the UTC face suggested by
64
# of the `Date` object `from`. This conversion is generally lossy if the face
65
# falls within a daylight/summer time ambiguity; such times are adjusted
66
# according to the following rules:
68
# - If the face date is skipped by a DST skip forward, the time is resolved to
69
# the first minute following the skip (e.g., if 01:59 is directly followed by
70
# 03:00, 02:30 would be mapped to 03:00).
71
# - If two events are scheduled at different times, the event scheduled
72
# earlier will occur no later than the event scheduled later. If one or
73
# both events are scheduled for a skipped time, they may occur at the same
75
# - If the face date happens twice due a DST skip backward, the time is
76
# resolved to the earlier instance (e.g. if 01:00-01:59 daylight time is
77
# directly followed by 01:00-01:59 standard time, 01:30 would be mapped to
78
# 01:30 daylight time).
79
# - An event scheduled for a time that is doubled will still occur only once,
80
# at the earlier instance of the time.
81
# - A face cannot be resolved to the later instance of a doubled time;
82
# specifically, even if the target application has a concept of *current
83
# time* and the later instance of the time is in the future but the earlier
84
# instance is in the past, the time itself is considered past.
86
dateUtcToLocalAdjusted = (dateWithUtcFace) ->
87
faceMs = dateWithUtcFace.getTime()
88
date = dateUtcToLocalRaw dateWithUtcFace
89
dateOffset = date.getTimezoneOffset()
91
previousDateOffset = (faceMsToLocalRaw faceMs - MS_PER_DAY).getTimezoneOffset()
93
if faceMs is dateLocalToUtc(date).getTime()
95
if dateOffset is previousDateOffset
96
# Matches earliest (or only) instance.
99
# If there are two instances, matches the later one and needs adjustment.
100
# Otherwise, matches the only instance.
101
offsetChangeMinutes = dateOffset - previousDateOffset
102
if offsetChangeMinutes <= 0
103
# This is a skipping rather than a doubling adjustment; the
104
# offset change is the wrong direction
107
# The offset change is subtracted from the date's epoch time
108
# rather than its face.
109
adjustedDate = new Date date.getTime() - (offsetChangeMinutes * MS_PER_MINUTE)
111
# If the offsets match, the recent past contains no discontinuity,
112
# so there is only one instance. Otherwise, adjust the time to be
113
# before the discontinuity.
114
if adjustedDate.getTimezoneOffset() is dateOffset then date else adjustedDate
116
# Face refers to a skipped time. Resolve to the first minute the end of
117
# the discontinuity. The discontinuity is located by binary search;
118
# minutes are differentiated based on whether a given time's local
119
# offset matches the previous day's offset (before the discontinuity)
120
# or not (after the discontinuity).
121
followingDateOffset = (faceMsToLocalRaw faceMs + MS_PER_DAY).getTimezoneOffset()
122
offsetChangeMinutes = previousDateOffset - followingDateOffset
123
faceMinute = faceMs / MS_PER_MINUTE
124
before = Math.floor( faceMinute - offsetChangeMinutes )
125
after = Math.ceil( faceMinute + offsetChangeMinutes )
126
while before + 1 < after
127
mid = Math.floor( (before + after) / 2 )
128
midOffset = (faceMsToLocalRaw mid * MS_PER_MINUTE).getTimezoneOffset()
129
if midOffset is previousDateOffset
133
faceMsToLocalRaw after * MS_PER_MINUTE