1#[cfg(test)]
9pub use crate::parsing::ast::Span;
10pub use crate::parsing::ast::{
11 ArithmeticComputation, ComparisonComputation, LogicalComputation, MathematicalComputation,
12 NegationType, VetoExpression,
13};
14pub use crate::parsing::source::Source;
15
16#[must_use]
19pub fn negated_comparison(op: ComparisonComputation) -> ComparisonComputation {
20 match op {
21 ComparisonComputation::LessThan => ComparisonComputation::GreaterThanOrEqual,
22 ComparisonComputation::LessThanOrEqual => ComparisonComputation::GreaterThan,
23 ComparisonComputation::GreaterThan => ComparisonComputation::LessThanOrEqual,
24 ComparisonComputation::GreaterThanOrEqual => ComparisonComputation::LessThan,
25 ComparisonComputation::Is => ComparisonComputation::IsNot,
26 ComparisonComputation::IsNot => ComparisonComputation::Is,
27 }
28}
29
30use crate::computation::rational::{checked_div, checked_mul, rational_new, RationalInteger};
32use crate::parsing::ast::Constraint;
33use crate::parsing::ast::{
34 BooleanValue, CalendarPeriodUnit, CommandArg, ConversionTarget, DateCalendarKind,
35 DateRelativeKind, DateTimeValue, LemmaSpec, PrimitiveKind, TimeValue, TypeConstraintCommand,
36};
37use crate::Error;
38use rust_decimal::Decimal;
39use serde::{Deserialize, Deserializer, Serialize, Serializer};
40use std::collections::HashMap;
41use std::fmt;
42use std::hash::Hash;
43use std::str::FromStr;
44use std::sync::{Arc, OnceLock};
45
46pub use crate::literals::{BaseMeasureVector, MeasureUnit, MeasureUnits, RatioUnit, RatioUnits};
53
54pub fn combine_decompositions(
57 left: &BaseMeasureVector,
58 right: &BaseMeasureVector,
59 is_multiply: bool,
60) -> BaseMeasureVector {
61 let mut result = left.clone();
62 for (dim, &exp) in right {
63 let delta = if is_multiply { exp } else { -exp };
64 let entry = result.entry(dim.clone()).or_insert(0);
65 *entry += delta;
66 if *entry == 0 {
67 result.remove(dim);
68 }
69 }
70 result
71}
72
73pub fn combine_signatures(
77 left: &[(String, i32)],
78 right: &[(String, i32)],
79 is_multiply: bool,
80) -> Vec<(String, i32)> {
81 use std::collections::BTreeMap;
82 let mut accumulator: BTreeMap<String, i32> = BTreeMap::new();
83 for (name, exponent) in left {
84 *accumulator.entry(name.clone()).or_insert(0) += exponent;
85 }
86 for (name, exponent) in right {
87 let delta = if is_multiply { *exponent } else { -*exponent };
88 *accumulator.entry(name.clone()).or_insert(0) += delta;
89 }
90 accumulator
91 .into_iter()
92 .filter(|(_, exponent)| *exponent != 0)
93 .collect()
94}
95
96pub fn format_signature_operator_style(signature: &[(String, i32)]) -> String {
109 let canonical = canonicalize_signature(signature);
110 let mut numerator: Vec<(String, i32)> = Vec::new();
111 let mut denominator: Vec<(String, i32)> = Vec::new();
112 for (name, exponent) in canonical {
113 if exponent > 0 {
114 numerator.push((name, exponent));
115 } else if exponent < 0 {
116 denominator.push((name, -exponent));
117 }
118 }
119 let render = |terms: &[(String, i32)]| -> String {
120 terms
121 .iter()
122 .map(|(name, exp)| {
123 if *exp == 1 {
124 name.clone()
125 } else {
126 format!("{name}^{exp}")
127 }
128 })
129 .collect::<Vec<_>>()
130 .join("*")
131 };
132 match (numerator.is_empty(), denominator.is_empty()) {
133 (true, true) => String::new(),
134 (false, true) => render(&numerator),
135 (true, false) => format!("1/{}", render(&denominator)),
136 (false, false) => format!("{}/{}", render(&numerator), render(&denominator)),
137 }
138}
139
140pub fn calendar_unit_factor(name: &str) -> Option<crate::computation::rational::RationalInteger> {
147 use crate::computation::rational::rational_one;
148 match name {
149 "month" | "months" => Some(rational_one()),
150 "year" | "years" => Some(rational_new(12, 1)),
151 _ => None,
152 }
153}
154
155fn owner_declares_measure_unit(owner: &LemmaType, unit_name: &str) -> bool {
156 owner
157 .measure_unit_names()
158 .is_some_and(|names| names.contains(&unit_name))
159}
160
161pub fn signature_factor(
170 signature: &[(String, i32)],
171 expression_units: &std::collections::HashMap<String, Arc<LemmaType>>,
172 owner: Option<&LemmaType>,
173) -> Result<
174 crate::computation::rational::RationalInteger,
175 crate::computation::rational::NumericFailure,
176> {
177 use crate::computation::rational::{checked_div, checked_mul, rational_one};
178 let mut acc = rational_one();
179 for (name, exponent) in signature {
180 let factor =
181 if let Some(owner) = owner.filter(|owner| owner_declares_measure_unit(owner, name)) {
182 owner.measure_unit_factor(name).clone()
183 } else if let Some(lemma_type) = expression_units.get(name) {
184 lemma_type.measure_unit_factor(name).clone()
185 } else {
186 panic!(
187 "BUG: signature_factor called with unresolved unit name '{}'",
188 name
189 );
190 };
191 let mut term = rational_one();
192 let abs_exp = exponent.unsigned_abs();
193 for _ in 0..abs_exp {
194 term = checked_mul(&term, &factor)?;
195 }
196 if *exponent >= 0 {
197 acc = checked_mul(&acc, &term)?;
198 } else {
199 acc = checked_div(&acc, &term)?;
200 }
201 }
202 Ok(acc)
203}
204
205pub fn canonicalize_signature(signature: &[(String, i32)]) -> Vec<(String, i32)> {
206 use std::collections::BTreeMap;
207 let mut accumulator: BTreeMap<String, i32> = BTreeMap::new();
208 for (name, exponent) in signature {
209 *accumulator.entry(name.clone()).or_insert(0) += exponent;
210 }
211 accumulator
212 .into_iter()
213 .filter(|(_, exponent)| *exponent != 0)
214 .collect()
215}
216
217pub const DURATION_DIMENSION: &str = "duration";
218pub const CALENDAR_DIMENSION: &str = "calendar";
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
221#[serde(rename_all = "snake_case")]
222pub enum MeasureTrait {
223 Duration,
224 Calendar,
225}
226
227pub fn duration_decomposition() -> BaseMeasureVector {
228 [(DURATION_DIMENSION.to_string(), 1i32)]
229 .into_iter()
230 .collect()
231}
232
233pub fn calendar_decomposition() -> BaseMeasureVector {
234 [(CALENDAR_DIMENSION.to_string(), 1i32)]
235 .into_iter()
236 .collect()
237}
238
239pub fn anonymous_measure_type() -> LemmaType {
243 LemmaType::anonymous_for_decomposition(BaseMeasureVector::new())
244}
245
246pub fn negate_signature(signature: &[(String, i32)]) -> Vec<(String, i32)> {
249 signature.iter().map(|(n, e)| (n.clone(), -*e)).collect()
250}
251
252mod stored_measure_declared_bound_serde {
253 use super::RationalInteger;
254 use crate::computation::rational::commit_rational_to_decimal;
255 use rust_decimal::Decimal;
256 use serde::{Deserialize, Deserializer, Serialize, Serializer};
257
258 fn lift(decimal: Decimal) -> Result<RationalInteger, String> {
259 crate::computation::rational::decimal_to_rational(decimal)
260 .map_err(|failure| failure.to_string())
261 }
262
263 pub mod option {
264 use super::*;
265
266 pub fn serialize<S: Serializer>(
267 value: &Option<(RationalInteger, String)>,
268 serializer: S,
269 ) -> Result<S::Ok, S::Error> {
270 match value {
271 None => serializer.serialize_none(),
272 Some((magnitude, unit_name)) => {
273 let decimal = commit_rational_to_decimal(magnitude)
274 .expect("BUG: planned measure declared bound must commit to decimal");
275 (decimal, unit_name.as_str()).serialize(serializer)
276 }
277 }
278 }
279
280 pub fn deserialize<'de, D: Deserializer<'de>>(
281 deserializer: D,
282 ) -> Result<Option<(RationalInteger, String)>, D::Error> {
283 let parsed: Option<(Decimal, String)> = Option::deserialize(deserializer)?;
284 parsed
285 .map(|(decimal, unit_name)| lift(decimal).map(|magnitude| (magnitude, unit_name)))
286 .transpose()
287 .map_err(serde::de::Error::custom)
288 }
289 }
290}
291
292#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
293#[serde(tag = "kind", rename_all = "lowercase")]
294pub enum TypeSpecification {
295 Boolean {
296 help: String,
297 },
298 Measure {
299 #[serde(with = "stored_measure_declared_bound_serde::option", default)]
300 minimum: Option<(RationalInteger, String)>,
301 #[serde(with = "stored_measure_declared_bound_serde::option", default)]
302 maximum: Option<(RationalInteger, String)>,
303 decimals: Option<u8>,
304 units: MeasureUnits,
305 #[serde(default)]
306 traits: Vec<MeasureTrait>,
307 #[serde(default)]
312 decomposition: Option<BaseMeasureVector>,
313 help: String,
314 },
315 Number {
316 #[serde(with = "crate::literals::stored_rational_serde::option", default)]
317 minimum: Option<RationalInteger>,
318 #[serde(with = "crate::literals::stored_rational_serde::option", default)]
319 maximum: Option<RationalInteger>,
320 decimals: Option<u8>,
321 help: String,
322 },
323 NumberRange {
324 help: String,
325 },
326 Ratio {
327 #[serde(with = "crate::literals::stored_rational_serde::option", default)]
328 minimum: Option<RationalInteger>,
329 #[serde(with = "crate::literals::stored_rational_serde::option", default)]
330 maximum: Option<RationalInteger>,
331 decimals: Option<u8>,
332 units: RatioUnits,
333 help: String,
334 },
335 RatioRange {
336 units: RatioUnits,
337 help: String,
338 },
339 Text {
340 length: Option<usize>,
341 options: Vec<String>,
342 help: String,
343 },
344 Date {
345 minimum: Option<DateTimeValue>,
346 maximum: Option<DateTimeValue>,
347 help: String,
348 },
349 DateRange {
350 help: String,
351 },
352 Time {
353 minimum: Option<TimeValue>,
354 maximum: Option<TimeValue>,
355 help: String,
356 },
357 TimeRange {
358 help: String,
359 },
360 MeasureRange {
361 units: MeasureUnits,
362 #[serde(default)]
363 decomposition: Option<BaseMeasureVector>,
364 help: String,
365 },
366 Veto {
367 message: Option<String>,
368 },
369 Undetermined,
373}
374
375impl std::fmt::Display for TypeSpecification {
376 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
377 let label = match self {
378 Self::Boolean { .. } => "boolean",
379 Self::Measure { .. } => "measure",
380 Self::MeasureRange { .. } => "measure range",
381 Self::Number { .. } => "number",
382 Self::NumberRange { .. } => "number range",
383 Self::Text { .. } => "text",
384 Self::Date { .. } => "date",
385 Self::DateRange { .. } => "date range",
386 Self::Time { .. } => "time",
387 Self::TimeRange { .. } => "time range",
388 Self::Ratio { .. } => "ratio",
389 Self::RatioRange { .. } => "ratio range",
390 Self::Veto { .. } => "veto",
391 Self::Undetermined => "undetermined",
392 };
393 f.write_str(label)
394 }
395}
396
397impl TypeSpecification {
398 pub fn help(&self) -> &str {
400 match self {
401 Self::Boolean { help, .. }
402 | Self::Measure { help, .. }
403 | Self::Number { help, .. }
404 | Self::NumberRange { help, .. }
405 | Self::Text { help, .. }
406 | Self::Date { help, .. }
407 | Self::DateRange { help, .. }
408 | Self::Time { help, .. }
409 | Self::TimeRange { help, .. }
410 | Self::Ratio { help, .. }
411 | Self::RatioRange { help, .. }
412 | Self::MeasureRange { help, .. } => help.as_str(),
413 Self::Veto { .. } | Self::Undetermined => "",
414 }
415 }
416}
417
418fn require_literal<'a>(
424 args: &'a [CommandArg],
425 cmd: &str,
426) -> Result<&'a crate::literals::Value, String> {
427 let arg = args
428 .first()
429 .ok_or_else(|| format!("{} requires an argument", cmd))?;
430 match arg {
431 CommandArg::Literal(v) => Ok(v),
432 CommandArg::Label(name) => Err(format!(
433 "{} requires a literal value, got identifier '{}'",
434 cmd, name
435 )),
436 CommandArg::UnitExpr(_) => Err(format!(
437 "{} requires a literal value, got a unit expression (only valid for 'unit' command)",
438 cmd
439 )),
440 }
441}
442
443fn apply_type_help_command(help: &mut String, args: &[CommandArg]) -> Result<(), String> {
444 match require_literal(args, "help")? {
445 crate::literals::Value::Text(s) => {
446 *help = s.clone();
447 Ok(())
448 }
449 other => Err(format!(
450 "help requires a text literal (quoted string), got {}",
451 value_kind_name(other)
452 )),
453 }
454}
455
456fn format_measure_units_list(units: &MeasureUnits) -> String {
457 units
458 .iter()
459 .map(|u| u.name.as_str())
460 .collect::<Vec<_>>()
461 .join(", ")
462}
463
464#[derive(Debug, Clone, Copy, PartialEq, Eq)]
466pub(crate) enum DefaultExpectation {
467 MeasureUnits,
468 Text,
469 Number,
470 Boolean,
471 Date,
472 Time,
473 Ratio,
474 NumberRange,
475 DateRange,
476 TimeRange,
477 MeasureRange,
478 RatioRange,
479}
480
481pub(crate) fn default_value_mismatch_error(
482 calendar_unit: &str,
483 type_name: &str,
484 expectation: DefaultExpectation,
485 measure_units: Option<&MeasureUnits>,
486) -> String {
487 let unit_label = calendar_unit;
488 let first = format!("Unit '{unit_label}' is for calendar data.");
489 match expectation {
490 DefaultExpectation::MeasureUnits => {
491 let list = measure_units
492 .map(format_measure_units_list)
493 .unwrap_or_default();
494 format!("{first} Valid '{type_name}' units are: {list}.")
495 }
496 DefaultExpectation::Text => format!(
497 "{first} Please provide a text value in double quotes, for example `-> default \"my default value\"`."
498 ),
499 DefaultExpectation::Number => format!(
500 "{first} Please provide a number, for example `-> default 42`."
501 ),
502 DefaultExpectation::Boolean => format!(
503 "{first} Please provide true or false, for example `-> default true`."
504 ),
505 DefaultExpectation::Date => format!(
506 "{first} Please provide a date, for example `-> default 2024-06-15`."
507 ),
508 DefaultExpectation::Time => format!(
509 "{first} Please provide a time, for example `-> default 09:00:00`."
510 ),
511 DefaultExpectation::Ratio | DefaultExpectation::RatioRange => format!(
512 "{first} Please provide a ratio, for example `-> default 25%`."
513 ),
514 DefaultExpectation::NumberRange => format!(
515 "{first} Please provide a number range, for example `-> default 10...100`."
516 ),
517 DefaultExpectation::DateRange => format!(
518 "{first} Please provide a date range, for example `-> default 2024-01-01...2024-12-31`."
519 ),
520 DefaultExpectation::TimeRange => format!(
521 "{first} Please provide a time range, for example `-> default 09:00...17:00`."
522 ),
523 DefaultExpectation::MeasureRange => format!(
524 "{first} Please provide a range with units valid for '{type_name}', for example `-> default 30 kilogram...35 kilogram`."
525 ),
526 }
527}
528
529fn measure_default_unit_error(unit: &str, type_name: &str, units: &MeasureUnits) -> String {
530 format!(
531 "Unit '{unit}' is not defined on '{type_name}'. Valid '{type_name}' units are: {}.",
532 format_measure_units_list(units)
533 )
534}
535
536fn measure_default_wrong_shape_error(type_name: &str, traits: &[MeasureTrait]) -> String {
537 let example = if traits.contains(&MeasureTrait::Duration) {
538 "4 weeks"
539 } else if traits.contains(&MeasureTrait::Calendar) {
540 "3 month"
541 } else {
542 "30 kilogram"
543 };
544 format!(
545 "Please provide a value with a unit valid for '{type_name}', for example `-> default {example}`."
546 )
547}
548
549fn validate_calendar_range_default_endpoint(
550 value: &crate::literals::Value,
551 type_name: &str,
552 units: &MeasureUnits,
553) -> Result<(), String> {
554 let unit_name = match value {
555 crate::literals::Value::NumberWithUnit(_, u) => u.as_str(),
556 _ => {
557 return Err(
558 "Please provide a range with calendar units, for example `-> default 18 year...67 year`."
559 .to_string(),
560 );
561 }
562 };
563 if calendar_unit_factor(unit_name).is_none() {
564 return Err(
565 "Please provide a range with calendar units, for example `-> default 18 year...67 year`."
566 .to_string(),
567 );
568 }
569 if units.get(unit_name).is_err() {
570 return Err(measure_default_unit_error(unit_name, type_name, units));
571 }
572 Ok(())
573}
574
575fn reject_calendar_for_default(
576 value: &crate::literals::Value,
577 type_name: &str,
578 expectation: DefaultExpectation,
579 measure_units: Option<&MeasureUnits>,
580) -> Result<(), String> {
581 if let crate::literals::Value::NumberWithUnit(_, unit) = value {
582 if calendar_unit_factor(unit).is_some() {
583 return Err(default_value_mismatch_error(
584 unit,
585 type_name,
586 expectation,
587 measure_units,
588 ));
589 }
590 }
591 Ok(())
592}
593
594fn value_kind_name(v: &crate::literals::Value) -> &'static str {
596 use crate::literals::Value;
597 match v {
598 Value::Number(_) => "number",
599 Value::NumberWithUnit(_, _) => "number_with_unit",
600 Value::Text(_) => "text",
601 Value::Date(_) => "date",
602 Value::Time(_) => "time",
603 Value::Boolean(_) => "boolean",
604 Value::Range(_, _) => "range",
605 }
606}
607
608fn require_default_range_endpoints<'a>(
609 args: &'a [CommandArg],
610 type_name: &str,
611 expectation: DefaultExpectation,
612 measure_units: Option<&MeasureUnits>,
613) -> Result<(&'a crate::literals::Value, &'a crate::literals::Value), String> {
614 match require_literal(args, "default")? {
615 crate::literals::Value::NumberWithUnit(_, unit)
616 if calendar_unit_factor(unit).is_some() =>
617 {
618 Err(default_value_mismatch_error(
619 unit,
620 type_name,
621 expectation,
622 measure_units,
623 ))
624 }
625 crate::literals::Value::Range(left, right) => Ok((left.as_ref(), right.as_ref())),
626 _ => Err(match expectation {
627 DefaultExpectation::NumberRange => {
628 "Please provide a number range, for example `-> default 10...100`.".to_string()
629 }
630 DefaultExpectation::DateRange => {
631 "Please provide a date range, for example `-> default 2024-01-01...2024-12-31`."
632 .to_string()
633 }
634 DefaultExpectation::RatioRange => {
635 "Please provide a ratio range, for example `-> default 10%...50%`.".to_string()
636 }
637 DefaultExpectation::MeasureRange => format!(
638 "Please provide a range with units valid for '{type_name}', for example `-> default 30 kilogram...35 kilogram`."
639 ),
640 _ => unreachable!("BUG: require_default_range_endpoints called with non-range expectation"),
641 }),
642 }
643}
644
645fn lift_parser_decimal(decimal: rust_decimal::Decimal) -> Result<RationalInteger, String> {
646 crate::computation::rational::decimal_to_rational(decimal)
647 .map_err(|failure| format!("literal failed rational lift: {failure}"))
648}
649
650pub fn range_element_type_specification(
652 range_spec: &TypeSpecification,
653) -> Option<TypeSpecification> {
654 range_spec.element_from_range()
655}
656
657fn range_endpoints_compatible(left: &LemmaType, right: &LemmaType) -> bool {
658 match (&left.specifications, &right.specifications) {
659 (TypeSpecification::Date { .. }, TypeSpecification::Date { .. }) => true,
660 (TypeSpecification::Time { .. }, TypeSpecification::Time { .. }) => true,
661 (TypeSpecification::Number { .. }, TypeSpecification::Number { .. }) => true,
662 (TypeSpecification::Measure { .. }, TypeSpecification::Measure { .. }) => {
663 left.same_measure_family(right)
664 || left.compatible_with_anonymous_measure(right)
665 || right.compatible_with_anonymous_measure(left)
666 }
667 (TypeSpecification::Ratio { .. }, TypeSpecification::Ratio { .. }) => true,
668 _ => false,
669 }
670}
671
672pub fn range_type_specification_from_endpoints(
674 left: &LemmaType,
675 right: &LemmaType,
676) -> Option<TypeSpecification> {
677 if !range_endpoints_compatible(left, right) {
678 return None;
679 }
680 left.specifications.range_from_element()
681}
682
683fn lift_range_endpoint(
687 value: &crate::parsing::ast::Value,
688 element_spec: &TypeSpecification,
689) -> Result<LiteralValue, String> {
690 use crate::parsing::ast::Value;
691 let kind = match value {
692 Value::NumberWithUnit(_, _) => parser_value_to_value_kind(value, element_spec)?,
693 _ => value_to_semantic(value)?,
694 };
695 Ok(LiteralValue {
696 value: kind,
697 lemma_type: Arc::new(LemmaType::primitive(element_spec.clone())),
698 })
699}
700
701fn literal_value_from_parser_value(
702 value: &crate::parsing::ast::Value,
703) -> Result<LiteralValue, String> {
704 use crate::parsing::ast::Value;
705
706 match value {
707 Value::Number(n) => Ok(LiteralValue::number(lift_parser_decimal(*n)?)),
708 Value::Text(s) => Ok(LiteralValue::text(s.clone())),
709 Value::Date(dt) => Ok(LiteralValue::date(date_time_to_semantic(dt))),
710 Value::Time(t) => Ok(LiteralValue::time(time_to_semantic(t))),
711 Value::Boolean(b) => Ok(LiteralValue::from_bool(bool::from(*b))),
712 Value::NumberWithUnit(n, unit) => Ok(LiteralValue::number_interpreted_as_measure(
713 lift_parser_decimal(*n)?,
714 unit.clone(),
715 )),
716 Value::Range(left, right) => {
717 let left = literal_value_from_parser_value(left)?;
718 let right = literal_value_from_parser_value(right)?;
719 let compatible = match (
720 &left.lemma_type.specifications,
721 &right.lemma_type.specifications,
722 ) {
723 (TypeSpecification::Date { .. }, TypeSpecification::Date { .. }) => true,
724 (TypeSpecification::Time { .. }, TypeSpecification::Time { .. }) => true,
725 (TypeSpecification::Number { .. }, TypeSpecification::Number { .. }) => true,
726 (TypeSpecification::Measure { .. }, TypeSpecification::Measure { .. }) => {
727 left.lemma_type.same_measure_family(&right.lemma_type)
728 || left
729 .lemma_type
730 .compatible_with_anonymous_measure(&right.lemma_type)
731 || right
732 .lemma_type
733 .compatible_with_anonymous_measure(&left.lemma_type)
734 }
735 (TypeSpecification::Ratio { .. }, TypeSpecification::Ratio { .. }) => true,
736 _ => false,
737 };
738 if !compatible {
739 return Err(format!(
740 "range endpoints must have the same supported base type, got {} and {}",
741 left.lemma_type.name(),
742 right.lemma_type.name()
743 ));
744 }
745 Ok(LiteralValue::range(left, right))
746 }
747 }
748}
749
750fn decimal_to_u8(d: RationalInteger, ctx: &str) -> Result<u8, String> {
752 use crate::computation::bigint::BigInt;
753 if d.denom() != &BigInt::one() {
754 return Err(format!(
755 "{} requires a whole number, got fractional value",
756 ctx
757 ));
758 }
759 d.numer()
760 .to_u8()
761 .ok_or_else(|| format!("{} value out of range for u8", ctx))
762}
763
764fn decimal_to_usize(d: RationalInteger, ctx: &str) -> Result<usize, String> {
766 use crate::computation::bigint::BigInt;
767 if d.denom() != &BigInt::one() {
768 return Err(format!(
769 "{} requires a whole number, got fractional value",
770 ctx
771 ));
772 }
773 d.numer()
774 .to_usize()
775 .ok_or_else(|| format!("{} value out of range for usize", ctx))
776}
777
778fn ratio_bound_to_canonical_rational(
784 args: &[CommandArg],
785 cmd: &str,
786 units: &RatioUnits,
787) -> Result<RationalInteger, String> {
788 use crate::computation::rational::{checked_div, decimal_to_rational};
789 let lit = require_literal(args, cmd)?;
790 match lit {
791 crate::literals::Value::NumberWithUnit(magnitude, unit_name) => {
792 let unit = units.get(unit_name.as_str())?;
793 let magnitude_rational = decimal_to_rational(*magnitude)
794 .map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
795 checked_div(&magnitude_rational, &unit.value)
796 .map_err(|failure| format!("{cmd}: unit conversion failed: {failure}"))
797 }
798 other => Err(format!(
799 "{cmd} requires a ratio literal with a unit, got {}",
800 value_kind_name(other)
801 )),
802 }
803}
804
805fn require_decimal_literal(args: &[CommandArg], cmd: &str) -> Result<RationalInteger, String> {
806 use crate::computation::rational::decimal_to_rational;
807 match require_literal(args, cmd)? {
808 crate::literals::Value::Number(d) => decimal_to_rational(*d)
809 .map_err(|failure| format!("{} literal failed rational lift: {}", cmd, failure)),
810 other => Err(format!(
811 "{} requires a number literal, got {}",
812 cmd,
813 value_kind_name(other)
814 )),
815 }
816}
817
818enum UnitConstraintField {
819 Minimum,
820 Maximum,
821 DefaultMagnitude,
822}
823
824fn measure_declared_bound_to_canonical(
825 magnitude: &RationalInteger,
826 unit_name: &str,
827 units: &MeasureUnits,
828 type_name: &str,
829 command: &str,
830) -> Result<RationalInteger, String> {
831 use crate::computation::rational::checked_mul;
832 let unit = units.get(unit_name).map_err(|_| {
833 format!(
834 "Unit '{unit_name}' is not defined on '{type_name}'. Valid units are: {}.",
835 format_measure_units_list(units)
836 )
837 })?;
838 checked_mul(magnitude, &unit.factor)
839 .map_err(|failure| format!("{command}: unit conversion overflow: {failure}"))
840}
841
842fn parse_measure_declared_bound(
843 args: &[CommandArg],
844 cmd: &str,
845 units: &MeasureUnits,
846 type_name: &str,
847) -> Result<(RationalInteger, String), String> {
848 use crate::computation::rational::decimal_to_rational;
849 let lit = require_literal(args, cmd)?;
850 let (magnitude, unit_name) = match lit {
851 crate::literals::Value::NumberWithUnit(n, unit) => (*n, unit.clone()),
852 other => {
853 return Err(format!(
854 "{cmd} requires a measure literal with a unit, got {}",
855 value_kind_name(other)
856 ));
857 }
858 };
859 units.get(unit_name.as_str()).map_err(|_| {
860 format!(
861 "Unit '{unit_name}' is not defined on '{type_name}'. Valid units are: {}.",
862 format_measure_units_list(units)
863 )
864 })?;
865 let magnitude_rational = decimal_to_rational(magnitude)
866 .map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
867 Ok((magnitude_rational, unit_name))
868}
869
870fn sync_measure_units_from_canonical(
871 units: &mut MeasureUnits,
872 canonical: &RationalInteger,
873 field: UnitConstraintField,
874) -> Result<(), String> {
875 use crate::computation::rational::checked_div;
876 for unit in &mut units.0 {
877 let magnitude = checked_div(canonical, &unit.factor).map_err(|failure| {
878 format!(
879 "cannot derive per-unit constraint for unit '{}': {failure}",
880 unit.name
881 )
882 })?;
883 match field {
884 UnitConstraintField::Minimum => unit.minimum = Some(magnitude),
885 UnitConstraintField::Maximum => unit.maximum = Some(magnitude),
886 UnitConstraintField::DefaultMagnitude => unit.default_magnitude = Some(magnitude),
887 }
888 }
889 Ok(())
890}
891
892fn sync_ratio_units_from_canonical(
893 units: &mut RatioUnits,
894 canonical: &RationalInteger,
895 field: UnitConstraintField,
896) -> Result<(), String> {
897 use crate::computation::rational::checked_mul;
898 for unit in &mut units.0 {
899 let magnitude = checked_mul(canonical, &unit.value).map_err(|failure| {
900 format!(
901 "cannot derive per-unit constraint for ratio unit '{}': {failure}",
902 unit.name
903 )
904 })?;
905 match field {
906 UnitConstraintField::Minimum => unit.minimum = Some(magnitude),
907 UnitConstraintField::Maximum => unit.maximum = Some(magnitude),
908 UnitConstraintField::DefaultMagnitude => unit.default_magnitude = Some(magnitude),
909 }
910 }
911 Ok(())
912}
913
914fn sync_measure_default_units(
915 units: &mut MeasureUnits,
916 default: &ValueKind,
917 type_name: &str,
918) -> Result<(), String> {
919 let ValueKind::Measure(magnitude, signature) = default else {
920 return Ok(());
921 };
922 let unit_name = signature.first().map(|(n, _)| n.as_str()).expect(
923 "BUG: Measure default value has empty signature; literal lift must produce single-term",
924 );
925 units.get(unit_name).map_err(|_| {
926 format!("Default unit '{unit_name}' is not defined on measure type '{type_name}'.")
927 })?;
928 sync_measure_units_from_canonical(units, magnitude, UnitConstraintField::DefaultMagnitude)
929}
930
931pub(crate) fn finalize_measure_unit_constraint_magnitudes(
932 specification: &mut TypeSpecification,
933 declared_default: Option<&ValueKind>,
934 type_name: &str,
935) -> Result<(), String> {
936 let TypeSpecification::Measure {
937 minimum,
938 maximum,
939 units,
940 traits,
941 ..
942 } = specification
943 else {
944 return Ok(());
945 };
946
947 if let Some((magnitude, unit_name)) = minimum.clone() {
948 let canonical = measure_declared_bound_to_canonical(
949 &magnitude, &unit_name, units, type_name, "minimum",
950 )?;
951 sync_measure_units_from_canonical(units, &canonical, UnitConstraintField::Minimum)?;
952 }
953 if let Some((magnitude, unit_name)) = maximum.clone() {
954 let canonical = measure_declared_bound_to_canonical(
955 &magnitude, &unit_name, units, type_name, "maximum",
956 )?;
957 sync_measure_units_from_canonical(units, &canonical, UnitConstraintField::Maximum)?;
958 }
959 if let Some(default) = declared_default {
960 sync_measure_default_units(units, default, type_name)?;
961 }
962
963 if minimum.is_some() {
964 for unit in units.iter() {
965 assert!(
966 unit.minimum.is_some(),
967 "BUG: type '{type_name}' has minimum but unit '{}' missing per-unit minimum after finalize",
968 unit.name
969 );
970 }
971 }
972 if maximum.is_some() {
973 for unit in units.iter() {
974 assert!(
975 unit.maximum.is_some(),
976 "BUG: type '{type_name}' has maximum but unit '{}' missing per-unit maximum after finalize",
977 unit.name
978 );
979 }
980 }
981 let calendar_range_default = traits.contains(&MeasureTrait::Calendar)
982 && matches!(declared_default, Some(ValueKind::Range(_, _)));
983 if declared_default.is_some() && !calendar_range_default {
984 for unit in units.iter() {
985 assert!(
986 unit.default_magnitude.is_some(),
987 "BUG: type '{type_name}' has default but unit '{}' missing per-unit default after finalize",
988 unit.name
989 );
990 }
991 }
992
993 Ok(())
994}
995
996pub(crate) fn measure_declared_bound_canonical(
997 bound: &(RationalInteger, String),
998 units: &MeasureUnits,
999 type_name: &str,
1000 command: &str,
1001) -> Result<RationalInteger, String> {
1002 let (magnitude, unit_name) = bound;
1003 measure_declared_bound_to_canonical(magnitude, unit_name, units, type_name, command)
1004}
1005
1006fn sync_ratio_default_units(units: &mut RatioUnits, default: &ValueKind) -> Result<(), String> {
1007 let ValueKind::Ratio(canonical, _) = default else {
1008 return Ok(());
1009 };
1010 sync_ratio_units_from_canonical(units, canonical, UnitConstraintField::DefaultMagnitude)
1011}
1012
1013fn option_name(arg: &CommandArg, cmd: &str) -> Result<String, String> {
1019 match arg {
1020 CommandArg::Literal(crate::literals::Value::Text(s)) => Ok(s.clone()),
1021 CommandArg::Label(name) => Ok(name.clone()),
1022 CommandArg::Literal(other) => Err(format!(
1023 "{} requires a text literal or identifier, got {}",
1024 cmd,
1025 value_kind_name(other)
1026 )),
1027 CommandArg::UnitExpr(_) => Err(format!(
1028 "{} requires a text literal or identifier, got a unit expression",
1029 cmd
1030 )),
1031 }
1032}
1033
1034fn label_name(arg: &CommandArg, cmd: &str) -> Result<String, String> {
1035 match arg {
1036 CommandArg::Label(name) => Ok(name.clone()),
1037 CommandArg::Literal(other) => Err(format!(
1038 "{} requires an identifier, got {}",
1039 cmd,
1040 value_kind_name(other)
1041 )),
1042 CommandArg::UnitExpr(_) => Err(format!(
1043 "{} requires an identifier, got a unit expression",
1044 cmd
1045 )),
1046 }
1047}
1048
1049fn measure_trait_name(measure_trait: MeasureTrait) -> &'static str {
1050 match measure_trait {
1051 MeasureTrait::Duration => "duration",
1052 MeasureTrait::Calendar => "calendar",
1053 }
1054}
1055
1056fn parse_measure_trait(args: &[CommandArg]) -> Result<MeasureTrait, String> {
1057 if args.len() != 1 {
1058 return Err("trait requires exactly one identifier argument".to_string());
1059 }
1060 match label_name(&args[0], "trait")?
1061 .trim()
1062 .to_lowercase()
1063 .as_str()
1064 {
1065 "duration" => Ok(MeasureTrait::Duration),
1066 "calendar" => Ok(MeasureTrait::Calendar),
1067 other => Err(format!("Unknown measure trait '{}'", other)),
1068 }
1069}
1070
1071fn validate_calendar_trait_requirements(units: &MeasureUnits) -> Result<(), String> {
1072 let month_unit = units
1073 .iter()
1074 .find(|unit| unit.name == "month")
1075 .ok_or_else(|| {
1076 "trait calendar requires a canonical 'month' unit declared before 'trait calendar'"
1077 .to_string()
1078 })?;
1079 if !month_unit.is_canonical_factor() {
1080 return Err("trait calendar requires unit month 1".to_string());
1081 }
1082 Ok(())
1083}
1084
1085fn validate_duration_trait_requirements(units: &MeasureUnits) -> Result<(), String> {
1086 let second_unit = units
1087 .iter()
1088 .find(|unit| unit.name == "second")
1089 .ok_or_else(|| {
1090 "trait duration requires a canonical 'second' unit declared before 'trait duration'"
1091 .to_string()
1092 })?;
1093 if !second_unit.is_canonical_factor() {
1094 return Err("trait duration requires unit second 1".to_string());
1095 }
1096 Ok(())
1097}
1098
1099fn require_date_literal(args: &[CommandArg], cmd: &str) -> Result<DateTimeValue, String> {
1101 match require_literal(args, cmd)? {
1102 crate::literals::Value::Date(dt) => Ok(dt.clone()),
1103 other => Err(format!(
1104 "{} requires a date literal (e.g. 2024-01-01), got {}",
1105 cmd,
1106 value_kind_name(other)
1107 )),
1108 }
1109}
1110
1111fn require_time_literal(args: &[CommandArg], cmd: &str) -> Result<TimeValue, String> {
1113 match require_literal(args, cmd)? {
1114 crate::literals::Value::Time(t) => Ok(t.clone()),
1115 other => Err(format!(
1116 "{} requires a time literal (e.g. 12:30:00), got {}",
1117 cmd,
1118 value_kind_name(other)
1119 )),
1120 }
1121}
1122
1123#[must_use]
1125pub fn default_help_for_primitive(kind: PrimitiveKind) -> &'static str {
1126 use PrimitiveKind::*;
1127 match kind {
1128 Boolean => "Whether this holds (true or false).",
1129 Number => "A dimensionless number.",
1130 NumberRange => "The lower and upper bound of the number range.",
1131 Text => "A text value.",
1132 Measure => "A numeric amount in one of this type's units.",
1133 MeasureRange => "The lower and upper bound of the measure range in the same unit.",
1134 Ratio => "A ratio in one of this type's units (e.g. percent).",
1135 RatioRange => "The lower and upper bound of the ratio range.",
1136 Date => "A date, or a date and time with optional timezone.",
1137 DateRange => "The start date and end date of the date range.",
1138 Time => "A time of day, with optional timezone.",
1139 TimeRange => "The start time and end time of the time range.",
1140 }
1141}
1142
1143impl TypeSpecification {
1144 pub fn boolean() -> Self {
1145 TypeSpecification::Boolean {
1146 help: default_help_for_primitive(PrimitiveKind::Boolean).to_string(),
1147 }
1148 }
1149 pub fn measure() -> Self {
1150 TypeSpecification::Measure {
1151 minimum: None,
1152 maximum: None,
1153 decimals: None,
1154 units: MeasureUnits::new(),
1155 traits: Vec::new(),
1156 decomposition: None,
1157 help: default_help_for_primitive(PrimitiveKind::Measure).to_string(),
1158 }
1159 }
1160 pub fn number() -> Self {
1161 TypeSpecification::Number {
1162 minimum: None,
1163 maximum: None,
1164 decimals: None,
1165 help: default_help_for_primitive(PrimitiveKind::Number).to_string(),
1166 }
1167 }
1168 pub fn number_range() -> Self {
1169 TypeSpecification::NumberRange {
1170 help: default_help_for_primitive(PrimitiveKind::NumberRange).to_string(),
1171 }
1172 }
1173 pub fn ratio() -> Self {
1174 TypeSpecification::Ratio {
1175 minimum: None,
1176 maximum: None,
1177 decimals: None,
1178 units: RatioUnits(vec![
1179 RatioUnit {
1180 name: "percent".to_string(),
1181 value: crate::computation::rational::rational_new(100, 1),
1182 minimum: None,
1183 maximum: None,
1184 default_magnitude: None,
1185 },
1186 RatioUnit {
1187 name: "permille".to_string(),
1188 value: crate::computation::rational::rational_new(1000, 1),
1189 minimum: None,
1190 maximum: None,
1191 default_magnitude: None,
1192 },
1193 ]),
1194 help: default_help_for_primitive(PrimitiveKind::Ratio).to_string(),
1195 }
1196 }
1197 pub fn ratio_range() -> Self {
1198 TypeSpecification::RatioRange {
1199 units: match TypeSpecification::ratio() {
1200 TypeSpecification::Ratio { units, .. } => units,
1201 _ => unreachable!("BUG: ratio constructor must return a ratio type"),
1202 },
1203 help: default_help_for_primitive(PrimitiveKind::RatioRange).to_string(),
1204 }
1205 }
1206 pub fn text() -> Self {
1207 TypeSpecification::Text {
1208 length: None,
1209 options: vec![],
1210 help: default_help_for_primitive(PrimitiveKind::Text).to_string(),
1211 }
1212 }
1213 pub fn date() -> Self {
1214 TypeSpecification::Date {
1215 minimum: None,
1216 maximum: None,
1217 help: default_help_for_primitive(PrimitiveKind::Date).to_string(),
1218 }
1219 }
1220 pub fn date_range() -> Self {
1221 TypeSpecification::DateRange {
1222 help: default_help_for_primitive(PrimitiveKind::DateRange).to_string(),
1223 }
1224 }
1225 pub fn time() -> Self {
1226 TypeSpecification::Time {
1227 minimum: None,
1228 maximum: None,
1229 help: default_help_for_primitive(PrimitiveKind::Time).to_string(),
1230 }
1231 }
1232 pub fn time_range() -> Self {
1233 TypeSpecification::TimeRange {
1234 help: default_help_for_primitive(PrimitiveKind::TimeRange).to_string(),
1235 }
1236 }
1237 pub fn measure_range() -> Self {
1238 TypeSpecification::MeasureRange {
1239 units: MeasureUnits::new(),
1240 decomposition: None,
1241 help: default_help_for_primitive(PrimitiveKind::MeasureRange).to_string(),
1242 }
1243 }
1244
1245 #[must_use]
1247 pub fn element_from_range(&self) -> Option<Self> {
1248 match self {
1249 TypeSpecification::NumberRange { .. } => Some(TypeSpecification::number()),
1250 TypeSpecification::MeasureRange {
1251 units,
1252 decomposition,
1253 ..
1254 } => Some(TypeSpecification::Measure {
1255 minimum: None,
1256 maximum: None,
1257 decimals: None,
1258 units: units.clone(),
1259 traits: Vec::new(),
1260 decomposition: decomposition.clone(),
1261 help: String::new(),
1262 }),
1263 TypeSpecification::DateRange { .. } => Some(TypeSpecification::date()),
1264 TypeSpecification::TimeRange { .. } => Some(TypeSpecification::time()),
1265 TypeSpecification::RatioRange { units, .. } => Some(TypeSpecification::Ratio {
1266 minimum: None,
1267 maximum: None,
1268 decimals: None,
1269 units: units.clone(),
1270 help: String::new(),
1271 }),
1272 _ => None,
1273 }
1274 }
1275
1276 #[must_use]
1278 pub fn range_from_element(&self) -> Option<Self> {
1279 match self {
1280 TypeSpecification::Number { .. } => Some(TypeSpecification::number_range()),
1281 TypeSpecification::Measure {
1282 units,
1283 decomposition,
1284 ..
1285 } => Some(TypeSpecification::MeasureRange {
1286 units: units.clone(),
1287 decomposition: decomposition.clone(),
1288 help: default_help_for_primitive(PrimitiveKind::MeasureRange).to_string(),
1289 }),
1290 TypeSpecification::Date { .. } => Some(TypeSpecification::date_range()),
1291 TypeSpecification::Time { .. } => Some(TypeSpecification::time_range()),
1292 TypeSpecification::Ratio { units, .. } => Some(TypeSpecification::RatioRange {
1293 units: units.clone(),
1294 help: default_help_for_primitive(PrimitiveKind::RatioRange).to_string(),
1295 }),
1296 _ => None,
1297 }
1298 }
1299
1300 #[must_use]
1302 pub fn minimum_decimal(&self) -> Option<Decimal> {
1303 use crate::computation::rational::commit_rational_to_decimal;
1304 match self {
1305 TypeSpecification::Number { minimum, .. }
1306 | TypeSpecification::Ratio { minimum, .. } => minimum.as_ref().map(|bound| {
1307 commit_rational_to_decimal(bound)
1308 .expect("BUG: planned minimum must commit to decimal")
1309 }),
1310 TypeSpecification::Measure { minimum, .. } => minimum.as_ref().map(|(bound, _unit)| {
1311 commit_rational_to_decimal(bound)
1312 .expect("BUG: planned minimum must commit to decimal")
1313 }),
1314 _ => None,
1315 }
1316 }
1317
1318 #[must_use]
1320 pub fn maximum_decimal(&self) -> Option<Decimal> {
1321 use crate::computation::rational::commit_rational_to_decimal;
1322 match self {
1323 TypeSpecification::Number { maximum, .. }
1324 | TypeSpecification::Ratio { maximum, .. } => maximum.as_ref().map(|bound| {
1325 commit_rational_to_decimal(bound)
1326 .expect("BUG: planned maximum must commit to decimal")
1327 }),
1328 TypeSpecification::Measure { maximum, .. } => maximum.as_ref().map(|(bound, _unit)| {
1329 commit_rational_to_decimal(bound)
1330 .expect("BUG: planned maximum must commit to decimal")
1331 }),
1332 _ => None,
1333 }
1334 }
1335
1336 pub fn veto() -> Self {
1337 TypeSpecification::Veto { message: None }
1338 }
1339
1340 pub fn apply_constraint(
1349 mut self,
1350 type_name: &str,
1351 command: TypeConstraintCommand,
1352 args: &[CommandArg],
1353 declared_default: &mut Option<RawDefault>,
1354 ) -> Result<Self, String> {
1355 if command == TypeConstraintCommand::Trait
1356 && !matches!(&self, TypeSpecification::Measure { .. })
1357 {
1358 return Err("trait command is only valid on measure types".to_string());
1359 }
1360 match &mut self {
1361 TypeSpecification::Boolean { help } => match command {
1362 TypeConstraintCommand::Help => {
1363 apply_type_help_command(help, args)?;
1364 }
1365 TypeConstraintCommand::Default => {
1366 let lit = require_literal(args, "default")?;
1367 reject_calendar_for_default(lit, type_name, DefaultExpectation::Boolean, None)?;
1368 match lit {
1369 crate::literals::Value::Boolean(bv) => {
1370 *declared_default =
1371 Some(RawDefault::Value(ValueKind::Boolean(bool::from(bv))));
1372 }
1373 _ => {
1374 return Err(
1375 "Please provide true or false, for example `-> default true`."
1376 .to_string(),
1377 );
1378 }
1379 }
1380 }
1381 other => {
1382 return Err(format!(
1383 "Invalid command '{}' for boolean type. Valid commands: help, default",
1384 other
1385 ));
1386 }
1387 },
1388 TypeSpecification::Measure {
1389 decimals,
1390 minimum,
1391 maximum,
1392 units,
1393 traits,
1394 decomposition,
1395 help,
1396 ..
1397 } => match command {
1398 TypeConstraintCommand::Decimals => {
1399 let d = require_decimal_literal(args, "decimals")?;
1400 *decimals = Some(decimal_to_u8(d, "decimals")?);
1401 }
1402 TypeConstraintCommand::Unit => {
1403 let (unit_name, value, derived_measure_factors) = match args {
1404 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
1405 (name.clone(), *v, Vec::new())
1406 }
1407 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Expr(
1408 prefix,
1409 factors,
1410 ))] => {
1411 let raw: Vec<(String, i32)> = factors
1412 .iter()
1413 .map(|f| (f.measure_ref.clone(), f.exp))
1414 .collect();
1415 (name.clone(), *prefix, raw)
1416 }
1417 _ => {
1418 return Err(
1419 "unit requires a unit name followed by a conversion factor or compound unit expression (e.g., 'unit eur 1.00' or 'unit mps meter/second')"
1420 .to_string(),
1421 );
1422 }
1423 };
1424 if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
1425 let new_factor = crate::computation::rational::decimal_to_rational(value)
1426 .map_err(|failure| failure.to_string())?;
1427 if existing.factor != new_factor
1428 || existing.derived_measure_factors != derived_measure_factors
1429 {
1430 return Err(format!(
1431 "Unit '{unit_name}' is already defined in this type's inherited units; \
1432 cannot change factor or decomposition. Add a new unit name instead."
1433 ));
1434 }
1435 } else {
1436 units.0.push(MeasureUnit::from_decimal_factor(
1437 unit_name,
1438 value,
1439 derived_measure_factors,
1440 )?);
1441 }
1442 }
1443 TypeConstraintCommand::Trait => {
1444 let measure_trait = parse_measure_trait(args)?;
1445 if traits.contains(&measure_trait) {
1446 return Err(format!(
1447 "Duplicate trait '{}' for measure type.",
1448 measure_trait_name(measure_trait)
1449 ));
1450 }
1451 if measure_trait == MeasureTrait::Duration {
1452 validate_duration_trait_requirements(units)?;
1453 }
1454 if measure_trait == MeasureTrait::Calendar {
1455 validate_calendar_trait_requirements(units)?;
1456 }
1457 traits.push(measure_trait);
1458 }
1459 TypeConstraintCommand::Minimum => {
1460 *minimum = Some(parse_measure_declared_bound(
1461 args, "minimum", units, type_name,
1462 )?);
1463 }
1464 TypeConstraintCommand::Maximum => {
1465 *maximum = Some(parse_measure_declared_bound(
1466 args, "maximum", units, type_name,
1467 )?);
1468 }
1469 TypeConstraintCommand::Help => {
1470 apply_type_help_command(help, args)?;
1471 }
1472 TypeConstraintCommand::Default => {
1473 let lit = require_literal(args, "default")?;
1474 if traits.contains(&MeasureTrait::Calendar) {
1475 match lit {
1476 crate::literals::Value::Range(left, right) => {
1477 validate_calendar_range_default_endpoint(left, type_name, units)?;
1478 validate_calendar_range_default_endpoint(right, type_name, units)?;
1479 let element_spec = TypeSpecification::Measure {
1480 minimum: minimum.clone(),
1481 maximum: maximum.clone(),
1482 decimals: *decimals,
1483 units: units.clone(),
1484 traits: traits.clone(),
1485 decomposition: decomposition.clone(),
1486 help: String::new(),
1487 };
1488 let left = lift_range_endpoint(left, &element_spec)?;
1489 let right = lift_range_endpoint(right, &element_spec)?;
1490 *declared_default = Some(RawDefault::Value(ValueKind::Range(
1491 Box::new(left),
1492 Box::new(right),
1493 )));
1494 }
1495 crate::literals::Value::NumberWithUnit(_, _) => {
1496 let (magnitude, unit_name) = parse_measure_declared_bound(
1497 args, "default", units, type_name,
1498 )?;
1499 *declared_default = Some(RawDefault::Measure {
1500 magnitude,
1501 unit_name,
1502 });
1503 }
1504 _ => {
1505 return Err(measure_default_wrong_shape_error(type_name, traits));
1506 }
1507 }
1508 } else {
1509 reject_calendar_for_default(
1510 lit,
1511 type_name,
1512 DefaultExpectation::MeasureUnits,
1513 Some(units),
1514 )?;
1515 let (magnitude, unit_name) =
1516 parse_measure_declared_bound(args, "default", units, type_name)?;
1517 *declared_default = Some(RawDefault::Measure {
1518 magnitude,
1519 unit_name,
1520 });
1521 }
1522 }
1523 _ => {
1524 return Err(format!(
1525 "Invalid command '{}' for measure type. Valid commands: unit, trait, minimum, maximum, decimals, help, default",
1526 command
1527 ));
1528 }
1529 },
1530 TypeSpecification::Number {
1531 decimals,
1532 minimum,
1533 maximum,
1534 help,
1535 } => match command {
1536 TypeConstraintCommand::Decimals => {
1537 let d = require_decimal_literal(args, "decimals")?;
1538 *decimals = Some(decimal_to_u8(d, "decimals")?);
1539 }
1540 TypeConstraintCommand::Unit => {
1541 return Err(
1542 "Invalid command 'unit' for number type. Number types are dimensionless and cannot have units. Use 'measure' type instead.".to_string()
1543 );
1544 }
1545 TypeConstraintCommand::Minimum => {
1546 *minimum = Some(require_decimal_literal(args, "minimum")?);
1547 }
1548 TypeConstraintCommand::Maximum => {
1549 *maximum = Some(require_decimal_literal(args, "maximum")?);
1550 }
1551 TypeConstraintCommand::Help => {
1552 apply_type_help_command(help, args)?;
1553 }
1554 TypeConstraintCommand::Default => {
1555 let lit = require_literal(args, "default")?;
1556 reject_calendar_for_default(lit, type_name, DefaultExpectation::Number, None)?;
1557 match lit {
1558 crate::literals::Value::Number(d) => {
1559 *declared_default = Some(RawDefault::Value(ValueKind::Number(
1560 lift_parser_decimal(*d)?,
1561 )));
1562 }
1563 _ => {
1564 return Err(
1565 "Please provide a number, for example `-> default 42`.".to_string()
1566 );
1567 }
1568 }
1569 }
1570 _ => {
1571 return Err(format!(
1572 "Invalid command '{}' for number type. Valid commands: minimum, maximum, decimals, help, default",
1573 command
1574 ));
1575 }
1576 },
1577 TypeSpecification::NumberRange { help } => match command {
1578 TypeConstraintCommand::Help => {
1579 apply_type_help_command(help, args)?;
1580 }
1581 TypeConstraintCommand::Default => {
1582 let (left, right) = require_default_range_endpoints(
1583 args,
1584 type_name,
1585 DefaultExpectation::NumberRange,
1586 None,
1587 )?;
1588 let left = literal_value_from_parser_value(left)?;
1589 let right = literal_value_from_parser_value(right)?;
1590 if !left.lemma_type.is_number() || !right.lemma_type.is_number() {
1591 return Err(
1592 "Please provide a number range, for example `-> default 10...100`."
1593 .to_string(),
1594 );
1595 }
1596 *declared_default = Some(RawDefault::Value(ValueKind::Range(
1597 Box::new(left),
1598 Box::new(right),
1599 )));
1600 }
1601 _ => {
1602 return Err(format!(
1603 "Invalid command '{}' for number range type. Valid commands: help, default",
1604 command
1605 ));
1606 }
1607 },
1608 TypeSpecification::Ratio {
1609 decimals,
1610 minimum,
1611 maximum,
1612 units,
1613 help,
1614 } => match command {
1615 TypeConstraintCommand::Decimals => {
1616 let d = require_decimal_literal(args, "decimals")?;
1617 *decimals = Some(decimal_to_u8(d, "decimals")?);
1618 }
1619 TypeConstraintCommand::Unit => {
1620 let (unit_name, value_dec) = match args {
1621 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
1622 (name.clone(), *v)
1623 }
1624 _ => {
1625 return Err(
1626 "unit requires a unit name followed by a numeric conversion factor (e.g., 'unit percent 100'). Compound unit expressions are not supported for ratio types."
1627 .to_string(),
1628 );
1629 }
1630 };
1631 let value = crate::computation::rational::decimal_to_rational(value_dec)
1632 .map_err(|failure| {
1633 format!(
1634 "ratio unit value is not exactly representable as a rational: {}",
1635 failure
1636 )
1637 })?;
1638 if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
1639 if existing.value != value {
1640 return Err(format!(
1641 "Unit '{unit_name}' is already defined in this type's inherited units; \
1642 cannot change factor. Add a new unit name instead."
1643 ));
1644 }
1645 } else {
1646 units.0.push(RatioUnit {
1647 name: unit_name,
1648 value,
1649 minimum: None,
1650 maximum: None,
1651 default_magnitude: None,
1652 });
1653 }
1654 }
1655 TypeConstraintCommand::Minimum => {
1656 let canonical = ratio_bound_to_canonical_rational(args, "minimum", units)?;
1657 sync_ratio_units_from_canonical(
1658 units,
1659 &canonical,
1660 UnitConstraintField::Minimum,
1661 )?;
1662 *minimum = Some(canonical);
1663 }
1664 TypeConstraintCommand::Maximum => {
1665 let canonical = ratio_bound_to_canonical_rational(args, "maximum", units)?;
1666 sync_ratio_units_from_canonical(
1667 units,
1668 &canonical,
1669 UnitConstraintField::Maximum,
1670 )?;
1671 *maximum = Some(canonical);
1672 }
1673 TypeConstraintCommand::Help => {
1674 apply_type_help_command(help, args)?;
1675 }
1676 TypeConstraintCommand::Default => {
1677 let lit = require_literal(args, "default")?;
1678 reject_calendar_for_default(lit, type_name, DefaultExpectation::Ratio, None)?;
1679 let default = match lit {
1680 crate::literals::Value::NumberWithUnit(_, _) => {
1681 let element_spec = TypeSpecification::Ratio {
1682 decimals: *decimals,
1683 minimum: minimum.clone(),
1684 maximum: maximum.clone(),
1685 units: units.clone(),
1686 help: help.clone(),
1687 };
1688 parser_value_to_value_kind(lit, &element_spec)?
1689 }
1690 other => {
1691 return Err(format!(
1692 "default requires a ratio literal with a unit, got {}. Please provide a ratio value with a unit, for example `-> default 25%`.",
1693 value_kind_name(other)
1694 ));
1695 }
1696 };
1697 sync_ratio_default_units(units, &default)?;
1698 *declared_default = Some(RawDefault::Value(default));
1699 }
1700 _ => {
1701 return Err(format!(
1702 "Invalid command '{}' for ratio type. Valid commands: unit, minimum, maximum, decimals, help, default",
1703 command
1704 ));
1705 }
1706 },
1707 TypeSpecification::RatioRange { units, help } => match command {
1708 TypeConstraintCommand::Unit => {
1709 let (unit_name, value_dec) = match args {
1710 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
1711 (name.clone(), *v)
1712 }
1713 _ => {
1714 return Err(
1715 "unit requires a unit name followed by a numeric conversion factor (e.g., 'unit percent 100'). Compound unit expressions are not supported for ratio range types."
1716 .to_string(),
1717 );
1718 }
1719 };
1720 let value = crate::computation::rational::decimal_to_rational(value_dec)
1721 .map_err(|e| {
1722 format!(
1723 "ratio unit value is not exactly representable as a rational: {e}"
1724 )
1725 })?;
1726 if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
1727 if existing.value != value {
1728 return Err(format!(
1729 "Unit '{unit_name}' is already defined in this type's inherited units; \
1730 cannot change factor. Add a new unit name instead."
1731 ));
1732 }
1733 } else {
1734 units.0.push(RatioUnit {
1735 name: unit_name,
1736 value,
1737 minimum: None,
1738 maximum: None,
1739 default_magnitude: None,
1740 });
1741 }
1742 }
1743 TypeConstraintCommand::Help => {
1744 apply_type_help_command(help, args)?;
1745 }
1746 TypeConstraintCommand::Default => {
1747 let (left, right) = require_default_range_endpoints(
1748 args,
1749 type_name,
1750 DefaultExpectation::RatioRange,
1751 None,
1752 )?;
1753 let element_spec = TypeSpecification::Ratio {
1754 decimals: None,
1755 minimum: None,
1756 maximum: None,
1757 units: units.clone(),
1758 help: String::new(),
1759 };
1760 let left = lift_range_endpoint(left, &element_spec)?;
1761 let right = lift_range_endpoint(right, &element_spec)?;
1762 if !left.lemma_type.is_ratio() || !right.lemma_type.is_ratio() {
1763 return Err(
1764 "Please provide a ratio range, for example `-> default 10%...50%`."
1765 .to_string(),
1766 );
1767 }
1768 *declared_default = Some(RawDefault::Value(ValueKind::Range(
1769 Box::new(left),
1770 Box::new(right),
1771 )));
1772 }
1773 _ => {
1774 return Err(format!(
1775 "Invalid command '{}' for ratio range type. Valid commands: unit, help, default",
1776 command
1777 ));
1778 }
1779 },
1780 TypeSpecification::Text {
1781 length,
1782 options,
1783 help,
1784 } => match command {
1785 TypeConstraintCommand::Option => {
1786 if args.len() != 1 {
1787 return Err("option takes exactly one argument".to_string());
1788 }
1789 options.push(option_name(&args[0], "option")?);
1790 }
1791 TypeConstraintCommand::Options => {
1792 let mut collected = Vec::with_capacity(args.len());
1793 for arg in args {
1794 collected.push(option_name(arg, "options")?);
1795 }
1796 *options = collected;
1797 }
1798 TypeConstraintCommand::Length => {
1799 let d = require_decimal_literal(args, "length")?;
1800 *length = Some(decimal_to_usize(d, "length")?);
1801 }
1802 TypeConstraintCommand::Help => {
1803 apply_type_help_command(help, args)?;
1804 }
1805 TypeConstraintCommand::Default => {
1806 let lit = require_literal(args, "default")?;
1807 reject_calendar_for_default(lit, type_name, DefaultExpectation::Text, None)?;
1808 match lit {
1809 crate::literals::Value::Text(s) => {
1810 *declared_default = Some(RawDefault::Value(ValueKind::Text(s.clone())));
1811 }
1812 _ => {
1813 return Err(
1814 "Please provide a text value in double quotes, for example `-> default \"my default value\"`."
1815 .to_string(),
1816 );
1817 }
1818 }
1819 }
1820 _ => {
1821 return Err(format!(
1822 "Invalid command '{}' for text type. Valid commands: options, length, help, default",
1823 command
1824 ));
1825 }
1826 },
1827 TypeSpecification::Date {
1828 minimum,
1829 maximum,
1830 help,
1831 } => match command {
1832 TypeConstraintCommand::Minimum => {
1833 let dt = require_date_literal(args, "minimum")?;
1834 *minimum = Some(dt);
1835 }
1836 TypeConstraintCommand::Maximum => {
1837 let dt = require_date_literal(args, "maximum")?;
1838 *maximum = Some(dt);
1839 }
1840 TypeConstraintCommand::Help => {
1841 apply_type_help_command(help, args)?;
1842 }
1843 TypeConstraintCommand::Default => {
1844 let lit = require_literal(args, "default")?;
1845 reject_calendar_for_default(lit, type_name, DefaultExpectation::Date, None)?;
1846 match lit {
1847 crate::literals::Value::Date(dt) => {
1848 *declared_default = Some(RawDefault::Value(ValueKind::Date(
1849 date_time_to_semantic(dt),
1850 )));
1851 }
1852 _ => {
1853 return Err(
1854 "Please provide a date, for example `-> default 2024-06-15`."
1855 .to_string(),
1856 );
1857 }
1858 }
1859 }
1860 _ => {
1861 return Err(format!(
1862 "Invalid command '{}' for date type. Valid commands: minimum, maximum, help, default",
1863 command
1864 ));
1865 }
1866 },
1867 TypeSpecification::DateRange { help } => match command {
1868 TypeConstraintCommand::Help => {
1869 apply_type_help_command(help, args)?;
1870 }
1871 TypeConstraintCommand::Default => {
1872 let (left, right) = require_default_range_endpoints(
1873 args,
1874 type_name,
1875 DefaultExpectation::DateRange,
1876 None,
1877 )?;
1878 let left = literal_value_from_parser_value(left)?;
1879 let right = literal_value_from_parser_value(right)?;
1880 if !left.lemma_type.is_date() || !right.lemma_type.is_date() {
1881 return Err(
1882 "Please provide a date range, for example `-> default 2024-01-01...2024-12-31`."
1883 .to_string(),
1884 );
1885 }
1886 *declared_default = Some(RawDefault::Value(ValueKind::Range(
1887 Box::new(left),
1888 Box::new(right),
1889 )));
1890 }
1891 _ => {
1892 return Err(format!(
1893 "Invalid command '{}' for date range type. Valid commands: help, default",
1894 command
1895 ));
1896 }
1897 },
1898 TypeSpecification::Time {
1899 minimum,
1900 maximum,
1901 help,
1902 } => match command {
1903 TypeConstraintCommand::Minimum => {
1904 let t = require_time_literal(args, "minimum")?;
1905 *minimum = Some(t);
1906 }
1907 TypeConstraintCommand::Maximum => {
1908 let t = require_time_literal(args, "maximum")?;
1909 *maximum = Some(t);
1910 }
1911 TypeConstraintCommand::Help => {
1912 apply_type_help_command(help, args)?;
1913 }
1914 TypeConstraintCommand::Default => {
1915 let lit = require_literal(args, "default")?;
1916 reject_calendar_for_default(lit, type_name, DefaultExpectation::Time, None)?;
1917 match lit {
1918 crate::literals::Value::Time(t) => {
1919 *declared_default =
1920 Some(RawDefault::Value(ValueKind::Time(time_to_semantic(t))));
1921 }
1922 _ => {
1923 return Err(
1924 "Please provide a time, for example `-> default 09:00:00`."
1925 .to_string(),
1926 );
1927 }
1928 }
1929 }
1930 _ => {
1931 return Err(format!(
1932 "Invalid command '{}' for time type. Valid commands: minimum, maximum, help, default",
1933 command
1934 ));
1935 }
1936 },
1937 TypeSpecification::TimeRange { help } => match command {
1938 TypeConstraintCommand::Help => {
1939 apply_type_help_command(help, args)?;
1940 }
1941 TypeConstraintCommand::Default => {
1942 let (left, right) = require_default_range_endpoints(
1943 args,
1944 type_name,
1945 DefaultExpectation::TimeRange,
1946 None,
1947 )?;
1948 let left = literal_value_from_parser_value(left)?;
1949 let right = literal_value_from_parser_value(right)?;
1950 if !left.lemma_type.is_time() || !right.lemma_type.is_time() {
1951 return Err(
1952 "Please provide a time range, for example `-> default 09:00...17:00`."
1953 .to_string(),
1954 );
1955 }
1956 *declared_default = Some(RawDefault::Value(ValueKind::Range(
1957 Box::new(left),
1958 Box::new(right),
1959 )));
1960 }
1961 _ => {
1962 return Err(format!(
1963 "Invalid command '{}' for time range type. Valid commands: help, default",
1964 command
1965 ));
1966 }
1967 },
1968 TypeSpecification::MeasureRange {
1969 units,
1970 decomposition,
1971 help,
1972 ..
1973 } => match command {
1974 TypeConstraintCommand::Unit => {
1975 let (unit_name, value, derived_measure_factors) = match args {
1976 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
1977 (name.clone(), *v, Vec::new())
1978 }
1979 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Expr(
1980 prefix,
1981 factors,
1982 ))] => {
1983 let raw: Vec<(String, i32)> = factors
1984 .iter()
1985 .map(|f| (f.measure_ref.clone(), f.exp))
1986 .collect();
1987 (name.clone(), *prefix, raw)
1988 }
1989 _ => {
1990 return Err(
1991 "unit requires a unit name followed by a conversion factor or compound unit expression (e.g., 'unit eur 1.00' or 'unit mps meter/second')"
1992 .to_string(),
1993 );
1994 }
1995 };
1996 if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
1997 let new_factor = crate::computation::rational::decimal_to_rational(value)
1998 .map_err(|failure| failure.to_string())?;
1999 if existing.factor != new_factor
2000 || existing.derived_measure_factors != derived_measure_factors
2001 {
2002 return Err(format!(
2003 "Unit '{unit_name}' is already defined in this type's inherited units; \
2004 cannot change factor or decomposition. Add a new unit name instead."
2005 ));
2006 }
2007 } else {
2008 units.0.push(MeasureUnit::from_decimal_factor(
2009 unit_name,
2010 value,
2011 derived_measure_factors,
2012 )?);
2013 }
2014 }
2015 TypeConstraintCommand::Help => {
2016 apply_type_help_command(help, args)?;
2017 }
2018 TypeConstraintCommand::Default => {
2019 let (left, right) = require_default_range_endpoints(
2020 args,
2021 type_name,
2022 DefaultExpectation::MeasureRange,
2023 Some(units),
2024 )?;
2025 let element_spec = TypeSpecification::Measure {
2026 minimum: None,
2027 maximum: None,
2028 decimals: None,
2029 units: units.clone(),
2030 traits: vec![],
2031 decomposition: decomposition.clone(),
2032 help: String::new(),
2033 };
2034 let left = lift_range_endpoint(left, &element_spec)?;
2035 let right = lift_range_endpoint(right, &element_spec)?;
2036 if !left.lemma_type.is_measure() || !right.lemma_type.is_measure() {
2037 return Err(format!(
2038 "Please provide a range with units valid for '{type_name}', for example `-> default 30 kilogram...35 kilogram`."
2039 ));
2040 }
2041 *declared_default = Some(RawDefault::Value(ValueKind::Range(
2042 Box::new(left),
2043 Box::new(right),
2044 )));
2045 }
2046 _ => {
2047 return Err(format!(
2048 "Invalid command '{}' for measure range type. Valid commands: unit, help, default",
2049 command
2050 ));
2051 }
2052 },
2053 TypeSpecification::Veto { .. } => {
2054 return Err(format!(
2055 "Invalid command '{}' for veto type. Veto is not a user-declarable type and cannot have constraints",
2056 command
2057 ));
2058 }
2059 TypeSpecification::Undetermined => {
2060 return Err(format!(
2061 "Invalid command '{}' for undetermined sentinel type. Undetermined is an internal type used during type inference and cannot have constraints",
2062 command
2063 ));
2064 }
2065 }
2066 Ok(self)
2067 }
2068}
2069
2070pub fn parse_number_unit(
2073 value_str: &str,
2074 type_spec: &TypeSpecification,
2075) -> Result<crate::parsing::ast::Value, String> {
2076 use crate::literals::{NumberWithUnit, RatioLiteral};
2077 use crate::parsing::ast::Value;
2078
2079 let trimmed = value_str.trim();
2080 match type_spec {
2081 TypeSpecification::Measure { units, .. } => {
2082 if units.is_empty() {
2083 unreachable!(
2084 "BUG: Measure type has no units; should have been validated during planning"
2085 );
2086 }
2087 match trimmed.parse::<NumberWithUnit>() {
2088 Ok(n) => {
2089 let unit = units.get(&n.1).map_err(|e| e.to_string())?;
2090 Ok(Value::NumberWithUnit(n.0, unit.name.clone()))
2091 }
2092 Err(e) => {
2093 if trimmed.split_whitespace().count() == 1 && !trimmed.is_empty() {
2094 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
2095 let example_unit = units
2096 .iter()
2097 .next()
2098 .expect("BUG: units non-empty after guard")
2099 .name
2100 .as_str();
2101 Err(format!(
2102 "Measure value must include a unit, for example: '{} {}'. Valid units: {}.",
2103 trimmed,
2104 example_unit,
2105 valid.join(", ")
2106 ))
2107 } else {
2108 Err(e)
2109 }
2110 }
2111 }
2112 }
2113 TypeSpecification::Ratio { units, .. } => {
2114 if units.is_empty() {
2115 unreachable!(
2116 "BUG: Ratio type has no units; should have been validated during planning"
2117 );
2118 }
2119 match trimmed.parse::<RatioLiteral>()? {
2120 RatioLiteral::Bare(_) => {
2121 Err("Ratio value requires a unit (e.g. '50%', '500 basis_points').".to_string())
2122 }
2123 RatioLiteral::Percent(n) => {
2124 let unit = units.get("percent").map_err(|e| e.to_string())?;
2125 Ok(Value::NumberWithUnit(n, unit.name.clone()))
2126 }
2127 RatioLiteral::Permille(n) => {
2128 let unit = units.get("permille").map_err(|e| e.to_string())?;
2129 Ok(Value::NumberWithUnit(n, unit.name.clone()))
2130 }
2131 RatioLiteral::Named { value, unit } => {
2132 let resolved = units.get(&unit).map_err(|e| e.to_string())?;
2133 Ok(Value::NumberWithUnit(value, resolved.name.clone()))
2134 }
2135 }
2136 }
2137 _ => Err("parse_number_unit only accepts Measure or Ratio type".to_string()),
2138 }
2139}
2140
2141pub fn parse_value_from_string(
2144 value_str: &str,
2145 type_spec: &TypeSpecification,
2146 source: &Source,
2147) -> Result<crate::parsing::ast::Value, Error> {
2148 use crate::parsing::ast::Value;
2149
2150 let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
2151
2152 let parse_range_value = |element_spec: TypeSpecification| -> Result<Value, Error> {
2153 let (left_str, right_str) = value_str.split_once("...").ok_or_else(|| {
2154 to_err("Range value must use '...' between the two endpoints".to_string())
2155 })?;
2156 if left_str.trim().is_empty() || right_str.trim().is_empty() {
2157 return Err(to_err(
2158 "Range value must contain a non-empty left and right endpoint".to_string(),
2159 ));
2160 }
2161 let left = parse_value_from_string(left_str.trim(), &element_spec, source)?;
2162 let right = parse_value_from_string(right_str.trim(), &element_spec, source)?;
2163 Ok(Value::Range(Box::new(left), Box::new(right)))
2164 };
2165
2166 match type_spec {
2167 TypeSpecification::Text { .. } => value_str
2168 .parse::<crate::literals::TextLiteral>()
2169 .map(|t| Value::Text(t.0))
2170 .map_err(to_err),
2171 TypeSpecification::Number { .. } => value_str
2172 .parse::<crate::literals::NumberLiteral>()
2173 .map(|n| Value::Number(n.0))
2174 .map_err(to_err),
2175 TypeSpecification::Measure { .. } => {
2176 parse_number_unit(value_str, type_spec).map_err(to_err)
2177 }
2178 TypeSpecification::Boolean { .. } => value_str
2179 .parse::<BooleanValue>()
2180 .map(Value::Boolean)
2181 .map_err(to_err),
2182 TypeSpecification::Date { .. } => {
2183 let date = value_str.parse::<DateTimeValue>().map_err(to_err)?;
2184 Ok(Value::Date(date))
2185 }
2186 TypeSpecification::Time { .. } => {
2187 let time = value_str.parse::<TimeValue>().map_err(to_err)?;
2188 Ok(Value::Time(time))
2189 }
2190 TypeSpecification::Ratio { .. } => {
2191 parse_number_unit(value_str, type_spec).map_err(to_err)
2192 }
2193 TypeSpecification::NumberRange { .. }
2194 | TypeSpecification::MeasureRange { .. }
2195 | TypeSpecification::DateRange { .. }
2196 | TypeSpecification::TimeRange { .. }
2197 | TypeSpecification::RatioRange { .. } => {
2198 let element_spec = range_element_type_specification(type_spec).unwrap_or_else(|| {
2199 unreachable!("BUG: range_element_type_specification missing arm for known range type")
2200 });
2201 parse_range_value(element_spec)
2202 }
2203 TypeSpecification::Veto { .. } => Err(to_err(
2204 "Veto type cannot be parsed from string".to_string(),
2205 )),
2206 TypeSpecification::Undetermined => unreachable!(
2207 "BUG: parse_value_from_string called with Undetermined sentinel type; this type exists only during type inference"
2208 ),
2209 }
2210}
2211
2212#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2217#[serde(rename_all = "snake_case")]
2218pub enum SemanticCalendarUnit {
2219 Month,
2220 Year,
2221}
2222
2223impl fmt::Display for SemanticCalendarUnit {
2224 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2225 let s = match self {
2226 SemanticCalendarUnit::Month => "month",
2227 SemanticCalendarUnit::Year => "year",
2228 };
2229 write!(f, "{}", s)
2230 }
2231}
2232
2233pub fn semantic_calendar_unit_from_unit_name(unit_name: &str) -> SemanticCalendarUnit {
2234 match unit_name {
2235 "month" | "months" => SemanticCalendarUnit::Month,
2236 "year" | "years" => SemanticCalendarUnit::Year,
2237 other => unreachable!(
2238 "BUG: calendar measure signature unit must be month or year, got '{other}'"
2239 ),
2240 }
2241}
2242
2243pub fn semantic_calendar_unit_from_measure_signature(
2244 signature: &[(String, i32)],
2245) -> SemanticCalendarUnit {
2246 let unit_name = signature
2247 .first()
2248 .map(|(name, _)| name.as_str())
2249 .expect("BUG: calendar measure must carry a unit signature");
2250 semantic_calendar_unit_from_unit_name(unit_name)
2251}
2252
2253mod arc_lemma_type {
2254 use super::LemmaType;
2255 use serde::{Deserialize, Deserializer, Serialize, Serializer};
2256 use std::sync::Arc;
2257
2258 pub fn serialize<S>(value: &Arc<LemmaType>, serializer: S) -> Result<S::Ok, S::Error>
2259 where
2260 S: Serializer,
2261 {
2262 value.as_ref().serialize(serializer)
2263 }
2264
2265 pub fn deserialize<'de, D>(deserializer: D) -> Result<Arc<LemmaType>, D::Error>
2266 where
2267 D: Deserializer<'de>,
2268 {
2269 LemmaType::deserialize(deserializer).map(Arc::new)
2270 }
2271}
2272
2273#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2275#[serde(rename_all = "snake_case")]
2276pub enum SemanticConversionTarget {
2277 Type(PrimitiveKind),
2278 Unit {
2280 unit_name: String,
2281 #[serde(with = "arc_lemma_type")]
2283 owning_type: Arc<LemmaType>,
2284 },
2285}
2286
2287impl std::hash::Hash for SemanticConversionTarget {
2288 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
2289 match self {
2290 Self::Type(kind) => {
2291 0u8.hash(state);
2292 kind.hash(state);
2293 }
2294 Self::Unit {
2295 unit_name,
2296 owning_type,
2297 } => {
2298 1u8.hash(state);
2299 unit_name.hash(state);
2300 owning_type.hash(state);
2301 }
2302 }
2303 }
2304}
2305
2306impl SemanticConversionTarget {
2307 #[must_use]
2308 pub fn primitive_kind(&self) -> Option<PrimitiveKind> {
2309 match self {
2310 SemanticConversionTarget::Type(kind) => Some(*kind),
2311 SemanticConversionTarget::Unit { .. } => None,
2312 }
2313 }
2314}
2315
2316impl fmt::Display for SemanticConversionTarget {
2317 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2318 match self {
2319 SemanticConversionTarget::Type(kind) => write!(f, "{:?}", kind),
2320 SemanticConversionTarget::Unit { unit_name, .. } => write!(f, "{unit_name}"),
2321 }
2322 }
2323}
2324
2325#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2327pub struct SemanticTimezone {
2328 pub offset_hours: i8,
2329 pub offset_minutes: u8,
2330}
2331
2332impl fmt::Display for SemanticTimezone {
2333 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2334 if self.offset_hours == 0 && self.offset_minutes == 0 {
2335 write!(f, "Z")
2336 } else {
2337 let sign = if self.offset_hours >= 0 { "+" } else { "-" };
2338 let hours = self.offset_hours.abs();
2339 write!(f, "{}{:02}:{:02}", sign, hours, self.offset_minutes)
2340 }
2341 }
2342}
2343
2344#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2346pub struct SemanticTime {
2347 pub hour: u32,
2348 pub minute: u32,
2349 pub second: u32,
2350 pub microsecond: u32,
2351 pub timezone: Option<SemanticTimezone>,
2352}
2353
2354impl fmt::Display for SemanticTime {
2355 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2356 write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
2357 if self.microsecond != 0 {
2358 write!(f, ".{:06}", self.microsecond)?;
2359 }
2360 if let Some(timezone) = &self.timezone {
2361 write!(f, "{}", timezone)?;
2362 }
2363 Ok(())
2364 }
2365}
2366
2367#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2369pub struct SemanticDateTime {
2370 pub year: i32,
2371 pub month: u32,
2372 pub day: u32,
2373 pub hour: u32,
2374 pub minute: u32,
2375 pub second: u32,
2376 #[serde(default)]
2377 pub microsecond: u32,
2378 pub timezone: Option<SemanticTimezone>,
2379}
2380
2381impl fmt::Display for SemanticDateTime {
2382 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2383 let has_time = self.hour != 0
2384 || self.minute != 0
2385 || self.second != 0
2386 || self.microsecond != 0
2387 || self.timezone.is_some();
2388 if !has_time {
2389 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
2390 } else {
2391 write!(
2392 f,
2393 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
2394 self.year, self.month, self.day, self.hour, self.minute, self.second
2395 )?;
2396 if self.microsecond != 0 {
2397 write!(f, ".{:06}", self.microsecond)?;
2398 }
2399 if let Some(tz) = &self.timezone {
2400 write!(f, "{}", tz)?;
2401 }
2402 Ok(())
2403 }
2404 }
2405}
2406
2407#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2411pub enum RawDefault {
2412 Value(ValueKind),
2413 Measure {
2414 magnitude: RationalInteger,
2415 unit_name: String,
2416 },
2417}
2418
2419pub fn materialize_raw_default(
2420 raw: RawDefault,
2421 specifications: &TypeSpecification,
2422 type_name: &str,
2423) -> Result<ValueKind, String> {
2424 match raw {
2425 RawDefault::Value(vk) => Ok(vk),
2426 RawDefault::Measure {
2427 magnitude,
2428 unit_name,
2429 } => {
2430 let TypeSpecification::Measure { units, .. } = specifications else {
2431 return Err(format!(
2432 "BUG: RawDefault::Measure for non-measure type '{type_name}'"
2433 ));
2434 };
2435 let canonical = measure_declared_bound_to_canonical(
2436 &magnitude, &unit_name, units, type_name, "default",
2437 )?;
2438 Ok(ValueKind::Measure(canonical, vec![(unit_name, 1)]))
2439 }
2440 }
2441}
2442
2443#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2446pub enum ValueKind {
2447 Number(RationalInteger),
2448 Measure(RationalInteger, Vec<(String, i32)>),
2454 Text(String),
2455 Date(SemanticDateTime),
2456 Time(SemanticTime),
2457 Boolean(bool),
2458 Ratio(RationalInteger, Option<String>),
2460 Range(Box<LiteralValue>, Box<LiteralValue>),
2461}
2462
2463impl ValueKind {
2464 pub fn as_decimal_magnitude(&self) -> Result<Decimal, String> {
2466 use crate::computation::rational::commit_rational_to_decimal;
2467 match self {
2468 ValueKind::Number(n) | ValueKind::Measure(n, _) | ValueKind::Ratio(n, _) => {
2469 commit_rational_to_decimal(n).map_err(|failure| failure.to_string())
2470 }
2471 other => Err(format!("expected numeric value kind, got {other}")),
2472 }
2473 }
2474}
2475
2476fn format_rational_magnitude_for_display(rational: &RationalInteger) -> String {
2477 crate::computation::rational::rational_to_display_str(rational)
2478}
2479
2480fn format_number_with_unit_for_display(rational: &RationalInteger, unit: &str) -> String {
2481 use crate::computation::rational::{commit_rational_to_decimal, rational_to_display_str};
2482 use crate::parsing::ast::Value;
2483 match commit_rational_to_decimal(rational) {
2484 Ok(decimal) => format!("{}", Value::NumberWithUnit(decimal, unit.to_string())),
2485 Err(_) => format!("{} {}", rational_to_display_str(rational), unit),
2486 }
2487}
2488
2489impl fmt::Display for ValueKind {
2490 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2491 use crate::computation::rational::{checked_mul, rational_to_display_str};
2492 match self {
2493 ValueKind::Number(rational) => {
2494 write!(f, "{}", format_rational_magnitude_for_display(rational))
2495 }
2496 ValueKind::Measure(rational, signature) => {
2497 let unit = signature.first().map(|(n, _)| n.as_str()).unwrap_or("");
2498 write!(f, "{}", format_number_with_unit_for_display(rational, unit))
2499 }
2500 ValueKind::Text(s) => write!(f, "{}", crate::parsing::ast::Value::Text(s.clone())),
2501 ValueKind::Ratio(rational, unit) => match unit.as_deref() {
2502 Some("percent") => {
2503 let display = match checked_mul(rational, &rational_new(100, 1)) {
2504 Ok(scaled) => format_number_with_unit_for_display(&scaled, "percent"),
2505 Err(_) => format!("{} percent", rational_to_display_str(rational)),
2506 };
2507 write!(f, "{}", display)
2508 }
2509 Some("permille") => {
2510 let display = match checked_mul(rational, &rational_new(1000, 1)) {
2511 Ok(scaled) => format_number_with_unit_for_display(&scaled, "permille"),
2512 Err(_) => format!("{} permille", rational_to_display_str(rational)),
2513 };
2514 write!(f, "{}", display)
2515 }
2516 Some(unit_name) => {
2517 write!(
2518 f,
2519 "{}",
2520 format_number_with_unit_for_display(rational, unit_name)
2521 )
2522 }
2523 None => write!(f, "{}", format_rational_magnitude_for_display(rational)),
2524 },
2525 ValueKind::Date(dt) => write!(f, "{}", dt),
2526 ValueKind::Time(t) => write!(
2527 f,
2528 "{}",
2529 crate::parsing::ast::Value::Time(crate::parsing::ast::TimeValue {
2530 hour: t.hour as u8,
2531 minute: t.minute as u8,
2532 second: t.second as u8,
2533 microsecond: t.microsecond,
2534 timezone: t
2535 .timezone
2536 .as_ref()
2537 .map(|tz| crate::parsing::ast::TimezoneValue {
2538 offset_hours: tz.offset_hours,
2539 offset_minutes: tz.offset_minutes,
2540 }),
2541 })
2542 ),
2543 ValueKind::Boolean(b) => write!(f, "{}", b),
2544 ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
2545 }
2546 }
2547}
2548
2549fn decimal_from_serialized_str(s: &str) -> Result<Decimal, String> {
2550 Decimal::from_str(s.trim()).map_err(|e| format!("invalid decimal '{s}': {e}"))
2551}
2552
2553#[derive(Serialize, Deserialize)]
2554struct SerializedValueUnit {
2555 value: String,
2556 unit: String,
2557}
2558
2559#[derive(Serialize, Deserialize)]
2560struct SerializedMeasure {
2561 value: String,
2562 signature: Vec<(String, i32)>,
2563}
2564
2565#[derive(Serialize, Deserialize)]
2566struct SerializedRange {
2567 from: ValueKind,
2568 to: ValueKind,
2569}
2570
2571impl Serialize for ValueKind {
2572 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2573 use serde::ser::SerializeMap;
2574 let mut map = serializer.serialize_map(Some(1))?;
2575 match self {
2576 ValueKind::Number(rational) => {
2577 map.serialize_entry(
2578 "number",
2579 &crate::literals::rational_to_serialized_str(rational)
2580 .map_err(serde::ser::Error::custom)?,
2581 )?;
2582 }
2583 ValueKind::Measure(rational, signature) => {
2584 map.serialize_entry(
2585 "measure",
2586 &SerializedMeasure {
2587 value: crate::literals::rational_to_serialized_str(rational)
2588 .map_err(serde::ser::Error::custom)?,
2589 signature: signature.clone(),
2590 },
2591 )?;
2592 }
2593 ValueKind::Text(s) => {
2594 map.serialize_entry("text", s)?;
2595 }
2596 ValueKind::Date(dt) => {
2597 map.serialize_entry("date", dt)?;
2598 }
2599 ValueKind::Time(t) => {
2600 map.serialize_entry("time", t)?;
2601 }
2602 ValueKind::Boolean(b) => {
2603 map.serialize_entry("boolean", b)?;
2604 }
2605 ValueKind::Ratio(rational, unit) => {
2606 map.serialize_entry(
2607 "ratio",
2608 &SerializedValueUnit {
2609 value: crate::literals::rational_to_serialized_str(rational)
2610 .map_err(serde::ser::Error::custom)?,
2611 unit: unit.clone().unwrap_or_default(),
2612 },
2613 )?;
2614 }
2615 ValueKind::Range(left, right) => {
2616 map.serialize_entry(
2617 "range",
2618 &SerializedRange {
2619 from: left.value.clone(),
2620 to: right.value.clone(),
2621 },
2622 )?;
2623 }
2624 }
2625 map.end()
2626 }
2627}
2628
2629impl<'de> Deserialize<'de> for ValueKind {
2630 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2631 let map = <serde_json::Map<String, serde_json::Value>>::deserialize(deserializer)?;
2632 if map.len() != 1 {
2633 return Err(serde::de::Error::custom(format!(
2634 "ValueKind must have exactly one variant key, got {}",
2635 map.len()
2636 )));
2637 }
2638 let (tag, payload) = map.into_iter().next().expect("BUG: len checked");
2639 deserialize_value_kind_variant(&tag, payload).map_err(serde::de::Error::custom)
2640 }
2641}
2642
2643fn deserialize_value_kind_variant(
2644 tag: &str,
2645 payload: serde_json::Value,
2646) -> Result<ValueKind, String> {
2647 match tag {
2648 "number" => {
2649 let s = payload
2650 .as_str()
2651 .ok_or_else(|| "number must be a JSON string".to_string())?;
2652 let decimal = decimal_from_serialized_str(s)?;
2653 Ok(ValueKind::Number(
2654 crate::literals::rational_from_parsed_decimal(decimal)?,
2655 ))
2656 }
2657 "measure" => {
2658 let pair: SerializedMeasure =
2659 serde_json::from_value(payload).map_err(|e| e.to_string())?;
2660 let decimal = decimal_from_serialized_str(&pair.value)?;
2661 Ok(ValueKind::Measure(
2662 crate::literals::rational_from_parsed_decimal(decimal)?,
2663 pair.signature,
2664 ))
2665 }
2666 "ratio" => {
2667 let pair: SerializedValueUnit =
2668 serde_json::from_value(payload).map_err(|e| e.to_string())?;
2669 let unit = if pair.unit.is_empty() {
2670 None
2671 } else {
2672 Some(pair.unit)
2673 };
2674 let decimal = decimal_from_serialized_str(&pair.value)?;
2675 Ok(ValueKind::Ratio(
2676 crate::literals::rational_from_parsed_decimal(decimal)?,
2677 unit,
2678 ))
2679 }
2680 "calendar" => {
2681 let pair: SerializedValueUnit =
2682 serde_json::from_value(payload).map_err(|e| e.to_string())?;
2683 let unit = match pair.unit.as_str() {
2684 "month" | "months" => SemanticCalendarUnit::Month,
2685 "year" | "years" => SemanticCalendarUnit::Year,
2686 other => {
2687 return Err(format!(
2688 "unknown calendar unit '{other}' (expected 'month' or 'year')"
2689 ));
2690 }
2691 };
2692 let decimal = decimal_from_serialized_str(&pair.value)?;
2693 Ok(ValueKind::Measure(
2694 crate::literals::rational_from_parsed_decimal(decimal)?,
2695 vec![(unit.to_string(), 1)],
2696 ))
2697 }
2698 "text" => {
2699 let s = payload
2700 .as_str()
2701 .ok_or_else(|| "text must be a JSON string".to_string())?;
2702 Ok(ValueKind::Text(s.to_string()))
2703 }
2704 "date" => {
2705 let dt: SemanticDateTime =
2706 serde_json::from_value(payload).map_err(|e| e.to_string())?;
2707 Ok(ValueKind::Date(dt))
2708 }
2709 "time" => {
2710 let t: SemanticTime = serde_json::from_value(payload).map_err(|e| e.to_string())?;
2711 Ok(ValueKind::Time(t))
2712 }
2713 "boolean" => {
2714 let b = payload
2715 .as_bool()
2716 .ok_or_else(|| "boolean must be a JSON bool".to_string())?;
2717 Ok(ValueKind::Boolean(b))
2718 }
2719 "range" => {
2720 let range: SerializedRange =
2721 serde_json::from_value(payload).map_err(|e| e.to_string())?;
2722 Ok(ValueKind::Range(
2723 Box::new(LiteralValue {
2724 value: range.from,
2725 lemma_type: primitive_number_arc().clone(),
2726 }),
2727 Box::new(LiteralValue {
2728 value: range.to,
2729 lemma_type: primitive_number_arc().clone(),
2730 }),
2731 ))
2732 }
2733 other => Err(format!("unknown ValueKind variant '{other}'")),
2734 }
2735}
2736
2737#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2746pub struct PathSegment {
2747 pub data: String,
2749 pub spec: String,
2751}
2752
2753#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2757pub struct DataPath {
2758 pub segments: Vec<PathSegment>,
2760 pub data: String,
2762}
2763
2764impl DataPath {
2765 pub fn new(segments: Vec<PathSegment>, data: String) -> Self {
2767 Self { segments, data }
2768 }
2769
2770 pub fn local(data: String) -> Self {
2772 Self {
2773 segments: vec![],
2774 data,
2775 }
2776 }
2777
2778 pub fn input_key(&self) -> String {
2781 let mut s = String::new();
2782 for segment in &self.segments {
2783 s.push_str(&segment.data);
2784 s.push('.');
2785 }
2786 s.push_str(&self.data);
2787 s
2788 }
2789}
2790
2791#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2795pub struct RulePath {
2796 pub segments: Vec<PathSegment>,
2798 pub rule: String,
2800}
2801
2802impl RulePath {
2803 pub fn new(segments: Vec<PathSegment>, rule: String) -> Self {
2805 Self { segments, rule }
2806 }
2807}
2808
2809#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2818pub struct Expression {
2819 pub kind: ExpressionKind,
2820 pub source_location: Option<Source>,
2821}
2822
2823impl Expression {
2824 pub fn new(kind: ExpressionKind, source_location: Source) -> Self {
2825 Self {
2826 kind,
2827 source_location: Some(source_location),
2828 }
2829 }
2830
2831 pub fn with_source(kind: ExpressionKind, source_location: Option<Source>) -> Self {
2833 Self {
2834 kind,
2835 source_location,
2836 }
2837 }
2838
2839 pub fn collect_data_paths(&self, data: &mut std::collections::HashSet<DataPath>) {
2841 self.kind.collect_data_paths(data);
2842 }
2843
2844 pub fn collect_rule_paths(&self, rules: &mut std::collections::HashSet<RulePath>) {
2846 self.kind.collect_rule_paths(rules);
2847 }
2848}
2849
2850#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2852#[serde(rename_all = "snake_case")]
2853pub enum ExpressionKind {
2854 Literal(Box<LiteralValue>),
2856 DataPath(DataPath),
2858 RulePath(RulePath),
2860 LogicalAnd(Arc<Expression>, Arc<Expression>),
2861 LogicalOr(Arc<Expression>, Arc<Expression>),
2862 Arithmetic(Arc<Expression>, ArithmeticComputation, Arc<Expression>),
2863 Comparison(Arc<Expression>, ComparisonComputation, Arc<Expression>),
2864 UnitConversion(Arc<Expression>, SemanticConversionTarget),
2865 LogicalNegation(Arc<Expression>, NegationType),
2866 MathematicalComputation(MathematicalComputation, Arc<Expression>),
2867 Veto(VetoExpression),
2868 Now,
2870 DateRelative(DateRelativeKind, Arc<Expression>),
2872 DateCalendar(DateCalendarKind, CalendarPeriodUnit, Arc<Expression>),
2874 RangeLiteral(Arc<Expression>, Arc<Expression>),
2875 PastFutureRange(DateRelativeKind, Arc<Expression>),
2876 RangeContainment(Arc<Expression>, Arc<Expression>),
2877 ResultIsVeto(Arc<Expression>),
2879 Piecewise(Vec<(Arc<Expression>, Arc<Expression>)>),
2882}
2883
2884impl ExpressionKind {
2885 pub(crate) fn collect_data_paths(&self, data: &mut std::collections::HashSet<DataPath>) {
2887 match self {
2888 ExpressionKind::DataPath(fp) => {
2889 data.insert(fp.clone());
2890 }
2891 ExpressionKind::LogicalAnd(left, right) | ExpressionKind::LogicalOr(left, right) => {
2892 left.collect_data_paths(data);
2893 right.collect_data_paths(data);
2894 }
2895 ExpressionKind::Arithmetic(left, _, right)
2896 | ExpressionKind::Comparison(left, _, right)
2897 | ExpressionKind::RangeLiteral(left, right)
2898 | ExpressionKind::RangeContainment(left, right) => {
2899 left.collect_data_paths(data);
2900 right.collect_data_paths(data);
2901 }
2902 ExpressionKind::UnitConversion(inner, _)
2903 | ExpressionKind::LogicalNegation(inner, _)
2904 | ExpressionKind::MathematicalComputation(_, inner)
2905 | ExpressionKind::PastFutureRange(_, inner) => {
2906 inner.collect_data_paths(data);
2907 }
2908 ExpressionKind::DateRelative(_, date_expr) => {
2909 date_expr.collect_data_paths(data);
2910 }
2911 ExpressionKind::DateCalendar(_, _, date_expr) => {
2912 date_expr.collect_data_paths(data);
2913 }
2914 ExpressionKind::Literal(_)
2915 | ExpressionKind::RulePath(_)
2916 | ExpressionKind::Veto(_)
2917 | ExpressionKind::Now => {}
2918 ExpressionKind::ResultIsVeto(operand) => {
2919 operand.collect_data_paths(data);
2920 }
2921 ExpressionKind::Piecewise(arms) => {
2922 for (condition, result) in arms {
2923 condition.collect_data_paths(data);
2924 result.collect_data_paths(data);
2925 }
2926 }
2927 }
2928 }
2929
2930 pub(crate) fn collect_rule_paths(&self, rules: &mut std::collections::HashSet<RulePath>) {
2932 match self {
2933 ExpressionKind::RulePath(rule_path) => {
2934 rules.insert(rule_path.clone());
2935 }
2936 ExpressionKind::LogicalAnd(left, right) | ExpressionKind::LogicalOr(left, right) => {
2937 left.collect_rule_paths(rules);
2938 right.collect_rule_paths(rules);
2939 }
2940 ExpressionKind::Arithmetic(left, _, right)
2941 | ExpressionKind::Comparison(left, _, right)
2942 | ExpressionKind::RangeLiteral(left, right)
2943 | ExpressionKind::RangeContainment(left, right) => {
2944 left.collect_rule_paths(rules);
2945 right.collect_rule_paths(rules);
2946 }
2947 ExpressionKind::UnitConversion(inner, _)
2948 | ExpressionKind::LogicalNegation(inner, _)
2949 | ExpressionKind::MathematicalComputation(_, inner)
2950 | ExpressionKind::PastFutureRange(_, inner) => {
2951 inner.collect_rule_paths(rules);
2952 }
2953 ExpressionKind::DateRelative(_, date_expr) => {
2954 date_expr.collect_rule_paths(rules);
2955 }
2956 ExpressionKind::DateCalendar(_, _, date_expr) => {
2957 date_expr.collect_rule_paths(rules);
2958 }
2959 ExpressionKind::Literal(_)
2960 | ExpressionKind::DataPath(_)
2961 | ExpressionKind::Veto(_)
2962 | ExpressionKind::Now => {}
2963 ExpressionKind::ResultIsVeto(operand) => {
2964 operand.collect_rule_paths(rules);
2965 }
2966 ExpressionKind::Piecewise(arms) => {
2967 for (condition, result) in arms {
2968 condition.collect_rule_paths(rules);
2969 result.collect_rule_paths(rules);
2970 }
2971 }
2972 }
2973 }
2974}
2975
2976#[derive(Clone, Debug, Serialize, Deserialize)]
2982#[serde(tag = "kind", rename_all = "snake_case")]
2983pub enum TypeDefiningSpec {
2984 Local,
2986 Import { spec: Arc<LemmaSpec> },
2988}
2989
2990#[derive(Clone, Debug, Serialize, Deserialize)]
2992#[serde(rename_all = "snake_case")]
2993pub enum TypeExtends {
2994 Primitive,
2996 Custom {
2999 parent: String,
3000 family: String,
3001 defining_spec: TypeDefiningSpec,
3002 },
3003}
3004
3005impl PartialEq for TypeExtends {
3006 fn eq(&self, other: &Self) -> bool {
3007 match (self, other) {
3008 (TypeExtends::Primitive, TypeExtends::Primitive) => true,
3009 (
3010 TypeExtends::Custom {
3011 parent: lp,
3012 family: lf,
3013 defining_spec: ld,
3014 },
3015 TypeExtends::Custom {
3016 parent: rp,
3017 family: rf,
3018 defining_spec: rd,
3019 },
3020 ) => {
3021 lp == rp
3022 && lf == rf
3023 && match (ld, rd) {
3024 (TypeDefiningSpec::Local, TypeDefiningSpec::Local) => true,
3025 (
3026 TypeDefiningSpec::Import { spec: left },
3027 TypeDefiningSpec::Import { spec: right },
3028 ) => Arc::ptr_eq(left, right),
3029 _ => false,
3030 }
3031 }
3032 _ => false,
3033 }
3034 }
3035}
3036
3037impl Eq for TypeExtends {}
3038
3039impl std::hash::Hash for TypeDefiningSpec {
3040 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
3041 match self {
3042 TypeDefiningSpec::Local => {
3043 0u8.hash(state);
3044 }
3045 TypeDefiningSpec::Import { spec } => {
3046 1u8.hash(state);
3047 Arc::as_ptr(spec).hash(state);
3048 }
3049 }
3050 }
3051}
3052
3053impl std::hash::Hash for TypeExtends {
3054 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
3055 match self {
3056 TypeExtends::Primitive => {
3057 0u8.hash(state);
3058 }
3059 TypeExtends::Custom {
3060 parent,
3061 family,
3062 defining_spec,
3063 } => {
3064 1u8.hash(state);
3065 parent.hash(state);
3066 family.hash(state);
3067 defining_spec.hash(state);
3068 }
3069 }
3070 }
3071}
3072
3073impl TypeExtends {
3074 #[must_use]
3076 pub fn custom_local(parent: String, family: String) -> Self {
3077 TypeExtends::Custom {
3078 parent,
3079 family,
3080 defining_spec: TypeDefiningSpec::Local,
3081 }
3082 }
3083
3084 #[must_use]
3086 pub fn parent_name(&self) -> Option<&str> {
3087 match self {
3088 TypeExtends::Primitive => None,
3089 TypeExtends::Custom { parent, .. } => Some(parent.as_str()),
3090 }
3091 }
3092}
3093
3094#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
3099pub struct LemmaType {
3100 pub name: Option<String>,
3102 #[serde(flatten)]
3107 pub specifications: TypeSpecification,
3108 pub extends: TypeExtends,
3110}
3111
3112impl LemmaType {
3113 pub fn map_measure<F>(self, f: F) -> Self
3117 where
3118 F: FnOnce(
3119 MeasureUnits,
3120 Option<BaseMeasureVector>,
3121 ) -> (MeasureUnits, Option<BaseMeasureVector>),
3122 {
3123 let LemmaType {
3124 name,
3125 specifications,
3126 extends,
3127 } = self;
3128 let specifications = match specifications {
3129 TypeSpecification::Measure {
3130 minimum,
3131 maximum,
3132 decimals,
3133 units,
3134 traits,
3135 decomposition,
3136 help,
3137 } => {
3138 let (units, decomposition) = f(units, decomposition);
3139 TypeSpecification::Measure {
3140 minimum,
3141 maximum,
3142 decimals,
3143 units,
3144 traits,
3145 decomposition,
3146 help,
3147 }
3148 }
3149 other => other,
3150 };
3151 LemmaType {
3152 name,
3153 specifications,
3154 extends,
3155 }
3156 }
3157
3158 pub fn new(name: String, specifications: TypeSpecification, extends: TypeExtends) -> Self {
3160 Self {
3161 name: Some(name),
3162 specifications,
3163 extends,
3164 }
3165 }
3166
3167 pub fn without_name(specifications: TypeSpecification, extends: TypeExtends) -> Self {
3169 Self {
3170 name: None,
3171 specifications,
3172 extends,
3173 }
3174 }
3175
3176 pub fn primitive(specifications: TypeSpecification) -> Self {
3178 Self {
3179 name: None,
3180 specifications,
3181 extends: TypeExtends::Primitive,
3182 }
3183 }
3184
3185 pub fn name(&self) -> String {
3187 self.name
3188 .clone()
3189 .unwrap_or_else(|| self.specifications.to_string())
3190 }
3191
3192 pub fn is_boolean(&self) -> bool {
3194 matches!(&self.specifications, TypeSpecification::Boolean { .. })
3195 }
3196
3197 pub fn matches_primitive_kind(&self, kind: PrimitiveKind) -> bool {
3198 matches!(
3199 (kind, &self.specifications),
3200 (PrimitiveKind::Number, TypeSpecification::Number { .. })
3201 | (PrimitiveKind::Text, TypeSpecification::Text { .. })
3202 | (PrimitiveKind::Boolean, TypeSpecification::Boolean { .. })
3203 | (PrimitiveKind::Date, TypeSpecification::Date { .. })
3204 | (PrimitiveKind::Time, TypeSpecification::Time { .. })
3205 | (PrimitiveKind::Ratio, TypeSpecification::Ratio { .. })
3206 | (PrimitiveKind::Measure, TypeSpecification::Measure { .. })
3207 )
3208 }
3209
3210 pub fn is_measure(&self) -> bool {
3212 matches!(&self.specifications, TypeSpecification::Measure { .. })
3213 }
3214
3215 pub fn is_measure_range(&self) -> bool {
3216 matches!(&self.specifications, TypeSpecification::MeasureRange { .. })
3217 }
3218
3219 pub fn is_number(&self) -> bool {
3221 matches!(&self.specifications, TypeSpecification::Number { .. })
3222 }
3223
3224 pub fn is_number_range(&self) -> bool {
3225 matches!(&self.specifications, TypeSpecification::NumberRange { .. })
3226 }
3227
3228 pub fn is_numeric(&self) -> bool {
3230 matches!(
3231 &self.specifications,
3232 TypeSpecification::Measure { .. } | TypeSpecification::Number { .. }
3233 )
3234 }
3235
3236 pub fn is_text(&self) -> bool {
3238 matches!(&self.specifications, TypeSpecification::Text { .. })
3239 }
3240
3241 pub fn is_date(&self) -> bool {
3243 matches!(&self.specifications, TypeSpecification::Date { .. })
3244 }
3245
3246 pub fn is_date_range(&self) -> bool {
3247 matches!(&self.specifications, TypeSpecification::DateRange { .. })
3248 }
3249
3250 pub fn is_time_range(&self) -> bool {
3251 matches!(&self.specifications, TypeSpecification::TimeRange { .. })
3252 }
3253
3254 pub fn is_time(&self) -> bool {
3256 matches!(&self.specifications, TypeSpecification::Time { .. })
3257 }
3258
3259 pub fn has_trait_duration(&self) -> bool {
3260 match &self.specifications {
3261 TypeSpecification::Measure { traits, .. } => traits.contains(&MeasureTrait::Duration),
3262 _ => false,
3263 }
3264 }
3265
3266 pub fn is_duration_like_measure(&self) -> bool {
3267 if !self.is_measure() {
3268 return false;
3269 }
3270 if self.has_trait_duration() {
3271 return true;
3272 }
3273 self.is_anonymous_measure()
3274 && self
3275 .measure_type_decomposition()
3276 .is_some_and(|d| *d == duration_decomposition())
3277 }
3278
3279 pub fn is_duration_like(&self) -> bool {
3280 self.is_duration_like_measure()
3281 }
3282
3283 pub fn has_trait_calendar(&self) -> bool {
3284 match &self.specifications {
3285 TypeSpecification::Measure { traits, .. } => traits.contains(&MeasureTrait::Calendar),
3286 _ => false,
3287 }
3288 }
3289
3290 pub fn is_calendar_like_measure(&self) -> bool {
3291 if !self.is_measure() {
3292 return false;
3293 }
3294 if self.has_trait_calendar() {
3295 return true;
3296 }
3297 self.is_anonymous_measure()
3298 && self
3299 .measure_type_decomposition()
3300 .is_some_and(|d| *d == calendar_decomposition())
3301 }
3302
3303 pub fn is_calendar_like(&self) -> bool {
3304 self.is_calendar_like_measure()
3305 }
3306
3307 pub fn is_ratio(&self) -> bool {
3309 matches!(&self.specifications, TypeSpecification::Ratio { .. })
3310 }
3311
3312 pub fn is_ratio_range(&self) -> bool {
3313 matches!(&self.specifications, TypeSpecification::RatioRange { .. })
3314 }
3315
3316 pub fn is_calendar_measure_range(&self) -> bool {
3317 matches!(
3318 &self.specifications,
3319 TypeSpecification::MeasureRange { decomposition: Some(decomposition), .. }
3320 if *decomposition == calendar_decomposition()
3321 )
3322 }
3323
3324 pub fn is_calendar_like_range(&self) -> bool {
3325 self.is_calendar_measure_range()
3326 }
3327
3328 pub fn is_range(&self) -> bool {
3329 matches!(
3330 &self.specifications,
3331 TypeSpecification::DateRange { .. }
3332 | TypeSpecification::TimeRange { .. }
3333 | TypeSpecification::NumberRange { .. }
3334 | TypeSpecification::MeasureRange { .. }
3335 | TypeSpecification::RatioRange { .. }
3336 )
3337 }
3338
3339 pub fn vetoed(&self) -> bool {
3341 matches!(&self.specifications, TypeSpecification::Veto { .. })
3342 }
3343
3344 pub fn is_undetermined(&self) -> bool {
3346 matches!(&self.specifications, TypeSpecification::Undetermined)
3347 }
3348
3349 pub fn has_same_base_type(&self, other: &LemmaType) -> bool {
3351 use TypeSpecification::*;
3352 matches!(
3353 (&self.specifications, &other.specifications),
3354 (Boolean { .. }, Boolean { .. })
3355 | (Number { .. }, Number { .. })
3356 | (NumberRange { .. }, NumberRange { .. })
3357 | (Measure { .. }, Measure { .. })
3358 | (MeasureRange { .. }, MeasureRange { .. })
3359 | (Text { .. }, Text { .. })
3360 | (Date { .. }, Date { .. })
3361 | (DateRange { .. }, DateRange { .. })
3362 | (Time { .. }, Time { .. })
3363 | (TimeRange { .. }, TimeRange { .. })
3364 | (Ratio { .. }, Ratio { .. })
3365 | (RatioRange { .. }, RatioRange { .. })
3366 | (Veto { .. }, Veto { .. })
3367 | (Undetermined, Undetermined)
3368 )
3369 }
3370
3371 #[must_use]
3373 pub fn measure_family_name(&self) -> Option<&str> {
3374 if !self.is_measure() {
3375 return None;
3376 }
3377 match &self.extends {
3378 TypeExtends::Custom { family, .. } => Some(family.as_str()),
3379 TypeExtends::Primitive => self.name.as_deref(),
3380 }
3381 }
3382
3383 #[must_use]
3385 pub fn same_measure_family(&self, other: &LemmaType) -> bool {
3386 if !self.is_measure() || !other.is_measure() {
3387 return false;
3388 }
3389 match (self.measure_family_name(), other.measure_family_name()) {
3390 (Some(self_family), Some(other_family)) => self_family == other_family,
3391 _ => false,
3392 }
3393 }
3394
3395 #[must_use]
3396 pub fn compatible_with_anonymous_measure(&self, other: &LemmaType) -> bool {
3397 if !self.is_measure() || !other.is_measure() {
3398 return false;
3399 }
3400 if !self.is_anonymous_measure() && !other.is_anonymous_measure() {
3401 return false;
3402 }
3403 match (
3404 self.measure_type_decomposition(),
3405 other.measure_type_decomposition(),
3406 ) {
3407 (Some(a), Some(b)) => a == b,
3408 _ => false,
3409 }
3410 }
3411
3412 pub fn veto_type() -> Self {
3414 Self::primitive(TypeSpecification::veto())
3415 }
3416
3417 pub fn undetermined_type() -> Self {
3420 Self::primitive(TypeSpecification::Undetermined)
3421 }
3422
3423 pub fn decimal_places(&self) -> Option<u8> {
3426 match &self.specifications {
3427 TypeSpecification::Number { decimals, .. } => *decimals,
3428 TypeSpecification::Measure { decimals, .. } => *decimals,
3429 TypeSpecification::Ratio { decimals, .. } => *decimals,
3430 _ => None,
3431 }
3432 }
3433
3434 pub fn try_materialize_rational_as_decimal_string(
3439 &self,
3440 magnitude: &crate::computation::rational::RationalInteger,
3441 ) -> Result<String, crate::computation::rational::NumericFailure> {
3442 use crate::computation::rational::commit_rational_to_decimal;
3443 let decimal = commit_rational_to_decimal(magnitude)?;
3444 Ok(format_committed_decimal_for_api(
3445 decimal,
3446 self.decimal_places(),
3447 ))
3448 }
3449
3450 pub fn try_materialize_measure_canonical_in_unit(
3452 &self,
3453 canonical_magnitude: &crate::computation::rational::RationalInteger,
3454 unit_name: &str,
3455 ) -> Result<String, crate::computation::rational::NumericFailure> {
3456 use crate::computation::rational::checked_div;
3457 let unit_factor = self.measure_unit_factor(unit_name);
3458 let magnitude_in_unit = checked_div(canonical_magnitude, unit_factor)?;
3459 self.try_materialize_rational_as_decimal_string(&magnitude_in_unit)
3460 }
3461
3462 pub fn try_materialize_ratio_canonical_in_unit(
3464 &self,
3465 canonical_magnitude: &crate::computation::rational::RationalInteger,
3466 unit_name: &str,
3467 ) -> Result<String, crate::computation::rational::NumericFailure> {
3468 use crate::computation::rational::checked_mul;
3469 let units = match &self.specifications {
3470 TypeSpecification::Ratio { units, .. } => units,
3471 _ => unreachable!(
3472 "BUG: try_materialize_ratio_canonical_in_unit called on non-ratio type {}",
3473 self.name()
3474 ),
3475 };
3476 let ratio_unit = units
3477 .iter()
3478 .find(|unit| unit.name == unit_name)
3479 .unwrap_or_else(|| {
3480 let valid: Vec<&str> = units.iter().map(|unit| unit.name.as_str()).collect();
3481 unreachable!(
3482 "BUG: unknown ratio unit '{}' for type {} (valid: {}); planning must reject invalid units",
3483 unit_name,
3484 self.name(),
3485 valid.join(", ")
3486 )
3487 });
3488 let magnitude_in_unit = checked_mul(canonical_magnitude, &ratio_unit.value)?;
3489 self.try_materialize_rational_as_decimal_string(&magnitude_in_unit)
3490 }
3491
3492 pub fn example_value(&self) -> &'static str {
3494 match &self.specifications {
3495 TypeSpecification::Text { .. } => "\"hello world\"",
3496 TypeSpecification::Measure { .. } => "12.50 eur",
3497 TypeSpecification::MeasureRange { .. } => "30 kilogram...35 kilogram",
3498 TypeSpecification::Number { .. } => "3.14",
3499 TypeSpecification::NumberRange { .. } => "0...100",
3500 TypeSpecification::Boolean { .. } => "true",
3501 TypeSpecification::Date { .. } => "2023-12-25T14:30:00Z",
3502 TypeSpecification::DateRange { .. } => "2024-01-01...2024-12-31",
3503 TypeSpecification::TimeRange { .. } => "09:00...17:00",
3504 TypeSpecification::Veto { .. } => "veto",
3505 TypeSpecification::Time { .. } => "14:30:00",
3506 TypeSpecification::Ratio { .. } => "50%",
3507 TypeSpecification::RatioRange { .. } => "10%...50%",
3508 TypeSpecification::Undetermined => unreachable!(
3509 "BUG: example_value called on Undetermined sentinel type; this type must never reach user-facing code"
3510 ),
3511 }
3512 }
3513
3514 #[must_use]
3518 pub fn measure_type_decomposition(&self) -> Option<&BaseMeasureVector> {
3522 match &self.specifications {
3523 TypeSpecification::Measure { decomposition, .. } => decomposition.as_ref(),
3524 _ => unreachable!(
3525 "BUG: measure_type_decomposition called on non-measure type {}",
3526 self.name()
3527 ),
3528 }
3529 }
3530
3531 pub fn is_anonymous_measure(&self) -> bool {
3534 self.name.is_none() && matches!(&self.specifications, TypeSpecification::Measure { .. })
3535 }
3536
3537 pub fn anonymous_for_decomposition(decomposition: BaseMeasureVector) -> Self {
3541 Self {
3542 name: None,
3543 specifications: TypeSpecification::Measure {
3544 minimum: None,
3545 maximum: None,
3546 decimals: None,
3547 units: crate::literals::MeasureUnits::new(),
3548 traits: Vec::new(),
3549 decomposition: Some(decomposition),
3550 help: String::new(),
3551 },
3552 extends: TypeExtends::Primitive,
3553 }
3554 }
3555
3556 #[must_use]
3558 pub fn measure_unit_names(&self) -> Option<Vec<&str>> {
3559 match &self.specifications {
3560 TypeSpecification::Measure { units, .. } if !units.is_empty() => {
3561 Some(units.iter().map(|unit| unit.name.as_str()).collect())
3562 }
3563 TypeSpecification::MeasureRange { units, .. } if !units.is_empty() => {
3564 Some(units.iter().map(|unit| unit.name.as_str()).collect())
3565 }
3566 _ => None,
3567 }
3568 }
3569
3570 pub fn measure_unit_factor(
3572 &self,
3573 unit_name: &str,
3574 ) -> &crate::computation::rational::RationalInteger {
3575 let units = match &self.specifications {
3576 TypeSpecification::Measure { units, .. } => units,
3577 TypeSpecification::MeasureRange { units, .. } => units,
3578 _ => unreachable!(
3579 "BUG: measure_unit_factor called with non-measure type {}; only call during evaluation after planning validated measure conversion",
3580 self.name()
3581 ),
3582 };
3583 match units.get(unit_name) {
3584 Ok(MeasureUnit { factor, .. }) => factor,
3585 Err(_) => {
3586 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
3587 unreachable!(
3588 "BUG: unknown unit '{}' for measure type {} (valid: {}); planning must reject invalid conversions with Error",
3589 unit_name,
3590 self.name(),
3591 valid.join(", ")
3592 );
3593 }
3594 }
3595 }
3596
3597 pub fn ratio_unit_factor(
3598 &self,
3599 unit_name: &str,
3600 ) -> &crate::computation::rational::RationalInteger {
3601 let units = match &self.specifications {
3602 TypeSpecification::Ratio { units, .. } => units,
3603 _ => unreachable!(
3604 "BUG: ratio_unit_factor called with non-ratio type {}; only call during evaluation after planning validated ratio conversion",
3605 self.name()
3606 ),
3607 };
3608 match units.get(unit_name) {
3609 Ok(RatioUnit { value, .. }) => value,
3610 Err(_) => {
3611 let valid: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
3612 unreachable!(
3613 "BUG: unknown unit '{}' for ratio type {} (valid: {}); planning must reject invalid conversions with Error",
3614 unit_name,
3615 self.name(),
3616 valid.join(", ")
3617 );
3618 }
3619 }
3620 }
3621}
3622
3623#[derive(Clone, Debug, PartialEq, Eq, Hash)]
3625pub struct LiteralValue {
3626 pub value: ValueKind,
3627 pub lemma_type: Arc<LemmaType>,
3628}
3629
3630impl Serialize for LiteralValue {
3631 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
3632 where
3633 S: serde::Serializer,
3634 {
3635 use serde::ser::SerializeStruct;
3636 let mut state = serializer.serialize_struct("LiteralValue", 3)?;
3637 state.serialize_field("value", &self.value)?;
3638 state.serialize_field("lemma_type", self.lemma_type.as_ref())?;
3639 state.serialize_field("display_value", &self.display_value())?;
3640 state.end()
3641 }
3642}
3643
3644impl<'de> Deserialize<'de> for LiteralValue {
3645 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3646 where
3647 D: serde::Deserializer<'de>,
3648 {
3649 #[derive(Deserialize)]
3650 struct Raw {
3651 value: ValueKind,
3652 lemma_type: LemmaType,
3653 }
3654 let raw = Raw::deserialize(deserializer)?;
3655 Ok(Self {
3656 value: raw.value,
3657 lemma_type: Arc::new(raw.lemma_type),
3658 })
3659 }
3660}
3661
3662impl LiteralValue {
3663 pub fn text(s: String) -> Self {
3664 Self {
3665 value: ValueKind::Text(s),
3666 lemma_type: primitive_text_arc().clone(),
3667 }
3668 }
3669
3670 pub fn text_with_type(s: String, lemma_type: Arc<LemmaType>) -> Self {
3671 Self {
3672 value: ValueKind::Text(s),
3673 lemma_type,
3674 }
3675 }
3676
3677 pub fn number(n: RationalInteger) -> Self {
3678 Self {
3679 value: ValueKind::Number(n),
3680 lemma_type: primitive_number_arc().clone(),
3681 }
3682 }
3683
3684 pub fn number_from_decimal(decimal: Decimal) -> Self {
3685 Self::number(
3686 crate::literals::rational_from_parsed_decimal(decimal)
3687 .expect("BUG: literal number from decimal must lift at boundary"),
3688 )
3689 }
3690
3691 pub fn number_with_type(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
3692 Self {
3693 value: ValueKind::Number(n),
3694 lemma_type,
3695 }
3696 }
3697
3698 pub fn number_with_type_from_decimal(decimal: Decimal, lemma_type: Arc<LemmaType>) -> Self {
3699 Self::number_with_type(
3700 crate::literals::rational_from_parsed_decimal(decimal)
3701 .expect("BUG: literal number from decimal must lift at boundary"),
3702 lemma_type,
3703 )
3704 }
3705
3706 pub fn measure_with_type(n: RationalInteger, unit: String, lemma_type: Arc<LemmaType>) -> Self {
3710 Self {
3711 value: ValueKind::Measure(n, vec![(unit, 1)]),
3712 lemma_type,
3713 }
3714 }
3715
3716 pub fn measure_with_signature(
3719 n: RationalInteger,
3720 signature: Vec<(String, i32)>,
3721 lemma_type: Arc<LemmaType>,
3722 ) -> Self {
3723 Self {
3724 value: ValueKind::Measure(n, signature),
3725 lemma_type,
3726 }
3727 }
3728
3729 pub fn number_interpreted_as_measure(value: RationalInteger, unit_name: String) -> Self {
3732 Self {
3733 value: ValueKind::Measure(value, vec![(unit_name, 1)]),
3734 lemma_type: Arc::new(anonymous_measure_type()),
3735 }
3736 }
3737
3738 pub fn from_bool(b: bool) -> Self {
3739 Self {
3740 value: ValueKind::Boolean(b),
3741 lemma_type: primitive_boolean_arc().clone(),
3742 }
3743 }
3744
3745 pub fn from_datetime(dt: &crate::parsing::ast::DateTimeValue) -> Self {
3746 Self::date(date_time_to_semantic(dt))
3747 }
3748
3749 #[must_use]
3751 pub fn magnitude_default_for_decimal_prompt(&self) -> Option<String> {
3752 use crate::computation::rational::{checked_mul, rational_to_display_str};
3753 match &self.value {
3754 ValueKind::Number(n) => Some(rational_to_display_str(n)),
3755 ValueKind::Measure(n, signature) if signature.len() == 1 && signature[0].1 == 1 => {
3756 Some(rational_to_display_str(n))
3757 }
3758 ValueKind::Ratio(n, Some(unit)) if unit == "percent" => {
3759 checked_mul(n, &rational_new(100, 1))
3760 .ok()
3761 .map(|scaled| rational_to_display_str(&scaled))
3762 }
3763 ValueKind::Ratio(n, Some(unit)) if unit == "permille" => {
3764 checked_mul(n, &rational_new(1000, 1))
3765 .ok()
3766 .map(|scaled| rational_to_display_str(&scaled))
3767 }
3768 ValueKind::Ratio(n, _) => Some(rational_to_display_str(n)),
3769 _ => None,
3770 }
3771 }
3772
3773 pub fn date(dt: SemanticDateTime) -> Self {
3774 Self {
3775 value: ValueKind::Date(dt),
3776 lemma_type: primitive_date_arc().clone(),
3777 }
3778 }
3779
3780 pub fn date_with_type(dt: SemanticDateTime, lemma_type: Arc<LemmaType>) -> Self {
3781 Self {
3782 value: ValueKind::Date(dt),
3783 lemma_type,
3784 }
3785 }
3786
3787 pub fn time(t: SemanticTime) -> Self {
3788 Self {
3789 value: ValueKind::Time(t),
3790 lemma_type: primitive_time_arc().clone(),
3791 }
3792 }
3793
3794 pub fn time_with_type(t: SemanticTime, lemma_type: Arc<LemmaType>) -> Self {
3795 Self {
3796 value: ValueKind::Time(t),
3797 lemma_type,
3798 }
3799 }
3800
3801 pub fn calendar(
3802 value: RationalInteger,
3803 unit: SemanticCalendarUnit,
3804 lemma_type: Arc<LemmaType>,
3805 ) -> Self {
3806 Self::measure_with_type(value, unit.to_string(), lemma_type)
3807 }
3808
3809 pub fn calendar_from_decimal(
3810 value: Decimal,
3811 unit: SemanticCalendarUnit,
3812 lemma_type: Arc<LemmaType>,
3813 ) -> Self {
3814 Self::calendar(
3815 crate::literals::rational_from_parsed_decimal(value)
3816 .expect("BUG: calendar literal from decimal must lift at boundary"),
3817 unit,
3818 lemma_type,
3819 )
3820 }
3821
3822 pub fn calendar_with_type(
3823 value: RationalInteger,
3824 unit: SemanticCalendarUnit,
3825 lemma_type: Arc<LemmaType>,
3826 ) -> Self {
3827 Self::calendar(value, unit, lemma_type)
3828 }
3829
3830 pub fn duration_canonical_seconds(&self) -> RationalInteger {
3832 let ValueKind::Measure(magnitude, _) = &self.value else {
3833 unreachable!(
3834 "BUG: duration_canonical_seconds called with {:?}",
3835 self.value
3836 );
3837 };
3838 if !self.lemma_type.is_duration_like_measure() {
3839 unreachable!(
3840 "BUG: duration_canonical_seconds called with type {}",
3841 self.lemma_type.name()
3842 );
3843 }
3844 let factor = self.lemma_type.measure_unit_factor("second");
3845 checked_div(magnitude, factor).expect("BUG: duration unit factor cannot be zero")
3846 }
3847
3848 pub fn calendar_canonical_months(&self) -> RationalInteger {
3850 let ValueKind::Measure(magnitude, _) = &self.value else {
3851 unreachable!(
3852 "BUG: calendar_canonical_months called with {:?}",
3853 self.value
3854 );
3855 };
3856 if !self.lemma_type.is_calendar_like() {
3857 unreachable!(
3858 "BUG: calendar_canonical_months called with type {}",
3859 self.lemma_type.name()
3860 );
3861 }
3862 let factor = self.lemma_type.measure_unit_factor("month");
3863 checked_div(magnitude, factor).expect("BUG: calendar unit factor cannot be zero")
3864 }
3865
3866 pub fn ratio(r: RationalInteger, unit: Option<String>) -> Self {
3867 Self {
3868 value: ValueKind::Ratio(r, unit),
3869 lemma_type: primitive_ratio_arc().clone(),
3870 }
3871 }
3872
3873 pub fn ratio_from_decimal(r: Decimal, unit: Option<String>) -> Self {
3874 Self::ratio(
3875 crate::literals::rational_from_parsed_decimal(r)
3876 .expect("BUG: ratio literal from decimal must lift at boundary"),
3877 unit,
3878 )
3879 }
3880
3881 pub fn ratio_with_type(
3882 r: RationalInteger,
3883 unit: Option<String>,
3884 lemma_type: Arc<LemmaType>,
3885 ) -> Self {
3886 Self {
3887 value: ValueKind::Ratio(r, unit),
3888 lemma_type,
3889 }
3890 }
3891
3892 pub fn range(left: LiteralValue, right: LiteralValue) -> Self {
3893 let specifications =
3894 range_type_specification_from_endpoints(&left.lemma_type, &right.lemma_type)
3895 .unwrap_or_else(|| {
3896 unreachable!(
3897 "BUG: attempted to construct a range literal from incompatible endpoint types"
3898 )
3899 });
3900
3901 Self {
3902 value: ValueKind::Range(Box::new(left), Box::new(right)),
3903 lemma_type: Arc::new(LemmaType::primitive(specifications)),
3904 }
3905 }
3906
3907 pub fn display_value(&self) -> String {
3909 format!("{}", self)
3910 }
3911
3912 pub fn byte_size(&self) -> usize {
3914 format!("{}", self).len()
3915 }
3916
3917 pub fn get_type(&self) -> &LemmaType {
3919 &self.lemma_type
3920 }
3921}
3922
3923#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3925#[serde(rename_all = "snake_case")]
3926pub enum DataValue {
3927 Definition {
3928 schema_type: LemmaType,
3929 #[serde(default, skip_serializing_if = "Option::is_none")]
3930 bound_value: Option<LiteralValue>,
3931 },
3932}
3933
3934impl DataValue {
3935 #[must_use]
3936 pub fn from_bound_literal(value: LiteralValue) -> Self {
3937 let schema_type = value.get_type().clone();
3938 Self::Definition {
3939 schema_type,
3940 bound_value: Some(value),
3941 }
3942 }
3943}
3944
3945#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3947pub struct Data {
3948 pub path: DataPath,
3949 pub value: DataValue,
3950 pub source: Option<Source>,
3951}
3952
3953#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3956#[serde(rename_all = "snake_case", tag = "kind")]
3957pub enum ReferenceTarget {
3958 Data(DataPath),
3959 Rule(RulePath),
3960}
3961
3962#[derive(Clone, Debug, Serialize, Deserialize)]
3964#[serde(rename_all = "snake_case")]
3965pub enum DataDefinition {
3966 Value { value: LiteralValue, source: Source },
3968 TypeDeclaration {
3973 resolved_type: Arc<LemmaType>,
3974 declared_default: Option<ValueKind>,
3975 source: Source,
3976 },
3977 Import {
3979 spec: Arc<crate::parsing::ast::LemmaSpec>,
3980 source: Source,
3981 },
3982 Reference {
4006 target: ReferenceTarget,
4007 resolved_type: Arc<LemmaType>,
4008 local_constraints: Option<Vec<Constraint>>,
4009 local_default: Option<ValueKind>,
4010 source: Source,
4011 },
4012}
4013
4014impl DataDefinition {
4015 pub fn schema_type(&self) -> Option<&LemmaType> {
4017 match self {
4018 DataDefinition::Value { value, .. } => Some(value.lemma_type.as_ref()),
4019 DataDefinition::TypeDeclaration { resolved_type, .. } => Some(resolved_type.as_ref()),
4020 DataDefinition::Reference { resolved_type, .. } => Some(resolved_type.as_ref()),
4021 DataDefinition::Import { .. } => None,
4022 }
4023 }
4024
4025 pub fn value(&self) -> Option<&LiteralValue> {
4029 match self {
4030 DataDefinition::Value { value, .. } => Some(value),
4031 DataDefinition::TypeDeclaration { .. }
4032 | DataDefinition::Import { .. }
4033 | DataDefinition::Reference { .. } => None,
4034 }
4035 }
4036
4037 #[inline]
4041 pub fn bound_value(&self) -> Option<&LiteralValue> {
4042 self.value()
4043 }
4044
4045 pub fn default_suggestion(&self) -> Option<LiteralValue> {
4049 match self {
4050 DataDefinition::TypeDeclaration {
4051 resolved_type,
4052 declared_default: Some(dv),
4053 ..
4054 } => Some(LiteralValue {
4055 value: dv.clone(),
4056 lemma_type: Arc::clone(resolved_type),
4057 }),
4058 DataDefinition::Reference {
4059 resolved_type,
4060 local_default: Some(dv),
4061 ..
4062 } => Some(LiteralValue {
4063 value: dv.clone(),
4064 lemma_type: Arc::clone(resolved_type),
4065 }),
4066 DataDefinition::Value { .. }
4067 | DataDefinition::TypeDeclaration {
4068 declared_default: None,
4069 ..
4070 }
4071 | DataDefinition::Reference {
4072 local_default: None,
4073 ..
4074 }
4075 | DataDefinition::Import { .. } => None,
4076 }
4077 }
4078
4079 pub fn source(&self) -> &Source {
4081 match self {
4082 DataDefinition::Value { source, .. } => source,
4083 DataDefinition::TypeDeclaration { source, .. } => source,
4084 DataDefinition::Import { source, .. } => source,
4085 DataDefinition::Reference { source, .. } => source,
4086 }
4087 }
4088
4089 pub fn reference_target(&self) -> Option<&ReferenceTarget> {
4092 match self {
4093 DataDefinition::Reference { target, .. } => Some(target),
4094 _ => None,
4095 }
4096 }
4097}
4098
4099pub fn number_with_unit_to_value_kind(
4101 magnitude: rust_decimal::Decimal,
4102 unit_name: &str,
4103 lemma_type: &LemmaType,
4104) -> Result<ValueKind, String> {
4105 match &lemma_type.specifications {
4106 TypeSpecification::Ratio { units, .. } => {
4107 use crate::computation::rational::{checked_div, decimal_to_rational};
4108 let unit = units.get(unit_name)?;
4109 let magnitude_rational = decimal_to_rational(magnitude)
4110 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
4111 let canonical_rational = checked_div(&magnitude_rational, &unit.value)
4112 .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
4113 Ok(ValueKind::Ratio(
4114 canonical_rational,
4115 Some(unit.name.clone()),
4116 ))
4117 }
4118 TypeSpecification::Measure { units, .. } => {
4119 use crate::computation::rational::checked_mul;
4120 let rational = lift_parser_decimal(magnitude)?;
4121 let unit = units.get(unit_name)?;
4122 let canonical = checked_mul(&rational, &unit.factor)
4123 .map_err(|failure| format!("measure canonicalization overflow: {failure}"))?;
4124 Ok(ValueKind::Measure(
4125 canonical,
4126 vec![(unit_name.to_string(), 1)],
4127 ))
4128 }
4129 _ => Err(format!(
4130 "Unit '{}' is defined on type '{}' which is not measure or ratio",
4131 unit_name,
4132 lemma_type.name()
4133 )),
4134 }
4135}
4136
4137pub(crate) fn value_kind_matches_spec(value: &ValueKind, type_spec: &TypeSpecification) -> bool {
4140 matches!(
4141 (type_spec, value),
4142 (TypeSpecification::Number { .. }, ValueKind::Number(_))
4143 | (TypeSpecification::Text { .. }, ValueKind::Text(_))
4144 | (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
4145 | (TypeSpecification::Date { .. }, ValueKind::Date(_))
4146 | (TypeSpecification::Time { .. }, ValueKind::Time(_))
4147 | (TypeSpecification::Measure { .. }, ValueKind::Measure(_, _))
4148 | (TypeSpecification::Ratio { .. }, ValueKind::Ratio(_, _))
4149 | (TypeSpecification::Ratio { .. }, ValueKind::Number(_))
4150 | (
4151 TypeSpecification::NumberRange { .. },
4152 ValueKind::Range(_, _)
4153 )
4154 | (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
4155 | (TypeSpecification::TimeRange { .. }, ValueKind::Range(_, _))
4156 | (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
4157 | (
4158 TypeSpecification::MeasureRange { .. },
4159 ValueKind::Range(_, _)
4160 )
4161 | (TypeSpecification::Veto { .. }, _)
4162 | (TypeSpecification::Undetermined, _)
4163 )
4164}
4165
4166fn value_kind_tag_for_type(spec: &TypeSpecification) -> &'static str {
4167 match spec {
4168 TypeSpecification::Boolean { .. } => "boolean",
4169 TypeSpecification::Measure { .. } => "measure",
4170 TypeSpecification::Number { .. } => "number",
4171 TypeSpecification::NumberRange { .. }
4172 | TypeSpecification::MeasureRange { .. }
4173 | TypeSpecification::DateRange { .. }
4174 | TypeSpecification::TimeRange { .. }
4175 | TypeSpecification::RatioRange { .. } => "range",
4176 TypeSpecification::Ratio { .. } => "ratio",
4177 TypeSpecification::Text { .. } => "text",
4178 TypeSpecification::Date { .. } => "date",
4179 TypeSpecification::Time { .. } => "time",
4180 TypeSpecification::Veto { .. } => "veto",
4181 TypeSpecification::Undetermined => "undetermined",
4182 }
4183}
4184
4185fn parser_value_type_mismatch(
4186 value: &crate::literals::Value,
4187 type_spec: &TypeSpecification,
4188) -> String {
4189 use crate::parsing::ast::AsLemmaSource;
4190 let value_str = format!("{}", AsLemmaSource(value));
4191 let expected = value_kind_tag_for_type(type_spec);
4192 match type_spec {
4193 TypeSpecification::Measure { units, .. } => {
4194 let unit_hint = units
4195 .iter()
4196 .find(|u| u.factor == crate::computation::rational::rational_one())
4197 .map(|u| u.name.as_str())
4198 .or_else(|| units.iter().next().map(|u| u.name.as_str()))
4199 .unwrap_or("unit");
4200 format!("cannot use {value_str} as {expected}: expected `<n> {unit_hint}`")
4201 }
4202 TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
4203 let unit_hint = units
4204 .iter()
4205 .next()
4206 .map(|u| u.name.as_str())
4207 .unwrap_or("unit");
4208 format!(
4209 "cannot use {value_str} as {expected}: expected `<n> {unit_hint}` or bare ratio"
4210 )
4211 }
4212 _ => format!("cannot use {value_str} as {expected}"),
4213 }
4214}
4215
4216pub fn refresh_measure_literal_canonical_magnitude(
4221 lit: &mut LiteralValue,
4222 resolved_type: &LemmaType,
4223) {
4224 let ValueKind::Measure(magnitude, signature) = &mut lit.value else {
4225 return;
4226 };
4227 let (unit_name, exponent) = signature
4228 .first()
4229 .expect("BUG: measure literal has empty signature during canonical magnitude refresh");
4230 if *exponent != 1 || signature.len() != 1 {
4231 return;
4232 }
4233 let stored_factor = lit.lemma_type.measure_unit_factor(unit_name);
4234 let resolved_factor = resolved_type.measure_unit_factor(unit_name);
4235 if stored_factor == resolved_factor {
4236 lit.lemma_type = Arc::new(resolved_type.clone());
4237 return;
4238 }
4239 let scaled = checked_mul(magnitude, resolved_factor)
4240 .expect("BUG: measure recanonicalization multiply overflow");
4241 *magnitude =
4242 checked_div(&scaled, stored_factor).expect("BUG: measure recanonicalization divide failed");
4243 lit.lemma_type = Arc::new(resolved_type.clone());
4244}
4245
4246pub fn parser_value_to_value_kind(
4248 value: &crate::literals::Value,
4249 type_spec: &TypeSpecification,
4250) -> Result<ValueKind, String> {
4251 use crate::computation::rational::decimal_to_rational;
4252 use crate::literals::Value;
4253 match (value, type_spec) {
4254 (Value::NumberWithUnit(magnitude, unit_name), TypeSpecification::Ratio { units, .. }) => {
4255 use crate::computation::rational::checked_div;
4256 let unit = units.get(unit_name.as_str())?;
4257 let magnitude_rational = decimal_to_rational(*magnitude)
4258 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
4259 let canonical_rational = checked_div(&magnitude_rational, &unit.value)
4260 .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
4261 Ok(ValueKind::Ratio(
4262 canonical_rational,
4263 Some(unit.name.clone()),
4264 ))
4265 }
4266 (Value::NumberWithUnit(magnitude, unit_name), TypeSpecification::Measure { units, .. }) => {
4267 use crate::computation::rational::checked_mul;
4268 let rational = lift_parser_decimal(*magnitude)?;
4269 let unit = units.get(unit_name.as_str())?;
4270 let canonical = checked_mul(&rational, &unit.factor)
4271 .map_err(|failure| format!("measure canonicalization overflow: {failure}"))?;
4272 Ok(ValueKind::Measure(canonical, vec![(unit_name.clone(), 1)]))
4273 }
4274 (Value::NumberWithUnit(_, _), _) => {
4275 Err("number_with_unit literal requires a measure or ratio type".to_string())
4276 }
4277 (Value::Number(n), TypeSpecification::Number { .. }) => {
4278 Ok(ValueKind::Number(lift_parser_decimal(*n)?))
4279 }
4280 (Value::Number(n), TypeSpecification::Ratio { .. }) => {
4281 let r = decimal_to_rational(*n)
4282 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
4283 Ok(ValueKind::Ratio(r, None))
4284 }
4285 (Value::Text(s), TypeSpecification::Text { .. }) => Ok(ValueKind::Text(s.clone())),
4286 (Value::Boolean(b), TypeSpecification::Boolean { .. }) => Ok(ValueKind::Boolean(b.into())),
4287 (Value::Date(dt), TypeSpecification::Date { .. }) => {
4288 Ok(ValueKind::Date(date_time_to_semantic(dt)))
4289 }
4290 (Value::Time(t), TypeSpecification::Time { .. }) => {
4291 Ok(ValueKind::Time(time_to_semantic(t)))
4292 }
4293 (
4294 Value::Range(left, right),
4295 range_spec @ (TypeSpecification::NumberRange { .. }
4296 | TypeSpecification::DateRange { .. }
4297 | TypeSpecification::TimeRange { .. }
4298 | TypeSpecification::RatioRange { .. }
4299 | TypeSpecification::MeasureRange { .. }),
4300 ) => {
4301 let endpoint = range_element_type_specification(range_spec).ok_or_else(|| {
4302 "BUG: range_element_type_specification missing arm for range type".to_string()
4303 })?;
4304 let left_lit = lift_range_endpoint(left, &endpoint)?;
4305 let right_lit = lift_range_endpoint(right, &endpoint)?;
4306 Ok(ValueKind::Range(Box::new(left_lit), Box::new(right_lit)))
4307 }
4308 (value, type_spec) => Err(parser_value_type_mismatch(value, type_spec)),
4309 }
4310}
4311
4312pub fn value_to_semantic(value: &crate::parsing::ast::Value) -> Result<ValueKind, String> {
4316 use crate::parsing::ast::Value;
4317 Ok(match value {
4318 Value::Number(n) => ValueKind::Number(lift_parser_decimal(*n)?),
4319 Value::Text(s) => ValueKind::Text(s.clone()),
4320 Value::Boolean(b) => ValueKind::Boolean(bool::from(*b)),
4321 Value::Date(dt) => ValueKind::Date(date_time_to_semantic(dt)),
4322 Value::Time(t) => ValueKind::Time(time_to_semantic(t)),
4323 Value::NumberWithUnit(_, _) => {
4324 return Err(
4325 "number_with_unit literal requires type context (measure or ratio)".to_string(),
4326 );
4327 }
4328 Value::Range(_, _) => literal_value_from_parser_value(value)?.value,
4329 })
4330}
4331
4332pub(crate) fn date_time_to_semantic(dt: &crate::parsing::ast::DateTimeValue) -> SemanticDateTime {
4334 SemanticDateTime {
4335 year: dt.year,
4336 month: dt.month,
4337 day: dt.day,
4338 hour: dt.hour,
4339 minute: dt.minute,
4340 second: dt.second,
4341 microsecond: dt.microsecond,
4342 timezone: dt.timezone.as_ref().map(|tz| SemanticTimezone {
4343 offset_hours: tz.offset_hours,
4344 offset_minutes: tz.offset_minutes,
4345 }),
4346 }
4347}
4348
4349pub(crate) fn time_to_semantic(t: &crate::parsing::ast::TimeValue) -> SemanticTime {
4351 SemanticTime {
4352 hour: t.hour.into(),
4353 minute: t.minute.into(),
4354 second: t.second.into(),
4355 microsecond: t.microsecond,
4356 timezone: t.timezone.as_ref().map(|tz| SemanticTimezone {
4357 offset_hours: tz.offset_hours,
4358 offset_minutes: tz.offset_minutes,
4359 }),
4360 }
4361}
4362
4363pub(crate) fn compare_semantic_dates(
4367 left: &SemanticDateTime,
4368 right: &SemanticDateTime,
4369) -> std::cmp::Ordering {
4370 left.year
4371 .cmp(&right.year)
4372 .then_with(|| left.month.cmp(&right.month))
4373 .then_with(|| left.day.cmp(&right.day))
4374 .then_with(|| left.hour.cmp(&right.hour))
4375 .then_with(|| left.minute.cmp(&right.minute))
4376 .then_with(|| left.second.cmp(&right.second))
4377 .then_with(|| left.microsecond.cmp(&right.microsecond))
4378}
4379
4380pub(crate) fn compare_semantic_times(
4383 left: &SemanticTime,
4384 right: &SemanticTime,
4385) -> std::cmp::Ordering {
4386 left.hour
4387 .cmp(&right.hour)
4388 .then_with(|| left.minute.cmp(&right.minute))
4389 .then_with(|| left.second.cmp(&right.second))
4390 .then_with(|| left.microsecond.cmp(&right.microsecond))
4391}
4392
4393pub fn conversion_target_to_semantic(
4395 ct: &ConversionTarget,
4396 unit_index: Option<&HashMap<String, Arc<LemmaType>>>,
4397) -> Result<SemanticConversionTarget, String> {
4398 match ct {
4399 ConversionTarget::Type(kind) => Ok(SemanticConversionTarget::Type(*kind)),
4400 ConversionTarget::Unit { unit_name } => {
4401 let unit_name = crate::parsing::ast::ascii_lowercase_logical_name(unit_name.clone());
4402 let index = unit_index.ok_or_else(|| format!("Unknown unit '{unit_name}'."))?;
4403 let owning_type = index
4404 .get(&unit_name)
4405 .ok_or_else(|| format!("Unknown unit '{unit_name}'."))?
4406 .clone();
4407 Ok(SemanticConversionTarget::Unit {
4408 unit_name,
4409 owning_type,
4410 })
4411 }
4412 }
4413}
4414
4415static PRIMITIVE_BOOLEAN: OnceLock<Arc<LemmaType>> = OnceLock::new();
4421static PRIMITIVE_NUMBER: OnceLock<Arc<LemmaType>> = OnceLock::new();
4422static PRIMITIVE_TEXT: OnceLock<Arc<LemmaType>> = OnceLock::new();
4423static PRIMITIVE_DATE: OnceLock<Arc<LemmaType>> = OnceLock::new();
4424static PRIMITIVE_DATE_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
4425static PRIMITIVE_TIME: OnceLock<Arc<LemmaType>> = OnceLock::new();
4426static PRIMITIVE_RATIO: OnceLock<Arc<LemmaType>> = OnceLock::new();
4427
4428#[must_use]
4429pub fn primitive_boolean_arc() -> &'static Arc<LemmaType> {
4430 PRIMITIVE_BOOLEAN.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::boolean())))
4431}
4432
4433#[must_use]
4434pub fn primitive_number_arc() -> &'static Arc<LemmaType> {
4435 PRIMITIVE_NUMBER.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::number())))
4436}
4437
4438#[must_use]
4439pub fn primitive_text_arc() -> &'static Arc<LemmaType> {
4440 PRIMITIVE_TEXT.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::text())))
4441}
4442
4443#[must_use]
4444pub fn primitive_date_arc() -> &'static Arc<LemmaType> {
4445 PRIMITIVE_DATE.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::date())))
4446}
4447
4448#[must_use]
4449pub fn primitive_date_range_arc() -> &'static Arc<LemmaType> {
4450 PRIMITIVE_DATE_RANGE
4451 .get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::date_range())))
4452}
4453
4454#[must_use]
4455pub fn primitive_time_arc() -> &'static Arc<LemmaType> {
4456 PRIMITIVE_TIME.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::time())))
4457}
4458
4459#[must_use]
4460pub fn primitive_ratio_arc() -> &'static Arc<LemmaType> {
4461 PRIMITIVE_RATIO.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::ratio())))
4462}
4463
4464#[cfg(test)]
4466static PRIMITIVE_MEASURE: OnceLock<Arc<LemmaType>> = OnceLock::new();
4467
4468#[cfg(test)]
4469#[must_use]
4470pub fn primitive_boolean() -> &'static LemmaType {
4471 primitive_boolean_arc().as_ref()
4472}
4473
4474#[cfg(test)]
4475#[must_use]
4476pub fn primitive_measure() -> &'static LemmaType {
4477 primitive_measure_arc().as_ref()
4478}
4479
4480#[cfg(test)]
4481#[must_use]
4482pub fn primitive_measure_arc() -> &'static Arc<LemmaType> {
4483 PRIMITIVE_MEASURE.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::measure())))
4484}
4485
4486#[cfg(test)]
4487#[must_use]
4488pub fn primitive_number() -> &'static LemmaType {
4489 primitive_number_arc().as_ref()
4490}
4491
4492#[cfg(test)]
4493#[must_use]
4494pub fn primitive_text() -> &'static LemmaType {
4495 primitive_text_arc().as_ref()
4496}
4497
4498#[cfg(test)]
4499#[must_use]
4500pub fn primitive_date() -> &'static LemmaType {
4501 primitive_date_arc().as_ref()
4502}
4503
4504#[cfg(test)]
4505#[must_use]
4506pub fn primitive_time() -> &'static LemmaType {
4507 primitive_time_arc().as_ref()
4508}
4509
4510#[cfg(test)]
4511#[must_use]
4512pub fn primitive_ratio() -> &'static LemmaType {
4513 primitive_ratio_arc().as_ref()
4514}
4515
4516#[must_use]
4518pub fn type_spec_for_primitive(kind: PrimitiveKind) -> TypeSpecification {
4519 match kind {
4520 PrimitiveKind::Boolean => TypeSpecification::boolean(),
4521 PrimitiveKind::Measure => TypeSpecification::measure(),
4522 PrimitiveKind::MeasureRange => TypeSpecification::measure_range(),
4523 PrimitiveKind::Number => TypeSpecification::number(),
4524 PrimitiveKind::NumberRange => TypeSpecification::number_range(),
4525 PrimitiveKind::Ratio => TypeSpecification::ratio(),
4526 PrimitiveKind::RatioRange => TypeSpecification::ratio_range(),
4527 PrimitiveKind::Text => TypeSpecification::text(),
4528 PrimitiveKind::Date => TypeSpecification::date(),
4529 PrimitiveKind::DateRange => TypeSpecification::date_range(),
4530 PrimitiveKind::Time => TypeSpecification::time(),
4531 PrimitiveKind::TimeRange => TypeSpecification::time_range(),
4532 }
4533}
4534
4535impl fmt::Display for PathSegment {
4540 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4541 write!(f, "{} → {}", self.data, self.spec)
4542 }
4543}
4544
4545impl fmt::Display for DataPath {
4546 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4547 for segment in &self.segments {
4548 write!(f, "{}.", segment)?;
4549 }
4550 write!(f, "{}", self.data)
4551 }
4552}
4553
4554impl fmt::Display for RulePath {
4555 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4556 for segment in &self.segments {
4557 write!(f, "{}.", segment)?;
4558 }
4559 write!(f, "{}", self.rule)
4560 }
4561}
4562
4563impl fmt::Display for LemmaType {
4564 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4565 write!(f, "{}", self.name())
4566 }
4567}
4568
4569fn decimal_places_in_display_value(decimal: &rust_decimal::Decimal) -> u32 {
4570 if decimal.is_integer() {
4571 return 0;
4572 }
4573 decimal.fract().normalize().scale()
4574}
4575
4576fn format_committed_decimal_for_api(
4577 decimal: rust_decimal::Decimal,
4578 decimal_places: Option<u8>,
4579) -> String {
4580 match decimal_places {
4581 Some(decimal_places) => {
4582 let rounded = decimal.round_dp(u32::from(decimal_places));
4583 format!("{:.prec$}", rounded, prec = decimal_places as usize)
4584 }
4585 None => {
4586 let normalized = decimal.normalize();
4587 if normalized.fract().is_zero() {
4588 normalized.trunc().to_string()
4589 } else {
4590 normalized.to_string()
4591 }
4592 }
4593 }
4594}
4595
4596fn format_committed_decimal_for_human_display(
4597 decimal: rust_decimal::Decimal,
4598 decimal_places: Option<u8>,
4599) -> String {
4600 match decimal_places {
4601 Some(decimal_places) => {
4602 let rounded = decimal.round_dp(u32::from(decimal_places));
4603 format!("{:.prec$}", rounded, prec = decimal_places as usize)
4604 }
4605 None => decimal.normalize().to_string(),
4606 }
4607}
4608
4609fn format_rational_for_human_display(
4610 magnitude: &crate::computation::rational::RationalInteger,
4611 decimal_places: Option<u8>,
4612) -> String {
4613 use crate::computation::rational::{commit_rational_to_decimal, rational_to_display_str};
4614 match commit_rational_to_decimal(magnitude) {
4615 Ok(decimal) => format_committed_decimal_for_human_display(decimal, decimal_places),
4616 Err(_) => rational_to_display_str(magnitude),
4617 }
4618}
4619
4620fn format_measure_canonical_for_display(
4621 canonical: &crate::computation::rational::RationalInteger,
4622 lemma_type: &LemmaType,
4623 signature: &[(String, i32)],
4624) -> String {
4625 use crate::computation::rational::{checked_div, commit_rational_to_decimal};
4626 use rust_decimal::Decimal;
4627
4628 let decimals = lemma_type.decimal_places();
4629
4630 if let TypeSpecification::Measure { units, .. } = &lemma_type.specifications {
4631 if !units.is_empty() {
4632 if let [(sig_unit, 1)] = signature {
4633 if let Some(unit) = units.iter().find(|u| u.name == *sig_unit) {
4634 let in_unit = checked_div(canonical, &unit.factor)
4635 .expect("BUG: de-canonicalization for measure display must not fail");
4636 let formatted = format_rational_for_human_display(&in_unit, decimals);
4637 return format!("{} {}", formatted, unit.name);
4638 }
4639 }
4640
4641 struct UnitDisplayCandidate {
4642 unit_name: String,
4643 decimal_places: u32,
4644 under_1000: bool,
4645 abs_magnitude: Decimal,
4646 formatted: String,
4647 }
4648
4649 let mut candidates: Vec<UnitDisplayCandidate> = Vec::with_capacity(units.len());
4650 for unit in units.iter() {
4651 let in_unit = checked_div(canonical, &unit.factor)
4652 .expect("BUG: de-canonicalization for measure display must not fail");
4653 let formatted = format_rational_for_human_display(&in_unit, decimals);
4654 let abs_magnitude = match commit_rational_to_decimal(&in_unit) {
4655 Ok(decimal) => decimal.abs(),
4656 Err(_) => Decimal::MAX,
4657 };
4658 let decimal_places = match commit_rational_to_decimal(&in_unit) {
4659 Ok(decimal) => decimal_places_in_display_value(&decimal),
4660 Err(_) => u32::MAX,
4661 };
4662 let under_1000 = abs_magnitude < Decimal::from(1000);
4663 candidates.push(UnitDisplayCandidate {
4664 unit_name: unit.name.clone(),
4665 decimal_places,
4666 under_1000,
4667 abs_magnitude,
4668 formatted,
4669 });
4670 }
4671
4672 let pool: Vec<&UnitDisplayCandidate> = {
4673 let under: Vec<_> = candidates.iter().filter(|c| c.under_1000).collect();
4674 if under.is_empty() {
4675 candidates.iter().collect()
4676 } else {
4677 under
4678 }
4679 };
4680 let best = pool
4681 .iter()
4682 .min_by(|left, right| {
4683 left.decimal_places
4684 .cmp(&right.decimal_places)
4685 .then_with(|| left.abs_magnitude.cmp(&right.abs_magnitude))
4686 })
4687 .expect("BUG: measure type must have at least one declared unit");
4688 return format!("{} {}", best.formatted, best.unit_name);
4689 }
4690 }
4691
4692 let unit_label = match signature {
4693 [] => String::new(),
4694 [(name, 1)] => name.clone(),
4695 _ => format_signature_operator_style(signature),
4696 };
4697 let formatted = format_rational_for_human_display(canonical, decimals);
4698 if unit_label.is_empty() {
4699 formatted
4700 } else {
4701 format!("{formatted} {unit_label}")
4702 }
4703}
4704
4705impl fmt::Display for LiteralValue {
4706 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4707 match &self.value {
4708 ValueKind::Measure(n, signature) => {
4709 write!(
4710 f,
4711 "{}",
4712 format_measure_canonical_for_display(n, &self.lemma_type, signature)
4713 )
4714 }
4715 ValueKind::Ratio(_, Some(_unit_name)) => write!(f, "{}", self.value),
4716 ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
4717 _ => write!(f, "{}", self.value),
4718 }
4719 }
4720}
4721
4722#[cfg(test)]
4727mod tests {
4728 use super::*;
4729 use crate::computation::rational::decimal_to_rational;
4730 use crate::literals::DateGranularity;
4731 use crate::literals::Value;
4732 use crate::parsing::ast::{BooleanValue, DateTimeValue, LemmaSpec, PrimitiveKind, TimeValue};
4733 use rust_decimal::Decimal;
4734 use std::str::FromStr;
4735 use std::sync::Arc;
4736
4737 #[test]
4738 fn default_primitive_help_is_goal_oriented() {
4739 let kinds = [
4740 PrimitiveKind::Boolean,
4741 PrimitiveKind::Measure,
4742 PrimitiveKind::MeasureRange,
4743 PrimitiveKind::Number,
4744 PrimitiveKind::NumberRange,
4745 PrimitiveKind::Ratio,
4746 PrimitiveKind::RatioRange,
4747 PrimitiveKind::Text,
4748 PrimitiveKind::Date,
4749 PrimitiveKind::DateRange,
4750 PrimitiveKind::Time,
4751 PrimitiveKind::TimeRange,
4752 ];
4753 for kind in kinds {
4754 let spec = type_spec_for_primitive(kind);
4755 let help = match &spec {
4756 TypeSpecification::Boolean { help, .. }
4757 | TypeSpecification::Number { help, .. }
4758 | TypeSpecification::NumberRange { help }
4759 | TypeSpecification::Text { help, .. }
4760 | TypeSpecification::Measure { help, .. }
4761 | TypeSpecification::MeasureRange { help, .. }
4762 | TypeSpecification::Ratio { help, .. }
4763 | TypeSpecification::RatioRange { help, .. }
4764 | TypeSpecification::Date { help, .. }
4765 | TypeSpecification::DateRange { help }
4766 | TypeSpecification::TimeRange { help }
4767 | TypeSpecification::Time { help, .. } => help,
4768 TypeSpecification::Veto { .. } | TypeSpecification::Undetermined => {
4769 unreachable!(
4770 "BUG: primitive kind {:?} mapped to non-primitive spec",
4771 kind
4772 )
4773 }
4774 };
4775 assert!(!help.is_empty(), "help for {:?}", kind);
4776 assert!(
4777 !help.to_ascii_lowercase().contains("format:"),
4778 "help for {:?} must not describe syntax: {:?}",
4779 kind,
4780 help
4781 );
4782 assert_eq!(help, default_help_for_primitive(kind));
4783 }
4784 }
4785
4786 #[test]
4787 fn test_negated_comparison() {
4788 assert_eq!(
4789 negated_comparison(ComparisonComputation::LessThan),
4790 ComparisonComputation::GreaterThanOrEqual
4791 );
4792 assert_eq!(
4793 negated_comparison(ComparisonComputation::GreaterThanOrEqual),
4794 ComparisonComputation::LessThan
4795 );
4796 assert_eq!(
4797 negated_comparison(ComparisonComputation::Is),
4798 ComparisonComputation::IsNot
4799 );
4800 assert_eq!(
4801 negated_comparison(ComparisonComputation::IsNot),
4802 ComparisonComputation::Is
4803 );
4804 }
4805
4806 #[test]
4807 fn value_to_semantic_number_is_decimal() {
4808 let kind = value_to_semantic(&Value::Number(Decimal::from(42))).unwrap();
4809 assert!(matches!(kind, ValueKind::Number(d) if d == rational_new(42, 1)));
4810 }
4811
4812 #[test]
4813 fn value_kind_measure_serializes_with_signature() {
4814 let kind = ValueKind::Measure(
4815 decimal_to_rational(Decimal::from_str("99.50").unwrap()).unwrap(),
4816 vec![("eur".to_string(), 1)],
4817 );
4818 let json = serde_json::to_value(&kind).unwrap();
4819 assert_eq!(json["measure"]["value"], "99.5");
4820 assert_eq!(json["measure"]["signature"][0][0], "eur");
4821 assert_eq!(json["measure"]["signature"][0][1], 1);
4822 }
4823
4824 #[test]
4825 fn value_kind_measure_compound_signature_roundtrips() {
4826 let original = ValueKind::Measure(
4827 decimal_to_rational(Decimal::from_str("4800").unwrap()).unwrap(),
4828 vec![
4829 ("eur".to_string(), 1),
4830 ("hour".to_string(), 1),
4831 ("minute".to_string(), -1),
4832 ],
4833 );
4834 let json = serde_json::to_string(&original).unwrap();
4835 let parsed: ValueKind = serde_json::from_str(&json).unwrap();
4836 assert_eq!(original, parsed);
4837 }
4838
4839 #[test]
4840 fn value_kind_measure_empty_signature_roundtrips() {
4841 let original = ValueKind::Measure(
4842 decimal_to_rational(Decimal::from_str("12.5").unwrap()).unwrap(),
4843 Vec::new(),
4844 );
4845 let json = serde_json::to_string(&original).unwrap();
4846 let parsed: ValueKind = serde_json::from_str(&json).unwrap();
4847 assert_eq!(original, parsed);
4848 }
4849
4850 #[test]
4851 fn literal_value_number_serde_not_rational_array() {
4852 let lit = LiteralValue::number_from_decimal(Decimal::from(20));
4853 let json = serde_json::to_value(&lit).unwrap();
4854 let number = json
4855 .get("value")
4856 .and_then(|v| v.get("number"))
4857 .expect("number field");
4858 assert!(number.is_string());
4859 assert_eq!(number.as_str(), Some("20"));
4860 assert!(
4861 !number.is_array(),
4862 "stored number must not serialize as [n,d]"
4863 );
4864 }
4865
4866 #[test]
4867 fn test_literal_value_to_primitive_type() {
4868 let one = rational_new(1, 1);
4869
4870 assert_eq!(LiteralValue::text("".to_string()).lemma_type.name(), "text");
4871 assert_eq!(
4872 LiteralValue::number(one.clone()).lemma_type.name(),
4873 "number"
4874 );
4875 assert_eq!(
4876 LiteralValue::from_bool(bool::from(BooleanValue::True))
4877 .lemma_type
4878 .name(),
4879 "boolean"
4880 );
4881
4882 let dt = DateTimeValue {
4883 year: 2024,
4884 month: 1,
4885 day: 1,
4886 hour: 0,
4887 minute: 0,
4888 second: 0,
4889 microsecond: 0,
4890 timezone: None,
4891
4892 granularity: DateGranularity::Full,
4893 };
4894 assert_eq!(
4895 LiteralValue::date(date_time_to_semantic(&dt))
4896 .lemma_type
4897 .name(),
4898 "date"
4899 );
4900 assert_eq!(
4901 LiteralValue::ratio_from_decimal(Decimal::new(1, 2), Some("percent".to_string()))
4902 .lemma_type
4903 .name(),
4904 "ratio"
4905 );
4906 let dur_type = LemmaType::new(
4907 "duration".to_string(),
4908 TypeSpecification::Measure {
4909 minimum: None,
4910 maximum: None,
4911 decimals: None,
4912 units: MeasureUnits::from(vec![MeasureUnit {
4913 name: "second".to_string(),
4914 factor: crate::computation::rational::rational_one(),
4915 derived_measure_factors: Vec::new(),
4916 decomposition: BaseMeasureVector::new(),
4917 minimum: None,
4918 maximum: None,
4919 default_magnitude: None,
4920 }]),
4921 traits: vec![MeasureTrait::Duration],
4922 decomposition: None,
4923 help: String::new(),
4924 },
4925 TypeExtends::Primitive,
4926 );
4927 assert_eq!(
4928 LiteralValue::measure_with_type(one.clone(), "second".to_string(), Arc::new(dur_type))
4929 .lemma_type
4930 .name(),
4931 "duration"
4932 );
4933 }
4934
4935 #[test]
4936 fn test_type_display() {
4937 let specs = TypeSpecification::text();
4938 let lemma_type = LemmaType::new("name".to_string(), specs, TypeExtends::Primitive);
4939 assert_eq!(format!("{}", lemma_type), "name");
4940 }
4941
4942 #[test]
4943 fn test_type_serialization() {
4944 let specs = TypeSpecification::number();
4945 let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
4946 let serialized = serde_json::to_string(&lemma_type).unwrap();
4947 let deserialized: LemmaType = serde_json::from_str(&serialized).unwrap();
4948 assert_eq!(lemma_type, deserialized);
4949 }
4950
4951 #[test]
4952 fn test_literal_value_display_value() {
4953 let ten = rational_new(10, 1);
4954
4955 assert_eq!(
4956 LiteralValue::text("hello".to_string()).display_value(),
4957 "hello"
4958 );
4959 assert_eq!(LiteralValue::number(ten).display_value(), "10");
4960 assert_eq!(LiteralValue::from_bool(true).display_value(), "true");
4961 assert_eq!(LiteralValue::from_bool(false).display_value(), "false");
4962
4963 let ten_percent_ratio =
4965 LiteralValue::ratio_from_decimal(Decimal::new(1, 1), Some("percent".to_string()));
4966 assert_eq!(ten_percent_ratio.display_value(), "10%");
4967
4968 let time = TimeValue {
4969 hour: 14,
4970 minute: 30,
4971 second: 0,
4972 microsecond: 0,
4973 timezone: None,
4974 };
4975 let time_display = LiteralValue::time(time_to_semantic(&time)).display_value();
4976 assert!(time_display.contains("14"));
4977 assert!(time_display.contains("30"));
4978 }
4979
4980 #[test]
4981 fn test_measure_display_respects_type_decimals() {
4982 let money_type = LemmaType {
4983 name: Some("money".to_string()),
4984 specifications: TypeSpecification::Measure {
4985 minimum: None,
4986 maximum: None,
4987 decimals: Some(2),
4988 units: MeasureUnits::from(vec![MeasureUnit {
4989 name: "eur".to_string(),
4990 factor: crate::computation::rational::rational_one(),
4991 derived_measure_factors: Vec::new(),
4992 decomposition: BaseMeasureVector::new(),
4993 minimum: None,
4994 maximum: None,
4995 default_magnitude: None,
4996 }]),
4997 traits: Vec::new(),
4998 decomposition: None,
4999 help: String::new(),
5000 },
5001 extends: TypeExtends::Primitive,
5002 };
5003 let money_type = Arc::new(money_type);
5004 let val = LiteralValue::measure_with_type(
5005 decimal_to_rational(Decimal::from_str("1.8").unwrap()).unwrap(),
5006 "eur".to_string(),
5007 money_type.clone(),
5008 );
5009 assert_eq!(val.display_value(), "1.80 eur");
5010 let more_precision = LiteralValue::measure_with_type(
5011 decimal_to_rational(Decimal::from_str("1.80000").unwrap()).unwrap(),
5012 "eur".to_string(),
5013 money_type,
5014 );
5015 assert_eq!(more_precision.display_value(), "1.80 eur");
5016 let measure_no_decimals = LemmaType {
5017 name: Some("count".to_string()),
5018 specifications: TypeSpecification::Measure {
5019 minimum: None,
5020 maximum: None,
5021 decimals: None,
5022 units: MeasureUnits::from(vec![MeasureUnit {
5023 name: "items".to_string(),
5024 factor: crate::computation::rational::rational_one(),
5025 derived_measure_factors: Vec::new(),
5026 decomposition: BaseMeasureVector::new(),
5027 minimum: None,
5028 maximum: None,
5029 default_magnitude: None,
5030 }]),
5031 traits: Vec::new(),
5032 decomposition: None,
5033 help: String::new(),
5034 },
5035 extends: TypeExtends::Primitive,
5036 };
5037 let val_any = LiteralValue::measure_with_type(
5038 decimal_to_rational(Decimal::from_str("42.50").unwrap()).unwrap(),
5039 "items".to_string(),
5040 Arc::new(measure_no_decimals),
5041 );
5042 assert_eq!(val_any.display_value(), "42.5 items");
5043 }
5044
5045 #[test]
5046 fn test_literal_value_time_type() {
5047 let time = TimeValue {
5048 hour: 14,
5049 minute: 30,
5050 second: 0,
5051 microsecond: 0,
5052 timezone: None,
5053 };
5054 let lit = LiteralValue::time(time_to_semantic(&time));
5055 assert_eq!(lit.lemma_type.name(), "time");
5056 }
5057
5058 #[test]
5059 fn test_measure_family_name_primitive_root() {
5060 let measure_spec = TypeSpecification::measure();
5061 let money_primitive = LemmaType::new(
5062 "money".to_string(),
5063 measure_spec.clone(),
5064 TypeExtends::Primitive,
5065 );
5066 assert_eq!(money_primitive.measure_family_name(), Some("money"));
5067 }
5068
5069 #[test]
5070 fn test_measure_family_name_custom() {
5071 let measure_spec = TypeSpecification::measure();
5072 let money_custom = LemmaType::new(
5073 "money".to_string(),
5074 measure_spec,
5075 TypeExtends::custom_local("money".to_string(), "money".to_string()),
5076 );
5077 assert_eq!(money_custom.measure_family_name(), Some("money"));
5078 }
5079
5080 #[test]
5081 fn test_same_measure_family_same_name_different_extends() {
5082 let measure_spec = TypeSpecification::measure();
5083 let money_primitive = LemmaType::new(
5084 "money".to_string(),
5085 measure_spec.clone(),
5086 TypeExtends::Primitive,
5087 );
5088 let money_custom = LemmaType::new(
5089 "money".to_string(),
5090 measure_spec,
5091 TypeExtends::custom_local("money".to_string(), "money".to_string()),
5092 );
5093 assert!(money_primitive.same_measure_family(&money_custom));
5094 assert!(money_custom.same_measure_family(&money_primitive));
5095 }
5096
5097 #[test]
5098 fn test_same_measure_family_parent_and_child() {
5099 let measure_spec = TypeSpecification::measure();
5100 let type_x = LemmaType::new(
5101 "x".to_string(),
5102 measure_spec.clone(),
5103 TypeExtends::Primitive,
5104 );
5105 let type_x2 = LemmaType::new(
5106 "x2".to_string(),
5107 measure_spec,
5108 TypeExtends::custom_local("x".to_string(), "x".to_string()),
5109 );
5110 assert_eq!(type_x.measure_family_name(), Some("x"));
5111 assert_eq!(type_x2.measure_family_name(), Some("x"));
5112 assert!(type_x.same_measure_family(&type_x2));
5113 assert!(type_x2.same_measure_family(&type_x));
5114 }
5115
5116 #[test]
5117 fn test_same_measure_family_siblings() {
5118 let measure_spec = TypeSpecification::measure();
5119 let type_x2_a = LemmaType::new(
5120 "x2a".to_string(),
5121 measure_spec.clone(),
5122 TypeExtends::custom_local("x".to_string(), "x".to_string()),
5123 );
5124 let type_x2_b = LemmaType::new(
5125 "x2b".to_string(),
5126 measure_spec,
5127 TypeExtends::custom_local("x".to_string(), "x".to_string()),
5128 );
5129 assert!(type_x2_a.same_measure_family(&type_x2_b));
5130 }
5131
5132 #[test]
5133 fn test_same_measure_family_different_families() {
5134 let measure_spec = TypeSpecification::measure();
5135 let money = LemmaType::new(
5136 "money".to_string(),
5137 measure_spec.clone(),
5138 TypeExtends::Primitive,
5139 );
5140 let temperature = LemmaType::new(
5141 "temperature".to_string(),
5142 measure_spec,
5143 TypeExtends::Primitive,
5144 );
5145 assert!(!money.same_measure_family(&temperature));
5146 assert!(!temperature.same_measure_family(&money));
5147 }
5148
5149 #[test]
5150 fn test_same_measure_family_measure_vs_non_measure() {
5151 let measure_spec = TypeSpecification::measure();
5152 let number_spec = TypeSpecification::number();
5153 let measure_type =
5154 LemmaType::new("money".to_string(), measure_spec, TypeExtends::Primitive);
5155 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
5156 assert!(!measure_type.same_measure_family(&number_type));
5157 assert!(!number_type.same_measure_family(&measure_type));
5158 }
5159
5160 #[test]
5161 fn test_same_measure_family_anonymous_measures_are_not_family_compatible() {
5162 let left = LemmaType::anonymous_for_decomposition(duration_decomposition());
5163 let right = LemmaType::anonymous_for_decomposition(duration_decomposition());
5164
5165 assert!(!left.same_measure_family(&right));
5166 assert!(left.compatible_with_anonymous_measure(&right));
5167 }
5168
5169 #[test]
5170 fn test_measure_family_name_non_measure_returns_none() {
5171 let number_spec = TypeSpecification::number();
5172 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
5173 assert_eq!(number_type.measure_family_name(), None);
5174 }
5175
5176 #[test]
5177 fn test_lemma_type_inequality_local_vs_import_same_shape() {
5178 let dep = Arc::new(LemmaSpec::new("dep".to_string()));
5179 let measure_spec = TypeSpecification::measure();
5180 let local = LemmaType::new(
5181 "t".to_string(),
5182 measure_spec.clone(),
5183 TypeExtends::custom_local("money".to_string(), "money".to_string()),
5184 );
5185 let imported = LemmaType::new(
5186 "t".to_string(),
5187 measure_spec,
5188 TypeExtends::Custom {
5189 parent: "money".to_string(),
5190 family: "money".to_string(),
5191 defining_spec: TypeDefiningSpec::Import {
5192 spec: Arc::clone(&dep),
5193 },
5194 },
5195 );
5196 assert_ne!(local, imported);
5197 }
5198
5199 #[test]
5200 fn test_lemma_type_equality_import_same_arc_pointer_identity() {
5201 let shared_spec = Arc::new(LemmaSpec::new("dep".to_string()));
5205 let measure_spec = TypeSpecification::measure();
5206 let left = LemmaType::new(
5207 "t".to_string(),
5208 measure_spec.clone(),
5209 TypeExtends::Custom {
5210 parent: "money".to_string(),
5211 family: "money".to_string(),
5212 defining_spec: TypeDefiningSpec::Import {
5213 spec: Arc::clone(&shared_spec),
5214 },
5215 },
5216 );
5217 let right = LemmaType::new(
5218 "t".to_string(),
5219 measure_spec,
5220 TypeExtends::Custom {
5221 parent: "money".to_string(),
5222 family: "money".to_string(),
5223 defining_spec: TypeDefiningSpec::Import {
5224 spec: Arc::clone(&shared_spec),
5225 },
5226 },
5227 );
5228 assert_eq!(left, right);
5229 }
5230
5231 #[test]
5232 fn test_lemma_type_inequality_import_different_arc_pointer() {
5233 let spec_a = Arc::new(LemmaSpec::new("dep".to_string()));
5235 let spec_b = Arc::new(LemmaSpec::new("dep".to_string()));
5236 let measure_spec = TypeSpecification::measure();
5237 let left = LemmaType::new(
5238 "t".to_string(),
5239 measure_spec.clone(),
5240 TypeExtends::Custom {
5241 parent: "money".to_string(),
5242 family: "money".to_string(),
5243 defining_spec: TypeDefiningSpec::Import {
5244 spec: Arc::clone(&spec_a),
5245 },
5246 },
5247 );
5248 let right = LemmaType::new(
5249 "t".to_string(),
5250 measure_spec,
5251 TypeExtends::Custom {
5252 parent: "money".to_string(),
5253 family: "money".to_string(),
5254 defining_spec: TypeDefiningSpec::Import { spec: spec_b },
5255 },
5256 );
5257 assert_ne!(left, right);
5258 }
5259
5260 fn month_default_arg() -> CommandArg {
5261 CommandArg::Literal(crate::literals::Value::NumberWithUnit(
5262 Decimal::ONE,
5263 "month".to_string(),
5264 ))
5265 }
5266
5267 fn unit_factor_arg(name: &str, factor: i64) -> [CommandArg; 2] {
5268 [
5269 CommandArg::Label(name.to_string()),
5270 CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::from(factor))),
5271 ]
5272 }
5273
5274 #[test]
5275 fn default_calendar_on_text_reports_hint() {
5276 let specs = TypeSpecification::text();
5277 let mut default = None;
5278 let err = specs
5279 .apply_constraint(
5280 "notes",
5281 TypeConstraintCommand::Default,
5282 &[month_default_arg()],
5283 &mut default,
5284 )
5285 .unwrap_err();
5286 assert!(err.contains("Unit 'month' is for calendar data"));
5287 assert!(err.contains("double quotes"));
5288 }
5289
5290 #[test]
5291 fn default_calendar_on_duration_reports_valid_units() {
5292 let mut specs = TypeSpecification::measure();
5293 specs = specs
5294 .apply_constraint(
5295 "duration",
5296 TypeConstraintCommand::Unit,
5297 &unit_factor_arg("second", 1),
5298 &mut None,
5299 )
5300 .unwrap();
5301 specs = specs
5302 .apply_constraint(
5303 "duration",
5304 TypeConstraintCommand::Unit,
5305 &unit_factor_arg("week", 604_800),
5306 &mut None,
5307 )
5308 .unwrap();
5309 specs = specs
5310 .apply_constraint(
5311 "duration",
5312 TypeConstraintCommand::Trait,
5313 &[CommandArg::Label("duration".to_string())],
5314 &mut None,
5315 )
5316 .unwrap();
5317 let mut default = None;
5318 let err = specs
5319 .apply_constraint(
5320 "duration",
5321 TypeConstraintCommand::Default,
5322 &[month_default_arg()],
5323 &mut default,
5324 )
5325 .unwrap_err();
5326 assert!(err.contains("Unit 'month' is for calendar data"));
5327 assert!(err.contains("Valid 'duration' units are"));
5328 assert!(err.contains("week"));
5329 }
5330
5331 #[test]
5332 fn default_valid_duration_weeks_accepted() {
5333 let mut specs = TypeSpecification::measure();
5334 specs = specs
5335 .apply_constraint(
5336 "duration",
5337 TypeConstraintCommand::Unit,
5338 &unit_factor_arg("second", 1),
5339 &mut None,
5340 )
5341 .unwrap();
5342 specs = specs
5343 .apply_constraint(
5344 "duration",
5345 TypeConstraintCommand::Unit,
5346 &unit_factor_arg("week", 604_800),
5347 &mut None,
5348 )
5349 .unwrap();
5350 specs = specs
5351 .apply_constraint(
5352 "duration",
5353 TypeConstraintCommand::Trait,
5354 &[CommandArg::Label("duration".to_string())],
5355 &mut None,
5356 )
5357 .unwrap();
5358 let mut default = None;
5359 specs
5360 .apply_constraint(
5361 "duration",
5362 TypeConstraintCommand::Default,
5363 &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
5364 Decimal::from(4),
5365 "week".to_string(),
5366 ))],
5367 &mut default,
5368 )
5369 .unwrap();
5370 assert!(matches!(
5371 default,
5372 Some(RawDefault::Measure {
5373 unit_name,
5374 ..
5375 }) if unit_name == "week"
5376 ));
5377 }
5378
5379 #[test]
5380 fn default_unknown_unit_on_duration_lists_valid_units() {
5381 let mut specs = TypeSpecification::measure();
5382 specs = specs
5383 .apply_constraint(
5384 "duration",
5385 TypeConstraintCommand::Unit,
5386 &unit_factor_arg("second", 1),
5387 &mut None,
5388 )
5389 .unwrap();
5390 specs = specs
5391 .apply_constraint(
5392 "duration",
5393 TypeConstraintCommand::Trait,
5394 &[CommandArg::Label("duration".to_string())],
5395 &mut None,
5396 )
5397 .unwrap();
5398 let mut default = None;
5399 let err = specs
5400 .apply_constraint(
5401 "duration",
5402 TypeConstraintCommand::Default,
5403 &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
5404 Decimal::ONE,
5405 "fortnight".to_string(),
5406 ))],
5407 &mut default,
5408 )
5409 .unwrap_err();
5410 assert!(err.contains("fortnight"));
5411 assert!(err.contains("not defined on 'duration'"));
5412 assert!(err.contains("Valid units are"));
5413 }
5414
5415 fn money_measure_type() -> LemmaType {
5416 LemmaType::new(
5417 "Money".to_string(),
5418 TypeSpecification::Measure {
5419 minimum: None,
5420 maximum: None,
5421 decimals: None,
5422 units: MeasureUnits::from(vec![
5423 MeasureUnit {
5424 name: "eur".to_string(),
5425 factor: crate::computation::rational::rational_one(),
5426 derived_measure_factors: Vec::new(),
5427 decomposition: BaseMeasureVector::new(),
5428 minimum: None,
5429 maximum: None,
5430 default_magnitude: None,
5431 },
5432 MeasureUnit {
5433 name: "usd".to_string(),
5434 factor: crate::computation::rational::decimal_to_rational(Decimal::new(
5435 91, 2,
5436 ))
5437 .expect("factor"),
5438 derived_measure_factors: Vec::new(),
5439 decomposition: BaseMeasureVector::new(),
5440 minimum: None,
5441 maximum: None,
5442 default_magnitude: None,
5443 },
5444 ]),
5445 traits: Vec::new(),
5446 decomposition: None,
5447 help: String::new(),
5448 },
5449 TypeExtends::Primitive,
5450 )
5451 }
5452
5453 #[test]
5454 fn measure_unit_names_for_named_measure() {
5455 let money = money_measure_type();
5456 assert_eq!(money.measure_unit_names(), Some(vec!["eur", "usd"]));
5457 }
5458
5459 fn sig(pairs: &[(&str, i32)]) -> Vec<(String, i32)> {
5464 pairs.iter().map(|(s, e)| (s.to_string(), *e)).collect()
5465 }
5466
5467 #[test]
5468 fn combine_signatures_multiply_adds_exponents() {
5469 let left = sig(&[("eur", 1)]);
5470 let right = sig(&[("hour", -1)]);
5471 let result = combine_signatures(&left, &right, true);
5472 assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
5473 }
5474
5475 #[test]
5476 fn combine_signatures_divide_subtracts_exponents() {
5477 let left = sig(&[("eur", 1)]);
5478 let right = sig(&[("hour", 1)]);
5479 let result = combine_signatures(&left, &right, false);
5480 assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
5481 }
5482
5483 #[test]
5484 fn combine_signatures_cancels_to_empty() {
5485 let left = sig(&[("ce", 1), ("minute", -1)]);
5486 let right = sig(&[("minute", 1)]);
5487 let result = combine_signatures(&left, &right, true);
5488 assert_eq!(result, sig(&[("ce", 1)]));
5490 }
5491
5492 #[test]
5493 fn combine_signatures_output_is_canonical_form() {
5494 let left = sig(&[("eur", 1), ("hour", 1)]);
5495 let right = sig(&[("minute", 1)]);
5496 let result = combine_signatures(&left, &right, false); let expected = sig(&[("eur", 1), ("hour", 1), ("minute", -1)]);
5499 assert_eq!(result, expected);
5500 }
5501
5502 #[test]
5503 fn canonicalize_signature_drops_zero_exponents() {
5504 let sig_with_zero = sig(&[("eur", 1), ("hour", 0), ("minute", -1)]);
5505 let result = canonicalize_signature(&sig_with_zero);
5506 assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
5507 }
5508
5509 #[test]
5510 fn canonicalize_signature_sorts_by_name() {
5511 let unsorted = sig(&[("minute", -1), ("eur", 1)]);
5512 let result = canonicalize_signature(&unsorted);
5513 assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
5514 }
5515
5516 #[test]
5522 fn format_signature_operator_style_numerator_only() {
5523 let signature = sig(&[("eur", 1)]);
5524 let result = format_signature_operator_style(&signature);
5525 assert_eq!(result, "eur");
5526 }
5527
5528 #[test]
5529 fn format_signature_operator_style_with_denominator() {
5530 let signature = sig(&[("eur", 1), ("hour", -1)]);
5531 let result = format_signature_operator_style(&signature);
5532 assert_eq!(result, "eur/hour");
5533 }
5534
5535 #[test]
5536 fn format_signature_operator_style_denominator_only() {
5537 let signature = sig(&[("meter", -1)]);
5538 let result = format_signature_operator_style(&signature);
5539 assert_eq!(result, "1/meter");
5540 }
5541
5542 #[test]
5543 fn format_signature_operator_style_with_exponents() {
5544 let signature = sig(&[("meter", 2), ("second", -2)]);
5545 let result = format_signature_operator_style(&signature);
5546 assert_eq!(result, "meter^2/second^2");
5547 }
5548
5549 #[test]
5554 fn calendar_unit_factor_table_completeness() {
5555 for unit in &[SemanticCalendarUnit::Month, SemanticCalendarUnit::Year] {
5558 let name = unit.to_string();
5559 assert!(
5560 calendar_unit_factor(&name).is_some(),
5561 "calendar_unit_factor('{}') must return Some",
5562 name
5563 );
5564 }
5565 }
5566
5567 #[test]
5568 fn semantic_calendar_unit_display_returns_singular() {
5569 assert_eq!(SemanticCalendarUnit::Month.to_string(), "month");
5572 assert_eq!(SemanticCalendarUnit::Year.to_string(), "year");
5573 }
5574
5575 #[test]
5580 fn signature_factor_with_calendar_units() {
5581 use std::collections::HashMap;
5582 let calendar = test_calendar_type_for_signature_factor();
5583 let unit_index: HashMap<String, Arc<LemmaType>> = HashMap::new();
5584 let sig_month_per_year = sig(&[("month", 1), ("year", -1)]);
5587 let factor = signature_factor(&sig_month_per_year, &unit_index, Some(&calendar))
5588 .expect("must not overflow");
5589 let expected = rational_new(1, 12);
5590 assert_eq!(factor, expected, "month/year factor must be 1/12");
5591 }
5592
5593 fn test_calendar_type_for_signature_factor() -> LemmaType {
5594 use crate::computation::rational::{decimal_to_rational, rational_one};
5595 use crate::literals::{MeasureUnit, MeasureUnits};
5596 use rust_decimal::Decimal;
5597 LemmaType::new(
5598 "calendar".to_string(),
5599 TypeSpecification::Measure {
5600 minimum: None,
5601 maximum: None,
5602 decimals: None,
5603 units: MeasureUnits::from(vec![
5604 MeasureUnit {
5605 name: "month".to_string(),
5606 factor: rational_one(),
5607 minimum: None,
5608 maximum: None,
5609 default_magnitude: None,
5610 decomposition: calendar_decomposition(),
5611 derived_measure_factors: Vec::new(),
5612 },
5613 MeasureUnit {
5614 name: "year".to_string(),
5615 factor: decimal_to_rational(Decimal::from(12)).expect("year factor"),
5616 minimum: None,
5617 maximum: None,
5618 default_magnitude: None,
5619 decomposition: calendar_decomposition(),
5620 derived_measure_factors: Vec::new(),
5621 },
5622 ]),
5623 traits: vec![MeasureTrait::Calendar],
5624 decomposition: Some(calendar_decomposition()),
5625 help: String::new(),
5626 },
5627 TypeExtends::Primitive,
5628 )
5629 }
5630
5631 #[test]
5632 #[should_panic(expected = "BUG: signature_factor called with unresolved unit name")]
5633 fn signature_factor_panics_on_unresolved_name() {
5634 use std::collections::HashMap;
5635 let unit_index: HashMap<String, Arc<LemmaType>> = HashMap::new();
5636 let bad_sig = sig(&[("nonexistent_unit_xyz", 1)]);
5637 let _ = signature_factor(&bad_sig, &unit_index, None);
5638 }
5639
5640 #[test]
5641 fn signature_factor_uses_owner_when_expression_index_empty() {
5642 use std::collections::HashMap;
5643 let money = test_money_type_for_signature_factor();
5644 let expression_units: HashMap<String, Arc<LemmaType>> = HashMap::new();
5645 let sig_usd = sig(&[("usd", 1)]);
5646 let factor =
5647 signature_factor(&sig_usd, &expression_units, Some(&money)).expect("must not overflow");
5648 assert_eq!(factor, rational_new(91, 100));
5649 }
5650
5651 fn test_money_type_for_signature_factor() -> LemmaType {
5652 use crate::computation::rational::decimal_to_rational;
5653 use crate::literals::{MeasureUnit, MeasureUnits};
5654 use rust_decimal::Decimal;
5655 LemmaType::new(
5656 "money".to_string(),
5657 TypeSpecification::Measure {
5658 minimum: None,
5659 maximum: None,
5660 decimals: Some(2),
5661 units: MeasureUnits::from(vec![
5662 MeasureUnit {
5663 name: "eur".to_string(),
5664 factor: crate::computation::rational::rational_one(),
5665 minimum: None,
5666 maximum: None,
5667 default_magnitude: None,
5668 decomposition: BaseMeasureVector::new(),
5669 derived_measure_factors: Vec::new(),
5670 },
5671 MeasureUnit {
5672 name: "usd".to_string(),
5673 factor: decimal_to_rational(Decimal::new(91, 2)).expect("usd factor"),
5674 minimum: None,
5675 maximum: None,
5676 default_magnitude: None,
5677 decomposition: BaseMeasureVector::new(),
5678 derived_measure_factors: Vec::new(),
5679 },
5680 ]),
5681 traits: Vec::new(),
5682 decomposition: None,
5683 help: String::new(),
5684 },
5685 TypeExtends::Primitive,
5686 )
5687 }
5688
5689 fn measure_type_with_kilogram() -> TypeSpecification {
5690 use crate::computation::rational::rational_one;
5691 use crate::literals::{MeasureUnit, MeasureUnits};
5692 let mut units = MeasureUnits::new();
5693 units.push(MeasureUnit {
5694 name: "kilogram".to_string(),
5695 factor: rational_one(),
5696 minimum: None,
5697 maximum: None,
5698 default_magnitude: None,
5699 decomposition: BaseMeasureVector::new(),
5700 derived_measure_factors: Vec::new(),
5701 });
5702 TypeSpecification::Measure {
5703 minimum: None,
5704 maximum: None,
5705 decimals: None,
5706 units,
5707 traits: Vec::new(),
5708 decomposition: None,
5709 help: String::new(),
5710 }
5711 }
5712
5713 #[test]
5714 fn parser_value_to_value_kind_rejects_bare_number_for_measure() {
5715 let ten = Value::Number(Decimal::from(10));
5716 let err = parser_value_to_value_kind(&ten, &measure_type_with_kilogram())
5717 .expect_err("bare number must not bind to measure");
5718 assert!(
5719 err.contains("kilogram"),
5720 "error must hint expected unit, got: {err}"
5721 );
5722 }
5723
5724 #[test]
5725 fn parser_value_to_value_kind_accepts_number_with_unit_for_measure() {
5726 let ten_kg = Value::NumberWithUnit(Decimal::from(10), "kilogram".to_string());
5727 let kind = parser_value_to_value_kind(&ten_kg, &measure_type_with_kilogram())
5728 .expect("10 kilogram must bind to measure");
5729 assert!(matches!(kind, ValueKind::Measure(_, _)));
5730 }
5731
5732 #[test]
5733 fn parser_value_to_value_kind_accepts_bare_number_for_ratio() {
5734 let ten = Value::Number(Decimal::from(10));
5735 let kind =
5736 parser_value_to_value_kind(&ten, &TypeSpecification::ratio()).expect("number -> ratio");
5737 assert!(matches!(kind, ValueKind::Ratio(_, None)));
5738 }
5739
5740 #[test]
5741 fn value_kind_matches_spec_rejects_number_for_measure() {
5742 let n = ValueKind::Number(rational_new(10, 1));
5743 assert!(!value_kind_matches_spec(&n, &measure_type_with_kilogram()));
5744 }
5745
5746 #[test]
5747 fn apply_constraint_rejects_inherited_unit_factor_change() {
5748 let mut specs = TypeSpecification::measure();
5749 specs = specs
5750 .apply_constraint(
5751 "money",
5752 TypeConstraintCommand::Unit,
5753 &unit_factor_arg("eur", 1),
5754 &mut None,
5755 )
5756 .expect("seed eur");
5757 let err = specs
5758 .apply_constraint(
5759 "money",
5760 TypeConstraintCommand::Unit,
5761 &[
5762 CommandArg::Label("eur".to_string()),
5763 CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::new(11, 1))),
5764 ],
5765 &mut None,
5766 )
5767 .expect_err("must not change inherited unit factor");
5768 assert!(err.contains("eur"), "error must name unit, got: {err}");
5769 assert!(
5770 err.contains("inherited") || err.contains("cannot change"),
5771 "error must reject factor change, got: {err}"
5772 );
5773 }
5774
5775 #[test]
5776 fn apply_constraint_allows_additive_unit_on_inherited_spec() {
5777 let mut specs = TypeSpecification::measure();
5778 specs = specs
5779 .apply_constraint(
5780 "money",
5781 TypeConstraintCommand::Unit,
5782 &unit_factor_arg("eur", 1),
5783 &mut None,
5784 )
5785 .expect("seed eur");
5786 specs = specs
5787 .apply_constraint(
5788 "money",
5789 TypeConstraintCommand::Unit,
5790 &unit_factor_arg("usd", 1),
5791 &mut None,
5792 )
5793 .expect("add usd");
5794 match &specs {
5795 TypeSpecification::Measure { units, .. } => assert_eq!(units.len(), 2),
5796 other => panic!("expected Measure, got {other:?}"),
5797 }
5798 }
5799
5800 #[test]
5801 fn apply_constraint_idempotent_inherited_unit_redeclare() {
5802 let mut specs = TypeSpecification::measure();
5803 specs = specs
5804 .apply_constraint(
5805 "money",
5806 TypeConstraintCommand::Unit,
5807 &unit_factor_arg("eur", 1),
5808 &mut None,
5809 )
5810 .expect("seed eur");
5811 specs = specs
5812 .apply_constraint(
5813 "money",
5814 TypeConstraintCommand::Unit,
5815 &unit_factor_arg("eur", 1),
5816 &mut None,
5817 )
5818 .expect("idempotent eur");
5819 match &specs {
5820 TypeSpecification::Measure { units, .. } => {
5821 assert_eq!(units.len(), 1);
5822 assert_eq!(
5823 units.iter().find(|u| u.name == "eur").expect("eur").factor,
5824 crate::computation::rational::rational_one()
5825 );
5826 }
5827 other => panic!("expected Measure, got {other:?}"),
5828 }
5829 }
5830}