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}"
);
}