Skip to content

Commit

Permalink
Implement UnitsConverter (#4605)
Browse files Browse the repository at this point in the history
This PR introduces the `UnitsConverter`, a `struct` that wraps three
distinct types of unit converters:

1. **ProportionalConverter**: Handles conversions between units where
the relationship is `unit1 = CR * unit2`.
2. **ReciprocalConverter**: Handles conversions between units where the
relationship is `unit1 = 1 / (CR * unit2)`.
3. **OffsetConverter**: Handles conversions with an offset, following
the formula `unit1 = CR * unit2 + Offset`.

_Note: CR refers to the Conversion Rate._

Additionally, this PR includes an organizational change:
- The `ConverterFactory` has been relocated to a separate folder,
enhancing the overall clarity and structure of the implementation.


Fixes: #4576
  • Loading branch information
younies committed Feb 28, 2024
1 parent 59a8e63 commit e55cc33
Show file tree
Hide file tree
Showing 4 changed files with 413 additions and 223 deletions.
288 changes: 71 additions & 217 deletions experimental/components/src/units/converter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,247 +2,101 @@
// called LICENSE at the top level of the ICU4X source tree
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).

use crate::units::{
measureunit::{MeasureUnit, MeasureUnitParser},
provider::{Base, MeasureUnitItem, SiPrefix, Sign, SignULE, UnitsInfoV1},
};
use litemap::LiteMap;
use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::identities::One;
use zerotrie::ZeroTrieSimpleAscii;
use zerovec::{ule::AsULE, ZeroSlice, ZeroVec};

