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