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}