1use anyhow::{Result, anyhow, ensure};
14use chrono::{DateTime, Local};
15use serde::{Deserialize, Serialize};
16use std::{
17 collections::{BTreeMap, HashMap},
18 sync::LazyLock,
19};
20
21const JMA_AREA_JSON: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/res/area.json"));
35
36const JMA_TELOPLS_JSON: &str = include_str!(concat!(
43 env!("CARGO_MANIFEST_DIR"),
44 "/res/forecast_telops.json"
45));
46
47#[derive(Clone, Debug, Serialize, Deserialize)]
48struct JmaAreaDef {
49 offices: BTreeMap<String, JmaOfficeInfo>,
50}
51
52#[derive(Clone, Debug, Serialize, Deserialize)]
53#[serde(rename_all = "camelCase")]
54pub struct JmaOfficeInfo {
55 #[serde(default)]
57 pub code: String,
58 pub name: String,
59 pub en_name: String,
60 pub office_name: String,
61}
62
63static OFFICE_LIST: LazyLock<Vec<JmaOfficeInfo>> = LazyLock::new(office_list);
64static WEATHER_CODE_MAP: LazyLock<HashMap<String, String>> = LazyLock::new(weather_code_map);
65
66fn office_list() -> Vec<JmaOfficeInfo> {
67 let root: JmaAreaDef = serde_json::from_str(JMA_AREA_JSON).unwrap();
68
69 let list: Vec<_> = root
70 .offices
71 .iter()
72 .map(|(code, info)| {
73 let mut modified = info.clone();
74 modified.code = code.to_string();
75 modified
76 })
77 .collect();
78
79 list
80}
81
82pub fn offices() -> &'static Vec<JmaOfficeInfo> {
83 &OFFICE_LIST
84}
85
86pub fn office_name_to_code(name: &str) -> Option<String> {
87 offices()
88 .iter()
89 .find(|&info| info.name == name)
90 .map(|info| info.code.to_string())
91}
92
93fn weather_code_map() -> HashMap<String, String> {
94 let mut result = HashMap::new();
95
96 type RawObj = HashMap<String, [String; 5]>;
97 let obj: RawObj = serde_json::from_str(JMA_TELOPLS_JSON).unwrap();
98 for (k, v) in obj.iter() {
99 result.insert(k.to_string(), v[3].to_string());
101 }
102
103 result
104}
105
106pub fn weather_code_to_string(code: &str) -> Result<&str> {
107 WEATHER_CODE_MAP
108 .get(code)
109 .map(String::as_str)
110 .ok_or_else(|| anyhow!("Weather code not found: {code}"))
111}
112
113#[derive(Clone, Debug, Serialize, Deserialize)]
114#[serde(rename_all = "camelCase")]
115pub struct OverviewForecast {
116 pub publishing_office: String,
117 pub report_datetime: String,
118 pub target_area: String,
119 pub headline_text: String,
120 pub text: String,
121}
122
123pub type ForecastRoot = Vec<Forecast>;
124
125#[derive(Clone, Debug, Serialize, Deserialize)]
126#[serde(rename_all = "camelCase")]
127pub struct Forecast {
128 pub publishing_office: String,
129 pub report_datetime: String,
130 pub time_series: Vec<TimeSeries>,
131}
132
133#[derive(Clone, Debug, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase")]
135pub struct TimeSeries {
136 pub time_defines: Vec<String>,
137 pub areas: Vec<AreaArrayElement>,
138 pub temp_average: Option<TempPrecipAverage>,
139 pub precip_average: Option<TempPrecipAverage>,
140}
141
142#[derive(Clone, Debug, Serialize, Deserialize)]
143#[serde(rename_all = "camelCase")]
144pub struct AreaArrayElement {
145 pub area: Area,
146 #[serde(flatten)]
147 pub data: AreaData,
148}
149
150#[derive(Clone, Debug, Serialize, Deserialize)]
151#[serde(rename_all = "camelCase")]
152pub struct Area {
153 pub name: String,
154 pub code: String,
155}
156
157#[derive(Clone, Debug, Serialize, Deserialize)]
158#[serde(untagged)]
159pub enum AreaData {
160 #[serde(rename_all = "camelCase")]
163 WheatherPop {
164 weather_codes: Vec<String>,
165 pops: Vec<String>,
166 reliabilities: Vec<String>,
167 },
168 #[serde(rename_all = "camelCase")]
170 DetailedTempreture {
171 temps_min: Vec<String>,
172 temps_min_upper: Vec<String>,
173 temps_min_lower: Vec<String>,
174 temps_max: Vec<String>,
175 temps_max_upper: Vec<String>,
176 temps_max_lower: Vec<String>,
177 },
178
179 #[serde(rename_all = "camelCase")]
182 Wheather {
183 weather_codes: Vec<String>,
184 weathers: Vec<String>,
185 winds: Vec<String>,
186 #[serde(default)]
188 waves: Vec<String>,
189 },
190 #[serde(rename_all = "camelCase")]
192 Pop { pops: Vec<String> },
193 #[serde(rename_all = "camelCase")]
195 Tempreture { temps: Vec<String> },
196}
197
198#[derive(Clone, Debug, Serialize, Deserialize)]
199#[serde(rename_all = "camelCase")]
200pub struct TempPrecipAverage {
201 area: Area,
202 min: String,
203 max: String,
204}
205
206pub fn url_overview_forecast(office_code: &str) -> String {
208 format!("https://www.jma.go.jp/bosai/forecast/data/overview_forecast/{office_code}.json")
209}
210
211pub fn url_forecast(office_code: &str) -> String {
213 format!("https://www.jma.go.jp/bosai/forecast/data/forecast/{office_code}.json")
214}
215
216#[derive(Clone, Debug, Serialize, Deserialize)]
217pub struct AiReadableWeather {
218 url_for_more_info: String,
219 now: String,
220
221 publishing_office: String,
222 report_datetime: String,
223
224 date_data: BTreeMap<String, DateDataElem>,
226
227 target_area: String,
228 headline: String,
229 overview: String,
230}
231
232#[derive(Default, Clone, Debug, Serialize, Deserialize)]
233struct DateDataElem {
234 #[serde(skip_serializing_if = "Option::is_none")]
235 weather_pop_area: Option<String>,
236 #[serde(skip_serializing_if = "Option::is_none")]
237 weather: Option<String>,
238 #[serde(skip_serializing_if = "Option::is_none")]
239 pop: Option<String>,
240 #[serde(skip_serializing_if = "Option::is_none")]
241 tempreture_area: Option<String>,
242 #[serde(skip_serializing_if = "Option::is_none")]
243 temp_min: Option<String>,
244 #[serde(skip_serializing_if = "Option::is_none")]
245 temp_max: Option<String>,
246}
247
248pub fn weather_to_ai_readable(
250 office_code: &str,
251 ov: &OverviewForecast,
252 fcr: &ForecastRoot,
253) -> Result<AiReadableWeather> {
254 let mut date_data: BTreeMap<String, DateDataElem> = BTreeMap::new();
255
256 for fc in fcr.iter() {
257 for ts in fc.time_series.iter() {
258 let td = &ts.time_defines;
259 let areas = &ts.areas;
260 if areas.is_empty() {
261 continue;
262 }
263 let area = &areas[0];
265 for (i, dt_str) in td.iter().enumerate() {
266 fill_by_element(&mut date_data, i, dt_str, area)?;
267 }
268 }
269 }
270
271 let now: DateTime<Local> = Local::now();
272 Ok(AiReadableWeather {
273 url_for_more_info: format!(
274 "https://www.jma.go.jp/bosai/forecast/#area_type=offices&area_code={office_code}"
275 ),
276 now: now.to_string(),
277
278 publishing_office: ov.publishing_office.to_string(),
279 report_datetime: ov.report_datetime.to_string(),
280
281 date_data,
282
283 target_area: ov.target_area.to_string(),
284 headline: ov.headline_text.to_string(),
285 overview: ov.text.to_string(),
286 })
287}
288
289fn fill_by_element(
290 result: &mut BTreeMap<String, DateDataElem>,
291 idx: usize,
292 dt_str: &str,
293 area: &AreaArrayElement,
294) -> Result<()> {
295 match &area.data {
296 AreaData::WheatherPop {
297 weather_codes,
298 pops,
299 reliabilities: _,
300 } => {
301 let v = result.entry(dt_str.to_string()).or_default();
303 let weather_code = weather_codes
304 .get(idx)
305 .ok_or_else(|| anyhow!("Parse error"))?;
306 let pop = pops.get(idx).ok_or_else(|| anyhow!("Parse error"))?;
307
308 v.weather_pop_area = Some(area.area.name.to_string());
309 if v.weather.is_none() && !weather_code.is_empty() {
310 v.weather = Some(weather_code_to_string(weather_code)?.to_string());
311 }
312 if v.pop.is_none() && !pop.is_empty() {
313 v.pop = Some(format!("{}%", &pop));
314 }
315 }
316 AreaData::DetailedTempreture {
317 temps_min,
318 temps_min_upper: _,
319 temps_min_lower: _,
320 temps_max,
321 temps_max_upper: _,
322 temps_max_lower: _,
323 } => {
324 let v = result.entry(dt_str.to_string()).or_default();
325 let min = temps_min.get(idx).ok_or_else(|| anyhow!("Parse error"))?;
326 let max = temps_max.get(idx).ok_or_else(|| anyhow!("Parse error"))?;
327
328 v.tempreture_area = Some(area.area.name.to_string());
329 if !min.is_empty() {
330 v.temp_min = Some(min.to_string());
331 }
332 if !max.is_empty() {
333 v.temp_max = Some(max.to_string());
334 }
335 }
336 AreaData::Wheather {
337 weather_codes: _,
338 weathers,
339 winds: _,
340 waves: _,
341 } => {
342 let v = result.entry(dt_str.to_string()).or_default();
343 let weather = weathers.get(idx).ok_or_else(|| anyhow!("Parse error"))?;
344
345 v.weather_pop_area = Some(area.area.name.to_string());
346 v.weather = Some(weather.to_string());
347 }
348 AreaData::Pop { pops } => {
349 let pop = pops.get(idx).ok_or_else(|| anyhow!("Parse error"))?;
350
351 result.entry(dt_str.to_string()).and_modify(|v| {
353 v.pop = Some(pop.to_string());
354 });
355 }
356 AreaData::Tempreture { temps } => {
357 let dt = format!("{}T00:00:00+09:00", &dt_str[0..10]);
358 let temp = temps.get(idx).ok_or_else(|| anyhow!("Parse error"))?;
359 result.entry(dt).and_modify(|v| {
360 if idx == 0 {
361 v.temp_min = Some(temp.to_string());
362 } else if idx == 1 {
363 v.temp_max = Some(temp.to_string());
364 }
365 });
366 }
367 }
368 Ok(())
369}
370
371#[allow(unused)]
372fn edit_distance_normalized(a: &str, b: &str) -> Result<f32> {
373 let maxlen = a.len().max(b.len());
374 if maxlen == 0 {
375 return Ok(0.0);
376 }
377
378 let dis = edit_distance(a, b)?;
379 Ok(dis as f32 / maxlen as f32)
380}
381
382#[allow(unused)]
386fn edit_distance(a: &str, b: &str) -> Result<u32> {
387 ensure!(a.len() < 1024);
388 ensure!(b.len() < 1024);
389
390 let a: Vec<_> = a.chars().collect();
391 let b: Vec<_> = b.chars().collect();
392 let pitch = b.len() + 1;
393 let mut dp = vec![0u16; (a.len() + 1) * (b.len() + 1)];
394 let idx = |ia: usize, ib: usize| -> usize { ia * pitch + ib };
395
396 for ia in 0..=a.len() {
397 dp[idx(ia, 0)] = ia as u16;
398 }
399 for ib in 0..=b.len() {
400 dp[idx(0, ib)] = ib as u16;
401 }
402
403 for ia in 1..=a.len() {
404 for ib in 1..=b.len() {
405 let cost = if a[ia - 1] == b[ib - 1] { 0 } else { 1 };
406 let d1 = dp[idx(ia - 1, ib)] + 1;
407 let d2 = dp[idx(ia, ib - 1)] + 1;
408 let d3 = dp[idx(ia - 1, ib - 1)] + cost;
409 dp[idx(ia, ib)] = d1.min(d2).min(d3);
410 }
411 }
412
413 Ok(dp[idx(a.len(), b.len())] as u32)
414}
415
416#[cfg(test)]
417mod tests {
418 use std::{
419 fs::{self, File},
420 io::Write,
421 };
422
423 use super::*;
424 use reqwest::Client;
425 use serde_json::Value;
426
427 #[tokio::test]
428 #[ignore]
429 async fn weather_test_json() -> Result<()> {
431 let olist = offices();
432 println!("Office count: {}", olist.len());
433 let client = Client::new();
434 for info in olist.iter() {
435 let url = format!(
436 "https://www.jma.go.jp/bosai/forecast/data/overview_forecast/{}.json",
437 info.code
438 );
439 let resp = client.get(url).send().await?;
440 if resp.status().is_success() {
441 let mut file = File::create(format!(
442 "{}/res/test/weather/overview_forecast/{}.json",
443 env!("CARGO_MANIFEST_DIR"),
444 info.code
445 ))?;
446 let value: Value = serde_json::from_str(&resp.text().await?)?;
447 let text = serde_json::to_string_pretty(&value)? + "\n";
448 file.write_all(text.as_bytes())?;
449 } else {
450 println!("overview_forecast not found: {info:?}");
451 }
452
453 let url = format!(
454 "https://www.jma.go.jp/bosai/forecast/data/forecast/{}.json",
455 info.code
456 );
457 let resp = client.get(url).send().await?;
458 if resp.status().is_success() {
459 let mut file = File::create(format!(
460 "{}/res/test/weather/forecast/{}.json",
461 env!("CARGO_MANIFEST_DIR"),
462 info.code
463 ))?;
464 let value: Value = serde_json::from_str(&resp.text().await?)?;
465 let text = serde_json::to_string_pretty(&value)? + "\n";
466 file.write_all(text.as_bytes())?;
467 } else {
468 println!("forecast not found: {info:?}");
469 }
470
471 let url = format!(
472 "https://www.jma.go.jp/bosai/forecast/data/overview_week/{}.json",
473 info.code
474 );
475 let resp = client.get(url).send().await?;
476 if resp.status().is_success() {
477 let mut file = File::create(format!(
478 "{}/res/test/weather/overview_week/{}.json",
479 env!("CARGO_MANIFEST_DIR"),
480 info.code
481 ))?;
482 let value: Value = serde_json::from_str(&resp.text().await?)?;
483 let text = serde_json::to_string_pretty(&value)? + "\n";
484 file.write_all(text.as_bytes())?;
485 } else {
486 println!("overview_week not found: {info:?}");
487 }
488 }
489
490 Ok(())
491 }
492
493 #[test]
494 fn parse_overview_forecast() -> Result<()> {
495 let ents = fs::read_dir(concat!(
496 env!("CARGO_MANIFEST_DIR"),
497 "/res/test/weather/overview_forecast"
498 ))?;
499
500 let mut count = 0;
501 for ent in ents {
502 let src = fs::read_to_string(ent?.path())?;
503 let _: OverviewForecast = serde_json::from_str(&src)?;
504 count += 1;
505 }
506 assert!(count > 40);
507
508 Ok(())
509 }
510
511 #[test]
512 fn parse_forecast() -> Result<()> {
513 let ents = fs::read_dir(concat!(
514 env!("CARGO_MANIFEST_DIR"),
515 "/res/test/weather/forecast"
516 ))?;
517
518 let mut count = 0;
519 for ent in ents {
520 let src = fs::read_to_string(ent?.path())?;
521 let _: ForecastRoot = serde_json::from_str(&src)?;
522 count += 1;
523 }
524 assert!(count > 40);
525
526 Ok(())
527 }
528
529 #[test]
530 fn weather_code() -> Result<()> {
531 assert_eq!("晴", weather_code_to_string("100")?);
532 assert_eq!("晴時々曇", weather_code_to_string("101")?);
533 assert_eq!("雪で雷を伴う", weather_code_to_string("450")?);
534
535 Ok(())
536 }
537
538 #[test]
539 fn ai_readable() -> Result<()> {
541 let src = include_str!(concat!(
542 env!("CARGO_MANIFEST_DIR"),
543 "/res/test/weather/overview_forecast/130000.json"
544 ));
545 let ov: OverviewForecast = serde_json::from_str(src)?;
546 let src = include_str!(concat!(
547 env!("CARGO_MANIFEST_DIR"),
548 "/res/test/weather/forecast/130000.json"
549 ));
550 let fcr: ForecastRoot = serde_json::from_str(src)?;
551
552 let obj = weather_to_ai_readable("130000", &ov, &fcr)?;
553 println!("{}", serde_json::to_string_pretty(&obj).unwrap());
554
555 Ok(())
556 }
557
558 #[test]
559 fn edit_distance_test() {
560 assert_eq!(3, edit_distance("", "abc").unwrap());
561 assert_eq!(3, edit_distance("def", "").unwrap());
562 assert_eq!(3, edit_distance("kitten", "sitting").unwrap());
563
564 assert_eq!(4, edit_distance("カラクリ", "ボンゴレ").unwrap());
565 assert_eq!(4, edit_distance("テスト", "テストパターン").unwrap());
566 }
567}