-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathjma-utils.el
242 lines (200 loc) · 8.14 KB
/
jma-utils.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
;;; jma-utils.el --- JMA Utilities -*- lexical-binding: t; -*-
;; Copyright (C) 2022 AKIYAMA Kouhei
;; Author: AKIYAMA Kouhei <[email protected]>
;; Keywords:
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;;; Code:
(require 'cl-lib)
(require 'calendar)
(require 'parse-time)
;;;; Customize
(defgroup jma nil
"Access to Japan Meteorological Agency"
:prefix "jma-"
:group 'comm)
;;;; HTTP
(defun jma-http-parse-response (buffer)
(with-current-buffer buffer
(set-buffer-multibyte t)
(goto-char (point-min))
(unless (looking-at "^HTTP/\\([^ ]+\\) +\\([0-9]+\\) +\\(.*\\)$")
(error "Invalid HTTP response : %s" (buffer-substring (line-beginning-position) (line-end-position))))
(let ((http-ver (match-string 1))
(status (string-to-number (match-string 2)))
(message (match-string 3))
(headers (cl-loop while (progn (forward-line) (not (eolp)))
when (looking-at "^\\([^:\n]+\\): \\([^\n]*\\)$")
collect (cons (match-string 1) (match-string 2))))
(body (progn
(forward-line)
(buffer-substring (point) (point-max)))))
(list http-ver status message headers body))))
(defun jma-http-ver (res) (nth 0 res))
(defun jma-http-status (res) (nth 1 res))
(defun jma-http-message (res) (nth 2 res))
(defun jma-http-headers (res) (nth 3 res))
(defun jma-http-body (res) (nth 4 res))
(defun jma-http-get (url)
(let ((buffer (url-retrieve-synchronously url)))
(unwind-protect
(jma-http-parse-response buffer)
(kill-buffer buffer))))
;;;; JSON
(defun jma-json-get (url)
(let* ((res (jma-http-get url))
(status (jma-http-status res)))
(unless (= status 200)
(error "HTTP status is not OK : %s" status))
(json-parse-string (jma-http-body res) :object-type 'alist)))
;;;; 日付処理
(defun jma-date (y m d)
(list m d y))
(defun jma-date-today (&optional tz)
(let ((dt (decode-time nil tz)))
(jma-date
(decoded-time-year dt)
(decoded-time-month dt)
(decoded-time-day dt))))
(defun jma-date-to-time (date)
(if (= (length date) 3)
(encode-time
(list
0 0 0
(calendar-extract-day date)
(calendar-extract-month date)
(calendar-extract-year date)
nil nil
32400 ;;常にJST(+09:00)で考える。
))
;; @todo support decoded time
;; encoded time?
date))
(defun jma-date-inc-day (date &optional n)
(jma-date
(calendar-extract-year date)
(calendar-extract-month date)
(+ (calendar-extract-day date) (or n 1))))
;;;; 時刻処理
(defun jma-time-next-hour (time hours)
"TIMEの次の時を返します。"
(let* ((dt (decode-time time 32400))
(y (decoded-time-year dt))
(m (decoded-time-month dt))
(d (decoded-time-day dt))
(hour
(cl-find-if
(lambda (hour)
(time-less-p
time
(encode-time (list 0 0 hour d m y nil nil 32400))))
hours)))
(if hour
(encode-time (list 0 0 hour d m y nil nil 32400))
(encode-time (list 0 0 (car hours) (1+ d) m y nil nil 32400)))))
;;(format-time-string "%Y-%m-%d %H:%M:%S" (jma-time-next-hour (encode-time '(0 0 17 22 2 2022 nil nil 32400)) '(5 11 17)))
;;;; 時系列データ処理
(defun jma-not-empty-range-p (pair)
(and pair
(< (car pair) (cdr pair))))
(defun jma-time-series-range-in-time (time-series lower-time upper-time)
"時系列データ TIME-SERIES 内で時間が LOWER-TIME 以上 UPPER-TIME 未満の範囲を要素インデックス値のペアで返します。"
(let ((times (mapcar #'parse-iso8601-time-string (alist-get 'timeDefines time-series)))
(i 0) lower-index upper-index)
(while (and times
(time-less-p (car times) lower-time))
(cl-incf i)
(setq times (cdr times)))
(setq lower-index i)
(while (and times
(time-less-p (car times) upper-time))
(cl-incf i)
(setq times (cdr times)))
(setq upper-index i)
(cons lower-index upper-index)))
(defun jma-time-series-range-in-date (time-series date)
"時系列データ TIME-SERIES 内の DATE で指定した日付に該当する範囲を要素インデックス値のペアで返します。"
(jma-time-series-range-in-time
time-series
(jma-date-to-time date)
(jma-date-to-time (jma-date-inc-day date))))
(defun jma-time-series-area (time-series area-code)
"時系列データ TIME-SERIES 内の AREA-CODE で指定された場所のデータを返します。"
(cond
;; シーケンスの場合は要素のコードから探す。
((listp area-code)
(seq-some
(lambda (code) (jma-time-series-area time-series code))
area-code))
(t
(seq-find
(lambda (area) (equal (alist-get 'code (alist-get 'area area)) area-code))
(alist-get 'areas time-series)))))
(defun jma-time-series-area-at (time-series area-index)
(elt (alist-get 'areas time-series) area-index))
(defun jma-time-series-area-value-at (area field-name time-index)
"時系列データの場所 AREA 内の FIELD-NAME で指定したデータの TIME-INDEX 番目を返します。"
(elt (alist-get field-name area) time-index))
(defun jma-time-series-value-at (time-series area-code field-name time-index)
"時系列データ TIME-SERIES から一つの値を取り出します。"
(elt (alist-get field-name (jma-time-series-area time-series area-code)) time-index))
;;;; 文字列テンプレート
(defun jma-expand-template (template params)
"TEMPLATEをPARAMSを使って展開します。
TEMPLATEが関数の場合、単にTEMPLATEにPARAMSを引き渡します。
TEMPLATEが文字列の場合、文字列中の展開指定を展開後文字列に置き換えます。
展開指定は {{{PNAME}}} または {{{PNAME:FMT}}} の形式です。"
(cond
((functionp template)
(funcall template params))
((stringp template)
(let ((result "")
(pos 0))
(while (string-match "{{{\\([^:}]+\\)\\(?::\\([^}]+\\)\\)?}}}" template pos)
(let* ((placeholder-beg (match-beginning 0))
(placeholder-end (match-end 0))
(pname (match-string 1 template))
(fmt (match-string 2 template))
;; paramsのキーは文字列でもシンボルでもどちらでもOK
(pvalue (cdr (or (assoc pname params)
(assq (intern pname) params))))
;; pvalueが関数なら、その関数を呼び出す
(pvalue (if (functionp pvalue)
(funcall pvalue params)
pvalue))
;; pvalueが非nilのときだけfmtを使って文字列化
;; {{{xxx: %s度}}}のように前後の文字を制御できるようにする
(fmted-pvalue
(if pvalue
(format (or fmt "%s") pvalue)
"")))
(setq result
(concat
result
(substring template pos placeholder-beg)
fmted-pvalue))
(setq pos placeholder-end)))
(setq result (concat result (substring template pos)))
result))))
;;;; ユーザー入力
(defun jma-choose-from-alist (prompt alist)
(cdr (assoc (completing-read prompt alist nil t) alist)))
;;;; シンボル
(defun jma-ensure-symbol (obj)
(cond
((stringp obj) (intern obj))
((symbolp obj) obj)
((integerp obj) (format "%s" obj))
(t (error "Not symbol or string"))))
(provide 'jma-utils)
;;; jma-utils.el ends here