sameplace/message/phenomenon.rs
1//! SAME/EAS Event Codes
2
3use std::fmt;
4
5use strum::{EnumMessage, EnumProperty};
6
7/// SAME message phenomenon
8///
9/// A Phenomenon code indicates what prompted the message. These include
10/// tests, such as the
11/// [required weekly test](Phenomenon::RequiredWeeklyTest),
12/// and live messages like [floods](Phenomenon::Flood). Some events
13/// have multiple significance levels: floods can be reported as both
14/// a "Flood Watch" and a "Flood Warning." The `Phenomenon` only encodes
15/// `Phenomenon::Flood`—the [significance](crate::SignificanceLevel)
16/// is left to other types.
17///
18/// Phenomenon may be matched individually if the user wishes to take
19/// special action…
20///
21/// ```
22/// # use sameplace::Phenomenon;
23/// # let phenomenon = Phenomenon::Flood;
24/// match phenomenon {
25/// Phenomenon::Flood => println!("this message describes a flood"),
26/// _ => { /* pass */ }
27/// }
28/// ```
29///
30/// … but the programmer must **exercise caution** here. Flooding may also
31/// result from a [`Phenomenon::FlashFlood`] or a larger event like a
32/// [`Phenomenon::Hurricane`]. An evacuation might be declared with
33/// [`Phenomenon::Evacuation`], but many other messages might prompt an
34/// evacuation as part of the response. So:
35///
36/// **⚠️ When in doubt, play the message and let the user decide! ⚠️**
37///
38/// sameplace *does* separate Phenomenon into broad categories. These include:
39///
40/// ```
41/// # use sameplace::Phenomenon;
42/// assert!(Phenomenon::NationalPeriodicTest.is_national());
43/// assert!(Phenomenon::NationalPeriodicTest.is_test());
44/// assert!(Phenomenon::SevereThunderstorm.is_weather());
45/// assert!(Phenomenon::Fire.is_non_weather());
46/// ```
47///
48/// All Phenomenon `Display` a human-readable description of the event,
49/// without its significance level.
50///
51/// ```
52/// # use sameplace::Phenomenon;
53/// use std::fmt;
54///
55/// assert_eq!(format!("{}", Phenomenon::HazardousMaterials), "Hazardous Materials");
56/// assert_eq!(Phenomenon::HazardousMaterials.as_brief_str(), "Hazardous Materials");
57/// ```
58///
59/// but you probably want to display the full
60/// [`EventCode`](crate::EventCode) instead.
61///
62/// NOTE: the strum traits on this type are **not** considered API.
63#[derive(
64 Clone,
65 Copy,
66 Debug,
67 PartialEq,
68 Eq,
69 Hash,
70 strum_macros::EnumMessage,
71 strum_macros::EnumProperty,
72 strum_macros::EnumIter,
73)]
74#[non_exhaustive]
75pub enum Phenomenon {
76 /// National Emergency Message
77 ///
78 /// This was previously known as Emergency Action Notification
79 #[strum(
80 message = "National Emergency",
81 detailed_message = "National Emergency Message",
82 props(national = "")
83 )]
84 NationalEmergency,
85
86 /// National Information Center (United States, part of national activation)
87 #[strum(message = "National Information Center", props(national = ""))]
88 NationalInformationCenter,
89
90 /// National Audible Test (Canada)
91 #[strum(message = "National Audible Test", props(national = "", test = ""))]
92 NationalAudibleTest,
93
94 /// National Periodic Test (United States)
95 #[strum(message = "National Periodic Test", props(national = "", test = ""))]
96 NationalPeriodicTest,
97
98 /// National Silent Test (Canada)
99 #[strum(message = "National Silent Test", props(national = "", test = ""))]
100 NationalSilentTest,
101
102 /// Required Monthly Test
103 #[strum(message = "Required Monthly Test", props(test = ""))]
104 RequiredMonthlyTest,
105
106 /// Required Weekly Test
107 #[strum(message = "Required Weekly Test", props(test = ""))]
108 RequiredWeeklyTest,
109
110 /// Administrative Message
111 ///
112 /// Used as follow-up for non-weather messages, including potentially
113 /// to issue an all-clear.
114 #[strum(message = "Administrative Message")]
115 AdministrativeMessage,
116
117 /// Avalanche
118 #[strum(message = "Avalanche", detailed_message = "Avalanche %")]
119 Avalanche,
120
121 /// Blizzard
122 #[strum(
123 message = "Blizzard",
124 detailed_message = "Blizzard %",
125 props(weather = "")
126 )]
127 Blizzard,
128
129 /// Blue Alert (state/local)
130 #[strum(message = "Blue Alert")]
131 BlueAlert,
132
133 /// Child Abduction Emergency (state/local)
134 #[strum(
135 message = "Child Abduction",
136 detailed_message = "Child Abduction Emergency"
137 )]
138 ChildAbduction,
139
140 /// Civil Danger Warning (state/local)
141 #[strum(message = "Civil Danger", detailed_message = "Civil Danger Warning")]
142 CivilDanger,
143
144 /// Civil Emergency Message (state/local)
145 #[strum(
146 message = "Civil Emergency",
147 detailed_message = "Civil Emergency Message"
148 )]
149 CivilEmergency,
150
151 /// Coastal Flood
152 #[strum(
153 message = "Coastal Flood",
154 detailed_message = "Coastal Flood %",
155 props(weather = "")
156 )]
157 CoastalFlood,
158
159 /// Dust Storm
160 #[strum(
161 message = "Dust Storm",
162 detailed_message = "Dust Storm %",
163 props(weather = "")
164 )]
165 DustStorm,
166
167 /// Earthquake Warning
168 ///
169 /// **NOTE:** It is unclear if SAME is fast enough to provide timely
170 /// notifications of earthquakes.
171 #[strum(message = "Earthquake", detailed_message = "Earthquake Warning")]
172 Earthquake,
173
174 /// Evacuation Immediate
175 #[strum(message = "Evacuation", detailed_message = "Evacuation Immediate")]
176 Evacuation,
177
178 /// Extreme Wind
179 #[strum(
180 message = "Extreme Wind",
181 detailed_message = "Extreme Wind %",
182 props(weather = "")
183 )]
184 ExtremeWind,
185
186 /// Fire Warning
187 #[strum(message = "Fire", detailed_message = "Fire %")]
188 Fire,
189
190 /// Flash Flood
191 #[strum(
192 message = "Flash Flood",
193 detailed_message = "Flash Flood %",
194 props(weather = "")
195 )]
196 FlashFlood,
197
198 /// Flash Freeze (Canada)
199 #[strum(
200 message = "Flash Freeze",
201 detailed_message = "Flash Freeze %",
202 props(weather = "")
203 )]
204 FlashFreeze,
205
206 /// Flood
207 #[strum(message = "Flood", detailed_message = "Flood %", props(weather = ""))]
208 Flood,
209
210 /// Freeze (Canada)
211 #[strum(message = "Freeze", detailed_message = "Freeze %", props(weather = ""))]
212 Freeze,
213
214 /// Hazardous Materials (Warning)
215 #[strum(
216 message = "Hazardous Materials",
217 detailed_message = "Hazardous Materials Warning"
218 )]
219 HazardousMaterials,
220
221 /// High Wind
222 #[strum(
223 message = "High Wind",
224 detailed_message = "High Wind %",
225 props(weather = "")
226 )]
227 HighWind,
228
229 /// Hurricane
230 #[strum(
231 message = "Hurricane",
232 detailed_message = "Hurricane %",
233 props(weather = "")
234 )]
235 Hurricane,
236
237 /// Hurricane Local Statement
238 #[strum(message = "Hurricane Local Statement", props(weather = ""))]
239 HurricaneLocalStatement,
240
241 /// Law Enforcement Warning
242 #[strum(message = "Law Enforcement Warning")]
243 LawEnforcementWarning,
244
245 /// Local Area Emergency
246 #[strum(message = "Local Area Emergency")]
247 LocalAreaEmergency,
248
249 /// Network Message Notification
250 #[strum(message = "Network Message Notification")]
251 NetworkMessageNotification,
252
253 /// 911 Telephone Outage Emergency
254 #[strum(
255 message = "911 Telephone Outage",
256 detailed_message = "911 Telephone Outage Emergency"
257 )]
258 TelephoneOutage,
259
260 /// Nuclear Power Plant (Warning)
261 #[strum(
262 message = "Nuclear Power Plant",
263 detailed_message = "Nuclear Power Plant Warning"
264 )]
265 NuclearPowerPlant,
266
267 /// Practice/Demo Warning
268 #[strum(message = "Practice/Demo Warning")]
269 PracticeDemoWarning,
270
271 /// Radiological Hazard
272 #[strum(
273 message = "Radiological Hazard",
274 detailed_message = "Radiological Hazard Warning"
275 )]
276 RadiologicalHazard,
277
278 /// Severe Thunderstorm
279 #[strum(
280 message = "Severe Thunderstorm",
281 detailed_message = "Severe Thunderstorm %",
282 props(weather = "")
283 )]
284 SevereThunderstorm,
285
286 /// Severe Weather Statement
287 #[strum(
288 message = "Severe Weather",
289 detailed_message = "Severe Weather %",
290 props(weather = "")
291 )]
292 SevereWeather,
293
294 /// Shelter In Place
295 #[strum(
296 message = "Shelter In Place",
297 detailed_message = "Shelter In Place Warning"
298 )]
299 ShelterInPlace,
300
301 /// Snow Squall
302 #[strum(
303 message = "Snow Squall",
304 detailed_message = "Snow Squall %",
305 props(weather = "")
306 )]
307 SnowSquall,
308
309 /// Special Marine
310 #[strum(
311 message = "Special Marine",
312 detailed_message = "Special Marine %",
313 props(weather = "")
314 )]
315 SpecialMarine,
316
317 /// Special Weather Statement
318 #[strum(message = "Special Weather Statement", props(weather = ""))]
319 SpecialWeatherStatement,
320
321 /// Storm Surge
322 #[strum(
323 message = "Storm Surge",
324 detailed_message = "Storm Surge %",
325 props(weather = "")
326 )]
327 StormSurge,
328
329 /// Tornado Warning
330 #[strum(
331 message = "Tornado",
332 detailed_message = "Tornado %",
333 props(weather = "")
334 )]
335 Tornado,
336
337 /// Tropical Storm
338 #[strum(
339 message = "Tropical Storm",
340 detailed_message = "Tropical Storm %",
341 props(weather = "")
342 )]
343 TropicalStorm,
344
345 /// Tsunami
346 #[strum(
347 message = "Tsunami",
348 detailed_message = "Tsunami %",
349 props(weather = "")
350 )]
351 Tsunami,
352
353 /// Volcano
354 #[strum(message = "Volcano", detailed_message = "Volcano Warning")]
355 Volcano,
356
357 /// Winter Storm
358 #[strum(
359 message = "Winter Storm",
360 detailed_message = "Winter Storm %",
361 props(weather = "")
362 )]
363 WinterStorm,
364
365 /// Unrecognized phenomenon
366 ///
367 /// A catch-all for unrecognized event codes which either did not
368 /// decode properly or are not known to sameold. If you encounter
369 /// an Unrecognized event code in a production message, please
370 /// [report it as a bug](https://github.com/cbs228/sameold/issues)
371 /// right away.
372 #[strum(message = "Unrecognized", detailed_message = "Unrecognized %")]
373 Unrecognized,
374}
375
376impl Phenomenon {
377 /// Describes the event without its accompanying severity information.
378 /// For example,
379 ///
380 /// ```
381 /// # use sameplace::Phenomenon;
382 /// assert_eq!(Phenomenon::RadiologicalHazard.as_brief_str(), "Radiological Hazard");
383 /// ```
384 ///
385 /// as opposed to the full human-readable description of the event code,
386 /// "Radiological Hazard *Warning*." If you want the full description,
387 /// use [`EventCode`](crate::EventCode) instead.
388 pub fn as_brief_str(&self) -> &'static str {
389 self.get_message().expect("missing phenomenon message")
390 }
391
392 /// True if the phenomenon is associated with a national activation
393 ///
394 /// Returns true if the underlying event code is *typically* used
395 /// for national activations. This includes both live
396 /// National Emergency Messages and the National Periodic Test.
397 ///
398 /// Clients should consult the message's location codes to
399 /// determine if the message actually has national scope.
400 pub fn is_national(&self) -> bool {
401 self.get_str("national").is_some()
402 }
403
404 /// True if the phenomenon is associated with tests
405 ///
406 /// Returns true if the underlying event code is used only for
407 /// tests. Test messages do not represent actual, real-world conditions.
408 /// Test messages should also have a
409 /// [`SignificanceLevel::Test`](crate::SignificanceLevel::Test).
410 pub fn is_test(&self) -> bool {
411 self.get_str("test").is_some()
412 }
413
414 /// True if the represented phenomenon is weather
415 ///
416 /// In the United States, weather phenomenon codes like
417 /// "Severe Thunderstorm Warning" (`SVR`) are typically
418 /// only issued by the National Weather Service. The list of
419 /// weather event codes is taken from:
420 ///
421 /// * "National Weather Service Instruction 10-1708," 11 Dec 2017,
422 /// <https://www.nws.noaa.gov/directives/sym/pd01017008curr.pdf>
423 ///
424 /// Not all **natural phenomenon** are considered **weather.**
425 /// Volcanoes, avalanches, and wildfires are examples of non-weather
426 /// phenomenon that are naturally occurring. The National Weather
427 /// Service does not itself issue these types of alerts; they are
428 /// generally left to state and local authorities.
429 pub fn is_weather(&self) -> bool {
430 self.get_str("weather").is_some()
431 }
432
433 /// True if the represented phenomenon is not weather
434 ///
435 /// The opposite of [`Phenomenon::is_weather()`]. The list of
436 /// non-weather event codes available for national, state, and/or
437 /// local use is taken from:
438 ///
439 /// * "National Weather Service Instruction 10-1708," 11 Dec 2017,
440 /// <https://www.nws.noaa.gov/directives/sym/pd01017008curr.pdf>
441 pub fn is_non_weather(&self) -> bool {
442 !self.is_weather()
443 }
444
445 /// True if the phenomenon is not recognized
446 ///
447 /// ```
448 /// # use sameplace::Phenomenon;
449 /// assert!(Phenomenon::Unrecognized.is_unrecognized());
450 /// ```
451 pub fn is_unrecognized(&self) -> bool {
452 self == &Self::Unrecognized
453 }
454
455 /// True if the phenomenon is recognized
456 ///
457 /// ```
458 /// # use sameplace::Phenomenon;
459 /// assert!(Phenomenon::TropicalStorm.is_recognized());
460 /// ```
461 ///
462 /// The opposite of [`is_unrecognized()`](Phenomenon::is_unrecognized).
463 pub fn is_recognized(&self) -> bool {
464 !self.is_unrecognized()
465 }
466
467 /// Pattern string for full representation
468 ///
469 /// Returns a string like "`Tornado %`" that is the full string
470 /// representation of a SAME event code, with significance information.
471 /// `%` signs should be replaced with a textual representation of
472 /// the event code's significance level.
473 pub(crate) fn as_full_pattern_str(&self) -> &'static str {
474 self.get_detailed_message()
475 .unwrap_or_else(|| self.get_message().expect("missing phenomenon message"))
476 }
477}
478
479impl Default for Phenomenon {
480 fn default() -> Self {
481 Self::Unrecognized
482 }
483}
484
485impl std::fmt::Display for Phenomenon {
486 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
487 self.as_brief_str().fmt(f)
488 }
489}
490
491impl AsRef<str> for Phenomenon {
492 fn as_ref(&self) -> &str {
493 self.as_brief_str()
494 }
495}
496
497#[cfg(test)]
498mod tests {
499 use strum::IntoEnumIterator;
500
501 use super::*;
502
503 #[test]
504 fn test_national() {
505 assert!(Phenomenon::NationalEmergency.is_national());
506 assert!(Phenomenon::NationalEmergency.is_non_weather());
507 assert!(Phenomenon::NationalPeriodicTest.is_national());
508 assert!(Phenomenon::NationalPeriodicTest.is_non_weather());
509 assert!(!Phenomenon::Hurricane.is_national());
510 assert!(Phenomenon::Hurricane.is_weather());
511 }
512
513 // all phenomenon have messages and are either tests,
514 // weather, or non-weather
515 #[test]
516 fn test_property_completeness() {
517 for phenom in Phenomenon::iter() {
518 // these must not panic
519 phenom.as_brief_str();
520 phenom.as_full_pattern_str();
521
522 if phenom.is_test() || phenom.is_national() {
523 assert!(phenom.is_non_weather());
524 }
525 if phenom.is_weather() {
526 assert!(!phenom.is_test());
527 }
528 }
529 }
530}