0 | module IotaTime.Tzdb.Windows
2 | import IotaTime.Internal.Gregorian
3 | import IotaTime.TimeZone.Core
4 | import IotaTime.Tzdb.Windows.Types
9 | byteValue : Bits8 -> Integer
12 | unsignedLittleEndian : List Bits8 -> Integer
13 | unsignedLittleEndian = go 1
15 | go : Integer -> List Bits8 -> Integer
16 | go multiplier [] = 0
17 | go multiplier (byte :: rest) =
18 | byteValue byte * multiplier + go (multiplier * 256) rest
20 | signedLittleEndian32 : List Bits8 -> Integer
21 | signedLittleEndian32 bytes =
22 | let unsigned = unsignedLittleEndian bytes
23 | in if unsigned >= 2147483648 then unsigned - 4294967296 else unsigned
25 | takeBytes : Nat -> List Bits8 -> Maybe (List Bits8, List Bits8)
26 | takeBytes Z bytes = Just ([], bytes)
27 | takeBytes (S count) [] = Nothing
28 | takeBytes (S count) (byte :: rest) = do
29 | (taken, remaining) <- takeBytes count rest
30 | Just (byte :: taken, remaining)
32 | readLittleEndian : Nat -> List Bits8 -> Maybe (Integer, List Bits8)
33 | readLittleEndian width bytes = do
34 | (value, remaining) <- takeBytes width bytes
35 | Just (unsignedLittleEndian value, remaining)
37 | readSigned32 : List Bits8 -> Maybe (Integer, List Bits8)
38 | readSigned32 bytes = do
39 | (value, remaining) <- takeBytes 4 bytes
40 | Just (signedLittleEndian32 value, remaining)
42 | prefixValue : List Char -> String -> Maybe String
43 | prefixValue expectedPrefix source = map pack (strip expectedPrefix (unpack source))
45 | strip : List Char -> List Char -> Maybe (List Char)
46 | strip [] remaining = Just remaining
47 | strip (expected :: rest) (actual :: remaining) =
48 | if expected == actual then strip rest remaining else Nothing
51 | hexDigit : Char -> Maybe Integer
53 | if value >= '0' && value <= '9' then Just (cast value - cast '0')
54 | else if value >= 'a' && value <= 'f' then Just (cast value - cast 'a' + 10)
55 | else if value >= 'A' && value <= 'F' then Just (cast value - cast 'A' + 10)
58 | hexBytes : String -> Maybe (List Bits8)
59 | hexBytes source = go (unpack source)
61 | go : List Char -> Maybe (List Bits8)
63 | go (high :: low :: rest) = do
64 | highValue <- hexDigit high
65 | lowValue <- hexDigit low
66 | remaining <- go rest
67 | Just (cast (highValue * 16 + lowValue) :: remaining)
70 | dynamicLine : String -> Maybe (Integer, List Bits8)
71 | dynamicLine source = do
72 | value <- prefixValue ['D', 'Y', 'N', 'A', 'M', 'I', 'C', '\t'] source
73 | parseYear [] (unpack value)
75 | decimalDigits : List Char -> Maybe Integer
76 | decimalDigits [] = Nothing
77 | decimalDigits digits = go 0 digits
79 | go : Integer -> List Char -> Maybe Integer
80 | go value [] = Just value
81 | go value (digit :: rest) = if digit >= '0' && digit <= '9'
82 | then go (value * 10 + cast digit - cast '0') rest
85 | parseYear : List Char -> List Char -> Maybe (Integer, List Bits8)
86 | parseYear digits ('\t' :: encoded) = do
87 | year <- decimalDigits (reverse digits)
88 | bytes <- hexBytes (pack encoded)
90 | parseYear digits (value :: rest) = parseYear (value :: digits) rest
91 | parseYear _ [] = Nothing
93 | parseDynamicLines : List String -> Either WindowsRegistryProtocolError
94 | (List (Integer, List Bits8), List String)
95 | parseDynamicLines [] = Left IncompleteRegistryZone
96 | parseDynamicLines ("END" :: rest) = Right ([], rest)
97 | parseDynamicLines (line :: rest) = case dynamicLine line of
98 | Nothing => Left (InvalidDynamicRegistryLine line)
100 | (remainingValues, remainingLines) <- parseDynamicLines rest
101 | Right (value :: remainingValues, remainingLines)
103 | parseRegistryZone : List String -> Either WindowsRegistryProtocolError
104 | (WindowsRegistryZone, List String)
105 | parseRegistryZone (idLine :: standardLine :: daylightLine :: tziLine :: rest) = do
106 | zoneId <- maybe (Left (UnexpectedRegistryLine idLine)) Right
107 | (prefixValue ['I', 'D', '\t'] idLine)
108 | standardName <- maybe (Left (UnexpectedRegistryLine standardLine)) Right
109 | (prefixValue ['S', 'T', 'D', '\t'] standardLine)
110 | daylightName <- maybe (Left (UnexpectedRegistryLine daylightLine)) Right
111 | (prefixValue ['D', 'S', 'T', '\t'] daylightLine)
112 | encoded <- maybe (Left (UnexpectedRegistryLine tziLine)) Right
113 | (prefixValue ['T', 'Z', 'I', '\t'] tziLine)
114 | defaultTzi <- maybe (Left (InvalidRegistryHex encoded)) Right
116 | (dynamicTzi, remaining) <- parseDynamicLines rest
117 | Right (MkWindowsRegistryZone zoneId standardName daylightName
118 | defaultTzi dynamicTzi, remaining)
119 | parseRegistryZone _ = Left IncompleteRegistryZone
121 | parseRegistryZones : List String -> Either WindowsRegistryProtocolError
122 | (List WindowsRegistryZone)
123 | parseRegistryZones [] = Right []
124 | parseRegistryZones ("ZONE" :: rest) = do
125 | (zone, remaining) <- parseRegistryZone rest
126 | zones <- assert_total (parseRegistryZones remaining)
127 | Right (zone :: zones)
128 | parseRegistryZones (line :: _) = Left (UnexpectedRegistryLine line)
132 | parseWindowsRegistrySnapshot : String ->
133 | Either WindowsRegistryProtocolError WindowsRegistrySnapshot
134 | parseWindowsRegistrySnapshot source = case lines source of
135 | [] => Left MissingLocalZoneId
136 | localLine :: rest => case prefixValue ['L', 'O', 'C', 'A', 'L', '\t'] localLine of
137 | Nothing => Left MissingLocalZoneId
138 | Just "" => Left MissingLocalZoneId
139 | Just localZoneId => map (MkWindowsRegistrySnapshot localZoneId)
140 | (parseRegistryZones rest)
142 | readTransitionDate : List Bits8 -> Maybe
143 | (WindowsTransitionDate, Integer, List Bits8)
144 | readTransitionDate bytes = do
145 | (year, afterYear) <- readLittleEndian 2 bytes
146 | (month, afterMonth) <- readLittleEndian 2 afterYear
147 | (weekday, afterWeekday) <- readLittleEndian 2 afterMonth
148 | (week, afterWeek) <- readLittleEndian 2 afterWeekday
149 | (hour, afterHour) <- readLittleEndian 2 afterWeek
150 | (minute, afterMinute) <- readLittleEndian 2 afterHour
151 | (second, afterSecond) <- readLittleEndian 2 afterMinute
152 | (milliseconds, remaining) <- readLittleEndian 2 afterSecond
153 | Just (MkWindowsTransitionDate year month week weekday hour minute second,
154 | milliseconds, remaining)
159 | parseWindowsTzi : String -> String -> List Bits8 ->
160 | Either WindowsZoneError WindowsZoneRule
161 | parseWindowsTzi standardName daylightName bytes =
162 | if length bytes /= 44
163 | then Left (WindowsTziLength (cast (length bytes)))
164 | else case decode bytes of
165 | Nothing => Left (WindowsTziLength (cast (length bytes)))
166 | Just (bias, standardBias, daylightBias,
167 | standardDate, standardMilliseconds,
168 | daylightDate, daylightMilliseconds) =>
169 | if standardMilliseconds /= 0
170 | then Left (WindowsTransitionMillisecondsUnsupported
171 | standardMilliseconds)
172 | else if daylightMilliseconds /= 0
173 | then Left (WindowsTransitionMillisecondsUnsupported
174 | daylightMilliseconds)
175 | else Right (MkWindowsZoneRule bias standardBias daylightBias
176 | standardName daylightName daylightDate standardDate)
178 | decode : List Bits8 -> Maybe
179 | (Integer, Integer, Integer, WindowsTransitionDate, Integer,
180 | WindowsTransitionDate, Integer)
182 | (bias, afterBias) <- readSigned32 source
183 | (standardBias, afterStandardBias) <- readSigned32 afterBias
184 | (daylightBias, afterDaylightBias) <- readSigned32 afterStandardBias
185 | (standardDate, standardMilliseconds, afterStandard) <-
186 | readTransitionDate afterDaylightBias
187 | (daylightDate, daylightMilliseconds, remaining) <-
188 | readTransitionDate afterStandard
190 | [] => Just (bias, standardBias, daylightBias,
191 | standardDate, standardMilliseconds,
192 | daylightDate, daylightMilliseconds)
195 | windowsOffset : Integer -> Either WindowsZoneError Offset
196 | windowsOffset bias =
197 | let seconds = negate (bias * 60)
198 | in case refineOffsetSeconds seconds of
199 | Left _ => Left (WindowsOffsetOutOfRange seconds)
200 | Right value => Right value
202 | transitionSeconds : WindowsTransitionDate -> Either WindowsZoneError Integer
203 | transitionSeconds transition =
204 | if transition.hour < 0 || transition.hour > 23 ||
205 | transition.minute < 0 || transition.minute > 59 ||
206 | transition.second < 0 || transition.second > 59
207 | then Left (WindowsTimeOutOfRange transition.hour transition.minute
209 | else Right (transition.hour * 3600 + transition.minute * 60 +
212 | windowsRule : WindowsTransitionDate -> Either WindowsZoneError RecurrenceRule
213 | windowsRule transition = do
214 | if transition.year /= 0
215 | then Left (WindowsAbsoluteTransitionUnsupported transition.year)
217 | seconds <- transitionSeconds transition
218 | case monthWeekDayRule transition.month transition.week transition.weekday
219 | seconds WallTime of
220 | Left error => Left (WindowsRecurrenceError error)
221 | Right value => Right value
223 | windowsZoneRecurrence : WindowsZoneRule ->
224 | Either WindowsZoneError ZoneRecurrence
225 | windowsZoneRecurrence rule = do
226 | standardOffset <- windowsOffset
227 | (rule.biasMinutes + rule.standardBiasMinutes)
228 | daylightOffset <- windowsOffset
229 | (rule.biasMinutes + rule.daylightBiasMinutes)
230 | start <- windowsRule rule.daylightStart
231 | end <- windowsRule rule.standardStart
232 | Right (zoneRecurrence
233 | (transitionInfo standardOffset False rule.standardName)
234 | (transitionInfoWithSavings daylightOffset
235 | (minusClamped daylightOffset standardOffset) rule.daylightName)
240 | windowsRecurringTimeZone : String -> TransitionInfo ->
241 | List (Instant, TransitionInfo) -> WindowsZoneRule ->
242 | Either WindowsTimeZoneError TimeZone
243 | windowsRecurringTimeZone valueId initial transitions rule = do
244 | recurrence <- case windowsZoneRecurrence rule of
245 | Left error => Left (InvalidWindowsRule error)
246 | Right value => Right value
247 | case refineRecurringTimeZone valueId initial transitions recurrence of
248 | Left error => Left (InvalidWindowsTransitions error)
249 | Right value => Right value
251 | noDaylightTransitions : WindowsZoneRule -> Bool
252 | noDaylightTransitions rule =
253 | rule.daylightStart.month == 0 && rule.standardStart.month == 0
255 | incompleteDaylightTransitions : WindowsZoneRule -> Bool
256 | incompleteDaylightTransitions rule =
257 | (rule.daylightStart.month == 0) /= (rule.standardStart.month == 0)
259 | windowsZoneEra : WindowsZoneRule -> Either WindowsZoneError
260 | (TransitionInfo, Maybe ZoneRecurrence)
261 | windowsZoneEra rule =
262 | if incompleteDaylightTransitions rule
263 | then Left IncompleteWindowsDaylightRule
265 | standardOffset <- windowsOffset
266 | (rule.biasMinutes + rule.standardBiasMinutes)
267 | let standardInfo = transitionInfo standardOffset False rule.standardName
268 | if noDaylightTransitions rule
269 | then Right (standardInfo, Nothing)
270 | else map (\recurrence => (standardInfo, Just recurrence))
271 | (windowsZoneRecurrence rule)
276 | windowsTimeZone : String -> WindowsZoneRule ->
277 | Either WindowsTimeZoneError TimeZone
278 | windowsTimeZone valueId rule =
279 | case windowsZoneEra rule of
280 | Left error => Left (InvalidWindowsRule error)
281 | Right (standardInfo, Nothing) =>
282 | Right (fixedTimeZone valueId (utcOffset standardInfo))
283 | Right (standardInfo, Just recurrence) =>
284 | case refineRecurringTimeZone valueId standardInfo [] recurrence of
285 | Left error => Left (InvalidWindowsTransitions error)
286 | Right value => Right value
288 | dynamicYearsValid : List WindowsDynamicRule -> Bool
289 | dynamicYearsValid [] = True
290 | dynamicYearsValid (first :: rest) = go first.effectiveYear rest
292 | go : Integer -> List WindowsDynamicRule -> Bool
293 | go previous [] = True
294 | go previous (next :: remaining) =
295 | previous < next.effectiveYear && go next.effectiveYear remaining
297 | yearStart : Integer -> Instant
298 | yearStart year = fromNanosecondsSinceEpoch
299 | (gregorianDaysFromCivil year 1 1 * 86400 * 1000000000)
301 | dynamicEraSpecs : Bool -> List WindowsDynamicRule -> Either WindowsZoneError
302 | (List (Maybe Instant, TransitionInfo, Maybe ZoneRecurrence))
303 | dynamicEraSpecs first [] = Right []
304 | dynamicEraSpecs first (entry :: rest) = do
305 | (initial, recurrence) <- windowsZoneEra entry.dynamicRule
306 | remaining <- dynamicEraSpecs False rest
307 | let boundary = if first then Nothing else Just (yearStart entry.effectiveYear)
308 | Right ((boundary, initial, recurrence) :: remaining)
313 | windowsDynamicTimeZone : String -> WindowsZoneRule -> List WindowsDynamicRule ->
314 | Either WindowsTimeZoneError TimeZone
315 | windowsDynamicTimeZone valueId defaultRule [] = windowsTimeZone valueId defaultRule
316 | windowsDynamicTimeZone valueId defaultRule dynamicRules =
317 | if not (dynamicYearsValid dynamicRules)
318 | then Left DynamicYearsNotStrictlyIncreasing
320 | specs <- case dynamicEraSpecs True dynamicRules of
321 | Left error => Left (InvalidWindowsRule error)
322 | Right value => Right value
323 | case refineTimeZoneEras valueId specs of
324 | Left error => Left (InvalidWindowsTransitions error)
325 | Right value => Right value
327 | parseDynamicTzi : String -> String -> List (Integer, List Bits8) ->
328 | Either WindowsRegistryError (List WindowsDynamicRule)
329 | parseDynamicTzi standardName daylightName [] = Right []
330 | parseDynamicTzi standardName daylightName ((year, bytes) :: rest) = do
331 | rule <- case parseWindowsTzi standardName daylightName bytes of
332 | Left error => Left (InvalidDynamicTzi year error)
333 | Right value => Right value
334 | remaining <- parseDynamicTzi standardName daylightName rest
335 | Right (MkWindowsDynamicRule year rule :: remaining)
339 | windowsRegistryTimeZoneAs : String -> WindowsRegistryZone ->
340 | Either WindowsRegistryError TimeZone
341 | windowsRegistryTimeZoneAs valueId registry = do
342 | defaultRule <- case parseWindowsTzi registry.registryStandardName
343 | registry.registryDaylightName registry.registryDefaultTzi of
344 | Left error => Left (InvalidDefaultTzi error)
345 | Right value => Right value
346 | dynamicRules <- parseDynamicTzi registry.registryStandardName
347 | registry.registryDaylightName registry.registryDynamicTzi
348 | case windowsDynamicTimeZone valueId defaultRule dynamicRules of
349 | Left error => Left (InvalidRegistryTimeZone error)
350 | Right value => Right value
355 | windowsRegistryTimeZone : WindowsRegistryZone ->
356 | Either WindowsRegistryError TimeZone
357 | windowsRegistryTimeZone registry =
358 | windowsRegistryTimeZoneAs registry.registryZoneId registry