Skip to main content

ferroui_protranslate_core/
measurement.rs

1use crate::{Culture, FormatArgument, RegionProfile, TranslationError};
2
3#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
4pub enum MeasurementSystem {
5    #[default]
6    Metric,
7    USCustomary,
8    Imperial,
9    Custom,
10}
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum MeasurementUnitCategory {
13    Length,
14    Temperature,
15    Mass,
16    Volume,
17    Speed,
18}
19macro_rules! units {
20    ($( $unit:ident, $category:ident, $symbol:literal, $scale:expr; )*) => {
21        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22        pub enum MeasurementUnit { $( $unit, )* }
23        impl MeasurementUnit {
24            pub const ALL: &'static [Self] = &[$(Self::$unit,)*];
25            pub fn category(self) -> MeasurementUnitCategory { match self { $(Self::$unit => MeasurementUnitCategory::$category,)* } }
26            pub fn symbol(self) -> &'static str { match self { $(Self::$unit => $symbol,)* } }
27            fn scale(self) -> f64 { match self { $(Self::$unit => $scale,)* } }
28        }
29    }
30}
31units! {
32    Meter, Length, "m", 1.0;
33    Kilometer, Length, "km", 1000.0;
34    Centimeter, Length, "cm", 0.01;
35    Millimeter, Length, "mm", 0.001;
36    Inch, Length, "in", 0.0254;
37    Foot, Length, "ft", 0.3048;
38    Yard, Length, "yd", 0.9144;
39    Mile, Length, "mi", 1609.344;
40    Celsius, Temperature, "°C", 1.0;
41    Fahrenheit, Temperature, "°F", 5.0 / 9.0;
42    Kelvin, Temperature, "K", 1.0;
43    Gram, Mass, "g", 0.001;
44    Kilogram, Mass, "kg", 1.0;
45    Ounce, Mass, "oz", 0.028349523125;
46    Pound, Mass, "lb", 0.45359237;
47    Stone, Mass, "st", 6.35029318;
48    Milliliter, Volume, "mL", 0.001;
49    Liter, Volume, "L", 1.0;
50    USFluidOunce, Volume, "us fl oz", 0.0295735295625;
51    USCup, Volume, "cup", 0.2365882365;
52    USPint, Volume, "us pt", 0.473176473;
53    USQuart, Volume, "us qt", 0.946352946;
54    USGallon, Volume, "gal", 3.785411784;
55    ImperialFluidOunce, Volume, "imp fl oz", 0.0284130625;
56    ImperialPint, Volume, "imp pt", 0.56826125;
57    ImperialQuart, Volume, "imp qt", 1.1365225;
58    ImperialGallon, Volume, "imp gal", 4.54609;
59    MetersPerSecond, Speed, "m/s", 1.0;
60    KilometersPerHour, Speed, "km/h", 1000.0 / 3600.0;
61    MilesPerHour, Speed, "mph", 1609.344 / 3600.0;
62    FeetPerSecond, Speed, "ft/s", 0.3048;
63}
64impl MeasurementUnit {
65    pub fn from_symbol(symbol: &str, system: MeasurementSystem) -> Result<Self, TranslationError> {
66        match symbol {
67            "C" => Ok(Self::Celsius),
68            "F" => Ok(Self::Fahrenheit),
69            "ml" => Ok(Self::Milliliter),
70            "l" => Ok(Self::Liter),
71            "pt" => Ok(Self::USPint),
72            "qt" => Ok(Self::USQuart),
73            "us cup" => Ok(Self::USCup),
74            "gal" if system == MeasurementSystem::Imperial => Ok(Self::ImperialGallon),
75            value => Self::ALL
76                .iter()
77                .find(|unit| unit.symbol() == value)
78                .copied()
79                .ok_or_else(|| TranslationError::UnsupportedProfileUnit(value.into())),
80        }
81    }
82}
83#[derive(Debug, Clone, Copy, PartialEq)]
84pub struct MeasurementValue {
85    pub value: f64,
86    pub unit: MeasurementUnit,
87}
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct MeasurementSystemProfile {
90    pub system: MeasurementSystem,
91    pub length_unit: String,
92    pub temperature_unit: String,
93    pub mass_unit: String,
94    pub volume_unit: String,
95    pub speed_unit: String,
96}
97impl MeasurementSystemProfile {
98    pub fn for_system(system: MeasurementSystem) -> Self {
99        let units = match system {
100            MeasurementSystem::Metric => ["m", "C", "kg", "l", "km/h"],
101            MeasurementSystem::USCustomary => ["ft", "F", "lb", "gal", "mph"],
102            MeasurementSystem::Imperial => ["ft", "C", "st", "imp gal", "mph"],
103            MeasurementSystem::Custom => ["", "", "", "", ""],
104        };
105        Self {
106            system,
107            length_unit: units[0].into(),
108            temperature_unit: units[1].into(),
109            mass_unit: units[2].into(),
110            volume_unit: units[3].into(),
111            speed_unit: units[4].into(),
112        }
113    }
114    pub fn unit_for(
115        &self,
116        category: MeasurementUnitCategory,
117    ) -> Result<MeasurementUnit, TranslationError> {
118        let symbol = match category {
119            MeasurementUnitCategory::Length => &self.length_unit,
120            MeasurementUnitCategory::Temperature => &self.temperature_unit,
121            MeasurementUnitCategory::Mass => &self.mass_unit,
122            MeasurementUnitCategory::Volume => &self.volume_unit,
123            MeasurementUnitCategory::Speed => &self.speed_unit,
124        };
125        let unit = MeasurementUnit::from_symbol(symbol, self.system)?;
126        if unit.category() != category {
127            return Err(TranslationError::IncompatibleUnits);
128        }
129        Ok(unit)
130    }
131}
132pub trait MeasurementSystemResolver {
133    fn resolve(
134        &self,
135        region: &RegionProfile,
136        override_system: Option<MeasurementSystem>,
137    ) -> MeasurementSystemProfile;
138}
139#[derive(Default)]
140pub struct DefaultMeasurementSystemResolver;
141impl MeasurementSystemResolver for DefaultMeasurementSystemResolver {
142    fn resolve(
143        &self,
144        region: &RegionProfile,
145        override_system: Option<MeasurementSystem>,
146    ) -> MeasurementSystemProfile {
147        MeasurementSystemProfile::for_system(override_system.unwrap_or({
148            match region.two_letter_iso_region_name.as_str() {
149                "US" | "LR" | "MM" => MeasurementSystem::USCustomary,
150                "GB" => MeasurementSystem::Imperial,
151                _ if region.is_metric => MeasurementSystem::Metric,
152                _ => MeasurementSystem::Custom,
153            }
154        }))
155    }
156}
157pub trait UnitConversionService {
158    fn get_category(&self, unit: MeasurementUnit) -> MeasurementUnitCategory {
159        unit.category()
160    }
161    fn convert(
162        &self,
163        value: f64,
164        from: MeasurementUnit,
165        to: MeasurementUnit,
166    ) -> Result<f64, TranslationError>;
167    fn convert_to_profile(
168        &self,
169        value: f64,
170        from: MeasurementUnit,
171        profile: &MeasurementSystemProfile,
172    ) -> Result<MeasurementValue, TranslationError> {
173        let unit = profile.unit_for(self.get_category(from))?;
174        Ok(MeasurementValue {
175            value: self.convert(value, from, unit)?,
176            unit,
177        })
178    }
179}
180#[derive(Default)]
181pub struct DefaultUnitConversionService;
182impl UnitConversionService for DefaultUnitConversionService {
183    fn convert(
184        &self,
185        value: f64,
186        from: MeasurementUnit,
187        to: MeasurementUnit,
188    ) -> Result<f64, TranslationError> {
189        if from.category() != to.category() {
190            return Err(TranslationError::IncompatibleUnits);
191        }
192        if from == to {
193            return Ok(value);
194        }
195        if from.category() == MeasurementUnitCategory::Temperature {
196            let celsius = match from {
197                MeasurementUnit::Fahrenheit => (value - 32.0) * 5.0 / 9.0,
198                MeasurementUnit::Kelvin => value - 273.15,
199                _ => value,
200            };
201            return Ok(match to {
202                MeasurementUnit::Fahrenheit => celsius * 9.0 / 5.0 + 32.0,
203                MeasurementUnit::Kelvin => celsius + 273.15,
204                _ => celsius,
205            });
206        }
207        Ok(value * from.scale() / to.scale())
208    }
209}
210pub trait LocalizedUnitFormatter {
211    fn format(
212        &self,
213        value: MeasurementValue,
214        culture: &Culture,
215        format: Option<&str>,
216    ) -> Result<String, TranslationError>;
217}
218#[derive(Default)]
219pub struct DefaultLocalizedUnitFormatter;
220impl LocalizedUnitFormatter for DefaultLocalizedUnitFormatter {
221    fn format(
222        &self,
223        value: MeasurementValue,
224        culture: &Culture,
225        format: Option<&str>,
226    ) -> Result<String, TranslationError> {
227        Ok(format!(
228            "{} {}",
229            FormatArgument::Number(value.value).format(format.unwrap_or("G"), culture)?,
230            value.unit.symbol()
231        ))
232    }
233}