]> gitweb.factorcode.org Git - factor.git/blob - extra/metar/metar.factor
metar: fix some international metar parsing.
[factor.git] / extra / metar / metar.factor
1 ! Copyright (C) 2013 John Benediktsson
2 ! See http://factorcode.org/license.txt for BSD license
3
4 USING: accessors arrays ascii assocs calendar calendar.format
5 combinators command-line continuations csv formatting fry
6 grouping http.client io io.encodings.ascii io.files io.styles
7 kernel math math.extras math.parser memoize namespaces regexp
8 sequences sorting.human splitting strings urls wrap.strings ;
9
10 IN: metar
11
12 TUPLE: station cccc name state country latitude longitude ;
13
14 C: <station> station
15
16 <PRIVATE
17
18 ERROR: bad-location str ;
19
20 : parse-location ( str -- n )
21     "-" split dup length {
22         { 3 [ first3 [ string>number ] tri@ 60.0 / + 60.0 / + ] }
23         { 2 [ first2 [ string>number ] bi@ 60.0 / + ] }
24         { 1 [ first string>number ] }
25         [ drop bad-location ]
26     } case ;
27
28 : string>longitude ( str -- lon/f )
29     dup R/ \d+-\d+(-\d+(\.\d+)?)?[WE]/ matches? [
30         unclip-last
31         [ parse-location ]
32         [ CHAR: W = [ neg ] when ] bi*
33     ] [ drop f ] if ;
34
35 : string>latitude ( str -- lat/f )
36     dup R/ \d+-\d+(-\d+(\.\d+)?)?[NS]/ matches? [
37         unclip-last
38         [ parse-location ]
39         [ CHAR: S = [ neg ] when ] bi*
40     ] [ drop f ] if ;
41
42 : stations-data ( -- seq )
43     URL" http://tgftp.nws.noaa.gov/data/nsd_cccc.txt"
44     http-get nip CHAR: ; [ string>csv ] with-delimiter ;
45
46 PRIVATE>
47
48 MEMO: all-stations ( -- seq )
49     stations-data [
50         {
51             [ 0 swap nth ]
52             [ 3 swap nth ]
53             [ 4 swap nth ]
54             [ 5 swap nth ]
55             [ 7 swap nth string>latitude ]
56             [ 8 swap nth string>longitude ]
57         } cleave <station>
58     ] map ;
59
60 : find-by-cccc ( cccc -- station )
61     all-stations swap '[ cccc>> _ = ] find nip ;
62
63 : find-by-country ( country -- stations )
64     all-stations swap '[ country>> _ = ] filter ;
65
66 : find-by-state ( state -- stations )
67     all-stations swap '[ state>> _ = ] filter ;
68
69 <PRIVATE
70
71 TUPLE: metar-report type station timestamp modifier wind
72 visibility rvr weather sky-condition temperature dew-point
73 altimeter remarks raw ;
74
75 CONSTANT: pressure-tendency H{
76     { "0" "increasing then decreasing" }
77     { "1" "increasing more slowly" }
78     { "2" "increasing" }
79     { "3" "increasing more quickly" }
80     { "4" "steady" }
81     { "5" "decreasing then increasing" }
82     { "6" "decreasing more slowly" }
83     { "7" "decreasing" }
84     { "8" "decreasing more quickly" }
85 }
86
87 CONSTANT: lightning H{
88     { "CA" "cloud-air lightning" }
89     { "CC" "cloud-cloud lightning" }
90     { "CG" "cloud-ground lightning" }
91     { "IC" "in-cloud lightning" }
92 }
93
94 CONSTANT: weather H{
95     { "BC" "patches" }
96     { "BL" "blowing" }
97     { "BR" "mist" }
98     { "DR" "low drifting" }
99     { "DS" "duststorm" }
100     { "DU" "widespread dust" }
101     { "DZ" "drizzle" }
102     { "FC" "funnel clouds" }
103     { "FG" "fog" }
104     { "FU" "smoke" }
105     { "FZ" "freezing" }
106     { "GR" "hail" }
107     { "GS" "small hail and/or snow pellets" }
108     { "HZ" "haze" }
109     { "IC" "ice crystals" }
110     { "MI" "shallow" }
111     { "PL" "ice pellets" }
112     { "PO" "well-developed dust/sand whirls" }
113     { "PR" "partial" }
114     { "PY" "spray" }
115     { "RA" "rain" }
116     { "RE" "recent" }
117     { "SA" "sand" }
118     { "SG" "snow grains" }
119     { "SH" "showers" }
120     { "SN" "snow" }
121     { "SQ" "squalls" }
122     { "SS" "sandstorm" }
123     { "TS" "thuderstorm" }
124     { "UP" "unknown" }
125     { "VA" "volcanic ash" }
126 }
127
128 MEMO: glossary ( -- assoc )
129     "vocab:metar/glossary.txt" ascii file-lines
130     [ "," split1 ] H{ } map>assoc ;
131
132 : parse-glossary ( str -- str' )
133     "/" split [
134         find-numbers [
135             dup number?
136             [ number>string ]
137             [ glossary ?at drop ] if
138         ] map " " join
139     ] map "/" join ;
140
141 : parse-timestamp ( str -- str' )
142     [ now [ year>> ] [ month>> ] bi ] dip
143     2 cut 2 cut 2 cut drop [ string>number ] tri@
144     over 24 = [
145         [ drop 0 ] dip 0 instant <timestamp> 1 days time+
146     ] [
147         0 instant <timestamp>
148     ] if timestamp>rfc822 ;
149
150 CONSTANT: compass-directions H{
151     { 0.0 "N" }
152     { 22.5 "NNE" }
153     { 45.0 "NE" }
154     { 67.5 "ENE" }
155     { 90.0 "E" }
156     { 112.5 "ESE" }
157     { 135.0 "SE" }
158     { 157.5 "SSE" }
159     { 180.0 "S" }
160     { 202.5 "SSW" }
161     { 225.0 "SW" }
162     { 247.5 "WSW" }
163     { 270.0 "W" }
164     { 292.5 "WNW" }
165     { 315.0 "NW" }
166     { 337.5 "NNW" }
167     { 360.0 "N" }
168 }
169
170 : direction>compass ( direction -- compass )
171     22.5 round-to-step compass-directions at ;
172
173 : parse-compass ( str -- str' )
174     string>number [ direction>compass ] keep "%s (%s°)" sprintf ;
175
176 : parse-direction ( str -- str' )
177     dup "VRB" = [ drop "variable" ] [
178         parse-compass "from %s" sprintf
179     ] if ;
180
181 : kt>mph ( kt -- mph ) 1.15077945 * ;
182
183 : mph>kt ( mph -- kt ) 1.15077945 / ;
184
185 : parse-speed ( str units -- str'/f )
186     [ string>number ] dip '[
187         _ dup "knots" =
188         [ drop dup kt>mph "%s knots (%.1f mph)" sprintf ]
189         [ "%s %s" sprintf ] if
190     ] [ f ] if* ;
191
192 : parse-wind ( str -- str' )
193     dup "00000" head? [ drop "calm" ] [
194         "/" split1 [ 3 cut ] unless*
195         [ parse-direction ] dip {
196             { [ "KT" ?tail ] [ "knots" ] }
197             { [ "MPS" ?tail ] [ "meters per second" ] }
198         } cond [ "G" split1 ] dip '[ _ parse-speed ] bi@
199         [ "%s at %s with gusts to %s " sprintf ]
200         [ "%s at %s" sprintf ] if*
201     ] if ;
202
203 : parse-wind-variable ( str -- str' )
204     "V" split1 [ parse-compass ] bi@
205     ", variable from %s to %s" sprintf ;
206
207 : parse-visibility ( str -- str' )
208     "SM" ?tail [
209         dup first {
210             { CHAR: M [ rest "less than " ] }
211             { CHAR: P [ rest "more than " ] }
212             [ drop "" ]
213         } case swap
214         CHAR: \s over index [ " " "+" replace ] when
215         string>number "%s%s statute miles" sprintf
216     ] [
217         4 cut [
218             string>number {
219                 { [ dup 800 < ] [ "%dm" sprintf ] }
220                 { [ dup 5000 < ] [ 1000 /f "%.1fkm" sprintf ] }
221                 { [ dup 9999 < ] [ 1000 /f "%dkm" sprintf ] }
222                 [ drop "more than 10km" ]
223             } cond
224         ] dip [
225             [
226                 H{
227                     { CHAR: N "north" }
228                     { CHAR: E "east" }
229                     { CHAR: S "south" }
230                     { CHAR: W "west" }
231                 } at
232             ] { } map-as unclip-last
233             [ "-" join ] dip append " " glue
234         ] unless-empty
235     ] if ;
236
237 : parse-rvr ( str -- str' )
238     {
239         { [ "U" ?tail ] [ " with improvement" ] }
240         { [ "D" ?tail ] [ " with aggravation" ] }
241         { [ "N" ?tail ] [ " with no change" ] }
242         [ "" ]
243     } cond [
244         "R" ?head drop "/" split1 "FT" ?tail [
245             "V" split1 [
246                 [ string>number ] bi@
247                 "varying between %s and %s" sprintf
248             ] [
249                 string>number "of %s" sprintf
250             ] if* "runway %s visibility %s" sprintf
251          ] dip " ft" " meters" ? append
252     ] dip append ;
253
254 : (parse-weather) ( str -- str' )
255     dup "+FC" = [ drop "tornadoes or waterspouts" ] [
256         dup first {
257             { CHAR: + [ rest "heavy " ] }
258             { CHAR: - [ rest "light " ] }
259             [ drop f ]
260         } case [
261             2 group dup [ weather key? ] all?
262             [ [ weather at ] map " " join ]
263             [ concat parse-glossary ] if
264         ] dip prepend
265     ] if ;
266
267 : parse-weather ( str -- str' )
268     "VC" over subseq? [ "VC" "" replace t ] [ f ] if
269     [ (parse-weather) ]
270     [ [ " in the vicinity" append ] when ] bi* ;
271
272 : parse-altitude ( str -- str' )
273     string>number " at %s00 ft" sprintf ;
274
275 CONSTANT: sky H{
276     { "BKN" "broken" }
277     { "FEW" "few" }
278     { "OVC" "overcast" }
279     { "SCT" "scattered" }
280     { "SKC" "clear sky" }
281     { "CLR" "clear sky" }
282     { "NSC" "clear sky" }
283
284     { "ACC" "altocumulus castellanus" }
285     { "ACSL" "standing lenticular altocumulus" }
286     { "CCSL" "cirrocumulus standing lenticular cloud" }
287     { "CU" "cumulus" }
288     { "SC" "stratocumulus" }
289     { "SCSL" "stratocumulus standing lenticular cloud" }
290     { "TCU" "towering cumulus" }
291 }
292
293 : parse-sky-condition ( str -- str' )
294     sky ?at [
295         3 cut 3 cut
296         [ sky at ]
297         [ parse-altitude ]
298         [ sky at [ " (%s)" sprintf ] [ f ] if* ]
299         tri* 3append
300     ] unless ;
301
302 : F>C ( F -- C ) 32 - 5/9 * ;
303
304 : C>F ( C -- F ) 9/5 * 32 + ;
305
306 : parse-temperature ( str -- temp dew-point )
307     "/" split1 [
308         [ f ] [
309             "M" ?head [ string>number ] [ [ neg ] when ] bi*
310             dup C>F "%d °C (%.1f °F)" sprintf
311         ] if-empty
312     ] bi@ ;
313
314 : parse-altimeter ( str -- str' )
315     unclip [ string>number ] [ CHAR: A = ] bi*
316     [ 100 /f "%.2f Hg" sprintf ] [ "%s hPa" sprintf ] if ;
317
318 CONSTANT: re-timestamp R/ \d{6}Z/
319 CONSTANT: re-station R/ \w{4}/
320 CONSTANT: re-temperature R/ [M]?\d{2}\/([M]?\d{2})?/
321 CONSTANT: re-wind R/ (VRB|\d{3})(\/\d+|\d{2,3})(G\d{2,3})?(KT|MPS)/
322 CONSTANT: re-wind-variable R/ \d{3}V\d{3}/
323 CONSTANT: re-visibility R/ ((\d+|[MP])?\d+(\/\d+)?SM|\d{4}[NSEW]{0,2})/
324 CONSTANT: re-rvr R/ R\d{2}[RLC]?\/[MP]?\d{4}(V\d{4})?(FT)?[UDN]?/
325 CONSTANT: re-weather R/ [+-]?(VC)?(\w{2}|\w{4})/
326 CONSTANT: re-sky-condition R/ (\w{2,3}\d{3}(\w+)?|\w{3}|CAVOK)/
327 CONSTANT: re-altimeter R/ [AQ]\d{4}/
328
329 : find-one ( seq quot: ( elt -- ? ) -- seq' elt/f )
330     dupd find [ [ swap remove-nth ] when* ] dip ; inline
331
332 : find-all ( seq quot: ( elt -- ? ) -- seq elts )
333     [ dupd find drop ] keep '[
334         cut
335         [ dup ?first _ [ f ] if* ] [ unclip ] produce
336         [ append ] dip
337     ] [ f ] if* ; inline
338
339 : fix-visibility ( seq -- seq' )
340     dup [ R/ \d+(\/\d+)?SM/ matches? ] find drop [
341         dup 1 - pick ?nth [ R/ \d+/ matches? ] [ f ] if* [
342             cut [ unclip-last ] [ unclip swap ] bi*
343             [ " " glue 1array ] [ 3append ] bi*
344         ] [ drop ] if
345     ] when* ;
346
347 : metar-body ( report seq -- report )
348     [ { "METAR" "SPECI" } member? ] find-one
349     [ pick type<< ] when*
350
351     [ re-station matches? ] find-one
352     [ pick station<< ] when*
353
354     [ re-timestamp matches? ] find-one
355     [ parse-timestamp pick timestamp<< ] when*
356
357     [ { "AUTO" "COR" } member? ] find-one
358     [ pick modifier<< ] when*
359
360     [ re-wind matches? ] find-one
361     [ parse-wind pick wind<< ] when*
362
363     [ re-wind-variable matches? ] find-one
364     [ parse-wind-variable pick wind>> prepend pick wind<< ] when*
365
366     fix-visibility
367     [ re-visibility matches? ] find-all
368     [ parse-visibility ] map ", " join pick visibility<<
369
370     [ re-rvr matches? ] find-all
371     [ parse-rvr ] map ", " join pick rvr<<
372
373     [ re-weather matches? ] find-all
374     [ parse-weather ] map ", " join pick weather<<
375
376     [ re-sky-condition matches? ] find-all
377     [ parse-sky-condition ] map ", " join pick sky-condition<<
378
379     [ re-temperature matches? ] find-one
380     [
381         parse-temperature
382         [ pick temperature<< ]
383         [ pick dew-point<< ] bi*
384     ] when*
385
386     [ re-altimeter matches? ] find-one
387     [ parse-altimeter pick altimeter<< ] when*
388
389     drop ;
390
391 : signed-number ( sign value -- n )
392     [ string>number ] bi@ swap zero? [ neg ] unless 10.0 / ;
393
394 : single-value ( str -- str' )
395     1 cut signed-number ;
396
397 : double-value ( str -- m n )
398     1 cut 3 cut [ signed-number ] dip 1 cut signed-number ;
399
400 : parse-1hr-temp ( str -- str' )
401     "T" ?head drop dup length 4 > [
402         double-value
403         [ dup C>F "%.1f °C (%.1f °F)" sprintf ] bi@
404         "hourly temperature %s and dew point %s" sprintf
405     ] [
406         single-value dup C>F
407         "hourly temperature %.1f °C (%.1f °F)" sprintf
408     ] if ;
409
410 : parse-6hr-max-temp ( str -- str' )
411     "1" ?head drop single-value dup C>F
412     "6-hour maximum temperature %.1f °C (%.1f °F)" sprintf ;
413
414 : parse-6hr-min-temp ( str -- str' )
415     "2" ?head drop single-value dup C>F
416     "6-hour minimum temperature %.1f °C (%.1f °F)" sprintf ;
417
418 : parse-24hr-temp ( str -- str' )
419     "4" ?head drop double-value
420     [ dup C>F "%.1f °C (%.1f °F)" sprintf ] bi@
421     "24-hour maximum temperature %s minimum temperature %s"
422     sprintf ;
423
424 : parse-1hr-pressure ( str -- str' )
425     "5" ?head drop 1 cut single-value [ pressure-tendency at ] dip
426     "hourly pressure %s %s hPa" sprintf ;
427
428 : parse-snow-depth ( str -- str' )
429     "4/" ?head drop string>number "snow depth %s inches" sprintf ;
430
431 CONSTANT: low-clouds H{
432     { 1 "cumulus (fair weather)" }
433     { 2 "cumulus (towering)" }
434     { 3 "cumulonimbus (no anvil)" }
435     { 4 "stratocumulus (from cumulus)" }
436     { 5 "stratocumuls (not cumulus)" }
437     { 6 "stratus or Fractostratus (fair)" }
438     { 7 "fractocumulus / fractostratus (bad weather)" }
439     { 8 "cumulus and stratocumulus" }
440     { 9 "cumulonimbus (thunderstorm)" }
441     { -1 "not valid" }
442 }
443
444 CONSTANT: mid-clouds H{
445     { 1 "altostratus (thin)" }
446     { 2 "altostratus (thick)" }
447     { 3 "altocumulus (thin)" }
448     { 4 "altocumulus (patchy)" }
449     { 5 "altocumulus (thickening)" }
450     { 6 "altocumulus (from cumulus)" }
451     { 7 "altocumulus (with altocumulus, altostratus, nimbostratus)" }
452     { 8 "altocumulus (with turrets)" }
453     { 9 "altocumulus (chaotic)" }
454     { -1 "above overcast" }
455 }
456
457 CONSTANT: high-clouds H{
458     { 1 "cirrus (filaments)" }
459     { 2 "cirrus (dense)" }
460     { 3 "cirrus (often with cumulonimbus)" }
461     { 4 "cirrus (thickening)" }
462     { 5 "cirrus / cirrostratus (low in sky)" }
463     { 6 "cirrus / cirrostratus (hi in sky)" }
464     { 7 "cirrostratus (entire sky)" }
465     { 8 "cirrostratus (partial)" }
466     { 9 "cirrocumulus or cirrocumulus / cirrus / cirrostratus" }
467     { -1 "above overcast" }
468 }
469
470 : parse-cloud-cover ( str -- str' )
471     "8/" ?head drop first3 [ CHAR: 0 - ] tri@
472     [ [ f ] [ low-clouds at "low clouds are %s" sprintf ] if-zero ]
473     [ [ f ] [ mid-clouds at "middle clouds are %s" sprintf ] if-zero ]
474     [ [ f ] [ high-clouds at "high clouds are %s" sprintf ] if-zero ]
475     tri* 3array " " join ;
476
477 : parse-inches ( str -- str' )
478     dup [ CHAR: / = ] all? [ drop "unknown" ] [
479         string>number
480         [ "trace" ] [ 100 /f "%.2f inches" sprintf ] if-zero
481     ] if ;
482
483 : parse-1hr-precipitation ( str -- str' )
484     "P" ?head drop parse-inches
485     "%s precipitation in last hour" sprintf ;
486
487 : parse-6hr-precipitation ( str -- str' )
488     "6" ?head drop parse-inches
489     "%s precipitation in last 6 hours" sprintf ;
490
491 : parse-24hr-precipitation ( str -- str' )
492     "7" ?head drop parse-inches
493     "%s precipitation in last 24 hours" sprintf ;
494
495 ! XXX: "on the hour" instead of "00 minutes past the hour" ?
496
497 : parse-recent-time ( str -- str' )
498     dup length 2 >
499     [ 2 cut ":" glue ]
500     [ " minutes past the hour" append ] if ;
501
502 : parse-peak-wind ( str -- str' )
503     "/" split1 [ parse-wind ] [ parse-recent-time ] bi*
504     "%s occuring at %s" sprintf ;
505
506 : parse-sea-level-pressure ( str -- str' )
507     "SLP" ?head drop string>number 10.0 /f 1000 +
508     "sea-level pressure is %s hPa" sprintf ;
509
510 : parse-lightning ( str -- str' )
511     "LTG" ?head drop 2 group [ lightning at ] map " " join ;
512
513 CONSTANT: re-recent-weather R/ ((\w{2})?[BE]\d{2,4}((\w{2})?[BE]\d{2,4})?)+/
514
515 : parse-began/ended ( str -- str' )
516     unclip swap
517     [ CHAR: B = "began" "ended" ? ]
518     [ parse-recent-time ] bi* "%s at %s" sprintf ;
519
520 : split-recent-weather ( str -- seq )
521     [ dup empty? not ] [
522         dup [ digit? ] find drop
523         over [ digit? not ] find-from drop
524         [ cut ] [ f ] if* swap
525     ] produce nip ;
526
527 : (parse-recent-weather) ( str -- str' )
528     dup [ digit? ] find drop 2 > [
529         2 cut [ weather at " " append ] dip
530     ] [ f swap ] if parse-began/ended "" append-as ;
531
532 : parse-recent-weather ( str -- str' )
533     split-recent-weather
534     [ (parse-recent-weather) ] map " " join ;
535
536 : parse-varying ( str -- str' )
537     "V" split1 [ string>number ] bi@
538     "varying between %s00 and %s00 ft" sprintf ;
539
540 : parse-from-to ( str -- str' )
541     "-" split [ parse-glossary ] map " to " join ;
542
543 : parse-water-equivalent-snow ( str -- str' )
544     "933" ?head drop parse-inches
545     "%s water equivalent of snow on ground" sprintf ;
546
547 : parse-duration-of-sunshine ( str -- str' )
548     "98" ?head drop string>number
549     [ "no" ] [ "%s minutes of" sprintf ] if-zero
550     "%s sunshine" sprintf ;
551
552 : parse-6hr-snowfall ( str -- str' )
553     "931" ?head drop parse-inches
554     "%s snowfall in last 6 hours" sprintf ;
555
556 : parse-probability ( str -- str' )
557     "PROB" ?head drop string>number
558     "probability of %d%%" sprintf ;
559
560 : parse-remark ( str -- str' )
561     {
562         { [ dup glossary key? ] [ glossary at ] }
563         { [ dup R/ 1\d{4}/ matches? ] [ parse-6hr-max-temp ] }
564         { [ dup R/ 2\d{4}/ matches? ] [ parse-6hr-min-temp ] }
565         { [ dup R/ 4\d{8}/ matches? ] [ parse-24hr-temp ] }
566         { [ dup R/ 4\/\d{3}/ matches? ] [ parse-snow-depth ] }
567         { [ dup R/ 5\d{4}/ matches? ] [ parse-1hr-pressure ] }
568         { [ dup R/ 6[\d\/]{4}/ matches? ] [ parse-6hr-precipitation ] }
569         { [ dup R/ 7\d{4}/ matches? ] [ parse-24hr-precipitation ] }
570         { [ dup R/ 8\/\d{3}/ matches? ] [ parse-cloud-cover ] }
571         { [ dup R/ 931\d{3}/ matches? ] [ parse-6hr-snowfall ] }
572         { [ dup R/ 933\d{3}/ matches? ] [ parse-water-equivalent-snow ] }
573         { [ dup R/ 98\d{3}/ matches? ] [ parse-duration-of-sunshine ] }
574         { [ dup R/ T\d{4,8}/ matches? ] [ parse-1hr-temp ] }
575         { [ dup R/ \d{3}\d{2,3}\/\d{2,4}/ matches? ] [ parse-peak-wind ] }
576         { [ dup R/ P\d{4}/ matches? ] [ parse-1hr-precipitation ] }
577         { [ dup R/ SLP\d{3}/ matches? ] [ parse-sea-level-pressure ] }
578         { [ dup R/ LTG\w+/ matches? ] [ parse-lightning ] }
579         { [ dup R/ PROB\d+/ matches? ] [ parse-probability ] }
580         { [ dup R/ \d{3}V\d{3}/ matches? ] [ parse-varying ] }
581         { [ dup R/ [^-]+(-[^-]+)+/ matches? ] [ parse-from-to ] }
582         { [ dup R/ [^\/]+(\/[^\/]+)+/ matches? ] [ ] }
583         { [ dup R/ \d+.\d+/ matches? ] [ ] }
584         { [ dup re-recent-weather matches? ] [ parse-recent-weather ] }
585         { [ dup re-weather matches? ] [ parse-weather ] }
586         { [ dup re-sky-condition matches? ] [ parse-sky-condition ] }
587         [ parse-glossary ]
588     } cond ;
589
590 : metar-remarks ( report seq -- report )
591     [ parse-remark ] map " " join >>remarks ;
592
593 : <metar-report> ( metar -- report )
594     [ metar-report new ] dip [ >>raw ] keep
595     [ blank? ] split-when { "RMK" } split1
596     [ metar-body ] [ metar-remarks ] bi* ;
597
598 : row. ( name quot -- )
599     '[
600         [ _ write ] with-cell
601         [ @ [ 65 wrap-string write ] when* ] with-cell
602     ] with-row ; inline
603
604 : metar-report. ( report -- )
605     standard-table-style [
606         {
607             [ "Station" [ station>> ] row. ]
608             [ "Timestamp" [ timestamp>> ] row. ]
609             [ "Wind" [ wind>> ] row. ]
610             [ "Visibility" [ visibility>> ] row. ]
611             [ "RVR" [ rvr>> ] row. ]
612             [ "Weather" [ weather>> ] row. ]
613             [ "Sky condition" [ sky-condition>> ] row. ]
614             [ "Temperature" [ temperature>> ] row. ]
615             [ "Dew point" [ dew-point>> ] row. ]
616             [ "Altimeter" [ altimeter>> ] row. ]
617             [ "Remarks" [ remarks>> ] row. ]
618             [ "Raw Text" [ raw>> ] row. ]
619         } cleave
620     ] tabular-output nl ;
621
622 PRIVATE>
623
624 GENERIC: metar ( station -- metar )
625
626 M: station metar cccc>> metar ;
627
628 M: string metar
629     "http://tgftp.nws.noaa.gov/data/observations/metar/stations/%s.TXT"
630     sprintf http-get nip ;
631
632 GENERIC: metar. ( station -- )
633
634 M: station metar. cccc>> metar. ;
635
636 M: string metar.
637     [ metar <metar-report> metar-report. ]
638     [ drop "%s METAR not found\n" printf ] recover ;
639
640 <PRIVATE
641
642 : parse-wind-shear ( str -- str' )
643     "WS" ?head drop "/" split1
644     [ parse-altitude ] [ parse-wind ] bi* prepend
645     "wind shear " prepend ;
646
647 CONSTANT: re-from-timestamp R/ FM\d{6}/
648
649 : parse-from-timestamp ( str -- str' )
650     "FM" ?head drop parse-timestamp ;
651
652 CONSTANT: re-valid-timestamp R/ \d{4}\/\d{4}/
653
654 : parse-valid-timestamp ( str -- str' )
655     "/" split1 [ "00" append parse-timestamp ] bi@ " to " glue ;
656
657 TUPLE: taf-report station timestamp valid-timestamp wind
658 visibility rvr weather sky-condition partials raw ;
659
660 TUPLE: taf-partial from-timestamp wind visibility rvr weather
661 sky-condition raw ;
662
663 : taf-body ( report str -- report )
664     [ blank? ] split-when
665
666     [ "TAF" = ] find-one drop
667
668     [ { "AMD" "COR" "RTD" } member? ] find-one drop
669
670     [ re-station matches? ] find-one
671     [ pick station<< ] when*
672
673     [ re-timestamp matches? ] find-one
674     [ parse-timestamp pick timestamp<< ] when*
675
676     [ re-valid-timestamp matches? ] find-one
677     [ parse-valid-timestamp pick valid-timestamp<< ] when*
678
679     [ re-wind matches? ] find-one
680     [ parse-wind pick wind<< ] when*
681
682     [ re-wind-variable matches? ] find-one
683     [ parse-wind-variable pick wind>> prepend pick wind<< ] when*
684
685     [ re-visibility matches? ] find-one
686     [ parse-visibility pick visibility<< ] when*
687
688     [ re-rvr matches? ] find-all " " join
689     [ parse-rvr ] map ", " join pick rvr<<
690
691     [ re-weather matches? ] find-all
692     [ parse-weather ] map ", " join pick weather<<
693
694     [ re-sky-condition matches? ] find-all
695     [ parse-sky-condition ] map ", " join pick sky-condition<<
696
697     drop ;
698
699 : <taf-partial> ( str -- partial )
700     [ taf-partial new ] dip [ blank? ] split-when
701
702     [ re-from-timestamp matches? ] find-one
703     [ parse-from-timestamp pick from-timestamp<< ] when*
704
705     [ re-wind matches? ] find-one
706     [ parse-wind pick wind<< ] when*
707
708     [ re-wind-variable matches? ] find-one
709     [ parse-wind-variable pick wind>> prepend pick wind<< ] when*
710
711     [ re-visibility matches? ] find-one
712     [ parse-visibility pick visibility<< ] when*
713
714     [ re-rvr matches? ] find-all " " join
715     [ parse-rvr ] map ", " join pick rvr<<
716
717     [ re-weather matches? ] find-all
718     [ parse-weather ] map ", " join pick weather<<
719
720     [ re-sky-condition matches? ] find-all
721     [ parse-sky-condition ] map ", " join pick sky-condition<<
722
723     drop ;
724
725 : taf-partials ( report seq -- report )
726     [ <taf-partial> ] map >>partials ;
727
728 : <taf-report> ( taf -- report )
729     [ taf-report new ] dip [ >>raw ] keep
730     string-lines [ [ blank? ] trim ] map
731     rest dup first "TAF" = [ rest ] when
732     harvest unclip swapd taf-body swap taf-partials ;
733
734 : taf-report. ( report -- )
735     [
736         standard-table-style [
737             {
738                 [ "Station" [ station>> ] row. ]
739                 [ "Timestamp" [ timestamp>> ] row. ]
740                 [ "Valid From" [ valid-timestamp>> ] row. ]
741                 [ "Wind" [ wind>> ] row. ]
742                 [ "Visibility" [ visibility>> ] row. ]
743                 [ "RVR" [ rvr>> ] row. ]
744                 [ "Weather" [ weather>> ] row. ]
745                 [ "Sky condition" [ sky-condition>> ] row. ]
746                 [ "Raw Text" [ raw>> ] row. ]
747             } cleave
748         ] tabular-output nl
749     ] [
750         partials>> [
751             standard-table-style [
752                 {
753                     [ "From" [ from-timestamp>> ] row. ]
754                     [ "Wind" [ wind>> ] row. ]
755                     [ "Visibility" [ visibility>> ] row. ]
756                     [ "RVR" [ rvr>> ] row. ]
757                     [ "Weather" [ weather>> ] row. ]
758                     [ "Sky condition" [ sky-condition>> ] row. ]
759                 } cleave
760             ] tabular-output nl
761         ] each
762     ] bi ;
763
764 PRIVATE>
765
766 GENERIC: taf ( station -- taf )
767
768 M: station taf cccc>> taf ;
769
770 M: string taf
771     "http://tgftp.nws.noaa.gov/data/forecasts/taf/stations/%s.TXT"
772     sprintf http-get nip ;
773
774 GENERIC: taf. ( station -- )
775
776 M: station taf. cccc>> taf. ;
777
778 M: string taf.
779     [ taf <taf-report> taf-report. ]
780     [ drop "%s TAF not found\n" printf ] recover ;
781
782 : metar-main ( -- )
783     command-line get [
784         [ metar print ] [ taf print ] bi nl
785     ] each ;
786
787 MAIN: metar-main