lemma-engine 0.8.19

A language that means business.
Documentation
//! Magnitude-preserving math (`ceil`, `floor`, `round`, `abs`) on quantity operands.

use lemma::{Engine, SourceType};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;

fn source() -> SourceType {
    SourceType::Path(Arc::new(PathBuf::from("quantity_math_ops.lemma")))
}

fn load_ok(code: &str) {
    let mut engine = Engine::new();
    engine
        .load(code, source())
        .unwrap_or_else(|errors| panic!("spec must load: {errors:?}"));
}

fn load_err(code: &str) -> String {
    let mut engine = Engine::new();
    match engine.load(code, source()) {
        Ok(()) => panic!("expected planning error"),
        Err(errors) => errors
            .iter()
            .map(|error| error.to_string())
            .collect::<Vec<_>>()
            .join("; "),
    }
}

fn eval_display(code: &str, spec: &str, rule: &str, data: HashMap<String, String>) -> String {
    let mut engine = Engine::new();
    engine.load(code, source()).expect("spec must load");
    let response = engine
        .run(None, spec, None, data, false, None)
        .expect("spec must evaluate");
    response
        .results
        .get(rule)
        .unwrap_or_else(|| panic!("rule '{rule}' missing"))
        .display
        .clone()
        .expect("rule must have display value")
}

#[test]
fn ceil_duration_as_weeks() {
    let code = r#"spec duration_math
uses lemma units
data storage_duration: 10 days
rule weeks_billed: ceil (storage_duration as weeks)"#;
    load_ok(code);
    let displayed = eval_display(code, "duration_math", "weeks_billed", HashMap::new());
    assert!(
        displayed.contains('2') && displayed.to_lowercase().contains("week"),
        "10 days as weeks ceiled must be 2 weeks, got: {displayed}"
    );
}

#[test]
fn warehousing_storage_cost_with_ceil_weeks() {
    let code = r#"spec warehousing
uses lemma units
data money: quantity -> unit eur 1
data rate: quantity -> unit eur_per_week eur/week
data storage_per_pallet_per_week: 10 eur_per_week
data storage_duration: 10 days
rule storage_cost_per_pallet:
  storage_per_pallet_per_week * ceil (storage_duration as weeks)"#;
    load_ok(code);
    let displayed = eval_display(
        code,
        "warehousing",
        "storage_cost_per_pallet",
        HashMap::new(),
    );
    assert!(
        displayed.contains("20") && displayed.to_lowercase().contains("eur"),
        "10 eur/week * ceil(10 days as weeks) must be 20 eur, got: {displayed}"
    );
}

#[test]
fn floor_round_abs_on_quantity() {
    let code = r#"spec mass_math
uses lemma units
data mass: quantity -> unit kilogram 1
data weight: 2.6 kilogram
rule floored: floor weight
rule rounded: round weight
rule absolute: abs (0 kilogram - weight)"#;
    load_ok(code);
    assert_eq!(
        eval_display(code, "mass_math", "floored", HashMap::new()),
        "2 kilogram"
    );
    assert_eq!(
        eval_display(code, "mass_math", "rounded", HashMap::new()),
        "3 kilogram"
    );
    assert_eq!(
        eval_display(code, "mass_math", "absolute", HashMap::new()),
        "2.6 kilogram"
    );
}

#[test]
fn sqrt_on_quantity_rejected_at_plan() {
    let code = r#"spec bad_math
uses lemma units
data storage_duration: 10 days
rule bad: sqrt (storage_duration as weeks)"#;
    let error = load_err(code);
    assert!(
        error.to_lowercase().contains("sqrt") && error.to_lowercase().contains("number"),
        "sqrt on quantity must fail at plan time, got: {error}"
    );
}

#[test]
fn ceil_anonymous_compound_at_rule_boundary_rejected_at_plan() {
    let code = r#"spec compound_ceil
uses lemma units
data length: quantity -> unit meter 1
data dist: 100 meter
data time: 20 seconds
rule bad:
  ceil (dist / time)"#;
    let error = load_err(code);
    assert!(
        error.to_lowercase().contains("anonymous"),
        "ceil on dist/time at rule boundary must fail at plan time, got: {error}"
    );
}

#[test]
fn ceil_negative_duration_as_weeks() {
    let code = r#"spec negative_duration
uses lemma units
data storage_duration: -10 days
rule weeks_billed: ceil (storage_duration as weeks)"#;
    load_ok(code);
    let displayed = eval_display(code, "negative_duration", "weeks_billed", HashMap::new());
    assert!(
        displayed.contains("-1") && displayed.to_lowercase().contains("week"),
        "-10 days as weeks ceiled must be -1 weeks, got: {displayed}"
    );
}