// TODO(#4576): Bikeshed the name of the converter.
/// LinearConverter is responsible for converting between two units that are linearly related.
/// For example: 1- `meter` to `foot`.
/// 2- `square-meter` to `square-foot`.
/// 3- `mile-per-gallon` and `liter-per-100-kilometer`.
/// A converter for converting between two single or compound units.
/// For example:
/// 1 - `meter` to `foot`
/// 2 - `mile-per-gallon` to `liter-per-100-kilometer`.
/// 3 - `celsius` to `fahrenheit`.
///
/// However, it cannot convert between two units that are not linearly related such as `celsius` to `fahrenheit`.
/// NOTE:
/// The converter is not able to convert between two units that are not single. such as "foot-and-inch" to "meter".
#[derive(Debug)]
pub struct LinearConverter {
// TODO(#4554): Implement a New Struct `IcuRatio` to Encapsulate `Ratio<BigInt>`.
/// The conversion rate between the input and output units.
conversion_rate: Ratio<BigInt>,
/// This converter does not support conversions between mixed units,
/// for example, from "meter" to "foot-and-inch".
pub struct UnitsConverter(pub(crate) UnitsConverterInner);

/// Determines if the units are reciprocal or not.
/// For example, `meter-per-second` and `second-per-meter` are reciprocal.
/// Real world case, `mile-per-gallon` and `liter-per-100-kilometer` which are reciprocal.
is_reciprocal: bool,
impl UnitsConverter {
/// Converts the given value from the input unit to the output unit.
pub fn convert(&self, value: &Ratio<BigInt>) -> Ratio<BigInt> {
self.0.convert(value)
}
}

/// ConverterFactory is a factory for creating a converter.
pub struct ConverterFactory<'data> {
// TODO(#4522): Make the converter factory owns the data.
/// Contains the necessary data for the conversion factory.
payload: &'data UnitsInfoV1<'data>,
payload_store: &'data ZeroTrieSimpleAscii<ZeroVec<'data, u8>>,
/// Enum containing all the of converters: Proportional, Reciprocal, and Offset converters as follows:
/// 1 - Proportional: Converts between two units that are proportionally related (e.g. `meter` to `foot`).
/// 2 - Reciprocal: Converts between two units that are reciprocal (e.g. `mile-per-gallon` to `liter-per-100-kilometer`).
/// 3 - Offset: Converts between two units that require an offset (e.g. `celsius` to `fahrenheit`).
#[derive(Debug)]
pub(crate) enum UnitsConverterInner {
Proportional(ProportionalConverter),
Reciprocal(ReciprocalConverter),
Offset(OffsetConverter),
}

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 UnitsConverterInner {
/// Converts the given value from the input unit to the output unit based on the inner converter type.
fn convert(&self, value: &Ratio<BigInt>) -> Ratio<BigInt> {
match self {
UnitsConverterInner::Proportional(converter) => converter.convert(value),
UnitsConverterInner::Reciprocal(converter) => converter.convert(value),
UnitsConverterInner::Offset(converter) => converter.convert(value),
}
}
}

impl<'data> ConverterFactory<'data> {
#[cfg(feature = "datagen")]
pub fn from_payload(
payload: &'data UnitsInfoV1<'data>,
payload_store: &'data ZeroTrieSimpleAscii<ZeroVec<'data, u8>>,
) -> Self {
Self {
payload,
payload_store,
}
}

pub fn parser(&self) -> MeasureUnitParser<'_> {
MeasureUnitParser::from_payload(self.payload_store)
}

// TODO(#4512): the need needs to be bikeshedded.
/// Checks if the given units are reciprocal or not.
/// If it is not reciprocal, this means that the units are convertible.
/// NOTE:
/// If the units are not convertible or reciprocal, the function will return `None`
/// which means that the units are not compatible.
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: &[MeasureUnitItem],
sign: i16,
map: &mut LiteMap<u16, PowersInfo>,
) -> Option<()> {
for item in units {
let items_from_item = factory.payload.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<MeasureUnitItem>,
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.iter_values()
.fold((true, true), |(sums, diffs), determine_convertibility| {
(
sums && determine_convertibility.sums == 0,
diffs && determine_convertibility.diffs == 0,
)
});

if power_diffs_are_zero {
Some(false)
} else if power_sums_are_zero {
Some(true)
} else {
None
}
}
/// A converter for converting between two units that are reciprocal.
/// For example:
/// 1 - `meter-per-second` to `second-per-meter`.
/// 2 - `mile-per-gallon` to `liter-per-100-kilometer`.
#[derive(Debug)]
pub(crate) struct ReciprocalConverter {
pub(crate) proportional: ProportionalConverter,
}

fn apply_si_prefix(si_prefix: &SiPrefix, ratio: &mut Ratio<BigInt>) {
match si_prefix.base {
Base::Decimal => {
*ratio *= Ratio::<BigInt>::from_integer(10.into()).pow(si_prefix.power as i32);
}
Base::Binary => {
*ratio *= Ratio::<BigInt>::from_integer(2.into()).pow(si_prefix.power as i32);
}
}
impl ReciprocalConverter {
/// Converts the given value from the input unit to the output unit.
pub(crate) fn convert(&self, value: &Ratio<BigInt>) -> Ratio<BigInt> {
self.proportional.convert(value).recip()
}
}

fn compute_conversion_term(
&self,
unit_item: &MeasureUnitItem,
sign: i8,
) -> Option<Ratio<BigInt>> {
let conversion_info = self.payload.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 = Self::extract_ratio_from_unaligned(
&conversion_info.factor_sign,
conversion_info.factor_num(),
conversion_info.factor_den(),
);
/// A converter for converting between two units that require an offset.
#[derive(Debug)]
pub(crate) struct OffsetConverter {
/// The proportional converter.
pub(crate) proportional: ProportionalConverter,

Self::apply_si_prefix(&unit_item.si_prefix, &mut conversion_info_factor);
conversion_info_factor = conversion_info_factor.pow((unit_item.power * sign) as i32);
Some(conversion_info_factor)
}
/// The offset value to be added to the result of the proportional converter.
pub(crate) offset: Ratio<BigInt>,
}

fn extract_ratio_from_unaligned(
sign_ule: &SignULE,
num_ule: &ZeroSlice<u8>,
den_ule: &ZeroSlice<u8>,
) -> Ratio<BigInt> {
let sign = Sign::from_unaligned(*sign_ule).into();
Ratio::<BigInt>::new(
BigInt::from_bytes_le(sign, num_ule.as_ule_slice()),
BigInt::from_bytes_le(num_bigint::Sign::Plus, den_ule.as_ule_slice()),
)
impl OffsetConverter {
/// Converts the given value from the input unit to the output unit.
pub(crate) fn convert(&self, value: &Ratio<BigInt>) -> Ratio<BigInt> {
let result = self.proportional.convert(value);
result + &self.offset
}
}

/// Creates a converter for converting between two units in the form of CLDR identifiers.
pub fn converter(
&self,
input_unit: &MeasureUnit,
output_unit: &MeasureUnit,
) -> Option<LinearConverter> {
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 = Ratio::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)?;
}

Some(LinearConverter {
conversion_rate,
is_reciprocal,
})
}
/// ProportionalConverter is responsible for converting between two units that are proportionally related.
/// For example: 1- `meter` to `foot`.
/// 2- `square-meter` to `square-foot`.
///
/// However, it cannot convert between two units that are not proportionally related,
/// such as `celsius` to `fahrenheit` and `mile-per-gallon` to `liter-per-100-kilometer`.
///
/// Also, it cannot convert between two units that are not single, such as `meter` to `foot-and-inch`.
#[derive(Debug)]
pub(crate) struct ProportionalConverter {
// TODO(#4554): Implement a New Struct `IcuRatio` to Encapsulate `Ratio<BigInt>`.
/// The conversion rate between the input and output units.
pub(crate) conversion_rate: Ratio<BigInt>,
}

impl LinearConverter {
impl ProportionalConverter {
/// Converts the given value from the input unit to the output unit.
pub fn convert(&self, value: &Ratio<BigInt>) -> Ratio<BigInt> {
let mut result: Ratio<BigInt> = value * &self.conversion_rate;
if self.is_reciprocal {
result = result.recip();
}

result
value * &self.conversion_rate
}
}
Loading

0 comments on commit e55cc33

Please sign in to comment.