grid_tariffs/
local_administrative_unit.rs

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