grid_tariffs/
local_administrative_unit.rs

1use std::str::FromStr;
2
3use serde::{Deserialize, Serialize};
4
5use crate::{
6    Country, TaxReductions, Taxes,
7    constants::{DEFAULT_SE_TAX_REDUCTIONS, DEFAULT_SE_TAXES, REDUCED_SE_TAXES},
8};
9
10/// Local administrative unit parsing error
11#[derive(Debug, thiserror::Error)]
12pub enum LauParseError {
13    #[error("Unsupported or invalid country code")]
14    UnsupportedCountryCode,
15    #[error("Invalid LAU code")]
16    InvalidCode,
17}
18
19#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
20#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
21pub struct LocalAdministrativeUnit {
22    code: &'static str,
23    name: &'static str,
24    country: Country,
25}
26
27impl LocalAdministrativeUnit {
28    pub fn eu_code(&self) -> String {
29        format!("{}_{:0width$}", self.country.code(), self.code, width = 4)
30    }
31
32    pub fn local_code(&self) -> &str {
33        self.code
34    }
35
36    pub fn info(&'static self) -> LocalAdministrativeUnitInfo {
37        self.into()
38    }
39
40    pub fn name(&self) -> &str {
41        self.name
42    }
43
44    pub fn country(&self) -> Country {
45        self.country
46    }
47
48    pub fn get(country: Country, code: &str) -> Option<&'static Self> {
49        match country {
50            Country::SE => registry::SE.get(code),
51        }
52    }
53
54    pub fn taxes(&self) -> Taxes {
55        match self.country {
56            Country::SE => {
57                let code = self.code.parse().unwrap();
58                if registry::SE_REDUCED_ENERGY_TAX_SUBDIVISIONS.contains(&code) {
59                    REDUCED_SE_TAXES
60                } else {
61                    DEFAULT_SE_TAXES
62                }
63            }
64        }
65    }
66
67    pub fn tax_reductions(&self) -> TaxReductions {
68        match self.country {
69            Country::SE => DEFAULT_SE_TAX_REDUCTIONS,
70        }
71    }
72
73    pub fn from_eu_code(value: &str) -> Result<&'static Self, LauParseError> {
74        let country: Country = value[0..2]
75            .parse()
76            .map_err(|_| LauParseError::UnsupportedCountryCode)?;
77        let lau_code = &value[3..];
78        match country {
79            Country::SE => registry::SE.get(lau_code).ok_or(LauParseError::InvalidCode),
80        }
81    }
82}
83
84impl Serialize for LocalAdministrativeUnit {
85    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
86    where
87        S: serde::Serializer,
88    {
89        serializer.serialize_str(&self.eu_code())
90    }
91}
92
93impl FromStr for LocalAdministrativeUnit {
94    type Err = LauParseError;
95
96    fn from_str(eu_lau_code: &str) -> Result<Self, Self::Err> {
97        Self::from_eu_code(eu_lau_code).copied()
98    }
99}
100
101impl<'de> Deserialize<'de> for &'static LocalAdministrativeUnit {
102    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
103    where
104        D: serde::Deserializer<'de>,
105    {
106        let s = String::deserialize(deserializer)?;
107        LocalAdministrativeUnit::from_eu_code(&s).map_err(serde::de::Error::custom)
108    }
109}
110
111impl<'de> Deserialize<'de> for LocalAdministrativeUnit {
112    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
113    where
114        D: serde::Deserializer<'de>,
115    {
116        let static_ref = <&'static LocalAdministrativeUnit>::deserialize(deserializer)?;
117        Ok(*static_ref)
118    }
119}
120
121#[derive(Debug, Clone, Copy, Serialize)]
122pub struct LocalAdministrativeUnitInfo {
123    pub code: &'static str,
124    pub name: &'static str,
125    pub country: Country,
126    pub taxes: Taxes,
127    pub tax_reductions: TaxReductions,
128}
129
130impl LocalAdministrativeUnitInfo {
131    pub fn only_current(mut self) -> Self {
132        self.taxes = self.taxes.with_current_only();
133        self.tax_reductions = self.tax_reductions.with_current_only();
134        self
135    }
136}
137
138impl From<&'static LocalAdministrativeUnit> for LocalAdministrativeUnitInfo {
139    fn from(value: &'static LocalAdministrativeUnit) -> Self {
140        Self {
141            code: value.local_code(),
142            name: value.name(),
143            country: value.country(),
144            taxes: value.taxes(),
145            tax_reductions: value.tax_reductions(),
146        }
147    }
148}
149
150pub(crate) mod registry {
151    static SE_DATA: &str = include_str!("../data/EU-27-LAU-2024-NUTS-2024-SE.csv");
152    use std::{
153        collections::{HashMap, HashSet},
154        str::FromStr,
155        sync::LazyLock,
156    };
157
158    use crate::{Country, local_administrative_unit::LocalAdministrativeUnit};
159
160    pub(crate) static SE: LazyLock<HashMap<&str, LocalAdministrativeUnit>> = LazyLock::new(|| {
161        let mut map = HashMap::with_capacity(290);
162
163        // Skip header line, then iterate over data lines
164        for line in SE_DATA.lines().skip(1) {
165            // Split on comma - no allocations, just borrows from SE_DATA
166            let mut parts = line.split(',');
167
168            let nuts_3_code = parts.next().unwrap();
169            let lau_code = parts.next().unwrap();
170            let name = parts.next().unwrap();
171            let country = Country::from_str(&nuts_3_code[0..2]).unwrap();
172            map.insert(
173                lau_code,
174                LocalAdministrativeUnit {
175                    code: lau_code,
176                    name,
177                    country,
178                },
179            );
180        }
181        map
182    });
183
184    pub(crate) static SE_REDUCED_ENERGY_TAX_SUBDIVISIONS: LazyLock<HashSet<u16>> =
185        LazyLock::new(|| {
186            HashSet::from_iter([
187                2506, 2505, 2326, 2403, 2582, 2305, 2425, 2523, 2583, 2361, 2510, 2514, 2584, 2309,
188                2161, 2580, 2481, 2023, 2418, 2062, 2401, 2417, 2034, 2521, 2581, 2303, 2409, 2482,
189                2283, 2422, 2421, 2313, 1737, 2480, 2462, 2404, 2460, 2260, 2321, 2463, 2039, 2560,
190                2284, 2380, 2513, 2518,
191            ])
192        });
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use std::str::FromStr;
199
200    #[test]
201    fn test_eu_code_formatting() {
202        // Test that EU code is properly formatted with country code and zero-padded LAU code
203        let lau = LocalAdministrativeUnit::get(Country::SE, "0180").unwrap();
204        assert_eq!(lau.eu_code(), "SE_0180");
205    }
206
207    #[test]
208    fn test_local_code() {
209        let lau = LocalAdministrativeUnit::get(Country::SE, "0180").unwrap();
210        assert_eq!(lau.local_code(), "0180");
211    }
212
213    #[test]
214    fn test_name() {
215        let lau = LocalAdministrativeUnit::get(Country::SE, "0180").unwrap();
216        assert_eq!(lau.name(), "Stockholm");
217    }
218
219    #[test]
220    fn test_country() {
221        let lau = LocalAdministrativeUnit::get(Country::SE, "0180").unwrap();
222        assert_eq!(lau.country(), Country::SE);
223    }
224
225    #[test]
226    fn test_get_valid_code() {
227        let lau = LocalAdministrativeUnit::get(Country::SE, "0180");
228        assert!(lau.is_some());
229    }
230
231    #[test]
232    fn test_get_invalid_code() {
233        let lau = LocalAdministrativeUnit::get(Country::SE, "9999");
234        assert!(lau.is_none());
235    }
236
237    #[test]
238    fn test_taxes_default() {
239        // Test a municipality that should have default taxes (e.g., Stockholm)
240        let lau = LocalAdministrativeUnit::get(Country::SE, "0180").unwrap();
241        let taxes = lau.taxes();
242        assert_eq!(taxes, DEFAULT_SE_TAXES);
243    }
244
245    #[test]
246    fn test_taxes_reduced() {
247        // Test a municipality that should have reduced energy tax (e.g., 2506)
248        let lau = LocalAdministrativeUnit::get(Country::SE, "2506").unwrap();
249        let taxes = lau.taxes();
250        assert_eq!(taxes, REDUCED_SE_TAXES);
251        assert_ne!(taxes, DEFAULT_SE_TAXES);
252    }
253
254    #[test]
255    fn test_tax_reductions() {
256        let lau = LocalAdministrativeUnit::get(Country::SE, "0180").unwrap();
257        let tax_reductions = lau.tax_reductions();
258        assert_eq!(tax_reductions, DEFAULT_SE_TAX_REDUCTIONS);
259    }
260
261    #[test]
262    fn test_from_eu_code_valid() {
263        let result = LocalAdministrativeUnit::from_eu_code("SE_0180");
264        assert!(result.is_ok());
265        let lau = result.unwrap();
266        assert_eq!(lau.local_code(), "0180");
267        assert_eq!(lau.name(), "Stockholm");
268    }
269
270    #[test]
271    fn test_from_eu_code_invalid_country() {
272        let result = LocalAdministrativeUnit::from_eu_code("XX_0180");
273        assert!(result.is_err());
274        assert!(matches!(
275            result.unwrap_err(),
276            LauParseError::UnsupportedCountryCode
277        ));
278    }
279
280    #[test]
281    fn test_from_eu_code_invalid_lau() {
282        let result = LocalAdministrativeUnit::from_eu_code("SE_9999");
283        assert!(result.is_err());
284        assert!(matches!(result.unwrap_err(), LauParseError::InvalidCode));
285    }
286
287    #[test]
288    fn test_from_str_valid() {
289        let result = LocalAdministrativeUnit::from_str("SE_0180");
290        assert!(result.is_ok());
291        let lau = result.unwrap();
292        assert_eq!(lau.local_code(), "0180");
293    }
294
295    #[test]
296    fn test_from_str_invalid() {
297        let result = LocalAdministrativeUnit::from_str("INVALID");
298        assert!(result.is_err());
299    }
300
301    #[test]
302    fn test_serialize() {
303        let lau = LocalAdministrativeUnit::get(Country::SE, "0180").unwrap();
304        let serialized = serde_json::to_string(lau).unwrap();
305        assert_eq!(serialized, "\"SE_0180\"");
306    }
307
308    #[test]
309    fn test_deserialize_static_ref() {
310        let json = "\"SE_0180\"";
311        let lau: &'static LocalAdministrativeUnit = serde_json::from_str(json).unwrap();
312        assert_eq!(lau.local_code(), "0180");
313        assert_eq!(lau.name(), "Stockholm");
314    }
315
316    #[test]
317    fn test_deserialize_owned() {
318        let json = "\"SE_0180\"";
319        let lau: LocalAdministrativeUnit = serde_json::from_str(json).unwrap();
320        assert_eq!(lau.local_code(), "0180");
321        assert_eq!(lau.name(), "Stockholm");
322    }
323
324    #[test]
325    fn test_deserialize_invalid() {
326        let json = "\"SE_9999\"";
327        let result: Result<&'static LocalAdministrativeUnit, _> = serde_json::from_str(json);
328        assert!(result.is_err());
329    }
330
331    #[test]
332    fn test_info_conversion() {
333        let lau = LocalAdministrativeUnit::get(Country::SE, "0180").unwrap();
334        let info = lau.info();
335        assert_eq!(info.code, "0180");
336        assert_eq!(info.name, "Stockholm");
337        assert_eq!(info.country, Country::SE);
338        assert_eq!(info.taxes, lau.taxes());
339        assert_eq!(info.tax_reductions, lau.tax_reductions());
340    }
341
342    #[test]
343    fn test_lau_ordering() {
344        let lau1 = LocalAdministrativeUnit::get(Country::SE, "0180").unwrap();
345        let lau2 = LocalAdministrativeUnit::get(Country::SE, "1480").unwrap();
346        // Test that LocalAdministrativeUnit implements Ord correctly
347        assert!(lau1 < lau2 || lau1 > lau2 || lau1 == lau1);
348    }
349
350    #[test]
351    fn test_lau_hash() {
352        use std::collections::HashSet;
353        let lau1 = LocalAdministrativeUnit::get(Country::SE, "0180").unwrap();
354        let lau2 = LocalAdministrativeUnit::get(Country::SE, "0180").unwrap();
355
356        let mut set = HashSet::new();
357        set.insert(lau1);
358        // Same LAU should be considered equal
359        assert!(set.contains(lau2));
360    }
361
362    #[test]
363    fn test_registry_loaded() {
364        // Test that the Swedish registry is properly loaded
365        let se_registry = &*registry::SE;
366        assert!(!se_registry.is_empty());
367        // Should contain Stockholm
368        assert!(se_registry.contains_key("0180"));
369    }
370
371    #[test]
372    fn test_reduced_energy_tax_subdivisions() {
373        // Test that the reduced energy tax subdivisions set is properly loaded
374        let reduced = &*registry::SE_REDUCED_ENERGY_TAX_SUBDIVISIONS;
375        assert!(!reduced.is_empty());
376        // Test a known reduced tax subdivision
377        assert!(reduced.contains(&2506));
378        // Test that Stockholm (180) is not in the reduced tax list
379        assert!(!reduced.contains(&180));
380    }
381
382    #[test]
383    fn test_roundtrip_serialization() {
384        let lau = LocalAdministrativeUnit::get(Country::SE, "0180").unwrap();
385        let serialized = serde_json::to_string(lau).unwrap();
386        let deserialized: LocalAdministrativeUnit = serde_json::from_str(&serialized).unwrap();
387        assert_eq!(lau.local_code(), deserialized.local_code());
388        assert_eq!(lau.name(), deserialized.name());
389        assert_eq!(lau.country(), deserialized.country());
390    }
391}