Skip to main content

ferric_fred/
frequency.rs

1use std::fmt;
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4
5/// The native reporting frequency of a FRED series.
6///
7/// Deserialized from FRED's long-form `frequency` label (e.g. `"Monthly"`).
8/// Labels this version does not model — for instance the week-ending variants
9/// like `"Weekly, Ending Friday"` — are preserved verbatim in
10/// [`Frequency::Other`] rather than failing to deserialize (ADR-0005:
11/// forward-compatibility over strictness). The enum is also `#[non_exhaustive]`
12/// so new named variants can be promoted out of `Other` later without breaking
13/// callers' `match` arms.
14#[derive(Debug, Clone, PartialEq, Eq)]
15#[non_exhaustive]
16pub enum Frequency {
17    /// Daily.
18    Daily,
19    /// Weekly.
20    Weekly,
21    /// Biweekly.
22    Biweekly,
23    /// Monthly.
24    Monthly,
25    /// Quarterly.
26    Quarterly,
27    /// Semiannual.
28    Semiannual,
29    /// Annual.
30    Annual,
31    /// A frequency FRED reported that this version does not model; holds the raw
32    /// label verbatim.
33    Other(String),
34}
35
36impl Frequency {
37    /// Map FRED's long-form frequency label to a [`Frequency`].
38    fn from_label(label: &str) -> Self {
39        match label {
40            "Daily" => Self::Daily,
41            "Weekly" => Self::Weekly,
42            "Biweekly" => Self::Biweekly,
43            "Monthly" => Self::Monthly,
44            "Quarterly" => Self::Quarterly,
45            "Semiannual" => Self::Semiannual,
46            "Annual" => Self::Annual,
47            other => Self::Other(other.to_owned()),
48        }
49    }
50
51    /// The frequency label, as FRED presents it.
52    pub fn label(&self) -> &str {
53        match self {
54            Self::Daily => "Daily",
55            Self::Weekly => "Weekly",
56            Self::Biweekly => "Biweekly",
57            Self::Monthly => "Monthly",
58            Self::Quarterly => "Quarterly",
59            Self::Semiannual => "Semiannual",
60            Self::Annual => "Annual",
61            Self::Other(label) => label,
62        }
63    }
64
65    /// The FRED query code for requesting aggregation to this frequency (the
66    /// observations `frequency` parameter): `d`, `w`, `bw`, `m`, `q`, `sa`,
67    /// `a`. For [`Frequency::Other`] this returns the raw label, which may not
68    /// be a valid FRED code.
69    pub fn query_code(&self) -> &str {
70        match self {
71            Self::Daily => "d",
72            Self::Weekly => "w",
73            Self::Biweekly => "bw",
74            Self::Monthly => "m",
75            Self::Quarterly => "q",
76            Self::Semiannual => "sa",
77            Self::Annual => "a",
78            Self::Other(label) => label,
79        }
80    }
81}
82
83impl fmt::Display for Frequency {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        f.write_str(self.label())
86    }
87}
88
89impl<'de> Deserialize<'de> for Frequency {
90    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
91    where
92        D: Deserializer<'de>,
93    {
94        let label = String::deserialize(deserializer)?;
95        Ok(Self::from_label(&label))
96    }
97}
98
99impl Serialize for Frequency {
100    /// Serializes as FRED's long-form label — symmetric with [`Deserialize`], so
101    /// the value round-trips.
102    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
103    where
104        S: Serializer,
105    {
106        serializer.serialize_str(self.label())
107    }
108}
109
110// A `Frequency` is carried on the wire as its long-form label (see `Serialize`),
111// so its JSON Schema is simply that of a string. The custom serde impls rule out
112// deriving `JsonSchema`, so mirror them by hand.
113#[cfg(feature = "schemars")]
114impl schemars::JsonSchema for Frequency {
115    fn schema_name() -> std::borrow::Cow<'static, str> {
116        "Frequency".into()
117    }
118
119    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
120        <String as schemars::JsonSchema>::json_schema(generator)
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn known_label_maps_to_variant() {
130        assert_eq!(
131            serde_json::from_str::<Frequency>("\"Monthly\"").unwrap(),
132            Frequency::Monthly
133        );
134    }
135
136    #[test]
137    fn unknown_label_is_preserved_verbatim() {
138        assert_eq!(
139            serde_json::from_str::<Frequency>("\"Weekly, Ending Friday\"").unwrap(),
140            Frequency::Other("Weekly, Ending Friday".to_owned())
141        );
142    }
143
144    #[test]
145    fn display_round_trips_the_label() {
146        assert_eq!(Frequency::Annual.to_string(), "Annual");
147        assert_eq!(
148            Frequency::Other("Weekly, Ending Friday".to_owned()).to_string(),
149            "Weekly, Ending Friday"
150        );
151    }
152
153    #[test]
154    fn query_codes_match_fred() {
155        assert_eq!(Frequency::Monthly.query_code(), "m");
156        assert_eq!(Frequency::Semiannual.query_code(), "sa");
157        assert_eq!(Frequency::Daily.query_code(), "d");
158    }
159
160    #[test]
161    fn serializes_to_its_label_and_round_trips() {
162        assert_eq!(
163            serde_json::to_string(&Frequency::Monthly).unwrap(),
164            "\"Monthly\""
165        );
166        let other = Frequency::Other("Weekly, Ending Friday".to_owned());
167        let json = serde_json::to_string(&other).unwrap();
168        assert_eq!(json, "\"Weekly, Ending Friday\"");
169        assert_eq!(serde_json::from_str::<Frequency>(&json).unwrap(), other);
170    }
171}