nws_forecast_zones/
lib.rs1use std::num::ParseIntError;
38
39mod gen;
40mod gen_fz;
41mod gen_mz;
42mod gen_oz;
43
44pub use gen::ForecastZone;
45pub use gen_fz::FireZone;
46pub use gen_mz::CoastalMarineZone;
47pub use gen_oz::OffshoreMarineZone;
48
49#[derive(Debug, Copy, Clone, Eq, PartialEq)]
51pub enum Zone {
52 Forecast(ForecastZone),
56 CoastalMarine(CoastalMarineZone),
60 Offshore(OffshoreMarineZone),
64
65 FireZone(FireZone),
69}
70
71impl Zone {
72 pub fn details(&self) -> ZoneDetails {
74 match self {
75 Zone::Forecast(z) => z.details(),
76 Zone::CoastalMarine(z) => z.details(),
77 Zone::Offshore(z) => z.details(),
78 Zone::FireZone(z) => z.details(),
79 }
80 }
81 pub fn new(two: &str, numeric: u16) -> Option<Self> {
85 if let Some(z) = ForecastZone::new(two, numeric) {
86 Some(Zone::Forecast(z))
87 } else if let Some(z) = CoastalMarineZone::new(two, numeric) {
88 Some(Zone::CoastalMarine(z))
89 } else if let Some(z) = OffshoreMarineZone::new(two, numeric) {
90 Some(Zone::Offshore(z))
91 } else if let Some(z) = FireZone::new(two, numeric) {
92 Some(Zone::FireZone(z))
93 } else {
94 None
95 }
96 }
97}
98impl From<ForecastZone> for Zone {
99 fn from(z: ForecastZone) -> Self {
100 Zone::Forecast(z)
101 }
102}
103
104impl From<CoastalMarineZone> for Zone {
105 fn from(z: CoastalMarineZone) -> Self {
106 Zone::CoastalMarine(z)
107 }
108}
109
110impl From<OffshoreMarineZone> for Zone {
111 fn from(z: OffshoreMarineZone) -> Self {
112 Zone::Offshore(z)
113 }
114}
115
116impl From<FireZone> for Zone {
117 fn from(z: FireZone) -> Self {
118 Zone::FireZone(z)
119 }
120}
121
122#[derive(Debug, Copy, Clone)]
124pub struct ZoneDetails {
125 pub state: &'static str,
127 pub zone: &'static str,
129 pub zone_numeric: u16,
131 pub name: &'static str,
133 pub wfo: &'static str,
135}
136
137#[cfg_attr(test, derive(Debug))]
138enum ZoneSetEnum {
139 All(String),
143 Range(String, u16, u16),
145 Specific(Zone),
147 UnknownZone(String, u16),
149 List(Vec<ZoneSetEnum>),
151}
152impl ZoneSetEnum {
153 fn contains(&self, zone: Zone) -> bool {
154 match self {
155 ZoneSetEnum::All(st) => {
156 if let Zone::Forecast(zone) = zone {
157 zone.details().state == st
158 } else {
159 false
160 }
161 }
162 ZoneSetEnum::Range(st, lo, hi) => {
163 let d = zone.details();
164 d.state == st && d.zone_numeric >= *lo && d.zone_numeric <= *hi
165 }
166 ZoneSetEnum::Specific(a) => *a == zone,
167 ZoneSetEnum::List(l) => {
168 for sub in l {
169 if sub.contains(zone) {
170 return true;
171 }
172 }
173 false
174 }
175 ZoneSetEnum::UnknownZone(..) => false,
176 }
177 }
178}
179
180#[cfg_attr(test, derive(Debug))]
182pub struct ZoneSet {
183 inner: ZoneSetEnum,
184}
185
186impl ZoneSet {
187 pub fn contains(&self, zone: impl Into<Zone>) -> bool {
190 self.inner.contains(zone.into())
191 }
192}
193
194#[derive(Debug)]
196pub enum ZoneSetError {
197 OutOfChars,
199 UnexpectedChar(char),
201
202 UnexpectedString(String),
204
205 ParsingError,
206
207 NoSuchZone(String, u16),
208}
209
210impl From<ParseIntError> for ZoneSetError {
211 fn from(_: ParseIntError) -> Self {
212 ZoneSetError::ParsingError
213 }
214}
215
216pub fn parse_zoneset(mut range: &str) -> Result<ZoneSet, ZoneSetError> {
230 macro_rules! read1 {
231 ($chars:expr) => {{
232 $chars.next().ok_or(ZoneSetError::OutOfChars)?
233 }};
234 }
235 macro_rules! read3 {
236 ($chars:expr) => {{
237 let mut n = String::with_capacity(3);
238 for _ in 0..3 {
239 n.push($chars.next().ok_or(ZoneSetError::OutOfChars)?);
240 }
241 n
242 }};
243 }
244
245 #[derive(Eq, PartialEq, Debug)]
246 enum State {
247 ExpectingStateZ,
249 ExpectingTricode(String),
251
252 ExpectingListOrRange(String, u16),
256
257 ExpectingEndOfRange(String, u16),
258
259 ExpectingStateOrCode(String),
261 }
262
263 let l = range.len();
265 if l > 8
266 && range[l - 8..]
267 .chars()
268 .all(|c| c == '-' || c.is_ascii_digit())
269 {
270 range = &range[..l - 8];
271 }
272
273 let mut chars = range.chars().peekable();
275
276 let mut list = Vec::new();
277
278 let mut parsing_state = State::ExpectingStateZ;
279
280 loop {
284 parsing_state = match parsing_state {
285 State::ExpectingStateZ => {
286 let t = read3!(chars);
287 assert!(t.ends_with('Z'));
288 State::ExpectingTricode(t)
289 }
290 State::ExpectingTricode(state) => {
291 let n = read3!(chars);
292 if n == "ALL" {
293 list.push(ZoneSetEnum::All(state[0..2].to_string()));
294 if chars.peek().is_none() {
295 break;
296 }
297 let x = read1!(chars);
298 if x != '-' {
299 return Err(ZoneSetError::UnexpectedChar(x));
300 }
301 State::ExpectingStateZ
302 } else if n.chars().all(|c| c.is_ascii_digit()) {
303 let numeric = n.parse()?;
304 State::ExpectingListOrRange(state, numeric)
307 } else {
308 return Err(ZoneSetError::UnexpectedString(n));
309 }
310 }
311 State::ExpectingListOrRange(state, numeric) => {
312 match chars.next() {
313 None => {
314 if let Some(z) = Zone::new(&state[..2], numeric) {
316 list.push(ZoneSetEnum::Specific(z));
317 } else {
318 list.push(ZoneSetEnum::UnknownZone(state, numeric));
319 }
321 break;
322 }
323 Some('>') => {
324 State::ExpectingEndOfRange(state, numeric)
327 }
328 Some('-') => {
329 if let Some(z) = Zone::new(&state[..2], numeric) {
332 list.push(ZoneSetEnum::Specific(z));
333 } else {
334 list.push(ZoneSetEnum::UnknownZone(state.clone(), numeric));
335 }
337
338 State::ExpectingStateOrCode(state)
339 }
340 Some(x) => {
341 return Err(ZoneSetError::UnexpectedChar(x));
342 }
343 }
344 }
345 State::ExpectingEndOfRange(state, numeric) => {
346 let code = read3!(chars);
347 let end_numeric = code.parse()?;
348 list.push(ZoneSetEnum::Range(
349 state[0..2].to_string(),
350 numeric,
351 end_numeric,
352 ));
353
354 match chars.next() {
355 None => break,
356 Some('-') => State::ExpectingStateOrCode(state),
357 Some(x) => return Err(ZoneSetError::UnexpectedChar(x)),
358 }
359 }
360 State::ExpectingStateOrCode(state) => {
361 let t = read3!(chars);
362 if t.ends_with('Z') {
363 State::ExpectingTricode(t)
364 } else if t.chars().all(|c| c.is_ascii_digit()) {
365 let numeric = t.parse()?;
366 State::ExpectingListOrRange(state, numeric)
367 } else {
368 return Err(ZoneSetError::UnexpectedString(t));
369 }
370 }
371 };
372 }
373
374 let inner = if list.len() == 1 {
375 list.pop().unwrap()
376 } else {
377 ZoneSetEnum::List(list)
378 };
379
380 Ok(ZoneSet { inner })
381}
382
383#[cfg(test)]
384mod tests {
385
386 use crate::parse_zoneset;
387 use crate::CoastalMarineZone;
388 use crate::ForecastZone;
389 use crate::OffshoreMarineZone;
390 use crate::Zone;
391
392 #[test]
393 fn test_parsing() {
394 let a = parse_zoneset("RIZALL").unwrap();
395 println!("{a:?}");
396 assert!(a.contains(ForecastZone::RI004));
397 assert!(!a.contains(ForecastZone::AK017));
398
399 let a = parse_zoneset("MEZALL-NHZALL-142200-").unwrap();
400 println!("{a:?}");
401 assert!(a.contains(ForecastZone::ME001));
402 assert!(a.contains(ForecastZone::NH001));
403 assert!(!a.contains(ForecastZone::RI001));
404
405 let a = parse_zoneset("RIZ001-002").unwrap();
406 println!("{a:?}");
407 assert!(a.contains(ForecastZone::RI001));
408 assert!(a.contains(ForecastZone::RI002));
409 assert!(!a.contains(ForecastZone::RI003));
410
411 let a = parse_zoneset("RIZ001-002-MAZ003").unwrap();
412 println!("{a:?}");
413 assert!(a.contains(ForecastZone::RI001));
414 assert!(a.contains(ForecastZone::RI002));
415 assert!(!a.contains(ForecastZone::RI003));
416 assert!(a.contains(ForecastZone::MA003));
417
418 let a = parse_zoneset("RIZ001>002").unwrap();
419 println!("{a:?}");
420 assert!(a.contains(ForecastZone::RI001));
421 assert!(a.contains(ForecastZone::RI002));
422 assert!(!a.contains(ForecastZone::RI003));
423
424 let a = parse_zoneset("MAZ017>019-RIZ001>003-140815-").unwrap();
425 println!("{a:?}");
426 assert!(!a.contains(ForecastZone::MA016));
427 assert!(a.contains(ForecastZone::MA017));
428 assert!(a.contains(ForecastZone::MA018));
429 assert!(a.contains(ForecastZone::MA019));
430 assert!(!a.contains(ForecastZone::MA020));
431
432 assert!(a.contains(ForecastZone::RI001));
433 assert!(a.contains(ForecastZone::RI002));
434 assert!(a.contains(ForecastZone::RI003));
435 assert!(!a.contains(ForecastZone::RI004));
436
437 let a =
438 parse_zoneset("CTZ002>004-MAZ002>006-008>014-017-020-026-RIZ001>007-110000-").unwrap();
439 println!("{a:?}");
440 assert!(a.contains(ForecastZone::CT002));
441 assert!(a.contains(ForecastZone::MA006));
442 assert!(!a.contains(ForecastZone::MA007));
443 assert!(a.contains(ForecastZone::MA008));
444 assert!(a.contains(ForecastZone::MA009));
445 assert!(a.contains(ForecastZone::MA014));
446 assert!(!a.contains(ForecastZone::MA015));
447 assert!(!a.contains(ForecastZone::MA016));
448 assert!(a.contains(ForecastZone::MA017));
449 assert!(a.contains(ForecastZone::RI001));
450 assert!(a.contains(ForecastZone::RI007));
451 assert!(!a.contains(ForecastZone::RI008));
452
453 let a = parse_zoneset("ILZ095>097-MOZ018-019-026-027-034>036-142200-").unwrap();
454 println!("{a:?}");
455 }
456
457 #[test]
458 fn test_parsing_marine() {
459 let a = parse_zoneset("GMZ634>636-650-655-675-222130-").unwrap();
460 println!("{a:?}");
461 assert!(a.contains(CoastalMarineZone::GMZ634));
462 assert!(a.contains(Zone::CoastalMarine(CoastalMarineZone::GMZ675)));
463 assert!(a.contains(CoastalMarineZone::GMZ650));
464 assert!(!a.contains(ForecastZone::MA001));
465 assert!(!a.contains(OffshoreMarineZone::AMZ040));
466
467 let a =
468 parse_zoneset("NYZ176-178-MAZ016-IL014-TNZ027-NJZ106-GAZ044-MD014-230500-").unwrap();
469 println!("{a:?}");
470 }
471
472 #[test]
473 fn test_nopanic() {
474 let a = parse_zoneset("AAZ000-123456-").unwrap();
475 println!("{a:?}");
476
477 let a = parse_zoneset("RIZ999-123456-").unwrap();
478 println!("{a:?}");
479
480 let a =
481 parse_zoneset("ANZ835-935-AMZ111-154-330-350-352-354-370-450-454-472-230500-").unwrap();
482 println!("{a:?}");
483
484 let a = parse_zoneset("ANZ600-241415-").unwrap();
485 println!("{a:?}");
486
487 parse_zoneset("LEZ161-240830-").unwrap();
488 parse_zoneset("LSZ261-241000-").unwrap();
489 parse_zoneset("LMZ867-665-643>646-240300-").unwrap();
490 }
491}
492
493