Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions components/experimental/src/measure/mixed_measureunit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// 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, parser::InvalidUnitError};

use super::single_unit_vec::SingleUnitVec;

/// The [`MixedMeasureUnit`] struct represents a CLDR mixed unit,
/// which is a combination of one or more single units used together to express a measurement.
///
/// # Examples
/// - `foot-and-inch`
/// - `kilometer-and-meter`
/// - `meter` - a special case of a mixed unit that contains only one single unit.
///
/// Note: Compound units (e.g. `meter-per-second`) or units with a constant denominator (e.g. `liter-per-100-kilometer`) are not supported in mixed units.
#[derive(Debug, Eq, Clone, PartialEq)]
pub struct MixedMeasureUnit {
/// Contains the single mixed units.
pub(crate) mixed_units: SingleUnitVec,
}

impl MixedMeasureUnit {
/// Returns a slice of the mixed units contained within this mixed unit.
pub fn try_from_str(mixed_units_str: &str) -> Result<MixedMeasureUnit, InvalidUnitError> {
// '-and-' is the separator for the mixed units and it is allowed to appear in the start or end of the string.
let mixed_units_strs = mixed_units_str.split("-and-");
let mut mixed_units = SingleUnitVec::Zero;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: ::Zero should be ::Empty?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for unit in mixed_units_strs {
let unit = MeasureUnit::try_from_str(unit)?;
let internal_single_units = unit.single_units();
if internal_single_units.len() > 1 {
return Err(InvalidUnitError);
}
if unit.constant_denominator != 0 {
return Err(InvalidUnitError);
}
let single_unit = match internal_single_units.first() {
Some(single_unit) => single_unit,
None => return Err(InvalidUnitError),
};
mixed_units.push(*single_unit);
}
Ok(MixedMeasureUnit { mixed_units })
}
}

#[cfg(test)]
mod tests {
use crate::measure::provider::{
si_prefix::{Base, SiPrefix},
single_unit::SingleUnit,
};

use super::*;

#[test]
fn test_mixed_measure_unit_from_str() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: what does kilometer-and-meter test that foot-and-inch doesn't? what does meter test that foot-and-inch doesn't?

the reason why I think excessive testing like this is an issue is that these tests don't test the whole construct-format cycle, which is the actual API. instead, they test internal representation, which might need to be changed in the future, and then you need to update every single one of these test cases.

I'd prefer if you added the whole construct-format functionality in the same PR and tested that instead of the internal representation.

// Meter
let mixed_measure_unit = MixedMeasureUnit::try_from_str("meter").unwrap();
assert_eq!(mixed_measure_unit.mixed_units.as_slice().len(), 1);
assert_eq!(
mixed_measure_unit.mixed_units.as_slice()[0],
SingleUnit {
power: 1,
si_prefix: SiPrefix {
power: 0,
base: Base::Decimal,
},
unit_id: *crate::provider::Baked::UNIT_IDS_V1_UND_METER,
}
);

// Foot and Inch
let mixed_measure_unit = MixedMeasureUnit::try_from_str("foot-and-inch").unwrap();
assert_eq!(mixed_measure_unit.mixed_units.as_slice().len(), 2);
assert_eq!(
mixed_measure_unit.mixed_units.as_slice()[0],
SingleUnit {
power: 1,
si_prefix: SiPrefix {
power: 0,
base: Base::Decimal,
},
unit_id: *crate::provider::Baked::UNIT_IDS_V1_UND_FOOT,
}
);
assert_eq!(
mixed_measure_unit.mixed_units.as_slice()[1],
SingleUnit {
power: 1,
si_prefix: SiPrefix {
power: 0,
base: Base::Decimal,
},
unit_id: *crate::provider::Baked::UNIT_IDS_V1_UND_INCH,
}
);

// Kilometer and Meter
let mixed_measure_unit = MixedMeasureUnit::try_from_str("kilometer-and-meter").unwrap();
assert_eq!(mixed_measure_unit.mixed_units.as_slice().len(), 2);
assert_eq!(
mixed_measure_unit.mixed_units.as_slice()[0],
SingleUnit {
power: 1,
si_prefix: SiPrefix {
power: 3,
base: Base::Decimal,
},
unit_id: *crate::provider::Baked::UNIT_IDS_V1_UND_METER,
}
);
assert_eq!(
mixed_measure_unit.mixed_units.as_slice()[1],
SingleUnit {
power: 1,
si_prefix: SiPrefix {
power: 0,
base: Base::Decimal,
},
unit_id: *crate::provider::Baked::UNIT_IDS_V1_UND_METER,
}
);
}

#[test]
fn test_invalid_mixed_measure_unit_from_str() {
let mixed_measure_unit =
MixedMeasureUnit::try_from_str("meter-per-second-and-mile-per-hour");
assert!(mixed_measure_unit.is_err());
}
}
1 change: 1 addition & 0 deletions components/experimental/src/measure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

pub mod category;
pub mod measureunit;
pub mod mixed_measureunit;
pub mod parser;
pub mod provider;
pub mod single_unit_vec;
Loading