]> gitweb.factorcode.org Git - factor.git/blob - extra/zoneinfo/zoneinfo.factor
zoneinfo: Fix zome timezone parsing.
[factor.git] / extra / zoneinfo / zoneinfo.factor
1 ! Copyright (C) 2009 Doug Coleman.
2 ! See http://factorcode.org/license.txt for BSD license.
3 USING: accessors arrays ascii assocs assocs.extras calendar
4 calendar.english combinators combinators.short-circuit
5 combinators.smart csv grouping interval-maps io.encodings.utf8
6 io.files kernel math math.parser memoize namespaces sequences
7 sequences.extras sorting splitting splitting.extras ;
8 QUALIFIED: sets
9 IN: zoneinfo
10
11 CONSTANT: zoneinfo-paths
12 {
13     "vocab:zoneinfo/africa"
14     "vocab:zoneinfo/antarctica"
15     "vocab:zoneinfo/asia"
16     "vocab:zoneinfo/australasia"
17     "vocab:zoneinfo/europe"
18     "vocab:zoneinfo/northamerica"
19     "vocab:zoneinfo/pacificnew"
20     "vocab:zoneinfo/southamerica"
21     "vocab:zoneinfo/backzone"
22 }
23
24 CONSTANT: zoneinfo-extra-paths
25 {
26     "vocab:zoneinfo/backward"
27     "vocab:zoneinfo/etcetera"
28     "vocab:zoneinfo/factory"
29     "vocab:zoneinfo/leapseconds"
30     "vocab:zoneinfo/systemv"
31 }
32
33 : zoneinfo-lines ( path -- seq )
34     utf8 file-lines
35     [
36         { [ length 0 = ] [ "#" head? ] } 1||
37     ] reject
38     [ "\t " split ] map ;
39
40 MEMO: zoneinfo-country-zones ( -- seq )
41     "vocab:zoneinfo/zone1970.tab" zoneinfo-lines ;
42
43 SYMBOL: last-zone
44
45 TUPLE: raw-zone name gmt-offset rules/save format until ;
46 TUPLE: raw-rule name from to type in on at-time save letters ;
47 TUPLE: raw-link from to ;
48 TUPLE: raw-leap year month day hms corr r/s ;
49
50 TUPLE: zone name ;
51 TUPLE: rule name from to at-time ;
52
53 : rule-to ( m string -- m n )
54     {
55         { "only" [ dup ] }
56         { "max" [ 1/0. ] }
57         [ string>number ]
58     } case ;
59
60 : parse-rule ( seq -- rule )
61     [
62         { [ drop ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] } spread
63     ] input<sequence raw-rule boa ;
64
65 : parse-link ( seq -- link )
66     [
67         { [ drop ] [ ] [ ] } spread
68     ] input<sequence raw-link boa ;
69
70 : parse-leap ( seq -- link )
71     [
72         { [ drop ] [ ] [ ] [ ] [ ] [ ] [ ] } spread
73     ] input<sequence raw-leap boa ;
74
75 : parse-zone ( seq -- zone )
76     {
77         [ second ]
78         [ third ]
79         [ fourth ]
80         [ 4 swap nth ]
81         [ 5 tail harvest ]
82     } cleave raw-zone boa ;
83
84 : parse-partial-zone ( seq -- zone )
85     [ last-zone get name>> ] dip
86     {
87         [ first ]
88         [ second ]
89         [ 2 swap nth ]
90         [ 3 tail harvest ]
91     } cleave raw-zone boa ;
92
93 : parse-zoneinfo-line ( seq -- tuple )
94     dup first >lower
95     {
96         { "rule" [ parse-rule ] }
97         { "link" [ parse-link ] }
98         { "leap" [ parse-leap ] }
99         { "zone" [ parse-zone dup last-zone set ] }
100         [ drop harvest parse-partial-zone ]
101     } case ;
102
103 : parse-zoneinfo-file ( path -- seq )
104     zoneinfo-lines [ parse-zoneinfo-line ] map ;
105
106 MEMO: zoneinfo-files ( -- seq )
107     zoneinfo-paths [ parse-zoneinfo-file ] map ;
108
109 MEMO: zoneinfo-array ( -- seq )
110     zoneinfo-files concat ;
111
112 MEMO: zoneinfo-assoc ( -- assoc )
113     zoneinfo-paths [ dup parse-zoneinfo-file ] { } map>assoc ;
114
115 : raw-rule-map ( -- assoc )
116     zoneinfo-array [ raw-rule? ] filter [ name>> ] collect-by ;
117
118 : current-rule-map ( -- assoc )
119     raw-rule-map
120     [ [ to>> "max" = ] filter ] assoc-map
121     harvest-values ;
122
123 : raw-zone-map ( -- assoc )
124     zoneinfo-array [ raw-zone? ] filter [ name>> ] collect-by ;
125
126 : zoneinfo-zones ( -- seq )
127     raw-zone-map keys
128     [ "/" swap subseq? ] partition
129     [ natural-sort ] bi@ append ;
130
131 GENERIC: zone-matches? ( string rule -- ? )
132
133 M: raw-rule zone-matches? name>> = ;
134 M: raw-link zone-matches? from>> = ;
135 M: raw-leap zone-matches? 2drop f ;
136 M: raw-zone zone-matches? name>> = ;
137
138 : find-rules ( string -- rules )
139     raw-rule-map
140     [ [ to>> "max" = ] filter ] assoc-map at ;
141
142 ERROR: zone-not-found name ;
143
144 : find-zone ( string -- zone )
145     raw-zone-map
146     [ last ] assoc-map ?at [ zone-not-found ] unless ;
147
148 : find-zone-rules ( string -- zone rules )
149     find-zone dup rules/save>> find-rules ;
150
151 : zone-abbrevs ( -- assoc )
152     zoneinfo-zones [
153         find-zone-rules
154         [ format>> ] dip
155         [
156             letters>> dup { "D" "S" } member? [ drop "" ] unless
157             swap "%" split1
158             [ 1 tail surround ] [ nip ] if*
159         ] with V{ } map-as sets:members
160     ] zip-with ;
161
162 : number>value ( n -- n' )
163     {
164         { "only" [ f ] }
165         { "min" [ f ] }
166         { "max" [ t ] }
167         [ string>number ]
168     } case ;
169
170 : on>value ( n -- n' )
171     ! "3", "Thu>=8" always >=, "lastFri"
172     {
173         { [ dup 3 swap ?nth CHAR: > = ] [
174             3 cut 2 tail [ day-abbreviation3-predicate ] [ string>number ] bi* 2array
175         ] }
176         { [ dup "last" head? ] [ 4 tail day-abbreviation3-index ] }
177         [ string>number ]
178     } cond ;
179
180 : zone-month ( timestamp month -- timestamp' )
181     month-abbreviation-index >>month ;
182
183 ERROR: unknown-day-abbrev day ;
184 : day-abbrev>= ( timestamp day -- timestamp' )
185     {
186         { "Sun" [ sunday>= ] }
187         { "Mon" [ monday>= ] }
188         { "Tue" [ tuesday>= ] }
189         { "Wed" [ wednesday>= ] }
190         { "Thu" [ thursday>= ] }
191         { "Fri" [ friday>= ] }
192         { "Sat" [ saturday>= ] }
193         [ unknown-day-abbrev ]
194     } case ;
195
196 : day-abbrev<= ( timestamp day -- timestamp' )
197     {
198         { "Sun" [ sunday<= ] }
199         { "Mon" [ monday<= ] }
200         { "Tue" [ tuesday<= ] }
201         { "Wed" [ wednesday<= ] }
202         { "Thu" [ thursday<= ] }
203         { "Fri" [ friday<= ] }
204         { "Sat" [ saturday<= ] }
205         [ unknown-day-abbrev ]
206     } case ;
207
208 : comparison-day-string ( timestamp string -- timestamp )
209     {
210         { [ ">=" over subseq? ] [ ">=" split1 swap [ string>number >>day ] dip day-abbrev>= ] }
211         { [ "<=" over subseq? ] [ "<=" split1 swap [ string>number >>day ] dip day-abbrev<= ] }
212         [ string>number >>day ]
213     } cond ;
214         
215 ERROR: unknown-last-day string ;
216
217 : last-day-string ( timestamp string -- timestamp )
218     {
219         { "lastSun" [ last-sunday-of-month ] }
220         { "lastMon" [ last-monday-of-month ] }
221         { "lastTue" [ last-tuesday-of-month ] }
222         { "lastWed" [ last-wednesday-of-month ] }
223         { "lastThu" [ last-thursday-of-month ] }
224         { "lastFri" [ last-friday-of-month ] }
225         { "lastSat" [ last-saturday-of-month ] }
226         [ unknown-last-day ]
227     } case ;
228
229 !  "lastFri" | "Fri<=1" | "Sat>=2" | "15"
230 : zone-day ( timestamp text -- timestamp' )
231     dup "last" head? [
232         last-day-string
233     ] [
234         comparison-day-string
235     ] if ;
236
237 : string>year ( str -- year )
238     string>number <year-gmt> ;
239
240 : rule-year>years ( rule -- from to )
241     [ from>> ] [ to>> ] bi
242     {
243         { [ over "min" = ] [ [ drop -1/0. ] [ string>year ] bi* ] }
244         { [ dup "max" = ] [ [ string>year ] [ drop 1/0. ] bi* ] }
245         { [ dup "only" = ] [ drop dup [ string>year ] bi@ ] }
246         [ [ string>year ] bi@ ]
247     } cond ;
248
249 : parse-hms ( str -- hms-seq )
250     ":" split [ string>number ] map 3 0 pad-tail ;
251
252 : parse-offset ( str -- hms-seq )
253     "-" ?head [ parse-hms ] dip [ [ neg ] map ] when ;
254
255 ! XXX: Don't just drop the s/u, e.g. 2:00:00s
256 : zone-time ( timestamp time -- timestamp' )
257     [ Letter? ] split-tail drop
258     parse-offset first3 set-time ;
259
260 : hm>duration ( str -- duration )
261     ":" split1 "0" or [ string>number ] bi@
262     [ instant ] 2dip 0 set-time ;
263
264 : rule>timestamp-rest ( timestamp zone -- from )
265     {
266         [ over fp-infinity? [ drop ] [ in>> month-abbreviation-index >>month ] if ]
267         [ over fp-infinity? [ drop ] [ on>> zone-day ] if ]
268         [ over fp-infinity? [ drop ] [ at-time>> zone-time ] if ]
269     } cleave ;
270
271 : rule>timestamps ( zone -- from to )
272     [ rule-year>years ] keep
273     [ nip rule>timestamp-rest ]
274     [ nipd rule>timestamp-rest ] 3bi ;
275
276 : until>timestamp ( seq -- unix-time )
277     [ 1/0. ] [
278         4 f pad-tail first4 {
279             [ string>number <year-gmt> ]
280             [ [ zone-month ] when* ]
281             [ [ zone-day ] when* ]
282             [ [ zone-time ] when* ]
283         } spread timestamp>unix-time
284     ] if-empty ;
285
286 : zones>interval-map ( zones -- interval-map )
287     [
288         [ until>> until>timestamp ] map
289         -1/0. prefix 2 <clumps> [ >array ] map
290     ] keep zip
291     [ first2 1 - 2array ] map-keys <interval-map> ;
292
293 : name>zones ( name -- interval-map )
294     raw-zone-map at zones>interval-map ;
295
296 : gmt-offset ( timestamp name -- gmt-offset )
297     [ timestamp>unix-time ]
298     [ zones>interval-map ] bi* interval-at ;
299
300 : name>rules ( name -- rules )
301     raw-rule-map at [
302         [
303             [ rule>timestamps [ dup fp-infinity? [ timestamp>unix-time ] unless ] bi@ 2array ]
304             [ [ save>> hm>duration ] [ letters>> ] bi 2array ] bi 2array
305         ] map
306     ] keep zip ;
307
308 : chicago-zones ( -- interval-map ) "America/Chicago" name>zones ;
309 : us-rules ( -- rules ) "US" name>rules ;