]> code.delx.au - gnu-emacs-elpa/blob - packages/metar/metar.el
Fix the case where the metar record contains M01 (bug#19401).
[gnu-emacs-elpa] / packages / metar / metar.el
1 ;;; metar.el --- Retrieve and decode METAR weather information
2
3 ;; Copyright (C) 2007, 2014 Free Software Foundation, Inc.
4
5 ;; Author: Mario Lang <mlang@delysid.org>
6 ;; Version: 0.1
7 ;; Package-Requires: ((cl-lib "0.5"))
8 ;; Keywords: comm
9
10 ;; This program is free software; you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation, either version 3 of the License, or
13 ;; (at your option) any later version.
14
15 ;; This program is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ;; GNU General Public License for more details.
19
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
22
23 ;;; Commentary:
24
25 ;; Run `M-x metar RET' to get a simple weather report from weather.noaa.gov.
26 ;; The value of `calendar-latitude' and `calendar-longitude' will be used to
27 ;; automatically determine a nearby station. If these variables are not set,
28 ;; you will be prompted to enter the location manually.
29 ;;
30 ;; With `C-u M-x metar RET', country and station name need to be entered.
31 ;; `C-u C-u M-x metar RET' will prompt for the METAR station code (4 letters).
32 ;;
33 ;; Customize `metar-units' to change length, speed, temperature or pressure
34 ;; units to your liking.
35 ;;
36 ;; For programmatic access to decoded weather reports, use:
37 ;;
38 ;; (metar-decode (metar-get-record "CODE"))
39
40 ;;; Code:
41
42 (require 'calc-units)
43 (require 'cl-lib)
44 (require 'format-spec)
45 (require 'rx)
46 (require 'solar)
47 (require 'url)
48
49 (defgroup metar ()
50 "METAR weather reports."
51 :group 'net-utils)
52
53 (defcustom metar-units '((length . m)
54 (pressure . hPa)
55 (speed . kph)
56 (temperature . degC))
57 "Default measurement units to use when reporting weather information."
58 :group 'metar
59 :type '(list (cons :format "%v"
60 (const :tag "Length: " length)
61 (choice (const :tag "Meter" m)
62 (const :tag "Inch" in)
63 (const :tag "Foot" ft)
64 (const :tag "Yard" yd)
65 (const :tag "Mile" mi)))
66 (cons :format "%v"
67 (const :tag "Pressure:" pressure)
68 (choice (const :tag "Pascal" Pa)
69 (const :tag "Hecto pascal" hPa)
70 (const :tag "Bar" bar)
71 (const :tag "Inch of mercury" inHg)
72 (const :tag "Standard atmosphere" atm)
73 (const :tag "Meter of mercury" mHg)
74 (const :tag "Punds per square inch" psi)))
75 (cons :format "%v"
76 (const :tag "Speed:" speed)
77 (choice (const :tag "Kilometers per hour" kph)
78 (const :tag "Miles per hour" mph)
79 (const :tag "Knot" knot)))
80 (cons :format "%v"
81 (const :tag "Temperature:" temperature)
82 (choice (const :tag "Degree Celsius" degC)
83 (const :tag "Degree Kelvin" degK)
84 (const :tag "Degree Fahrenheit" degF)))))
85
86 (defcustom metar-stations-info-url "http://weather.noaa.gov/data/nsd_bbsss.txt"
87 "URL to use for retrieving station meta information."
88 :group 'metar
89 :type 'string)
90
91 (defvar metar-stations nil
92 "Variable containing (cached) METAR station information.
93 Use the function `metar-stations' to get the actual station list.")
94
95 (defun metar-station-convert-dms-to-deg (string)
96 "Convert degrees, minutes and optional seconds, to degrees."
97 (when (string-match (rx string-start
98 (group (1+ digit)) ?- (group (1+ digit))
99 (optional ?- (group (1+ digit)))
100 (group (char ?N ?E ?S ?W))
101 string-end) string)
102 (funcall (if (memq (aref (match-string 4 string) 0) '(?N ?E)) #'+ #'-)
103 (+ (string-to-number (match-string 1 string))
104 (/ (string-to-number (match-string 2 string)) 60.0)
105 (if (match-string 3 string)
106 (/ (string-to-number (match-string 3 string)) 3600.0)
107 0)))))
108
109 (defun metar-stations ()
110 "Retrieve a list of METAR stations.
111 Results are cached in variable `metar-stations'.
112 If this variable is nil, the information is retrieved from the Internet."
113 (or metar-stations
114 (let ((data (with-temp-buffer
115 (url-insert-file-contents metar-stations-info-url)
116 (mapcar (lambda (entry)
117 (split-string entry ";"))
118 (split-string (buffer-string) "\n")))))
119 (setq metar-stations nil)
120 (while data
121 (when (and (nth 7 (car data)) (nth 8 (car data))
122 (not (string= (nth 2 (car data)) "----")))
123 (setq metar-stations
124 (append
125 (let ((item (car data)))
126 (list
127 (list (cons 'code (nth 2 item))
128 (cons 'name (nth 3 item))
129 (cons 'country (nth 5 item))
130 (cons 'latitude
131 (metar-station-convert-dms-to-deg (nth 7 item)))
132 (cons 'longitude
133 (metar-station-convert-dms-to-deg (nth 8 item)))
134 (cons 'altitude (string-to-number (nth 12 item))))))
135 metar-stations)))
136 (setq data (cdr data)))
137 ;; (unless metar-timer
138 ;; (setq metar-timer
139 ;; (run-with-timer 600 nil (lambda () (setq metar-stations nil)))))
140 metar-stations)))
141
142 (defun metar-stations-get (station-code key)
143 "Get meta information for station with STATION-CODE and KEY.
144 KEY can be one of the symbols `code', `name', `country', `latitude',
145 `longitude' or `altitude'."
146 (let ((stations (metar-stations)) result)
147 (while stations
148 (when (string= (cdr (assoc 'code (car stations))) station-code)
149 (setq result (cdr (assoc key (car stations)))
150 stations nil))
151 (setq stations (cdr stations)))
152 result))
153
154 (defun metar-latitude-longitude-bearing (latitude1 longitude1
155 latitude2 longitude2)
156 "Calculate bearing from start point LATITUDE1/LONGITUDE1 to end point
157 LATITUDE2/LONGITUDE2."
158 (% (+ 360
159 (truncate
160 (radians-to-degrees
161 (atan (* (sin (degrees-to-radians (- longitude2 longitude1)))
162 (cos (degrees-to-radians latitude2)))
163 (- (* (cos (degrees-to-radians latitude1))
164 (sin (degrees-to-radians latitude2)))
165 (* (sin (degrees-to-radians latitude1))
166 (cos (degrees-to-radians latitude2))
167 (cos (degrees-to-radians (- longitude2 longitude1)))))))))
168 360))
169
170 (defun metar-latitude-longitude-distance-haversine (latitude1 longitude1
171 latitude2 longitude2)
172 "Caluclate the distance (in kilometers) between two points on the
173 surface of the earth given as LATITUDE1, LONGITUDE1, LATITUDE2 and LONGITUDE2."
174 (cl-macrolet ((distance (d1 d2)
175 `(expt (sin (/ (degrees-to-radians (- ,d2 ,d1)) 2)) 2)))
176 (let ((a (+ (distance latitude1 latitude2)
177 (* (cos (degrees-to-radians latitude1))
178 (cos (degrees-to-radians latitude2))
179 (distance longitude1 longitude2)))))
180 (* 6371 (* 2 (atan (sqrt a) (sqrt (- 1 a))))))))
181
182 (defun metar-find-station-by-latitude/longitude (latitude longitude &optional
183 radius)
184 "Find a station near the coordinates given by LATITUDE and LONGITUDE.
185 Returns a cons where car is the station code and cdr is the distance in
186 kilometers.
187 If RADIUS is non-nil, only stations within this range (in kilometers) are
188 considered.
189 If no match if found, nil is returned."
190 (interactive
191 (list
192 (solar-get-number "Enter latitude (decimal fraction; + north, - south): ")
193 (solar-get-number "Enter longitude (decimal fraction; + east, - west): ")))
194 (let ((stations (metar-stations))
195 (best-distance (or radius 10000))
196 (station-code nil))
197 (while stations
198 (let ((station-latitude (cdr (assoc 'latitude (car stations))))
199 (station-longitude (cdr (assoc 'longitude (car stations)))))
200 (when (and station-latitude station-longitude)
201 (let ((distance (metar-latitude-longitude-distance-haversine
202 latitude longitude
203 station-latitude station-longitude)))
204 (when (< distance best-distance)
205 (setq best-distance distance
206 station-code (cdr (assoc 'code (car stations))))))))
207 (setq stations (cdr stations)))
208 (if (called-interactively-p 'interactive)
209 (if station-code
210 (message "%s, %s (%s) at %s is %d km away from %s."
211 (metar-stations-get station-code 'name)
212 (metar-stations-get station-code 'country)
213 station-code
214 (let ((float-output-format "%.1f"))
215 (format "%s%s, %s%s"
216 (abs (metar-stations-get station-code 'latitude))
217 (if (> (metar-stations-get station-code 'latitude) 0) "N" "S")
218 (abs (metar-stations-get station-code 'longitude))
219 (if (> (metar-stations-get station-code 'longitude) 0) "E" "W")))
220 best-distance
221 (let ((float-output-format "%.1f"))
222 (format "%s%s, %s%s"
223 (if (numberp latitude)
224 (abs latitude)
225 (+ (aref latitude 0)
226 (/ (aref latitude 1) 60.0)))
227 (if (numberp latitude)
228 (if (> latitude 0) "N" "S")
229 (if (equal (aref latitude 2) 'north) "N" "S"))
230 (if (numberp longitude)
231 (abs longitude)
232 (+ (aref longitude 0)
233 (/ (aref longitude 1) 60.0)))
234 (if (numberp longitude)
235 (if (> longitude 0) "E" "W")
236 (if (equal (aref longitude 2) 'east)
237 "E" "W")))))
238 (message "No appropriate station found."))
239 (when station-code
240 (cons station-code (round best-distance))))))
241
242 (defun metar-convert-unit (value new-unit &optional convert-units-function)
243 "Convert VALUE to NEW-UNIT.
244 VALUE is a string with the value followed by the unit, like \"5 knot\"
245 and NEW-UNIT should be a unit name like \"kph\" or similar.
246 CONVERT-UNITS-FUNCTION designates the function actually doing the conversion.
247 It must have the signature of `math-convert-units', which is the default."
248 (cl-check-type value string)
249 (unless (symbolp new-unit)
250 (setq new-unit (intern new-unit)))
251 (let ((expr (math-simplify (math-read-expr value))))
252 (cl-assert (or (math-zerop expr)
253 (not (memq (math-single-units-in-expr-p expr) '(nil wrong))))
254 nil
255 "Metar: Not exactly one unit in expression: %S" expr)
256 (let ((res (math-simplify-units
257 (funcall (or convert-units-function 'math-convert-units)
258 expr
259 (math-build-var-name new-unit)
260 t))))
261 (cl-assert (math-realp res) nil
262 "Metar: Not a Calc real number: %S" res)
263 (cons (string-to-number (math-format-value (if (integerp res)
264 res
265 (math-float res))))
266 new-unit))))
267
268 (defun metar-convert-temperature (string &optional unit)
269 (metar-convert-unit (concat (if (= (aref string 0) ?M)
270 (concat "-" (substring string 1))
271 string)
272 "degC")
273 (or unit (cdr (assq 'temperature metar-units)))
274 (lambda (expr new-unit-var pure)
275 (math-convert-temperature expr
276 (math-build-var-name 'degC)
277 new-unit-var
278 pure))))
279
280 (defcustom metar-url
281 "http://weather.noaa.gov/pub/data/observations/metar/stations/%s.TXT"
282 "URL used to fetch station specific information.
283 %s is replaced with the 4 letter station code."
284 :group 'metar
285 :type 'string)
286
287 (defun metar-url (station)
288 (format metar-url
289 (upcase (cl-etypecase station
290 (string station)
291 (symbol (symbol-name station))))))
292
293 (defconst metar-record-regexp
294 (rx (group (1+ digit)) ?/ (group (1+ digit)) ?/ (group (1+ digit))
295 space
296 (group (1+ digit)) ?: (group (1+ digit))
297 ?\n
298 (group "%s" (* not-newline)))
299 "Regular expression used to extract METAR information from `metar-url'.
300 %s is replaced with the station code which always has to be present in a METAR
301 record.")
302
303 (defun metar-get-record (station)
304 "Retrieve a METAR/SPECI record for STATION from the Internet.
305 Return a cons where `car' is the time of the measurement (as an emacs-lsip
306 time value) and `cdr' is a string containing the actual METAR code.
307 If no record was found for STATION, nil is returned."
308 (with-temp-buffer
309 (url-insert-file-contents (metar-url station))
310 (when (re-search-forward (format metar-record-regexp station) nil t)
311 (cons (encode-time
312 0
313 (string-to-number (match-string 5))
314 (string-to-number (match-string 4))
315 (string-to-number (match-string 3))
316 (string-to-number (match-string 2))
317 (string-to-number (match-string 1))
318 0)
319 (match-string 6)))))
320
321 (defconst metar-could-regexp
322 (rx symbol-start
323 (group (or "FEW" "SCT" "BKN" "OVC"))
324 (group (= 3 digit))
325 (optional (group (or "TCU" "CB")))
326 symbol-end)
327 "Regular expression to match cloud information in METAR records.")
328
329 (defun metar-clouds (info)
330 (let ((clouds ())
331 (from 0))
332 (while (string-match metar-could-regexp info from)
333 (setq from (match-end 0)
334 clouds (push (append (list (match-string 1 info)
335 (metar-convert-unit
336 (concat (match-string 2 info) " ft")
337 (cdr (assq 'length metar-units))))
338 (when (match-string 3 info)
339 (list (match-string 3 info))))
340 clouds)))
341 clouds))
342
343 (defconst metar-phenomena '(("BC" . "patches")
344 ("BL" . "blowing")
345 ("BR" . "mist")
346 ("DR" . "drifting")
347 ("DS" . "dust storm")
348 ("DU" . "widespread dust")
349 ("DZ" . "drizzle")
350 ("FC" . "funnel cloud")
351 ("FG" . "fog")
352 ("FU" . "smoke")
353 ("FZ" . "freezing")
354 ("GR" . "hail")
355 ("GS" . "small hail/snow pellets")
356 ("HZ" . "haze")
357 ("IC" . "ice crystals")
358 ("MI" . "shallow")
359 ("PL" . "ice pellets")
360 ("PO" . "well developed dust/sand swirls")
361 ("PR" . "partials")
362 ("PY" . "spray")
363 ("RA" . "rain")
364 ("SA" . "sand")
365 ("SG" . "snow grains")
366 ("SH" . "showers")
367 ("SN" . "snow")
368 ("SQ" . "squall")
369 ("SS" . "sand storm")
370 ("TS" . "thunderstorm")
371 ("VA" . "volcanic ash")
372 ("VC" . "vicinity"))
373 "Alist of codes and descriptions for METAR weather phenomenoa.")
374
375 (defconst metar-phenomena-regexp
376 (eval `(rx symbol-start
377 (group (optional (char ?+ ?-)))
378 (group (1+ (or ,@(mapcar #'car metar-phenomena))))
379 symbol-end))
380 "Regular expression to match weather phenomena in METAR records.")
381
382 (defun metar-phenomena (info)
383 (when (string-match metar-phenomena-regexp info)
384 (let ((words ()))
385 (when (string= (match-string 1 info) "-")
386 (push "light" words))
387 (let ((obs (match-string 2 info)))
388 (while (> (length obs) 0)
389 (setq words (nconc words
390 (list (cdr (assoc-string (substring obs 0 2)
391 metar-phenomena))))
392 obs (substring obs 2))))
393 (mapconcat #'identity words " "))))
394
395 (defconst metar-wind-regexp
396 (rx symbol-start
397 (group (or "VRB" (= 3 digit)))
398 (group (repeat 2 3 digit)) (optional (char ?G) (group (1+ digit)))
399 "KT"
400 symbol-end
401 (optional (one-or-more not-newline)
402 symbol-start
403 (group (= 3 digit)) (char ?V) (group (= 3 digit))
404 symbol-end))
405 "Regular expression to match wind information in METAR records.")
406
407 (defsubst metar-degrees (value)
408 (cons value 'degrees))
409
410 (defun metar-wind (info)
411 (when (string-match metar-wind-regexp info)
412 (append
413 (if (string= (match-string 1 info) "VRB")
414 (when (and (match-string 4 info) (match-string 5 info))
415 (list :from (string-to-number (match-string 4 info))
416 :to (string-to-number (match-string 5 info))))
417 (append
418 (list :direction (metar-degrees
419 (string-to-number (match-string 1 info))))
420 (when (and (match-string 4 info) (match-string 5 info))
421 (list :from (metar-degrees (string-to-number (match-string 4 info)))
422 :to (metar-degrees (string-to-number (match-string 5 info)))))))
423 (list :speed (metar-convert-unit (concat (match-string 2 info) "knot")
424 (cdr (assq 'speed metar-units))))
425 (when (match-string 3 info)
426 (list :gust (metar-convert-unit (concat (match-string 3 info) "knot")
427 (cdr (assq 'speed metar-units))))))))
428
429 (defconst metar-visibility-regexp
430 (rx symbol-start (group (1+ digit)) (optional (group "SM")) symbol-end)
431 "Regular expression to match information about visibility in METAR records.")
432
433 (defconst metar-temperature-and-dewpoint-regexp
434 (rx symbol-start
435 (group (group (optional (char ?M))) (1+ digit))
436 (char ?/)
437 (group (group (optional (char ?M))) (1+ digit))
438 symbol-end)
439 "Regular expression to match temperature and dewpoint information in METAR
440 records.")
441
442 (defun metar-temperature (info)
443 (when (string-match metar-temperature-and-dewpoint-regexp info)
444 (metar-convert-temperature (match-string 1 info))))
445
446 (defun metar-dewpoint (info)
447 (when (string-match metar-temperature-and-dewpoint-regexp info)
448 (metar-convert-temperature (match-string 3 info))))
449
450 (defun metar-humidity (info)
451 (when (string-match metar-temperature-and-dewpoint-regexp info)
452 (cons (round
453 (metar-magnus-formula-humidity-from-dewpoint
454 (save-match-data (car (metar-convert-temperature
455 (match-string 1 info) 'degC)))
456 (car (metar-convert-temperature (match-string 3 info) 'degC))))
457 'percent)))
458
459 (defconst metar-pressure-regexp
460 (rx symbol-start (group (char ?Q ?A)) (group (1+ digit)) symbol-end)
461 "Regular expression to match air pressure information in METAR records.")
462
463 (defun metar-pressure (info)
464 (when (string-match metar-pressure-regexp info)
465 (metar-convert-unit
466 (concat (match-string 2 info)
467 (cond
468 ((string= (match-string 1 info) "Q") "hPa")
469 ((string= (match-string 1 info) "A") "cinHg")))
470 (cdr (assq 'pressure metar-units)))))
471
472 (defun metar-decode (record)
473 "Return a lisp structure describing the weather information in RECORD."
474 (when record
475 (let* ((codes (cdr record))
476 (temperature (metar-temperature codes))
477 (dewpoint (metar-dewpoint codes))
478 (humidity (metar-humidity codes))
479 (pressure (metar-pressure codes))
480 (wind (metar-wind codes)))
481 (append
482 (list (cons 'station (car (split-string codes " ")))
483 (cons 'timestamp (car record))
484 (cons 'wind wind)
485 (cons 'temperature temperature)
486 (cons 'dewpoint dewpoint)
487 (cons 'humidity humidity)
488 (cons 'pressure pressure))
489 (when (metar-phenomena codes)
490 (list (cons 'phenomena (metar-phenomena codes))))))))
491
492 (defun metar-magnus-formula-humidity-from-dewpoint (temperature dewpoint)
493 "Calculate relative humidity (in %) from TEMPERATURE and DEWPOINT (in
494 degrees celsius)."
495 (* 10000
496 (expt 10
497 (- (/ (- (* 0.4343
498 (+ 243.12 temperature)
499 (/ (* dewpoint 17.62)
500 (+ 243.12 dewpoint)))
501 (* 0.4343 17.62 temperature))
502 (+ 243.12 temperature))
503 2))))
504
505 ;;;###autoload
506 (defun metar (&optional arg)
507 "Display recent weather information.
508 If a prefix argument is given, prompt for country and station name.
509 If two prefix arguments are given, prompt for exact station code.
510 Otherwise, determine the best station via latitude/longitude."
511 (interactive "p")
512 (unless arg (setq arg 1))
513 (let (station)
514 (cond
515 ((= arg 1)
516 (unless calendar-longitude
517 (setq calendar-longitude
518 (solar-get-number
519 "Enter longitude (decimal fraction; + east, - west): ")))
520 (unless calendar-latitude
521 (setq calendar-latitude
522 (solar-get-number
523 "Enter latitude (decimal fraction; + north, - south): ")))
524 (when (and calendar-latitude calendar-longitude
525 (setq station (metar-find-station-by-latitude/longitude
526 (calendar-latitude) (calendar-longitude))))
527 (message "Found %s %d kilometers away." (car station) (cdr station))
528 (setq station (car station))))
529 ((= arg 4)
530 (let* ((country (completing-read "Country: " (metar-station-countries) nil t))
531 (name (completing-read "Station name: " (mapcar (lambda (s) (cdr (assq 'name s)))
532 (metar-stations-in-country country))
533 nil t)))
534 (setq station (cdr (assq 'code (cl-find-if (lambda (s)
535 (and (string= name (cdr (assq 'name s)))
536 (string= country (cdr (assq 'country s)))))
537 (metar-stations)))))))
538 ((= arg 16)
539 (setq station (completing-read "Enter METAR station code: "
540 (mapcar (lambda (station-info)
541 (cdr (assq 'code station-info)))
542 (metar-stations))
543 nil t))))
544 (let ((info (metar-decode (metar-get-record station))))
545 (if info
546 (message "%d minutes ago at %s: %d°%c, %s%d%% humidity, %.1f %S."
547 (/ (truncate (float-time (time-since
548 (cdr (assoc 'timestamp info)))))
549 60)
550 (or (metar-stations-get (cdr (assoc 'station info)) 'name)
551 (cdr (assoc 'station info)))
552 (cadr (assoc 'temperature info))
553 (cond
554 ((eq (cdr (assq 'temperature metar-units)) 'degC) ?C)
555 ((eq (cdr (assq 'temperature metar-units)) 'degF) ?F))
556 (if (assoc 'phenomena info)
557 (concat (cdr (assoc 'phenomena info)) ", ")
558 "")
559 (cadr (assoc 'humidity info))
560 (cadr (assoc 'pressure info)) (cddr (assoc 'pressure info)))
561 (message "No weather information found, sorry.")))))
562
563 (defun metar-station-countries ()
564 (let (countries)
565 (dolist (station (metar-stations))
566 (let ((country (cdr (assq 'country station))))
567 (cl-pushnew country countries :test #'equal)))
568 countries))
569
570 (defun metar-stations-in-country (country)
571 (cl-loop for station-info in (metar-stations)
572 when (string= country (cdr (assq 'country station-info)))
573 collect station-info))
574
575 (defun metar-average-temperature (country)
576 "Display average temperature from all stations in COUNTRY."
577 (interactive
578 (list (completing-read "Country: " (metar-station-countries) nil t)))
579 (let ((count 0) (temp-sum 0)
580 (stations (metar-stations))
581 (url-show-status nil)
582 (progress (make-progress-reporter
583 "Downloading METAR records..."
584 0
585 (cl-count-if (lambda (station)
586 (string= (cdr (assoc 'country station))
587 country))
588 (metar-stations)))))
589 (while stations
590 (when (string= (cdr (assoc 'country (car stations))) country)
591 (let ((temp (cdr (assoc 'temperature
592 (metar-decode
593 (metar-get-record
594 (cdr (assoc 'code (car stations)))))))))
595 (when temp
596 (setq temp-sum (+ temp-sum temp)
597 count (+ count 1))
598 (progress-reporter-update progress count))))
599 (setq stations (cdr stations)))
600 (progress-reporter-done progress)
601 (if (called-interactively-p 'interactive)
602 (message "Average temperature in %s is %s"
603 country
604 (if (> count 0)
605 (format "%.1f°C (%d stations)"
606 (/ (float temp-sum) count)
607 count)
608 "unknown"))
609 (when (> count 0)
610 (/ (float temp-sum) count)))))
611
612 (defun metar-format (format report)
613 (format-spec
614 format
615 (list (cons ?d
616 (let ((dewpoint (cdr (assq 'dewpoint report))))
617 (format "%.1f°%c"
618 (car dewpoint)
619 (cond ((eq (cdr dewpoint) 'degC) ?C)
620 ((eq (cdr dewpoint) 'degF) ?F)
621 ((eq (cdr dewpoint) 'degK) ?K)))))
622 (cons ?h
623 (let ((humidity (cdr (assq 'humidity report))))
624 (format "%d%%" (car humidity))))
625 (cons ?p
626 (let ((pressure (cdr (assq 'pressure report))))
627 (format "%.1f %S" (car pressure) (cdr pressure))))
628 (cons ?s (cdr (assq 'station report)))
629 (cons ?t
630 (let ((temperature (cdr (assq 'temperature report))))
631 (format "%.1f°%c"
632 (car temperature)
633 (cond ((eq (cdr temperature) 'degC) ?C)
634 ((eq (cdr temperature) 'degF) ?F))))))))
635
636 (provide 'metar)
637 ;;; metar.el ends here