///|
/// A unit of measurement: a unit-symbol monomial paired with the physical
/// dimension it measures.
///
/// The type is named `Un` (short for "unit").
///
/// The monomial's symbols are unit symbols (e.g. "m", "s") and its coefficient
/// is the scale relative to the coherent base unit of that dimension — one of
/// this unit equals `scale` base units. Composite units (e.g. m/s) arise from
/// `mul` / `div` / `pow`, which keep the symbol structure, the scale and the
/// dimension in sync.
///
/// # Example
///
/// ```mbt check
/// test {
/// let meter = Un::base("m", @dimension.Dimension::length())
/// let second = Un::base("s", @dimension.Dimension::time())
/// let mps = meter.div(second)
/// let expected = @dimension.Dimension::length().div(
/// @dimension.Dimension::time(),
/// )
/// assert_true(mps.dimension().is_same(expected))
/// }
/// ```
pub struct Un {
repr : @algebra.Monomial
dim : @dimension.Dimension
} derive(Eq, Debug)
///|
/// Manual `Show` (so `assert_eq` can print diffs on failure).
pub impl Show for Un with fn output(self, logger) {
logger.write_string("Un(")
Show::output(self.repr, logger)
logger.write_string(", ")
Show::output(self.dim, logger)
logger.write_string(")")
}
///|
/// Allows `lhs * rhs` as shorthand for `lhs.mul(rhs)`.
///
/// # Example
///
/// ```mbt check
/// test {
/// let m = Un::base("m", @dimension.Dimension::length())
/// let area = m * m
/// assert_true(area.dimension().is_same(@dimension.Dimension::length().pow(2)))
/// }
/// ```
pub impl Mul for Un with fn mul(self : Un, other : Un) -> Un {
{ repr: self.repr.mul(other.repr), dim: self.dim.mul(other.dim) }
}
///|
/// Allows `lhs / rhs` as shorthand for `lhs.div(rhs)`.
///
/// # Example
///
/// ```mbt check
/// test {
/// let m = Un::base("m", @dimension.Dimension::length())
/// let s = Un::base("s", @dimension.Dimension::time())
/// assert_true((m / s).dimension().is_same(m.div(s).dimension()))
/// }
/// ```
pub impl Div for Un with fn div(self : Un, other : Un) -> Un {
{ repr: self.repr.div(other.repr), dim: self.dim.div(other.dim) }
}
///|
/// Defines a coherent base unit (scale 1) with the given symbol and dimension.
///
/// # Example
///
/// ```mbt check
/// test {
/// let meter = Un::base("m", @dimension.Dimension::length())
/// assert_eq(meter.scale(), 1.0)
/// assert_true(meter.dimension().is_same(@dimension.Dimension::length()))
/// }
/// ```
pub fn Un::base(symbol : String, dim : @dimension.Dimension) -> Un {
{ repr: @algebra.Monomial::symbol(symbol), dim }
}
///|
/// Defines a scaled unit: one of it equals `scale` coherent base units (e.g. a
/// kilometre is 1000 metres).
///
/// # Example
///
/// ```mbt check
/// test {
/// let km = Un::scaled("km", @dimension.Dimension::length(), 1000.0)
/// assert_eq(km.scale(), 1000.0)
/// }
/// ```
pub fn Un::scaled(
symbol : String,
dim : @dimension.Dimension,
scale : Double,
) -> Un {
{
repr: @algebra.Monomial::scalar(scale).mul(
@algebra.Monomial::symbol(symbol),
),
dim,
}
}
///|
/// Returns the scale factor to the coherent base unit (one unit equals `scale`
/// base units).
///
/// # Example
///
/// ```mbt check
/// test {
/// let km = Un::scaled("km", @dimension.Dimension::length(), 1000.0)
/// assert_eq(km.scale(), 1000.0)
/// }
/// ```
pub fn Un::scale(self : Un) -> Double {
self.repr.coefficient()
}
///|
/// Returns the physical dimension this unit measures.
///
/// # Example
///
/// ```mbt check
/// test {
/// let meter = Un::base("m", @dimension.Dimension::length())
/// assert_true(meter.dimension().is_same(@dimension.Dimension::length()))
/// }
/// ```
pub fn Un::dimension(self : Un) -> @dimension.Dimension {
self.dim
}
///|
/// Multiplies two units, composing both their symbol structure and their
/// dimensions.
///
/// # Example
///
/// ```mbt check
/// test {
/// let m = Un::base("m", @dimension.Dimension::length())
/// let area = m.mul(m)
/// assert_true(area.dimension().is_same(@dimension.Dimension::length().pow(2)))
/// }
/// ```
pub fn Un::mul(self : Un, other : Un) -> Un {
{ repr: self.repr.mul(other.repr), dim: self.dim.mul(other.dim) }
}
///|
/// Divides one unit by another, composing both their symbol structure and their
/// dimensions.
///
/// # Example
///
/// ```mbt check
/// test {
/// let m = Un::base("m", @dimension.Dimension::length())
/// let s = Un::base("s", @dimension.Dimension::time())
/// let velocity = m.div(s)
/// let expected = @dimension.Dimension::length().div(
/// @dimension.Dimension::time(),
/// )
/// assert_true(velocity.dimension().is_same(expected))
/// }
/// ```
pub fn Un::div(self : Un, other : Un) -> Un {
{ repr: self.repr.div(other.repr), dim: self.dim.div(other.dim) }
}
///|
/// Raises a unit to an integer power.
///
/// # Example
///
/// ```mbt check
/// test {
/// let m = Un::base("m", @dimension.Dimension::length())
/// let volume = m.pow(3)
/// assert_true(volume.dimension().is_same(@dimension.Dimension::length().pow(3)))
/// }
/// ```
pub fn Un::pow(self : Un, n : Int) -> Un {
{ repr: self.repr.pow(n), dim: self.dim.pow(n) }
}
///|
/// Returns whether two units measure the same dimension (and can therefore be
/// converted into one another).
///
/// # Example
///
/// ```mbt check
/// test {
/// let m = Un::base("m", @dimension.Dimension::length())
/// let km = Un::scaled("km", @dimension.Dimension::length(), 1000.0)
/// let s = Un::base("s", @dimension.Dimension::time())
/// assert_true(m.is_compatible(km))
/// assert_false(m.is_compatible(s))
/// }
/// ```
pub fn Un::is_compatible(self : Un, other : Un) -> Bool {
self.dim.is_same(other.dim)
}
///|
/// Returns the factor that converts a value expressed in `self` into one
/// expressed in `target`: `value_in_target = value_in_self * factor`. Returns
/// `None` when the units are not dimensionally compatible.
///
/// # Example
///
/// ```mbt check
/// test {
/// let m = Un::base("m", @dimension.Dimension::length())
/// let km = Un::scaled("km", @dimension.Dimension::length(), 1000.0)
/// // 1 km equals 1000 m; 1 m equals 0.001 km.
/// assert_true(km.conversion_factor(m) is Some(1000.0))
/// assert_true(m.conversion_factor(km) is Some(0.001))
/// // Incompatible dimensions cannot be converted.
/// let s = Un::base("s", @dimension.Dimension::time())
/// assert_true(m.conversion_factor(s) is None)
/// }
/// ```
pub fn Un::conversion_factor(self : Un, target : Un) -> Double? {
if self.is_compatible(target) {
Some(self.scale() / target.scale())
} else {
None
}
}
///|
/// Display style for [`format_unit_with`] and quantity formatters.
///
/// - `Ascii`: compact ASCII, e.g. `m/s^2`, factors joined with `*`.
/// - `Si`: Unicode/SI notation, e.g. `m/s²`, factors joined with `·` and
/// exponents printed as superscripts.
/// - `Latex`: LaTeX math, e.g. `\mathrm{m}/\mathrm{s}^{2}`, factors joined with
/// `\cdot`.
pub(all) enum FormatStyle {
Ascii
Si
Latex
} derive(Eq)
///|
/// Maps a non-negative integer to its Unicode superscript digits.
fn superscript_digits(value : Int) -> String {
let supers = [
"⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹",
]
if value < 10 {
return supers[value]
}
let digits : Array[Int] = []
let mut n = value
while n > 0 {
digits.push(n % 10)
n = n / 10
}
let mut result = ""
let mut i = digits.length() - 1
while i >= 0 {
result = "\{result}\{supers[digits[i]]}"
i = i - 1
}
result
}
///|
/// Renders a unit symbol for the given style.
fn render_symbol(style : FormatStyle, symbol : String) -> String {
match style {
Latex => "\\mathrm{\{symbol}}"
_ => symbol
}
}
///|
/// Renders an exponent suffix for the given style. An exponent of 1 is implicit
/// and renders as the empty string.
fn render_exponent(style : FormatStyle, exponent : Int) -> String {
if exponent == 1 {
return ""
}
match style {
Ascii => "^\{exponent}"
Si => superscript_digits(exponent)
Latex => "^{\{exponent}}"
}
}
///|
/// Renders one unit factor (`symbol` raised to a positive `exponent`).
fn render_factor(
style : FormatStyle,
symbol : String,
exponent : Int,
) -> String {
"\{render_symbol(style, symbol)}\{render_exponent(style, exponent)}"
}
///|
/// Returns the multiplication separator between factors for the given style.
fn multiply_separator(style : FormatStyle) -> String {
match style {
Ascii => "*"
Si => "·"
Latex => "\\cdot "
}
}
///|
/// Joins ordered `(symbol, exponent)` factors using the style's separator.
fn join_rendered(style : FormatStyle, factors : Array[(String, Int)]) -> String {
let separator = multiply_separator(style)
let mut result = ""
for i in 0.. String {
let body = join_rendered(style, factors)
if factors.length() == 1 {
body
} else {
"(\{body})"
}
}
///|
/// Returns whether a symbol has a conventional display priority.
fn has_display_priority(symbol : String) -> Bool {
symbol == "kg" ||
symbol == "m" ||
symbol == "s" ||
symbol == "A" ||
symbol == "K" ||
symbol == "mol" ||
symbol == "cd"
}
///|
/// Appends priority factors in conventional SI display order.
fn push_priority_pairs(
output : Array[(String, Int)],
factors : Array[(String, Int)],
) -> Unit {
let priority = ["kg", "m", "s", "A", "K", "mol", "cd"]
for target in priority {
for pair in factors {
let (symbol, _) = pair
if symbol == target {
output.push(pair)
}
}
}
}
///|
/// Appends all factors from `source` to `output`.
fn push_all_pairs(
output : Array[(String, Int)],
source : Array[(String, Int)],
) -> Unit {
for pair in source {
output.push(pair)
}
}
///|
/// Orders factors into numerator and denominator lists, scanning terms once.
///
/// Both lists hold `(symbol, exponent)` pairs with positive exponents. This
/// ordering is style-independent and shared by every [`FormatStyle`]; only the
/// leaf rendering differs.
fn ordered_format_factors(
terms : Array[(String, Int)],
) -> (Array[(String, Int)], Array[(String, Int)]) {
let numerator_priority : Array[(String, Int)] = []
let denominator_priority : Array[(String, Int)] = []
let numerator_other : Array[(String, Int)] = []
let denominator_other : Array[(String, Int)] = []
for pair in terms {
let (symbol, exponent) = pair
if exponent > 0 {
if has_display_priority(symbol) {
numerator_priority.push((symbol, exponent))
} else {
numerator_other.push((symbol, exponent))
}
} else if has_display_priority(symbol) {
denominator_priority.push((symbol, -exponent))
} else {
denominator_other.push((symbol, -exponent))
}
}
let numerator : Array[(String, Int)] = []
let denominator : Array[(String, Int)] = []
push_priority_pairs(numerator, numerator_priority)
push_all_pairs(numerator, numerator_other)
push_priority_pairs(denominator, denominator_priority)
push_all_pairs(denominator, denominator_other)
(numerator, denominator)
}
///|
/// Renders an ordered numerator/denominator split into a fraction string.
fn render_split(
style : FormatStyle,
numerator : Array[(String, Int)],
denominator : Array[(String, Int)],
) -> String {
if denominator.is_empty() {
return if numerator.is_empty() {
"1"
} else {
join_rendered(style, numerator)
}
}
let denominator_text = render_denominator(style, denominator)
if numerator.is_empty() {
"1/\{denominator_text}"
} else {
"\{join_rendered(style, numerator)}/\{denominator_text}"
}
}
///|
/// Formats a unit in compact ASCII notation for user-facing display.
///
/// Positive exponents are printed in the numerator and negative exponents in
/// the denominator. Unit scale factors are not printed; the symbolic unit name
/// chosen at construction time is used instead. For other notations see
/// [`format_unit_with`].
///
/// # Example
///
/// ```mbt check
/// test {
/// let meter = Un::base("m", @dimension.Dimension::length())
/// let second = Un::base("s", @dimension.Dimension::time())
/// assert_eq(format_unit(meter / second.pow(2)), "m/s^2")
/// assert_eq(format_unit(meter.pow(2)), "m^2")
/// }
/// ```
pub fn format_unit(unit : Un) -> String {
format_unit_with(unit, FormatStyle::Ascii)
}
///|
/// Formats a unit using the given [`FormatStyle`].
///
/// The numerator/denominator split and SI ordering are shared across styles;
/// only symbol, exponent and separator rendering differ.
///
/// # Example
///
/// ```mbt check
/// test {
/// let meter = Un::base("m", @dimension.Dimension::length())
/// let second = Un::base("s", @dimension.Dimension::time())
/// let accel = meter / second.pow(2)
/// assert_eq(format_unit_with(accel, FormatStyle::Ascii), "m/s^2")
/// assert_eq(format_unit_with(accel, FormatStyle::Si), "m/s²")
/// assert_eq(
/// format_unit_with(accel, FormatStyle::Latex),
/// "\\mathrm{m}/\\mathrm{s}^{2}",
/// )
/// }
/// ```
pub fn format_unit_with(unit : Un, style : FormatStyle) -> String {
let terms = unit.repr.terms()
let (numerator, denominator) = ordered_format_factors(terms)
render_split(style, numerator, denominator)
}