1 ! Copyright (C) 2013 John Benediktsson
2 ! See http://factorcode.org/license.txt for BSD license
4 USING: accessors arrays ascii assocs calendar calendar.format
5 combinators continuations csv formatting fry grouping
6 http.client io io.encodings.ascii io.files io.styles kernel math
7 math.extras math.parser memoize regexp sequences sorting.human
8 splitting strings urls wrap.strings ;
12 TUPLE: station cccc name state country latitude longitude ;
18 ERROR: bad-location str ;
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 ] }
28 : string>longitude ( str -- lon/f )
29 dup R/ \d+-\d+(-\d+(\.\d+)?)?[WE]/ matches? [
32 [ CHAR: W = [ neg ] when ] bi*
35 : string>latitude ( str -- lat/f )
36 dup R/ \d+-\d+(-\d+(\.\d+)?)?[NS]/ matches? [
39 [ CHAR: S = [ neg ] when ] bi*
42 : stations-data ( -- seq )
43 URL" http://weather.noaa.gov/data/nsd_cccc.txt"
44 http-get nip CHAR: ; [ string>csv ] with-delimiter ;
48 MEMO: all-stations ( -- seq )
55 [ 7 swap nth string>latitude ]
56 [ 8 swap nth string>longitude ]
60 : find-by-cccc ( cccc -- station )
61 all-stations swap '[ cccc>> _ = ] find nip ;
63 : find-by-country ( country -- stations )
64 all-stations swap '[ country>> _ = ] filter ;
66 : find-by-state ( state -- stations )
67 all-stations swap '[ state>> _ = ] filter ;
71 TUPLE: metar-report type station timestamp modifier wind
72 visibility rvr weather sky-condition temperature dew-point
73 altimeter remarks raw ;
75 CONSTANT: pressure-tendency H{
76 { "0" "increasing then decreasing" }
77 { "1" "increasing more slowly" }
79 { "3" "increasing more quickly" }
81 { "5" "decreasing then increasing" }
82 { "6" "decreasing more slowly" }
84 { "8" "decreasing more quickly" }
87 CONSTANT: lightning H{
88 { "CA" "cloud-air lightning" }
89 { "CC" "cloud-cloud lightning" }
90 { "CG" "cloud-ground lightning" }
91 { "IC" "in-cloud lightning" }
98 { "DR" "low drifting" }
100 { "DU" "widespread dust" }
102 { "FC" "funnel clouds" }
107 { "GS" "small hail and/or snow pellets" }
109 { "IC" "ice crystals" }
111 { "PL" "ice pellets" }
112 { "PO" "well-developed dust/sand whirls" }
118 { "SG" "snow grains" }
123 { "TS" "thuderstorm" }
125 { "VA" "volcanic ash" }
128 MEMO: glossary ( -- assoc )
129 "vocab:metar/glossary.txt" ascii file-lines
130 [ "," split1 ] H{ } map>assoc ;
132 : parse-glossary ( str -- str' )
137 [ glossary ?at drop ] if
141 : parse-timestamp ( str -- str' )
142 [ now [ year>> ] [ month>> ] bi ] dip
143 2 cut 2 cut 2 cut drop [ string>number ] tri@
144 0 instant <timestamp> timestamp>rfc822 ;
146 CONSTANT: compass-directions H{
166 : direction>compass ( direction -- compass )
167 22.5 round-to-step compass-directions at ;
169 : parse-compass ( str -- str' )
170 string>number [ direction>compass ] keep "%s (%s°)" sprintf ;
172 : parse-direction ( str -- str' )
173 dup "VRB" = [ drop "variable" ] [
174 parse-compass "from %s" sprintf
177 : kt>mph ( kt -- mph ) 1.15077945 * ;
179 : mph>kt ( mph -- kt ) 1.15077945 / ;
181 : parse-speed ( str -- str'/f )
183 dup kt>mph "%s knots (%.1f mph)" sprintf
186 : parse-wind ( str -- str' )
187 dup "00000KT" = [ drop "calm" ] [
188 3 cut "KT" ?tail drop "G" split1
189 [ parse-direction ] [ parse-speed ] [ parse-speed ] tri*
190 [ "%s at %s with gusts to %s " sprintf ]
191 [ "%s at %s" sprintf ] if*
194 : parse-wind-variable ( str -- str' )
195 "V" split1 [ parse-compass ] bi@
196 ", variable from %s to %s" sprintf ;
198 : parse-visibility ( str -- str' )
200 { CHAR: M [ rest "less than " ] }
201 { CHAR: P [ rest "more than " ] }
203 } case swap "SM" ?tail drop
204 CHAR: / over index [ 1 > [ 1 cut "+" glue ] when ] when*
205 string>number "%s%s statute miles" sprintf ;
207 : parse-rvr ( str -- str' )
208 "R" ?head drop "/" split1 "FT" ?tail drop
210 [ string>number ] bi@
211 "varying between %s and %s" sprintf
213 string>number "of %s" sprintf
214 ] if* "runway %s visibility %s ft" sprintf ;
216 : (parse-weather) ( str -- str' )
217 dup "+FC" = [ drop "tornadoes or waterspouts" ] [
219 { CHAR: + [ rest "heavy " ] }
220 { CHAR: - [ rest "light " ] }
223 2 group dup [ weather key? ] all?
224 [ [ weather at ] map " " join ]
225 [ concat parse-glossary ] if
229 : parse-weather ( str -- str' )
230 "VC" over subseq? [ "VC" "" replace t ] [ f ] if
232 [ [ " in the vicinity" append ] when ] bi* ;
234 : parse-altitude ( str -- str' )
235 string>number " at %s00 ft" sprintf ;
241 { "SCT" "scattered" }
242 { "SKC" "clear sky" }
243 { "CLR" "clear sky" }
244 { "NSC" "clear sky" }
246 { "ACC" "altocumulus castellanus" }
247 { "ACSL" "standing lenticular altocumulus" }
248 { "CCSL" "cirrocumulus standing lenticular cloud" }
250 { "SC" "stratocumulus" }
251 { "SCSL" "stratocumulus standing lenticular cloud" }
252 { "TCU" "towering cumulus" }
255 : parse-sky-condition ( str -- str' )
260 [ sky at [ " (%s)" sprintf ] [ f ] if* ]
264 : F>C ( F -- C ) 32 - 5/9 * ;
266 : C>F ( C -- F ) 9/5 * 32 + ;
268 : parse-temperature ( str -- temp dew-point )
271 "M" ?head [ string>number ] [ [ neg ] when ] bi*
272 dup C>F "%d °C (%.1f °F)" sprintf
276 : parse-altimeter ( str -- str' )
277 unclip [ string>number ] [ CHAR: A = ] bi*
278 [ 100 /f "%.2f Hg" sprintf ] [ "%s hPa" sprintf ] if ;
280 CONSTANT: re-timestamp R/ \d{6}Z/
281 CONSTANT: re-station R/ \w{4}/
282 CONSTANT: re-temperature R/ [M]?\d{2}\\/([M]?\d{2})?/
283 CONSTANT: re-wind R/ (VRB|\d{3})\d{2,3}(G\d{2,3})?KT/
284 CONSTANT: re-wind-variable R/ \d{3}V\d{3}/
285 CONSTANT: re-visibility R/ [MP]?\d+(\\/\d+)?SM/
286 CONSTANT: re-rvr R/ R\d{2}[RLC]?\\/\d{4}(V\d{4})?FT/
287 CONSTANT: re-weather R/ [+-]?(VC)?(\w{2}|\w{4})/
288 CONSTANT: re-sky-condition R/ (\w{2,3}\d{3}(\w+)?|\w{3}|CAVOK)/
289 CONSTANT: re-altimeter R/ [AQ]\d{4}/
291 : find-one ( seq quot: ( elt -- ? ) -- seq elt/f )
292 dupd find drop [ tail unclip ] [ f ] if* ; inline
294 : find-all ( seq quot: ( elt -- ? ) -- seq elts )
295 [ find-one swap ] keep '[
296 dup [ f ] [ first @ ] if-empty
297 ] [ unclip ] produce rot [ prefix ] when* ; inline
299 : metar-body ( report seq -- report )
301 [ { "METAR" "SPECI" } member? ] find-one
302 [ pick type<< ] when*
304 [ re-station matches? ] find-one
305 [ pick station<< ] when*
307 [ re-timestamp matches? ] find-one
308 [ parse-timestamp pick timestamp<< ] when*
310 [ { "AUTO" "COR" } member? ] find-one
311 [ pick modifier<< ] when*
313 [ re-wind matches? ] find-one
314 [ parse-wind pick wind<< ] when*
316 [ re-wind-variable matches? ] find-one
317 [ parse-wind-variable pick wind>> prepend pick wind<< ] when*
319 [ re-visibility matches? ] find-one
320 [ parse-visibility pick visibility<< ] when*
322 [ re-rvr matches? ] find-all " " join
323 [ parse-rvr ] map ", " join pick rvr<<
325 [ re-weather matches? ] find-all
326 [ parse-weather ] map ", " join pick weather<<
328 [ re-sky-condition matches? ] find-all
329 [ parse-sky-condition ] map ", " join pick sky-condition<<
331 [ re-temperature matches? ] find-one
334 [ pick temperature<< ]
335 [ pick dew-point<< ] bi*
338 [ re-altimeter matches? ] find-one
339 [ parse-altimeter pick altimeter<< ] when*
343 : signed-number ( sign value -- n )
344 [ string>number ] bi@ swap zero? [ neg ] unless 10.0 / ;
346 : single-value ( str -- str' )
347 1 cut signed-number ;
349 : double-value ( str -- m n )
350 1 cut 3 cut [ signed-number ] dip 1 cut signed-number ;
352 : parse-1hr-temp ( str -- str' )
353 "T" ?head drop dup length 4 > [
355 [ dup C>F "%.1f °C (%.1f °F)" sprintf ] bi@
356 "hourly temperature %s and dew point %s" sprintf
359 "hourly temperature %.1f °C (%.1f °F)" sprintf
362 : parse-6hr-max-temp ( str -- str' )
363 "1" ?head drop single-value dup C>F
364 "6-hour maximum temperature %.1f °C (%.1f °F)" sprintf ;
366 : parse-6hr-min-temp ( str -- str' )
367 "2" ?head drop single-value dup C>F
368 "6-hour minimum temperature %.1f °C (%.1f °F)" sprintf ;
370 : parse-24hr-temp ( str -- str' )
371 "4" ?head drop double-value
372 [ dup C>F "%.1f °C (%.1f °F)" sprintf ] bi@
373 "24-hour maximum temperature %s minimum temperature %s"
376 : parse-1hr-pressure ( str -- str' )
377 "5" ?head drop 1 cut single-value [ pressure-tendency at ] dip
378 "hourly pressure %s %s hPa" sprintf ;
380 : parse-snow-depth ( str -- str' )
381 "4/" ?head drop string>number "snow depth %s inches" sprintf ;
383 CONSTANT: low-clouds H{
384 { 1 "cumulus (fair weather)" }
385 { 2 "cumulus (towering)" }
386 { 3 "cumulonimbus (no anvil)" }
387 { 4 "stratocumulus (from cumulus)" }
388 { 5 "stratocumuls (not cumulus)" }
389 { 6 "stratus or Fractostratus (fair)" }
390 { 7 "fractocumulus / fractostratus (bad weather)" }
391 { 8 "cumulus and stratocumulus" }
392 { 9 "cumulonimbus (thunderstorm)" }
396 CONSTANT: mid-clouds H{
397 { 1 "altostratus (thin)" }
398 { 2 "altostratus (thick)" }
399 { 3 "altocumulus (thin)" }
400 { 4 "altocumulus (patchy)" }
401 { 5 "altocumulus (thickening)" }
402 { 6 "altocumulus (from cumulus)" }
403 { 7 "altocumulus (with altocumulus, altostratus, nimbostratus)" }
404 { 8 "altocumulus (with turrets)" }
405 { 9 "altocumulus (chaotic)" }
406 { -1 "above overcast" }
409 CONSTANT: high-clouds H{
410 { 1 "cirrus (filaments)" }
411 { 2 "cirrus (dense)" }
412 { 3 "cirrus (often with cumulonimbus)" }
413 { 4 "cirrus (thickening)" }
414 { 5 "cirrus / cirrostratus (low in sky)" }
415 { 6 "cirrus / cirrostratus (hi in sky)" }
416 { 7 "cirrostratus (entire sky)" }
417 { 8 "cirrostratus (partial)" }
418 { 9 "cirrocumulus or cirrocumulus / cirrus / cirrostratus" }
419 { -1 "above overcast" }
422 : parse-cloud-cover ( str -- str' )
423 "8/" ?head drop first3 [ CHAR: 0 - ] tri@
424 [ [ f ] [ low-clouds at "low clouds are %s" sprintf ] if-zero ]
425 [ [ f ] [ mid-clouds at "middle clouds are %s" sprintf ] if-zero ]
426 [ [ f ] [ high-clouds at "high clouds are %s" sprintf ] if-zero ]
427 tri* 3array " " join ;
429 : parse-inches ( str -- str' )
430 dup [ CHAR: / = ] all? [ drop "unknown" ] [
432 [ "trace" ] [ 100 /f "%.2f inches" sprintf ] if-zero
435 : parse-1hr-precipitation ( str -- str' )
436 "P" ?head drop parse-inches
437 "%s precipitation in last hour" sprintf ;
439 : parse-6hr-precipitation ( str -- str' )
440 "6" ?head drop parse-inches
441 "%s precipitation in last 6 hours" sprintf ;
443 : parse-24hr-precipitation ( str -- str' )
444 "7" ?head drop parse-inches
445 "%s precipitation in last 24 hours" sprintf ;
447 ! XXX: "on the hour" instead of "00 minutes past the hour" ?
449 : parse-recent-time ( str -- str' )
452 [ " minutes past the hour" append ] if ;
454 : parse-peak-wind ( str -- str' )
455 "/" split1 [ parse-wind ] [ parse-recent-time ] bi*
456 "%s occuring at %s" sprintf ;
458 : parse-sea-level-pressure ( str -- str' )
459 "SLP" ?head drop string>number 10.0 /f 1000 +
460 "sea-level pressure is %s hPa" sprintf ;
462 : parse-lightning ( str -- str' )
463 "LTG" ?head drop 2 group [ lightning at ] map " " join ;
465 CONSTANT: re-recent-weather R/ ((\w{2})?[BE]\d{2,4}((\w{2})?[BE]\d{2,4})?)+/
467 : parse-began/ended ( str -- str' )
469 [ CHAR: B = "began" "ended" ? ]
470 [ parse-recent-time ] bi* "%s at %s" sprintf ;
472 : split-recent-weather ( str -- seq )
474 dup [ digit? ] find drop
475 over [ digit? not ] find-from drop
476 [ cut ] [ f ] if* swap
479 : (parse-recent-weather) ( str -- str' )
480 dup [ digit? ] find drop 2 > [
481 2 cut [ weather at " " append ] dip
482 ] [ f swap ] if parse-began/ended "" append-as ;
484 : parse-recent-weather ( str -- str' )
486 [ (parse-recent-weather) ] map " " join ;
488 : parse-varying ( str -- str' )
489 "V" split1 [ string>number ] bi@
490 "varying between %s00 and %s00 ft" sprintf ;
492 : parse-from-to ( str -- str' )
493 "-" split [ parse-glossary ] map " to " join ;
495 : parse-water-equivalent-snow ( str -- str' )
496 "933" ?head drop parse-inches
497 "%s water equivalent of snow on ground" sprintf ;
499 : parse-duration-of-sunshine ( str -- str' )
500 "98" ?head drop string>number
501 [ "no" ] [ "%s minutes of" sprintf ] if-zero
502 "%s sunshine" sprintf ;
504 : parse-6hr-snowfall ( str -- str' )
505 "931" ?head drop parse-inches
506 "%s snowfall in last 6 hours" sprintf ;
508 : parse-probability ( str -- str' )
509 "PROB" ?head drop string>number
510 "probability of %d%%" sprintf ;
512 : parse-remark ( str -- str' )
514 { [ dup glossary key? ] [ glossary at ] }
515 { [ dup R/ 1\d{4}/ matches? ] [ parse-6hr-max-temp ] }
516 { [ dup R/ 2\d{4}/ matches? ] [ parse-6hr-min-temp ] }
517 { [ dup R/ 4\d{8}/ matches? ] [ parse-24hr-temp ] }
518 { [ dup R/ 4\\/\d{3}/ matches? ] [ parse-snow-depth ] }
519 { [ dup R/ 5\d{4}/ matches? ] [ parse-1hr-pressure ] }
520 { [ dup R/ 6[\d\\/]{4}/ matches? ] [ parse-6hr-precipitation ] }
521 { [ dup R/ 7\d{4}/ matches? ] [ parse-24hr-precipitation ] }
522 { [ dup R/ 8\\/\d{3}/ matches? ] [ parse-cloud-cover ] }
523 { [ dup R/ 931\d{3}/ matches? ] [ parse-6hr-snowfall ] }
524 { [ dup R/ 933\d{3}/ matches? ] [ parse-water-equivalent-snow ] }
525 { [ dup R/ 98\d{3}/ matches? ] [ parse-duration-of-sunshine ] }
526 { [ dup R/ T\d{4,8}/ matches? ] [ parse-1hr-temp ] }
527 { [ dup R/ \d{3}\d{2,3}\\/\d{2,4}/ matches? ] [ parse-peak-wind ] }
528 { [ dup R/ P\d{4}/ matches? ] [ parse-1hr-precipitation ] }
529 { [ dup R/ SLP\d{3}/ matches? ] [ parse-sea-level-pressure ] }
530 { [ dup R/ LTG\w+/ matches? ] [ parse-lightning ] }
531 { [ dup R/ PROB\d+/ matches? ] [ parse-probability ] }
532 { [ dup R/ \d{3}V\d{3}/ matches? ] [ parse-varying ] }
533 { [ dup R/ [^-]+(-[^-]+)+/ matches? ] [ parse-from-to ] }
534 { [ dup R/ [^\\/]+(\\/[^\\/]+)+/ matches? ] [ ] }
535 { [ dup R/ \d+.\d+/ matches? ] [ ] }
536 { [ dup re-recent-weather matches? ] [ parse-recent-weather ] }
537 { [ dup re-weather matches? ] [ parse-weather ] }
538 { [ dup re-sky-condition matches? ] [ parse-sky-condition ] }
542 : metar-remarks ( report seq -- report )
543 [ parse-remark ] map " " join >>remarks ;
545 : <metar-report> ( metar -- report )
546 [ metar-report new ] dip [ >>raw ] keep
547 [ blank? ] split-when { "RMK" } split1
548 [ metar-body ] [ metar-remarks ] bi* ;
550 : row. ( name quot -- )
552 [ _ write ] with-cell
553 [ @ [ 65 wrap-string write ] when* ] with-cell
556 : metar-report. ( report -- )
557 standard-table-style [
559 [ "Station" [ station>> ] row. ]
560 [ "Timestamp" [ timestamp>> ] row. ]
561 [ "Wind" [ wind>> ] row. ]
562 [ "Visibility" [ visibility>> ] row. ]
563 [ "RVR" [ rvr>> ] row. ]
564 [ "Weather" [ weather>> ] row. ]
565 [ "Sky condition" [ sky-condition>> ] row. ]
566 [ "Temperature" [ temperature>> ] row. ]
567 [ "Dew point" [ dew-point>> ] row. ]
568 [ "Altimeter" [ altimeter>> ] row. ]
569 [ "Remarks" [ remarks>> ] row. ]
570 [ "Raw Text" [ raw>> ] row. ]
572 ] tabular-output nl ;
576 GENERIC: metar ( station -- metar )
578 M: station metar cccc>> metar ;
581 "http://weather.noaa.gov/pub/data/observations/metar/stations/%s.TXT"
582 sprintf http-get nip ;
584 GENERIC: metar. ( station -- )
586 M: station metar. cccc>> metar. ;
589 [ metar <metar-report> metar-report. ]
590 [ drop "%s METAR not found\n" printf ] recover ;
594 : parse-wind-shear ( str -- str' )
595 "WS" ?head drop "/" split1
596 [ parse-altitude ] [ parse-wind ] bi* prepend
597 "wind shear " prepend ;
599 CONSTANT: re-from-timestamp R/ FM\d{6}/
601 : parse-from-timestamp ( str -- str' )
602 "FM" ?head drop parse-timestamp ;
604 CONSTANT: re-valid-timestamp R/ \d{4}\/\d{4}/
606 : parse-valid-timestamp ( str -- str' )
607 "/" split1 [ "00" append parse-timestamp ] bi@ " to " glue ;
609 TUPLE: taf-report station timestamp valid-timestamp wind
610 visibility rvr weather sky-condition partials raw ;
612 TUPLE: taf-partial from-timestamp wind visibility rvr weather
615 : taf-body ( report str -- report )
616 [ blank? ] split-when
618 [ { "AMD" "COR" "RTD" } member? ] find-one drop
620 [ re-station matches? ] find-one
621 [ pick station<< ] when*
623 [ re-timestamp matches? ] find-one
624 [ parse-timestamp pick timestamp<< ] when*
626 [ re-valid-timestamp matches? ] find-one
627 [ parse-valid-timestamp pick valid-timestamp<< ] when*
629 [ re-wind matches? ] find-one
630 [ parse-wind pick wind<< ] when*
632 [ re-wind-variable matches? ] find-one
633 [ parse-wind-variable pick wind>> prepend pick wind<< ] when*
635 [ re-visibility matches? ] find-one
636 [ parse-visibility pick visibility<< ] when*
638 [ re-rvr matches? ] find-all " " join
639 [ parse-rvr ] map ", " join pick rvr<<
641 [ re-weather matches? ] find-all
642 [ parse-weather ] map ", " join pick weather<<
644 [ re-sky-condition matches? ] find-all
645 [ parse-sky-condition ] map ", " join pick sky-condition<<
649 : <taf-partial> ( str -- partial )
650 [ taf-partial new ] dip [ blank? ] split-when
652 [ re-from-timestamp matches? ] find-one
653 [ parse-from-timestamp pick from-timestamp<< ] when*
655 [ re-wind matches? ] find-one
656 [ parse-wind pick wind<< ] when*
658 [ re-wind-variable matches? ] find-one
659 [ parse-wind-variable pick wind>> prepend pick wind<< ] when*
661 [ re-visibility matches? ] find-one
662 [ parse-visibility pick visibility<< ] when*
664 [ re-rvr matches? ] find-all " " join
665 [ parse-rvr ] map ", " join pick rvr<<
667 [ re-weather matches? ] find-all
668 [ parse-weather ] map ", " join pick weather<<
670 [ re-sky-condition matches? ] find-all
671 [ parse-sky-condition ] map ", " join pick sky-condition<<
675 : taf-partials ( report seq -- report )
676 [ <taf-partial> ] map >>partials ;
678 : <taf-report> ( taf -- report )
679 [ taf-report new ] dip [ >>raw ] keep
680 string-lines [ [ blank? ] trim ] map
681 rest dup first "TAF" = [ rest ] when
682 harvest unclip taf-body taf-partials ;
684 : taf-report. ( report -- )
686 standard-table-style [
688 [ "Station" [ station>> ] row. ]
689 [ "Timestamp" [ timestamp>> ] row. ]
690 [ "Valid From" [ valid-timestamp>> ] row. ]
691 [ "Wind" [ wind>> ] row. ]
692 [ "Visibility" [ visibility>> ] row. ]
693 [ "RVR" [ rvr>> ] row. ]
694 [ "Weather" [ weather>> ] row. ]
695 [ "Sky condition" [ sky-condition>> ] row. ]
696 [ "Raw Text" [ raw>> ] row. ]
701 standard-table-style [
703 [ "From" [ from-timestamp>> ] row. ]
704 [ "Wind" [ wind>> ] row. ]
705 [ "Visibility" [ visibility>> ] row. ]
706 [ "RVR" [ rvr>> ] row. ]
707 [ "Weather" [ weather>> ] row. ]
708 [ "Sky condition" [ sky-condition>> ] row. ]
716 GENERIC: taf ( station -- taf )
718 M: station taf cccc>> taf ;
721 "http://weather.noaa.gov/pub/data/forecasts/taf/stations/%s.TXT"
722 sprintf http-get nip ;
724 GENERIC: taf. ( station -- )
726 M: station taf. cccc>> taf. ;
729 [ taf <taf-report> taf-report. ]
730 [ drop "%s TAF not found\n" printf ] recover ;