pub fn format_number(value: f64) -> String {
let rounded = (value * 1000000.0).round() / 1000000.0;
if (rounded.fract()).abs() < 0.0000001 {
return format!("{rounded:.0}");
}
if let Some(fraction) = decimal_to_fraction(rounded) {
return fraction;
}
let rounded_to_3 = (rounded * 1000.0).round() / 1000.0;
let mut result = if (rounded_to_3 * 100.0).fract().abs() < 0.001 {
format!("{rounded_to_3:.2}")
} else {
format!("{rounded_to_3:.3}")
};
if result.contains('.') {
result = result
.trim_end_matches('0')
.trim_end_matches('.')
.to_string();
}
result
}
fn decimal_to_fraction(value: f64) -> Option<String> {
const EPSILON: f64 = 0.0001;
if value < 0.0 {
return None;
}
let whole = value.floor();
let fract = value - whole;
let common_fractions = [
(0.125, "1/8"),
(0.25, "1/4"),
(0.333333, "1/3"),
(0.375, "3/8"),
(0.5, "1/2"),
(0.625, "5/8"),
(0.666667, "2/3"),
(0.75, "3/4"),
(0.875, "7/8"),
];
for &(decimal, fraction_str) in &common_fractions {
if (fract - decimal).abs() < EPSILON {
if whole > 0.0 {
return None;
} else {
return Some(fraction_str.to_string());
}
}
}
None
}
pub fn format_quantity(value: &cooklang::Value) -> Option<String> {
match value {
cooklang::Value::Number(n) => Some(format_number(n.value())),
cooklang::Value::Range { start, end } => Some(format!(
"{} - {}",
format_number(start.value()),
format_number(end.value())
)),
cooklang::Value::Text(s) => Some(s.clone()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_fractions() {
assert_eq!(format_number(0.5), "1/2");
assert_eq!(format_number(0.25), "1/4");
assert_eq!(format_number(0.75), "3/4");
assert_eq!(format_number(0.333333), "1/3");
assert_eq!(format_number(0.666667), "2/3");
assert_eq!(format_number(1.5), "1.5");
assert_eq!(format_number(2.25), "2.25");
assert_eq!(format_number(1.75), "1.75");
assert_eq!(format_number(2.333333), "2.333");
}
#[test]
fn test_format_whole_numbers() {
assert_eq!(format_number(2.0), "2");
assert_eq!(format_number(1.9999999999), "2");
}
#[test]
fn negatives_keep_their_sign_and_magnitude() {
assert_eq!(format_number(-0.5), "-0.5");
assert_eq!(format_number(-0.25), "-0.25");
assert_eq!(format_number(-0.75), "-0.75");
assert_eq!(format_number(-1.5), "-1.5");
assert_eq!(format_number(-2.333333), "-2.333");
assert_eq!(format_number(-2.0), "-2");
assert_eq!(format_number(-1.9999999999), "-2");
}
#[test]
fn quantities_render_numbers_ranges_and_text() {
use cooklang::Value;
let num = |v: f64| Value::Number(v.into());
assert_eq!(format_quantity(&num(0.5)).as_deref(), Some("1/2"));
assert_eq!(format_quantity(&num(-0.5)).as_deref(), Some("-0.5"));
assert_eq!(
format_quantity(&Value::Range {
start: 0.5.into(),
end: 2.25.into()
})
.as_deref(),
Some("1/2 - 2.25")
);
assert_eq!(
format_quantity(&Value::Text("a pinch".into())).as_deref(),
Some("a pinch")
);
}
#[test]
fn test_format_decimals() {
assert_eq!(format_number(1.23), "1.23");
assert_eq!(format_number(0.899), "0.899");
assert_eq!(format_number(0.89999999999), "0.9");
assert_eq!(format_number(0.30000000001), "0.3");
}
}