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}