smart-calculator 1.0.3

Natural-language calculator for math, units, time zones, dates, fiat, and crypto conversions
Documentation
use fixed_decimal::{Decimal as FixedDecimal, FloatPrecision};
use serde_json::json;
use std::collections::HashMap;

use crate::data::units::{convert_unit, lookup_unit};
use crate::evaluators::error::{EvaluatorError, Result};
use crate::formatting::{MAX_DISPLAY_FRACTION_DIGITS, format_display_number, normalize_locale};
use crate::types::{AnswerType, CalculatorResult, ResultType};

const DEFAULT_PRECISION: u8 = 10;

pub fn evaluate_unit(
    amount: f64,
    from_token: String,
    to_token: String,
    locale: Option<String>,
    precision: Option<u8>,
) -> Result<CalculatorResult> {
    let input_str = format!("{amount} {from_token} to {to_token}");
    let from = lookup_unit(&from_token).ok_or_else(|| {
        EvaluatorError::UnitConversionFailed(format!("Unknown unit: '{from_token}'"))
    })?;
    let to = lookup_unit(&to_token).ok_or_else(|| {
        EvaluatorError::UnitConversionFailed(format!("Unknown unit: '{to_token}'"))
    })?;

    if from.category_name != to.category_name {
        return Err(EvaluatorError::UnitConversionFailed(format!(
            "Cannot convert {} to {}",
            from.category_name, to.category_name
        )));
    }

    let precision = precision.unwrap_or(DEFAULT_PRECISION).clamp(1, 21);
    let locale = normalize_locale(locale);
    let converted =
        convert_unit(amount, &from, &to).map_err(EvaluatorError::UnitConversionFailed)?;
    let rounded = round_to_significant_digits(converted, precision);
    let numeric =
        format_display_number(rounded, &locale, precision.min(MAX_DISPLAY_FRACTION_DIGITS));
    let formatted = format!("{numeric} {}", to.def.name);

    let mut metadata = HashMap::new();
    metadata.insert("fromUnit".to_string(), json!(from.def.name));
    metadata.insert("toUnit".to_string(), json!(to.def.name));
    metadata.insert("category".to_string(), json!(from.category_name));
    metadata.insert("amount".to_string(), json!(amount));

    Ok(CalculatorResult {
        res_type: ResultType::Unit,
        input: input_str,
        result: AnswerType::Number(rounded),
        formatted,
        metadata: Some(metadata),
    })
}

fn round_to_significant_digits(value: f64, precision: u8) -> f64 {
    if !value.is_finite() || value == 0.0 {
        return value;
    }

    FixedDecimal::try_from_f64(value, FloatPrecision::SignificantDigits(precision))
        .ok()
        .and_then(|decimal| decimal.to_string().parse::<f64>().ok())
        .unwrap_or(value)
}