Skip to content

Commit

Permalink
Implement LinearConverter (#4404)
Browse files Browse the repository at this point in the history
  • Loading branch information
younies authored Feb 5, 2024
1 parent 764cbda commit 6b6f6df
Show file tree
Hide file tree
Showing 7 changed files with 318 additions and 35 deletions.
8 changes: 6 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion experimental/unitsconversion/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ databake = { workspace = true, optional = true, features = ["derive"]}
displaydoc = { version = "0.2.3", default-features = false }
icu_locid = { workspace = true }
icu_provider = { workspace = true, features = ["macros"] }

litemap = { workspace = true }
num-bigint = "0.4.4"
num-rational = "0.4.1"
num-traits = "0.2.17"
serde = { version = "1.0", default-features = false, features = ["derive", "alloc"], optional = true }
smallvec = "1.11.2"
zerotrie = { workspace = true, features = ["yoke", "zerofrom"] }
Expand Down
248 changes: 248 additions & 0 deletions experimental/unitsconversion/src/converter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
// 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::{
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`.
///
/// 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>,

/// 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,
}

/// 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>>,
}

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<'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
}
}

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);
}
}
}

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(),
);

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)
}

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()),
)
}

/// 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,
})
}
}

impl LinearConverter {
/// 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
}
}
7 changes: 1 addition & 6 deletions experimental/unitsconversion/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,17 @@
extern crate alloc;

pub mod converter;
pub mod measureunit;
pub mod power;
pub mod provider;
pub mod si_prefix;

/// Represents the possible errors that can occur during the measurement unit operations.
#[derive(Debug)]
pub enum ConversionError {
/// The unit is not valid.
/// This can happen if the unit id is not following the CLDR specification.
/// For example, `meter` is a valid unit id, but `metre` is not.
InvalidUnit,

/// The conversion is not valid.
/// This can happen if the units are not compatible.
/// For example, `meter` and `foot` are compatible, but `meter` and `second` are not.
InvalidConversion,
}
8 changes: 5 additions & 3 deletions experimental/unitsconversion/src/measureunit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ pub struct MeasureUnitParser<'data> {
impl<'data> MeasureUnitParser<'data> {
// TODO: revisit the public nature of the API. Maybe we should make it private and add a function to create it from a ConverterFactory.
/// Creates a new MeasureUnitParser from a ZeroTrie payload.
#[cfg(feature = "datagen")]
pub fn from_payload(payload: &'data ZeroTrieSimpleAscii<ZeroVec<u8>>) -> Self {
Self { payload }
}
Expand Down Expand Up @@ -129,7 +128,7 @@ impl<'data> MeasureUnitParser<'data> {
pub fn try_from_identifier(
&self,
identifier: &'data str,
) -> Result<Vec<MeasureUnitItem>, ConversionError> {
) -> Result<MeasureUnit, ConversionError> {
if identifier.starts_with('-') || identifier.ends_with('-') {
return Err(ConversionError::InvalidUnit);
}
Expand All @@ -143,11 +142,14 @@ impl<'data> MeasureUnitParser<'data> {

self.analyze_identifier_part(num_part, 1, &mut measure_unit_items)?;
self.analyze_identifier_part(den_part, -1, &mut measure_unit_items)?;
Ok(measure_unit_items)
Ok(MeasureUnit {
contained_units: measure_unit_items.into(),
})
}
}

// TODO NOTE: the MeasureUnitParser takes the trie and the ConverterFactory takes the full payload and an instance of MeasureUnitParser.
#[derive(Debug)]
pub struct MeasureUnit {
/// Contains the processed units.
pub contained_units: SmallVec<[MeasureUnitItem; 8]>,
Expand Down
Loading

0 comments on commit 6b6f6df

Please sign in to comment.