use crate::syntax::dimension::Rational;
#[must_use]
pub fn format_number(value: f64) -> String {
if value.fract() == 0.0 && value.abs() < 1e15 {
#[expect(
clippy::cast_possible_truncation,
reason = "value.abs() < 1e15 guarantees it fits in i64"
)]
let int_val = value as i64;
format!("{int_val}")
} else {
let s = format!("{value:.6}");
let s = s.trim_end_matches('0');
let s = s.trim_end_matches('.');
s.to_string()
}
}
#[must_use]
pub fn format_exponent(exp: Rational) -> String {
if exp.is_integer() {
format!("^{}", exp.num())
} else {
format!("^({}/{})", exp.num(), exp.den())
}
}
fn negate_exponent(exp: Rational) -> Rational {
Rational::try_new(exp.num().checked_neg().unwrap_or(i32::MAX), exp.den()).unwrap_or(exp)
}
#[must_use]
pub fn format_unit_expr_with_config(
expr: &crate::syntax::ast::UnitExpr,
parenthesize_multi_denom: bool,
) -> String {
use crate::syntax::ast::MulDivOp;
let mut numerator = Vec::new();
let mut denominator = Vec::new();
for item in &expr.terms {
let mut part = item.name.value.to_string();
if let Some(pow) = item.power
&& pow != Rational::ONE
{
part = format!("{part}{}", format_exponent(pow));
}
match item.op {
MulDivOp::Mul => numerator.push(part),
MulDivOp::Div => denominator.push(part),
}
}
if denominator.is_empty() {
numerator.join(" * ")
} else if numerator.len() == 1 && denominator.len() == 1 {
format!("{}/{}", numerator[0], denominator[0])
} else {
let num = numerator.join(" * ");
let den = denominator.join(" * ");
if parenthesize_multi_denom && denominator.len() > 1 {
format!("{num} / ({den})")
} else {
format!("{num}/{den}")
}
}
}
#[must_use]
pub fn format_unit_expr(expr: &crate::syntax::ast::UnitExpr) -> String {
format_unit_expr_with_config(expr, false)
}
#[must_use]
pub fn format_unit_expr_canonical(expr: &crate::syntax::ast::UnitExpr) -> String {
use crate::syntax::ast::MulDivOp;
use std::collections::BTreeMap;
let mut exponents: BTreeMap<String, Rational> = BTreeMap::new();
for item in &expr.terms {
let pow = item.power.unwrap_or(Rational::ONE);
let signed = match item.op {
MulDivOp::Mul => pow,
MulDivOp::Div => negate_exponent(pow),
};
let name = item.name.value.to_string();
let entry = exponents.entry(name).or_insert(Rational::ZERO);
*entry = (*entry + signed).unwrap_or(*entry);
}
let render = |name: &str, exp: Rational| -> String {
if exp == Rational::ONE {
name.to_string()
} else {
format!("{name}{}", format_exponent(exp))
}
};
let mut numerator: Vec<String> = Vec::new();
let mut denominator: Vec<String> = Vec::new();
for (name, exp) in &exponents {
match exp.num().cmp(&0) {
std::cmp::Ordering::Greater => numerator.push(render(name, *exp)),
std::cmp::Ordering::Less => denominator.push(render(name, negate_exponent(*exp))),
std::cmp::Ordering::Equal => {}
}
}
match (numerator.is_empty(), denominator.is_empty()) {
(true, true) => String::new(),
(false, true) => numerator.join(" * "),
(true, false) => format!("1/{}", denominator.join(" * ")),
(false, false) => {
let num = numerator.join(" * ");
let den = denominator.join(" * ");
if denominator.len() == 1 {
format!("{num}/{den}")
} else {
format!("{num} / ({den})")
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::syntax::ast::{MulDivOp, UnitExpr, UnitExprItem};
use crate::syntax::names::{UnitName, UnitRef};
use crate::syntax::span::Span;
use crate::syntax::span::Spanned;
fn unit_term(op: MulDivOp, name: &str, power: Option<i32>) -> UnitExprItem {
UnitExprItem {
op,
name: Spanned::new(UnitRef::local(UnitName::new(name)), Span::new(0, 0)),
power: power.map(Rational::from_int),
}
}
fn unit_expr(terms: Vec<UnitExprItem>) -> UnitExpr {
UnitExpr {
terms,
span: Span::new(0, 0),
}
}
#[test]
fn canonical_combines_repeated_denominator_terms() {
let expr = unit_expr(vec![
unit_term(MulDivOp::Mul, "m", None),
unit_term(MulDivOp::Div, "s", None),
unit_term(MulDivOp::Div, "s", None),
]);
assert_eq!(format_unit_expr_canonical(&expr), "m/s^2");
}
#[test]
fn canonical_parenthesizes_multi_denominator() {
let expr = unit_expr(vec![
unit_term(MulDivOp::Mul, "kg", None),
unit_term(MulDivOp::Mul, "m", Some(2)),
unit_term(MulDivOp::Div, "A", None),
unit_term(MulDivOp::Div, "s", Some(3)),
]);
assert_eq!(format_unit_expr_canonical(&expr), "kg * m^2 / (A * s^3)");
}
#[test]
fn canonical_cancels_to_dimensionless() {
let expr = unit_expr(vec![
unit_term(MulDivOp::Mul, "s", None),
unit_term(MulDivOp::Div, "s", None),
]);
assert_eq!(format_unit_expr_canonical(&expr), "");
}
}