1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328
// This file is part of ICU4X. For terms of use, please see the file
// called LICENSE at the top level of the ICU4X source tree
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
use crate::measure::measureunit::MeasureUnit;
use crate::measure::parser::MeasureUnitParser;
use crate::measure::provider::single_unit::SingleUnit;
use crate::units::provider;
use crate::units::ratio::IcuRatio;
use crate::units::{
converter::{
OffsetConverter, ProportionalConverter, ReciprocalConverter, UnitsConverter,
UnitsConverterInner,
},
provider::Sign,
};
use icu_provider::prelude::*;
use icu_provider::DataError;
use litemap::LiteMap;
use num_traits::Pow;
use num_traits::{One, Zero};
use zerovec::ZeroSlice;
use super::convertible::Convertible;
/// ConverterFactory is a factory for creating a converter.
pub struct ConverterFactory {
/// Contains the necessary data for the conversion factory.
payload: DataPayload<provider::UnitsInfoV1Marker>,
}
impl From<Sign> for num_bigint::Sign {
fn from(val: Sign) -> Self {
match val {
Sign::Positive => num_bigint::Sign::Plus,
Sign::Negative => num_bigint::Sign::Minus,
}
}
}
impl ConverterFactory {
icu_provider::gen_any_buffer_data_constructors!(
() -> error: DataError,
functions: [
new: skip,
try_new_with_any_provider,
try_new_with_buffer_provider,
try_new_unstable,
Self,
]
);
/// Creates a new [`ConverterFactory`] from compiled data.
///
/// ✨ *Enabled with the `compiled_data` Cargo feature.*
///
/// [📚 Help choosing a constructor](icu_provider::constructors)
#[cfg(feature = "compiled_data")]
pub const fn new() -> Self {
Self {
payload: DataPayload::from_static_ref(
crate::provider::Baked::SINGLETON_UNITS_INFO_V1_MARKER,
),
}
}
#[doc = icu_provider::gen_any_buffer_unstable_docs!(UNSTABLE, Self::new)]
pub fn try_new_unstable<D>(provider: &D) -> Result<Self, DataError>
where
D: ?Sized + DataProvider<provider::UnitsInfoV1Marker>,
{
let payload = provider.load(DataRequest::default())?.payload;
Ok(Self { payload })
}
pub fn parser(&self) -> MeasureUnitParser<'_> {
MeasureUnitParser::from_payload(self.payload.get().units_conversion_trie.as_borrowed())
}
/// Calculates the offset between two units by performing the following steps:
/// 1. Identify the conversion rate from the first unit to the base unit as ConversionRate1: N1/D1 with an Offset1: OffsetN1/OffsetD1.
/// 2. Identify the conversion rate from the second unit to the base unit as ConversionRate2: N2/D2 with an Offset2: OffsetN2/OffsetD2.
/// 3. The conversion from the base unit to the second unit is represented by ConversionRateBaseToUnit2: (D2/N2) and an OffsetBaseToUnit2: - (OffsetN2/OffsetD2) * (D2/N2).
/// 4. To convert a value V from the first unit to the second unit, first convert V to the base unit using ConversionRate1:
/// (V * N1/D1) + OffsetN1/OffsetD1, referred to as V_TO_Base.
/// 5. Then, convert V_TO_Base to the second unit using the formula: CR: (D2/N2) and Offset: - (OffsetN2/OffsetD2) * (D2/N2).
/// The result is: (V_TO_Base * (D2/N2)) - (OffsetN2/OffsetD2) * (D2/N2).
/// 6. By inserting V_TO_Base from step 4 into step 5, the equation becomes:
/// (((V * N1/D1) + OffsetN1/OffsetD1) * D2/N2) - (OffsetN2/OffsetD2) * (D2/N2).
/// 7. Simplifying the equation gives:
/// (V * (N1/D1) * (D2/N2)) + (OffsetN1/OffsetD1 * (D2/N2)) - (OffsetN2/OffsetD2) * (D2/N2).
/// 8. Focusing on the constants to find the offset formula, we get:
/// Offset = ((OffsetN1/OffsetD1) - (OffsetN2/OffsetD2)) * (D2/N2),
/// which simplifies to: Offset = (Offset1 - Offset2) * (1/ConversionRate2).
///
/// NOTE:
/// An offset can be calculated if both the input and output units are simple.
/// A unit is considered simple if it is made up of a single unit item, with a power of 1 and an SI prefix of 0.
///
/// For example:
/// `meter` and `foot` are simple units.
/// `meter-per-second` and `foot-per-second` are not simple units.
fn compute_offset(
&self,
input_unit: &MeasureUnit,
output_unit: &MeasureUnit,
) -> Option<IcuRatio> {
if !(input_unit.contained_units.len() == 1
&& output_unit.contained_units.len() == 1
&& input_unit.contained_units[0].power == 1
&& output_unit.contained_units[0].power == 1
&& input_unit.contained_units[0].si_prefix.power == 0
&& output_unit.contained_units[0].si_prefix.power == 0)
{
return Some(IcuRatio::zero());
}
let input_conversion_info = self
.payload
.get()
.convert_infos
.get(input_unit.contained_units[0].unit_id as usize);
debug_assert!(
input_conversion_info.is_some(),
"Failed to get input conversion info"
);
let input_conversion_info = input_conversion_info?;
let output_conversion_info = self
.payload
.get()
.convert_infos
.get(output_unit.contained_units[0].unit_id as usize);
debug_assert!(
output_conversion_info.is_some(),
"Failed to get output conversion info"
);
let output_conversion_info = output_conversion_info?;
let input_offset = input_conversion_info.offset_as_ratio();
let output_offset = output_conversion_info.offset_as_ratio();
if input_offset.is_zero() && output_offset.is_zero() {
return Some(IcuRatio::zero());
}
let output_conversion_rate_recip = output_conversion_info.factor_as_ratio().recip();
Some((input_offset - output_offset) * output_conversion_rate_recip)
}
/// Checks whether the given units are reciprocal.
/// If they are not reciprocal, it implies that the units are proportional.
/// NOTE:
/// If the units are neither proportional nor reciprocal, the function will return `None`,
/// indicating that the units are incompatible.
fn is_reciprocal(&self, unit1: &MeasureUnit, unit2: &MeasureUnit) -> Option<bool> {
/// A struct that contains the sum and difference of base unit powers.
/// For example:
/// For the input unit `meter-per-second`, the base units are `meter` (power: 1) and `second` (power: -1).
/// For the output unit `foot-per-second`, the base units are `meter` (power: 1) and `second` (power: -1).
/// The differences are: meter: 1 - 1 = 0, second: -1 - (-1) = 0.
/// The sums are: meter: 1 + 1 = 2, second: -1 + (-1) = -2.
/// If all the sums are zeros, then the units are reciprocal.
/// If all the diffs are zeros, then the units are convertible.
/// If none of the above, then the units are not convertible.
#[derive(Debug)]
struct PowersInfo {
diffs: i16,
sums: i16,
}
/// Inserting the units item into the map.
/// NOTE:
/// This will require to go through the basic units of the given unit items.
/// For example, `newton` has the basic units: `gram`, `meter`, and `second` (each one has it is own power and si prefix).
fn insert_non_basic_units(
factory: &ConverterFactory,
units: &[SingleUnit],
sign: i16,
map: &mut LiteMap<u16, PowersInfo>,
) -> Option<()> {
for item in units {
let items_from_item = factory
.payload
.get()
.convert_infos
.get(item.unit_id as usize);
debug_assert!(items_from_item.is_some(), "Failed to get convert info");
insert_base_units(items_from_item?.basic_units(), item.power as i16, sign, map);
}
Some(())
}
/// Inserting the basic units into the map.
/// NOTE:
/// The base units should be multiplied by the original power.
/// For example, `square-foot` , the base unit is `meter` with power 1.
/// Thus, the inserted power should be `1 * 2 = 2`.
fn insert_base_units(
basic_units: &ZeroSlice<SingleUnit>,
original_power: i16,
sign: i16,
map: &mut LiteMap<u16, PowersInfo>,
) {
for item in basic_units.iter() {
let item_power = (item.power as i16) * original_power;
let signed_item_power = item_power * sign;
if let Some(powers) = map.get_mut(&item.unit_id) {
powers.diffs += signed_item_power;
powers.sums += item_power;
} else {
map.insert(
item.unit_id,
PowersInfo {
diffs: (signed_item_power),
sums: (item_power),
},
);
}
}
}
let unit1 = &unit1.contained_units;
let unit2 = &unit2.contained_units;
let mut map = LiteMap::new();
insert_non_basic_units(self, unit1, 1, &mut map)?;
insert_non_basic_units(self, unit2, -1, &mut map)?;
let (power_sums_are_zero, power_diffs_are_zero) =
map.values()
.fold((true, true), |(sums, diffs), powers_info| {
(
sums && powers_info.sums == 0,
diffs && powers_info.diffs == 0,
)
});
if power_diffs_are_zero {
Some(false)
} else if power_sums_are_zero {
Some(true)
} else {
None
}
}
fn compute_conversion_term(&self, unit_item: &SingleUnit, sign: i8) -> Option<IcuRatio> {
let conversion_info = self
.payload
.get()
.convert_infos
.get(unit_item.unit_id as usize);
debug_assert!(conversion_info.is_some(), "Failed to get conversion info");
let conversion_info = conversion_info?;
let mut conversion_info_factor = conversion_info.factor_as_ratio();
conversion_info_factor *= &unit_item.si_prefix;
conversion_info_factor = conversion_info_factor.pow((unit_item.power * sign) as i32);
Some(conversion_info_factor)
}
/// Creates a converter for converting between two single or compound units.
/// For example:
/// 1 - `meter` to `foot` --> Simple
/// 2 - `kilometer-per-hour` to `mile-per-hour` --> Compound
/// 3 - `mile-per-gallon` to `liter-per-100-kilometer` --> Reciprocal
/// 4 - `celsius` to `fahrenheit` --> Needs an offset
///
/// NOTE:
/// This converter does not support conversions between mixed units,
/// such as, from "meter" to "foot-and-inch".
pub fn converter<T: Convertible>(
&self,
input_unit: &MeasureUnit,
output_unit: &MeasureUnit,
) -> Option<UnitsConverter<T>> {
let is_reciprocal = self.is_reciprocal(input_unit, output_unit)?;
// Determine the sign of the powers of the units from root to unit2.
let root_to_unit2_direction_sign = if is_reciprocal { 1 } else { -1 };
let mut conversion_rate = IcuRatio::one();
for input_item in input_unit.contained_units.iter() {
conversion_rate *= Self::compute_conversion_term(self, input_item, 1)?;
}
for output_item in output_unit.contained_units.iter() {
conversion_rate *=
Self::compute_conversion_term(self, output_item, root_to_unit2_direction_sign)?;
}
let offset = self.compute_offset(input_unit, output_unit)?;
if is_reciprocal && !offset.is_zero() {
debug_assert!(
false,
"The units are reciprocal, but the offset is not zero. This is should not happen!.",
);
return None;
}
let conversion_rate = T::from_ratio_bigint(conversion_rate.get_ratio())?;
let proportional = ProportionalConverter { conversion_rate };
if is_reciprocal {
Some(UnitsConverter(UnitsConverterInner::Reciprocal(
ReciprocalConverter { proportional },
)))
} else if offset.is_zero() {
Some(UnitsConverter(UnitsConverterInner::Proportional(
proportional,
)))
} else {
let offset = T::from_ratio_bigint(offset.get_ratio())?;
Some(UnitsConverter(UnitsConverterInner::Offset(
OffsetConverter {
proportional,
offset,
},
)))
}
}
}