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#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
11#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
12pub struct LocalAdministrativeUnit {
13    code: &'static str,
14    name: &'static str,
15    country: Country,
16}
17
18impl LocalAdministrativeUnit {
19    pub fn eu_code(&self) -> String {
20        format!("{}_{:0width$}", self.country.code(), self.code, width = 4)
21    }
22
23    pub fn local_code(&self) -> &str {
24        self.code
25    }
26
27    pub fn info(&'static self) -> LocalAdministrativeUnitInfo {
28        self.into()
29    }
30
31    pub fn name(&self) -> &str {
32        self.name
33    }
34
35    pub fn country(&self) -> Country {
36        self.country
37    }
38
39    pub fn get(country: Country, code: &str) -> Option<&'static Self> {
40        match country {
41            Country::SE => registry::SE.get(code),
42        }
43    }
44
45    pub fn taxes(&self) -> Taxes {
46        match self.country {
47            Country::SE => {
48                let code = self.code.parse().unwrap();
49                if registry::SE_REDUCED_ENERGY_TAX_SUBDIVISIONS.contains(&code) {
50                    REDUCED_SE_TAXES
51                } else {
52                    DEFAULT_SE_TAXES
53                }
54            }
55        }
56    }
57
58    pub fn tax_reductions(&self) -> TaxReductions {
59        match self.country {
60            Country::SE => DEFAULT_SE_TAX_REDUCTIONS,
61        }
62    }
63
64    pub fn from_eu_code(value: &str) -> Result<Self, &'static str> {
65        let country: Country = value[0..2]
66            .parse()
67            .map_err(|_| "failed to extract country code")?;
68        let lau_code = &value[3..];
69        match country {
70            Country::SE => registry::SE
71                .get(lau_code)
72                .ok_or("code not found in registry")
73                .copied(),
74        }
75    }
76}
77
78impl Serialize for LocalAdministrativeUnit {
79    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
80    where
81        S: serde::Serializer,
82    {
83        serializer.serialize_str(&self.eu_code())
84    }
85}
86
87impl FromStr for LocalAdministrativeUnit {
88    type Err = &'static str;
89
90    fn from_str(eu_lau_code: &str) -> Result<Self, Self::Err> {
91        Self::from_eu_code(eu_lau_code)
92    }
93}
94
95impl<'de> Deserialize<'de> for &'static LocalAdministrativeUnit {
96    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
97    where
98        D: serde::Deserializer<'de>,
99    {
100        let s = String::deserialize(deserializer)?;
101
102        // Parse the EU LAU code format: 2-letter country code + 4-digit LAU code
103        if s.len() < 6 {
104            return Err(serde::de::Error::custom(format!(
105                "Invalid LAU code '{}': must be at least 6 characters",
106                s
107            )));
108        }
109
110        let country_code = &s[0..2];
111        let lau_code = &s[2..];
112
113        let country = Country::from_str(country_code).map_err(|_| {
114            serde::de::Error::custom(format!("Invalid country code '{}'", country_code))
115        })?;
116
117        // Look up in registry - need to leak the string to get 'static lifetime
118        let lau_code_static: &'static str = Box::leak(lau_code.to_string().into_boxed_str());
119
120        LocalAdministrativeUnit::get(country, lau_code_static).ok_or_else(|| {
121            serde::de::Error::custom(format!(
122                "Unknown LAU code '{}' for country {}",
123                lau_code, country_code
124            ))
125        })
126    }
127}
128
129impl<'de> Deserialize<'de> for LocalAdministrativeUnit {
130    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
131    where
132        D: serde::Deserializer<'de>,
133    {
134        let static_ref = <&'static LocalAdministrativeUnit>::deserialize(deserializer)?;
135        Ok(*static_ref)
136    }
137}
138
139#[derive(Debug, Clone, Copy, Serialize)]
140pub struct LocalAdministrativeUnitInfo {
141    pub code: &'static str,
142    pub name: &'static str,
143    pub country: Country,
144    pub taxes: Taxes,
145    pub tax_reductions: TaxReductions,
146}
147
148impl LocalAdministrativeUnitInfo {
149    pub fn only_current(mut self) -> Self {
150        self.taxes = self.taxes.with_current_only();
151        self.tax_reductions = self.tax_reductions.with_current_only();
152        self
153    }
154}
155
156impl From<&'static LocalAdministrativeUnit> for LocalAdministrativeUnitInfo {
157    fn from(value: &'static LocalAdministrativeUnit) -> Self {
158        Self {
159            code: value.local_code(),
160            name: value.name(),
161            country: value.country(),
162            taxes: value.taxes(),
163            tax_reductions: value.tax_reductions(),
164        }
165    }
166}
167
168pub(crate) mod registry {
169    static SE_DATA: &str = include_str!("../data/EU-27-LAU-2024-NUTS-2024-SE.csv");
170    use std::{
171        collections::{HashMap, HashSet},
172        str::FromStr,
173        sync::LazyLock,
174    };
175
176    use crate::{Country, local_administrative_unit::LocalAdministrativeUnit};
177
178    pub(crate) static SE: LazyLock<HashMap<&str, LocalAdministrativeUnit>> = LazyLock::new(|| {
179        let mut map = HashMap::with_capacity(290);
180
181        // Skip header line, then iterate over data lines
182        for line in SE_DATA.lines().skip(1) {
183            // Split on comma - no allocations, just borrows from SE_DATA
184            let mut parts = line.split(',');
185
186            let nuts_3_code = parts.next().unwrap();
187            let lau_code = parts.next().unwrap();
188            let name = parts.next().unwrap();
189            let country = Country::from_str(&nuts_3_code[0..2]).unwrap();
190            map.insert(
191                lau_code,
192                LocalAdministrativeUnit {
193                    code: lau_code,
194                    name,
195                    country,
196                },
197            );
198        }
199        map
200    });
201
202    pub(crate) static SE_REDUCED_ENERGY_TAX_SUBDIVISIONS: LazyLock<HashSet<u16>> =
203        LazyLock::new(|| {
204            HashSet::from_iter([
205                2506, 2505, 2326, 2403, 2582, 2305, 2425, 2523, 2583, 2361, 2510, 2514, 2584, 2309,
206                2161, 2580, 2481, 2023, 2418, 2062, 2401, 2417, 2034, 2521, 2581, 2303, 2409, 2482,
207                2283, 2422, 2421, 2313, 1737, 2480, 2462, 2404, 2460, 2260, 2321, 2463, 2039, 2560,
208                2284, 2380, 2513, 2518,
209            ])
210        });
211}