1pub use crate::parsing::ast::{
9 ArithmeticComputation, ComparisonComputation, MathematicalComputation, NegationType,
10 RangeBound, VetoExpression,
11};
12pub use crate::parsing::source::Source;
13
14#[must_use]
17pub fn negated_comparison(op: ComparisonComputation) -> ComparisonComputation {
18 match op {
19 ComparisonComputation::LessThan => ComparisonComputation::GreaterThanOrEqual,
20 ComparisonComputation::LessThanOrEqual => ComparisonComputation::GreaterThan,
21 ComparisonComputation::GreaterThan => ComparisonComputation::LessThanOrEqual,
22 ComparisonComputation::GreaterThanOrEqual => ComparisonComputation::LessThan,
23 ComparisonComputation::Is => ComparisonComputation::IsNot,
24 ComparisonComputation::IsNot => ComparisonComputation::Is,
25 }
26}
27
28#[must_use]
31pub fn mirrored_comparison(op: ComparisonComputation) -> ComparisonComputation {
32 match op {
33 ComparisonComputation::LessThan => ComparisonComputation::GreaterThan,
34 ComparisonComputation::LessThanOrEqual => ComparisonComputation::GreaterThanOrEqual,
35 ComparisonComputation::GreaterThan => ComparisonComputation::LessThan,
36 ComparisonComputation::GreaterThanOrEqual => ComparisonComputation::LessThanOrEqual,
37 ComparisonComputation::Is => ComparisonComputation::Is,
38 ComparisonComputation::IsNot => ComparisonComputation::IsNot,
39 }
40}
41
42use crate::computation::rational::{checked_div, rational_new, RationalInteger};
44use crate::parsing::ast::Constraint;
45use crate::parsing::ast::{
46 BooleanValue, CalendarPeriodUnit, CommandArg, ConversionTarget, DateCalendarKind,
47 DateRelativeKind, DateTimeValue, PrimitiveKind, TimeValue, TimezoneValue,
48 TypeConstraintCommand,
49};
50use crate::Error;
51use indexmap::IndexMap;
52use rust_decimal::Decimal;
53use serde::{Deserialize, Deserializer, Serialize, Serializer};
54use std::collections::BTreeMap;
55use std::fmt;
56use std::hash::Hash;
57use std::str::FromStr;
58use std::sync::{Arc, OnceLock};
59
60pub use crate::literals::{BaseMeasureVector, MeasureUnit, MeasureUnits, RatioUnit, RatioUnits};
67
68pub fn combine_decompositions(
71 left: &BaseMeasureVector,
72 right: &BaseMeasureVector,
73 is_multiply: bool,
74) -> BaseMeasureVector {
75 let mut result = left.clone();
76 for (dim, &exp) in right {
77 let delta = if is_multiply { exp } else { -exp };
78 let entry = result.entry(dim.clone()).or_insert(0);
79 *entry += delta;
80 if *entry == 0 {
81 result.remove(dim);
82 }
83 }
84 result
85}
86
87#[cfg(test)]
94pub fn combine_signatures(
95 left: &[(String, i32)],
96 right: &[(String, i32)],
97 is_multiply: bool,
98) -> Vec<(String, i32)> {
99 use std::collections::BTreeMap;
100 let mut accumulator: BTreeMap<String, i32> = BTreeMap::new();
101 for (name, exponent) in left {
102 *accumulator.entry(name.clone()).or_insert(0) += exponent;
103 }
104 for (name, exponent) in right {
105 let delta = if is_multiply { *exponent } else { -*exponent };
106 *accumulator.entry(name.clone()).or_insert(0) += delta;
107 }
108 accumulator
109 .into_iter()
110 .filter(|(_, exponent)| *exponent != 0)
111 .collect()
112}
113
114pub fn format_signature_operator_style(signature: &[(String, i32)]) -> String {
127 let canonical = canonicalize_signature(signature);
128 let mut numerator: Vec<(String, i32)> = Vec::new();
129 let mut denominator: Vec<(String, i32)> = Vec::new();
130 for (name, exponent) in canonical {
131 if exponent > 0 {
132 numerator.push((name, exponent));
133 } else if exponent < 0 {
134 denominator.push((name, -exponent));
135 }
136 }
137 let render = |terms: &[(String, i32)]| -> String {
138 terms
139 .iter()
140 .map(|(name, exp)| {
141 if *exp == 1 {
142 name.clone()
143 } else {
144 format!("{name}^{exp}")
145 }
146 })
147 .collect::<Vec<_>>()
148 .join("*")
149 };
150 match (numerator.is_empty(), denominator.is_empty()) {
151 (true, true) => String::new(),
152 (false, true) => render(&numerator),
153 (true, false) => format!("1/{}", render(&denominator)),
154 (false, false) => format!("{}/{}", render(&numerator), render(&denominator)),
155 }
156}
157
158pub fn calendar_unit_factor(name: &str) -> Option<crate::computation::rational::RationalInteger> {
165 use crate::computation::rational::rational_one;
166 match name {
167 "month" => Some(rational_one()),
168 "year" => Some(rational_new(12, 1)),
169 _ => None,
170 }
171}
172
173fn reject_negative_width_magnitude(magnitude: &RationalInteger, cmd: &str) -> Result<(), String> {
174 use crate::computation::rational::rational_zero;
175 use std::cmp::Ordering;
176 match magnitude.try_cmp(&rational_zero()) {
177 Ok(Ordering::Less) => Err(format!("{cmd} width must not be negative")),
178 Ok(Ordering::Equal | Ordering::Greater) => Ok(()),
179 Err(failure) => Err(format!("{cmd} width compare failed: {failure}")),
180 }
181}
182
183fn parse_unresolved_width_bound(
186 args: &[CommandArg],
187 cmd: &str,
188) -> Result<(RationalInteger, String), String> {
189 use crate::computation::rational::decimal_to_rational;
190 let lit = require_literal(args, cmd)?;
191 let (magnitude, unit_name) = match lit {
192 crate::literals::Value::NumberWithUnit(n, unit) => (*n, unit.clone()),
193 other => {
194 return Err(format!(
195 "{cmd} requires a measure literal with a unit, got {}",
196 value_kind_name(other)
197 ));
198 }
199 };
200 let magnitude_rational = decimal_to_rational(magnitude)
201 .map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
202 reject_negative_width_magnitude(&magnitude_rational, cmd)?;
203 Ok((magnitude_rational, unit_name))
204}
205
206pub(crate) fn check_range_bound_consistency(
211 spec: &TypeSpecification,
212 unit_index: &crate::planning::unit_index::UnitIndex,
213) -> Result<(), String> {
214 use std::cmp::Ordering;
215
216 fn endpoint_order_ok_dates(lo: &DateTimeValue, hi: &DateTimeValue) -> bool {
217 compare_semantic_dates(&date_time_to_semantic(lo), &date_time_to_semantic(hi))
218 != Ordering::Greater
219 }
220 fn endpoint_order_ok_times(lo: &TimeValue, hi: &TimeValue) -> bool {
221 compare_semantic_times(&time_to_semantic(lo), &time_to_semantic(hi)) != Ordering::Greater
222 }
223
224 match spec {
225 TypeSpecification::NumberRange {
226 lower,
227 upper,
228 minimum,
229 maximum,
230 ..
231 }
232 | TypeSpecification::RatioRange {
233 lower,
234 upper,
235 minimum,
236 maximum,
237 ..
238 } => {
239 if let (Some(lo), Some(hi)) = (lower, upper) {
240 if lo
241 .try_cmp(hi)
242 .map_err(|failure| format!("range endpoint compare failed: {failure}"))?
243 == Ordering::Greater
244 {
245 return Err(format!(
246 "invalid range: lower {} is greater than upper {}",
247 lo.display_str(),
248 hi.display_str()
249 ));
250 }
251 }
252 if let (Some(min_w), Some(max_w)) = (minimum, maximum) {
253 if min_w
254 .try_cmp(max_w)
255 .map_err(|failure| format!("range width compare failed: {failure}"))?
256 == Ordering::Greater
257 {
258 return Err(format!(
259 "invalid range: minimum width {} is greater than maximum width {}",
260 min_w.display_str(),
261 max_w.display_str()
262 ));
263 }
264 }
265 Ok(())
266 }
267 TypeSpecification::MeasureRange {
268 lower,
269 upper,
270 minimum,
271 maximum,
272 units,
273 ..
274 } => {
275 if let (Some(lo), Some(hi)) = (lower, upper) {
276 let lo_c =
277 measure_declared_bound_to_canonical(&lo.0, &lo.1, units, "range", "lower")?;
278 let hi_c =
279 measure_declared_bound_to_canonical(&hi.0, &hi.1, units, "range", "upper")?;
280 if lo_c
281 .try_cmp(&hi_c)
282 .map_err(|failure| format!("range endpoint compare failed: {failure}"))?
283 == Ordering::Greater
284 {
285 return Err(format!(
286 "invalid range: lower {} {} is greater than upper {} {}",
287 lo.0.display_str(),
288 lo.1,
289 hi.0.display_str(),
290 hi.1
291 ));
292 }
293 }
294 if let (Some(min_w), Some(max_w)) = (minimum, maximum) {
295 let min_c = measure_declared_bound_to_canonical(
296 &min_w.0, &min_w.1, units, "range", "minimum",
297 )?;
298 let max_c = measure_declared_bound_to_canonical(
299 &max_w.0, &max_w.1, units, "range", "maximum",
300 )?;
301 if min_c
302 .try_cmp(&max_c)
303 .map_err(|failure| format!("range width compare failed: {failure}"))?
304 == Ordering::Greater
305 {
306 return Err(format!(
307 "invalid range: minimum width {} {} is greater than maximum width {} {}",
308 min_w.0.display_str(),
309 min_w.1,
310 max_w.0.display_str(),
311 max_w.1
312 ));
313 }
314 }
315 Ok(())
316 }
317 TypeSpecification::DateRange {
318 lower,
319 upper,
320 minimum,
321 maximum,
322 ..
323 } => {
324 if let (Some(lo), Some(hi)) = (lower, upper) {
325 if !endpoint_order_ok_dates(lo, hi) {
326 return Err(format!(
327 "invalid range: lower {lo} is greater than upper {hi}"
328 ));
329 }
330 }
331 check_temporal_width_pair_consistency(minimum, maximum, unit_index, true)
332 }
333 TypeSpecification::TimeRange {
334 lower,
335 upper,
336 minimum,
337 maximum,
338 ..
339 } => {
340 if let (Some(lo), Some(hi)) = (lower, upper) {
341 if !endpoint_order_ok_times(lo, hi) {
342 return Err(format!(
343 "invalid range: lower {lo} is greater than upper {hi}"
344 ));
345 }
346 }
347 check_temporal_width_pair_consistency(minimum, maximum, unit_index, false)
348 }
349 _ => Ok(()),
350 }
351}
352
353fn check_temporal_width_pair_consistency(
354 minimum: &Option<(RationalInteger, String)>,
355 maximum: &Option<(RationalInteger, String)>,
356 unit_index: &crate::planning::unit_index::UnitIndex,
357 allow_calendar: bool,
358) -> Result<(), String> {
359 use std::cmp::Ordering;
360 let resolve = |bound: &(RationalInteger, String),
361 command: &str|
362 -> Result<(RationalInteger, Arc<LemmaType>), String> {
363 let (bare, owner) = unit_index.resolve(bound.1.as_str()).map_err(|err| {
364 format!(
365 "{command} width unit '{}': {err} (add `uses lemma units` or declare the unit)",
366 bound.1
367 )
368 })?;
369 if allow_calendar {
370 if !owner.is_duration_like() && !owner.is_calendar_like() {
371 return Err(format!(
372 "{command} width unit '{bare}' must be a duration or calendar unit",
373 ));
374 }
375 } else if !owner.is_duration_like() {
376 return Err(format!(
377 "{command} width unit '{bare}' must be a duration unit",
378 ));
379 }
380 let TypeSpecification::Measure { units, .. } = &owner.specifications else {
381 return Err(format!(
382 "{command} width unit '{bare}' must resolve to a measure type",
383 ));
384 };
385 let canonical = measure_declared_bound_to_canonical(
386 &bound.0,
387 &bare,
388 units,
389 owner.name().as_str(),
390 command,
391 )?;
392 Ok((canonical, Arc::clone(&owner)))
393 };
394
395 match (minimum, maximum) {
396 (None, None) => Ok(()),
397 (Some(min_w), None) => {
398 let _ = resolve(min_w, "minimum")?;
399 Ok(())
400 }
401 (None, Some(max_w)) => {
402 let _ = resolve(max_w, "maximum")?;
403 Ok(())
404 }
405 (Some(min_w), Some(max_w)) => {
406 let (min_c, min_owner) = resolve(min_w, "minimum")?;
407 let (max_c, max_owner) = resolve(max_w, "maximum")?;
408 if min_owner.is_calendar_like() != max_owner.is_calendar_like() {
409 return Err(
410 "invalid range: minimum and maximum width must not mix calendar and duration units"
411 .to_string(),
412 );
413 }
414 if min_c
415 .try_cmp(&max_c)
416 .map_err(|failure| format!("range width compare failed: {failure}"))?
417 == Ordering::Greater
418 {
419 return Err(format!(
420 "invalid range: minimum width {} {} is greater than maximum width {} {}",
421 min_w.0.display_str(),
422 min_w.1,
423 max_w.0.display_str(),
424 max_w.1
425 ));
426 }
427 Ok(())
428 }
429 }
430}
431
432fn owner_declares_measure_unit(owner: &LemmaType, unit_name: &str) -> bool {
433 owner
434 .measure_unit_names()
435 .is_some_and(|names| names.contains(&unit_name))
436}
437
438pub fn signature_factor(
448 signature: &[(String, i32)],
449 expression_units: &crate::planning::unit_index::UnitIndex,
450 owner: Option<&LemmaType>,
451) -> Result<
452 crate::computation::rational::RationalInteger,
453 crate::computation::rational::NumericFailure,
454> {
455 use crate::computation::rational::{checked_div, checked_mul, rational_one};
456 let mut acc = rational_one();
457 for (name, exponent) in signature {
458 let factor =
459 if let Some(owner) = owner.filter(|owner| owner_declares_measure_unit(owner, name)) {
460 owner.measure_unit_factor(name).clone()
461 } else if let Some(lemma_type) = expression_units.unique_owner(name) {
462 lemma_type.measure_unit_factor(name).clone()
463 } else if !expression_units.owners_for(name).is_empty() {
464 panic!(
465 "BUG: signature_factor called with ambiguous unit name '{}' (pass declaring owner)",
466 name
467 );
468 } else {
469 panic!(
470 "BUG: signature_factor called with unresolved unit name '{}'",
471 name
472 );
473 };
474 let mut term = rational_one();
475 let abs_exp = exponent.unsigned_abs();
476 for _ in 0..abs_exp {
477 term = checked_mul(&term, &factor)?;
478 }
479 if *exponent >= 0 {
480 acc = checked_mul(&acc, &term)?;
481 } else {
482 acc = checked_div(&acc, &term)?;
483 }
484 }
485 Ok(acc)
486}
487
488pub fn canonicalize_signature(signature: &[(String, i32)]) -> Vec<(String, i32)> {
489 use std::collections::BTreeMap;
490 let mut accumulator: BTreeMap<String, i32> = BTreeMap::new();
491 for (name, exponent) in signature {
492 *accumulator.entry(name.clone()).or_insert(0) += exponent;
493 }
494 accumulator
495 .into_iter()
496 .filter(|(_, exponent)| *exponent != 0)
497 .collect()
498}
499
500pub fn base_measure_vector_as_signature(decomposition: &BaseMeasureVector) -> Vec<(String, i32)> {
502 decomposition
503 .iter()
504 .filter(|(_, exponent)| **exponent != 0)
505 .map(|(name, exponent)| (name.clone(), *exponent))
506 .collect()
507}
508
509pub const DURATION_DIMENSION: &str = "duration";
510pub const CALENDAR_DIMENSION: &str = "calendar";
511
512#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
513#[serde(rename_all = "snake_case")]
514pub enum MeasureTrait {
515 Duration,
516 Calendar,
517}
518
519pub fn duration_decomposition() -> BaseMeasureVector {
520 [(DURATION_DIMENSION.to_string(), 1i32)]
521 .into_iter()
522 .collect()
523}
524
525pub fn calendar_decomposition() -> BaseMeasureVector {
526 [(CALENDAR_DIMENSION.to_string(), 1i32)]
527 .into_iter()
528 .collect()
529}
530
531pub fn anonymous_measure_type() -> LemmaType {
535 LemmaType::anonymous_for_decomposition(BaseMeasureVector::new())
536}
537
538pub fn negate_signature(signature: &[(String, i32)]) -> Vec<(String, i32)> {
541 signature.iter().map(|(n, e)| (n.clone(), -*e)).collect()
542}
543
544#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
545pub enum TypeSpecification {
546 Boolean {
547 help: String,
548 },
549 Measure {
550 minimum: Option<(RationalInteger, String)>,
551 maximum: Option<(RationalInteger, String)>,
552 decimals: Option<u8>,
553 units: MeasureUnits,
554 traits: Vec<MeasureTrait>,
555 decomposition: Option<BaseMeasureVector>,
560 unit: Option<String>,
562 help: String,
563 },
564 Number {
565 minimum: Option<RationalInteger>,
566 maximum: Option<RationalInteger>,
567 decimals: Option<u8>,
568 help: String,
569 },
570 NumberRange {
571 lower: Option<RationalInteger>,
572 upper: Option<RationalInteger>,
573 minimum: Option<RationalInteger>,
574 maximum: Option<RationalInteger>,
575 help: String,
576 },
577 Ratio {
578 minimum: Option<RationalInteger>,
579 maximum: Option<RationalInteger>,
580 decimals: Option<u8>,
581 units: RatioUnits,
582 unit: Option<String>,
584 help: String,
585 },
586 RatioRange {
587 lower: Option<RationalInteger>,
588 upper: Option<RationalInteger>,
589 minimum: Option<RationalInteger>,
590 maximum: Option<RationalInteger>,
591 units: RatioUnits,
592 help: String,
593 },
594 Text {
595 length: Option<usize>,
596 options: Vec<String>,
597 help: String,
598 },
599 Date {
600 minimum: Option<DateTimeValue>,
601 maximum: Option<DateTimeValue>,
602 help: String,
603 },
604 DateRange {
605 lower: Option<DateTimeValue>,
606 upper: Option<DateTimeValue>,
607 minimum: Option<(RationalInteger, String)>,
608 maximum: Option<(RationalInteger, String)>,
609 help: String,
610 },
611 Time {
612 minimum: Option<TimeValue>,
613 maximum: Option<TimeValue>,
614 help: String,
615 },
616 TimeRange {
617 lower: Option<TimeValue>,
618 upper: Option<TimeValue>,
619 minimum: Option<(RationalInteger, String)>,
620 maximum: Option<(RationalInteger, String)>,
621 help: String,
622 },
623 MeasureRange {
624 lower: Option<(RationalInteger, String)>,
625 upper: Option<(RationalInteger, String)>,
626 minimum: Option<(RationalInteger, String)>,
627 maximum: Option<(RationalInteger, String)>,
628 units: MeasureUnits,
629 decomposition: Option<BaseMeasureVector>,
630 help: String,
631 },
632 Veto {
633 message: Option<String>,
634 },
635 Undetermined,
639 Spec {
643 spec: SpecIdentity,
644 },
645}
646
647#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
651pub struct SpecIdentity {
652 pub repository: Option<String>,
653 pub spec: String,
654 pub effective: crate::parsing::ast::EffectiveDate,
655}
656
657impl fmt::Display for SpecIdentity {
658 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
659 if let Some(repository) = &self.repository {
660 write!(f, "{repository}/")?;
661 }
662 write!(f, "{}", self.spec)?;
663 if let crate::parsing::ast::EffectiveDate::DateTimeValue(at) = &self.effective {
664 write!(f, " {at}")?;
665 }
666 Ok(())
667 }
668}
669
670impl std::fmt::Display for TypeSpecification {
671 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
672 match self {
673 Self::Boolean { .. } => f.write_str("boolean"),
674 Self::Measure { .. } => f.write_str("measure"),
675 Self::MeasureRange { .. } => f.write_str("measure range"),
676 Self::Number { .. } => f.write_str("number"),
677 Self::NumberRange { .. } => f.write_str("number range"),
678 Self::Text { .. } => f.write_str("text"),
679 Self::Date { .. } => f.write_str("date"),
680 Self::DateRange { .. } => f.write_str("date range"),
681 Self::Time { .. } => f.write_str("time"),
682 Self::TimeRange { .. } => f.write_str("time range"),
683 Self::Ratio { .. } => f.write_str("ratio"),
684 Self::RatioRange { .. } => f.write_str("ratio range"),
685 Self::Veto { .. } => f.write_str("veto"),
686 Self::Undetermined => f.write_str("undetermined"),
687 Self::Spec { spec } => write!(f, "{spec}"),
688 }
689 }
690}
691
692impl TypeSpecification {
693 pub fn help(&self) -> &str {
695 match self {
696 Self::Boolean { help, .. }
697 | Self::Measure { help, .. }
698 | Self::Number { help, .. }
699 | Self::NumberRange { help, .. }
700 | Self::Text { help, .. }
701 | Self::Date { help, .. }
702 | Self::DateRange { help, .. }
703 | Self::Time { help, .. }
704 | Self::TimeRange { help, .. }
705 | Self::Ratio { help, .. }
706 | Self::RatioRange { help, .. }
707 | Self::MeasureRange { help, .. } => help.as_str(),
708 Self::Veto { .. } | Self::Undetermined | Self::Spec { .. } => "",
709 }
710 }
711
712 #[must_use]
716 pub fn equal_ignoring_unit(&self, other: &Self) -> bool {
717 match (self, other) {
718 (
719 Self::Measure {
720 minimum: a_min,
721 maximum: a_max,
722 decimals: a_dec,
723 units: a_units,
724 traits: a_traits,
725 decomposition: a_decomp,
726 help: a_help,
727 unit: _,
728 },
729 Self::Measure {
730 minimum: b_min,
731 maximum: b_max,
732 decimals: b_dec,
733 units: b_units,
734 traits: b_traits,
735 decomposition: b_decomp,
736 help: b_help,
737 unit: _,
738 },
739 ) => {
740 a_min == b_min
741 && a_max == b_max
742 && a_dec == b_dec
743 && a_units == b_units
744 && a_traits == b_traits
745 && a_decomp == b_decomp
746 && a_help == b_help
747 }
748 (
749 Self::Ratio {
750 minimum: a_min,
751 maximum: a_max,
752 decimals: a_dec,
753 units: a_units,
754 help: a_help,
755 unit: _,
756 },
757 Self::Ratio {
758 minimum: b_min,
759 maximum: b_max,
760 decimals: b_dec,
761 units: b_units,
762 help: b_help,
763 unit: _,
764 },
765 ) => {
766 a_min == b_min
767 && a_max == b_max
768 && a_dec == b_dec
769 && a_units == b_units
770 && a_help == b_help
771 }
772 _ => self == other,
773 }
774 }
775}
776
777fn require_literal<'a>(
783 args: &'a [CommandArg],
784 cmd: &str,
785) -> Result<&'a crate::literals::Value, String> {
786 let arg = args
787 .first()
788 .ok_or_else(|| format!("{} requires an argument", cmd))?;
789 match arg {
790 CommandArg::Literal(v) => Ok(v),
791 CommandArg::Label(name) => Err(format!(
792 "{} requires a literal value, got identifier '{}'",
793 cmd, name
794 )),
795 CommandArg::UnitExpr(_) => Err(format!(
796 "{} requires a literal value, got a unit expression (only valid for 'unit' command)",
797 cmd
798 )),
799 }
800}
801
802fn apply_type_help_command(help: &mut String, args: &[CommandArg]) -> Result<(), String> {
803 match require_literal(args, "help")? {
804 crate::literals::Value::Text(s) => {
805 *help = s.clone();
806 Ok(())
807 }
808 other => Err(format!(
809 "help requires a text literal (quoted string), got {}",
810 value_kind_name(other)
811 )),
812 }
813}
814
815fn format_measure_units_list(units: &MeasureUnits) -> String {
816 units
817 .iter()
818 .map(|u| u.name.as_str())
819 .collect::<Vec<_>>()
820 .join(", ")
821}
822
823#[derive(Debug, Clone, Copy, PartialEq, Eq)]
825pub(crate) enum SuggestionExpectation {
826 MeasureUnits,
827 Text,
828 Number,
829 Boolean,
830 Date,
831 Time,
832 Ratio,
833 NumberRange,
834 DateRange,
835 TimeRange,
836 MeasureRange,
837 RatioRange,
838}
839
840pub(crate) fn suggestion_value_mismatch_error(
841 calendar_unit: &str,
842 type_name: &str,
843 expectation: SuggestionExpectation,
844 measure_units: Option<&MeasureUnits>,
845) -> String {
846 let unit_label = calendar_unit;
847 let first = format!("Unit '{unit_label}' is for calendar data.");
848 match expectation {
849 SuggestionExpectation::MeasureUnits => {
850 let list = measure_units
851 .map(format_measure_units_list)
852 .unwrap_or_default();
853 format!("{first} Valid '{type_name}' units are: {list}.")
854 }
855 SuggestionExpectation::Text => format!(
856 "{first} Please provide a text value in double quotes, for example `-> suggest \"my default value\"`."
857 ),
858 SuggestionExpectation::Number => format!(
859 "{first} Please provide a number, for example `-> suggest 42`."
860 ),
861 SuggestionExpectation::Boolean => format!(
862 "{first} Please provide true or false, for example `-> suggest true`."
863 ),
864 SuggestionExpectation::Date => format!(
865 "{first} Please provide a date, for example `-> suggest 2024-06-15`."
866 ),
867 SuggestionExpectation::Time => format!(
868 "{first} Please provide a time, for example `-> suggest 09:00:00`."
869 ),
870 SuggestionExpectation::Ratio | SuggestionExpectation::RatioRange => format!(
871 "{first} Please provide a ratio, for example `-> suggest 25%`."
872 ),
873 SuggestionExpectation::NumberRange => format!(
874 "{first} Please provide a number range, for example `-> suggest 10...100`."
875 ),
876 SuggestionExpectation::DateRange => format!(
877 "{first} Please provide a date range, for example `-> suggest 2024-01-01...2024-12-31`."
878 ),
879 SuggestionExpectation::TimeRange => format!(
880 "{first} Please provide a time range, for example `-> suggest 09:00...17:00`."
881 ),
882 SuggestionExpectation::MeasureRange => format!(
883 "{first} Please provide a range with units valid for '{type_name}', for example `-> suggest 30 kilogram...35 kilogram`."
884 ),
885 }
886}
887
888fn measure_suggestion_wrong_shape_error(type_name: &str, traits: &[MeasureTrait]) -> String {
889 let example = if traits.contains(&MeasureTrait::Duration) {
890 "4 week"
891 } else if traits.contains(&MeasureTrait::Calendar) {
892 "3 month"
893 } else {
894 "30 kilogram"
895 };
896 format!(
897 "Please provide a value with a unit valid for '{type_name}', for example `-> suggest {example}`."
898 )
899}
900
901fn reject_calendar_for_suggestion(
902 value: &crate::literals::Value,
903 type_name: &str,
904 expectation: SuggestionExpectation,
905 measure_units: Option<&MeasureUnits>,
906) -> Result<(), String> {
907 if let crate::literals::Value::NumberWithUnit(_, unit) = value {
908 if calendar_unit_factor(unit).is_some() {
909 return Err(suggestion_value_mismatch_error(
910 unit,
911 type_name,
912 expectation,
913 measure_units,
914 ));
915 }
916 }
917 Ok(())
918}
919
920fn value_kind_name(v: &crate::literals::Value) -> &'static str {
922 use crate::literals::Value;
923 match v {
924 Value::Number(_) => "number",
925 Value::NumberWithUnit(_, _) => "number_with_unit",
926 Value::Text(_) => "text",
927 Value::Date(_) => "date",
928 Value::Time(_) => "time",
929 Value::Boolean(_) => "boolean",
930 Value::Range(_, _) => "range",
931 }
932}
933
934fn require_suggestion_range_endpoints<'a>(
935 args: &'a [CommandArg],
936 type_name: &str,
937 expectation: SuggestionExpectation,
938 measure_units: Option<&MeasureUnits>,
939) -> Result<(&'a crate::literals::Value, &'a crate::literals::Value), String> {
940 match require_literal(args, "suggest")? {
941 crate::literals::Value::NumberWithUnit(_, unit)
942 if calendar_unit_factor(unit).is_some() =>
943 {
944 Err(suggestion_value_mismatch_error(
945 unit,
946 type_name,
947 expectation,
948 measure_units,
949 ))
950 }
951 crate::literals::Value::Range(left, right) => Ok((left.as_ref(), right.as_ref())),
952 _ => Err(match expectation {
953 SuggestionExpectation::NumberRange => {
954 "Please provide a number range, for example `-> suggest 10...100`.".to_string()
955 }
956 SuggestionExpectation::DateRange => {
957 "Please provide a date range, for example `-> suggest 2024-01-01...2024-12-31`."
958 .to_string()
959 }
960 SuggestionExpectation::RatioRange => {
961 "Please provide a ratio range, for example `-> suggest 10%...50%`.".to_string()
962 }
963 SuggestionExpectation::MeasureRange => format!(
964 "Please provide a range with units valid for '{type_name}', for example `-> suggest 30 kilogram...35 kilogram`."
965 ),
966 _ => unreachable!("BUG: require_suggestion_range_endpoints called with non-range expectation"),
967 }),
968 }
969}
970
971fn lift_parser_decimal(decimal: rust_decimal::Decimal) -> Result<RationalInteger, String> {
972 crate::computation::rational::decimal_to_rational(decimal)
973 .map_err(|failure| format!("literal failed rational lift: {failure}"))
974}
975
976pub fn range_element_type_specification(
978 range_spec: &TypeSpecification,
979) -> Option<TypeSpecification> {
980 range_spec.element_from_range()
981}
982
983fn range_endpoints_compatible(left: &LemmaType, right: &LemmaType) -> bool {
984 match (&left.specifications, &right.specifications) {
985 (TypeSpecification::Date { .. }, TypeSpecification::Date { .. }) => true,
986 (TypeSpecification::Time { .. }, TypeSpecification::Time { .. }) => true,
987 (TypeSpecification::Number { .. }, TypeSpecification::Number { .. }) => true,
988 (TypeSpecification::Measure { .. }, TypeSpecification::Measure { .. }) => {
989 left.same_measure_family(right)
990 || left.compatible_with_anonymous_measure(right)
991 || right.compatible_with_anonymous_measure(left)
992 }
993 (TypeSpecification::Ratio { .. }, TypeSpecification::Ratio { .. }) => true,
994 _ => false,
995 }
996}
997
998pub fn range_type_specification_from_endpoints(
1000 left: &LemmaType,
1001 right: &LemmaType,
1002) -> Option<TypeSpecification> {
1003 if !range_endpoints_compatible(left, right) {
1004 return None;
1005 }
1006 left.specifications.range_from_element()
1007}
1008
1009pub type UnitTokenResolver<'a> = dyn Fn(&str) -> Result<String, String> + 'a;
1011
1012fn bare_declared_unit_on_element(
1014 element_spec: &TypeSpecification,
1015 token: &str,
1016) -> Result<String, String> {
1017 match element_spec {
1018 TypeSpecification::Measure { units, .. } => Ok(units.get(token)?.name.clone()),
1019 TypeSpecification::Ratio { units, .. } => Ok(units.get(token)?.name.clone()),
1020 other => panic!("BUG: bare_declared_unit_on_element on non-measure/ratio element {other}"),
1021 }
1022}
1023
1024pub fn resolve_endpoint_unit_token(
1030 token: &str,
1031 element_spec: &TypeSpecification,
1032 resolve_unit: Option<&UnitTokenResolver<'_>>,
1033) -> Result<String, String> {
1034 match bare_declared_unit_on_element(element_spec, token) {
1035 Ok(bare) => Ok(bare),
1036 Err(declared_err) => {
1037 let Some(resolve) = resolve_unit else {
1038 return Err(declared_err);
1039 };
1040 let bare = resolve(token)?;
1041 bare_declared_unit_on_element(element_spec, &bare).map_err(|_| {
1042 format!("Unit '{token}' resolves to '{bare}', which is not declared on this type")
1043 })
1044 }
1045 }
1046}
1047
1048pub(crate) fn lift_range_endpoint(
1054 value: &crate::parsing::ast::Value,
1055 element_spec: &TypeSpecification,
1056 resolve_unit: Option<&UnitTokenResolver<'_>>,
1057) -> Result<TypedLiteral, String> {
1058 use crate::parsing::ast::Value;
1059 match value {
1060 Value::NumberWithUnit(magnitude, unit_token) => {
1061 let bare = resolve_endpoint_unit_token(unit_token, element_spec, resolve_unit)?;
1062 let rewritten = Value::NumberWithUnit(*magnitude, bare.clone());
1063 let kind = parser_value_to_value_kind(&rewritten, element_spec, None)?;
1064 let lemma_type = match &kind {
1065 ValueKind::Measure(_) | ValueKind::Ratio(_) => {
1066 Arc::new(LemmaType::primitive(element_spec.clone()).with_unit(bare))
1067 }
1068 _ => Arc::new(LemmaType::primitive(element_spec.clone())),
1069 };
1070 Ok(TypedLiteral {
1071 value: kind,
1072 lemma_type,
1073 })
1074 }
1075 _ => {
1076 let kind = value_to_semantic(value)?;
1077 Ok(TypedLiteral {
1078 value: kind,
1079 lemma_type: Arc::new(LemmaType::primitive(element_spec.clone())),
1080 })
1081 }
1082 }
1083}
1084
1085fn literal_value_from_parser_value(
1086 value: &crate::parsing::ast::Value,
1087) -> Result<TypedLiteral, String> {
1088 use crate::parsing::ast::Value;
1089
1090 match value {
1091 Value::Number(n) => Ok(TypedLiteral::number(lift_parser_decimal(*n)?)),
1092 Value::Text(s) => Ok(TypedLiteral::text(s.clone())),
1093 Value::Date(dt) => Ok(TypedLiteral::date(date_time_to_semantic(dt))),
1094 Value::Time(t) => Ok(TypedLiteral::time(time_to_semantic(t))),
1095 Value::Boolean(b) => Ok(TypedLiteral::from_bool(bool::from(*b))),
1096 Value::NumberWithUnit(n, unit) => Ok(TypedLiteral::number_interpreted_as_measure(
1097 lift_parser_decimal(*n)?,
1098 unit.clone(),
1099 )),
1100 Value::Range(left, right) => {
1101 let left = literal_value_from_parser_value(left)?;
1102 let right = literal_value_from_parser_value(right)?;
1103 let compatible = match (
1104 &left.lemma_type.specifications,
1105 &right.lemma_type.specifications,
1106 ) {
1107 (TypeSpecification::Date { .. }, TypeSpecification::Date { .. }) => true,
1108 (TypeSpecification::Time { .. }, TypeSpecification::Time { .. }) => true,
1109 (TypeSpecification::Number { .. }, TypeSpecification::Number { .. }) => true,
1110 (TypeSpecification::Measure { .. }, TypeSpecification::Measure { .. }) => {
1111 left.lemma_type.same_measure_family(&right.lemma_type)
1112 || left
1113 .lemma_type
1114 .compatible_with_anonymous_measure(&right.lemma_type)
1115 || right
1116 .lemma_type
1117 .compatible_with_anonymous_measure(&left.lemma_type)
1118 }
1119 (TypeSpecification::Ratio { .. }, TypeSpecification::Ratio { .. }) => true,
1120 _ => false,
1121 };
1122 if !compatible {
1123 return Err(format!(
1124 "range endpoints must have the same supported base type, got {} and {}",
1125 left.lemma_type.name(),
1126 right.lemma_type.name()
1127 ));
1128 }
1129 Ok(TypedLiteral::range(left, right))
1130 }
1131 }
1132}
1133
1134fn decimal_to_u8(d: RationalInteger, ctx: &str) -> Result<u8, String> {
1136 if !d.is_integer() {
1137 return Err(format!(
1138 "{} requires a whole number, got fractional value",
1139 ctx
1140 ));
1141 }
1142 d.numer_to_u8()
1143 .ok_or_else(|| format!("{} value out of range for u8", ctx))
1144}
1145
1146fn decimal_to_usize(d: RationalInteger, ctx: &str) -> Result<usize, String> {
1148 if !d.is_integer() {
1149 return Err(format!(
1150 "{} requires a whole number, got fractional value",
1151 ctx
1152 ));
1153 }
1154 d.numer_to_usize()
1155 .ok_or_else(|| format!("{} value out of range for usize", ctx))
1156}
1157
1158fn ratio_bound_to_canonical_rational(
1164 args: &[CommandArg],
1165 cmd: &str,
1166 units: &RatioUnits,
1167) -> Result<RationalInteger, String> {
1168 use crate::computation::rational::{checked_div, decimal_to_rational};
1169 let lit = require_literal(args, cmd)?;
1170 match lit {
1171 crate::literals::Value::NumberWithUnit(magnitude, unit_name) => {
1172 let unit = units.get(unit_name.as_str())?;
1173 let magnitude_rational = decimal_to_rational(*magnitude)
1174 .map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
1175 checked_div(&magnitude_rational, &unit.value)
1176 .map_err(|failure| format!("{cmd}: unit conversion failed: {failure}"))
1177 }
1178 other => Err(format!(
1179 "{cmd} requires a ratio literal with a unit, got {}",
1180 value_kind_name(other)
1181 )),
1182 }
1183}
1184
1185fn require_decimal_literal(args: &[CommandArg], cmd: &str) -> Result<RationalInteger, String> {
1186 use crate::computation::rational::decimal_to_rational;
1187 match require_literal(args, cmd)? {
1188 crate::literals::Value::Number(d) => decimal_to_rational(*d)
1189 .map_err(|failure| format!("{} literal failed rational lift: {}", cmd, failure)),
1190 other => Err(format!(
1191 "{} requires a number literal, got {}",
1192 cmd,
1193 value_kind_name(other)
1194 )),
1195 }
1196}
1197
1198enum UnitConstraintField {
1199 Minimum,
1200 Maximum,
1201 SuggestionMagnitude,
1202}
1203
1204pub(crate) fn measure_declared_bound_to_canonical(
1205 magnitude: &RationalInteger,
1206 unit_name: &str,
1207 units: &MeasureUnits,
1208 type_name: &str,
1209 command: &str,
1210) -> Result<RationalInteger, String> {
1211 use crate::computation::rational::checked_mul;
1212 let unit = units.get(unit_name).map_err(|_| {
1213 format!(
1214 "Unit '{unit_name}' is not defined on '{type_name}'. Valid units are: {}.",
1215 format_measure_units_list(units)
1216 )
1217 })?;
1218 checked_mul(magnitude, &unit.factor)
1219 .map_err(|failure| format!("{command}: unit conversion overflow: {failure}"))
1220}
1221
1222fn parse_measure_declared_bound(
1223 args: &[CommandArg],
1224 cmd: &str,
1225 units: &MeasureUnits,
1226 type_name: &str,
1227) -> Result<(RationalInteger, String), String> {
1228 use crate::computation::rational::decimal_to_rational;
1229 let lit = require_literal(args, cmd)?;
1230 let (magnitude, unit_name) = match lit {
1231 crate::literals::Value::NumberWithUnit(n, unit) => (*n, unit.clone()),
1232 other => {
1233 return Err(format!(
1234 "{cmd} requires a measure literal with a unit, got {}",
1235 value_kind_name(other)
1236 ));
1237 }
1238 };
1239 units.get(unit_name.as_str()).map_err(|_| {
1240 format!(
1241 "Unit '{unit_name}' is not defined on '{type_name}'. Valid units are: {}.",
1242 format_measure_units_list(units)
1243 )
1244 })?;
1245 let magnitude_rational = decimal_to_rational(magnitude)
1246 .map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
1247 Ok((magnitude_rational, unit_name))
1248}
1249
1250fn sync_measure_units_from_canonical(
1251 units: &mut MeasureUnits,
1252 canonical: &RationalInteger,
1253 field: UnitConstraintField,
1254) -> Result<(), String> {
1255 use crate::computation::rational::checked_div;
1256 for unit in &mut units.0 {
1257 let magnitude = checked_div(canonical, &unit.factor).map_err(|failure| {
1258 format!(
1259 "cannot derive per-unit constraint for unit '{}': {failure}",
1260 unit.name
1261 )
1262 })?;
1263 match field {
1264 UnitConstraintField::Minimum => unit.minimum = Some(magnitude),
1265 UnitConstraintField::Maximum => unit.maximum = Some(magnitude),
1266 UnitConstraintField::SuggestionMagnitude => unit.suggestion_magnitude = Some(magnitude),
1267 }
1268 }
1269 Ok(())
1270}
1271
1272fn sync_ratio_units_from_canonical(
1273 units: &mut RatioUnits,
1274 canonical: &RationalInteger,
1275 field: UnitConstraintField,
1276) -> Result<(), String> {
1277 use crate::computation::rational::checked_mul;
1278 for unit in &mut units.0 {
1279 let magnitude = checked_mul(canonical, &unit.value).map_err(|failure| {
1280 format!(
1281 "cannot derive per-unit constraint for ratio unit '{}': {failure}",
1282 unit.name
1283 )
1284 })?;
1285 match field {
1286 UnitConstraintField::Minimum => unit.minimum = Some(magnitude),
1287 UnitConstraintField::Maximum => unit.maximum = Some(magnitude),
1288 UnitConstraintField::SuggestionMagnitude => unit.suggestion_magnitude = Some(magnitude),
1289 }
1290 }
1291 Ok(())
1292}
1293
1294fn sync_measure_suggestion_units(
1295 units: &mut MeasureUnits,
1296 default: &ValueKind,
1297 type_name: &str,
1298) -> Result<(), String> {
1299 let ValueKind::Measure(magnitude) = default else {
1300 return Ok(());
1301 };
1302 let unit_name = units
1303 .iter()
1304 .find(|unit| unit.is_canonical_factor())
1305 .or_else(|| units.iter().next())
1306 .map(|unit| unit.name.as_str())
1307 .expect(
1308 "BUG: Measure suggestion value requires at least one declared unit on the measure type",
1309 );
1310 units.get(unit_name).map_err(|_| {
1311 format!("Suggestion unit '{unit_name}' is not defined on measure type '{type_name}'.")
1312 })?;
1313 sync_measure_units_from_canonical(units, magnitude, UnitConstraintField::SuggestionMagnitude)
1314}
1315
1316pub(crate) fn finalize_measure_unit_constraint_magnitudes(
1317 specification: &mut TypeSpecification,
1318 declared_suggestion: Option<&ValueKind>,
1319 type_name: &str,
1320) -> Result<(), String> {
1321 let TypeSpecification::Measure {
1322 minimum,
1323 maximum,
1324 units,
1325 ..
1326 } = specification
1327 else {
1328 return Ok(());
1329 };
1330
1331 if let Some(bound) = minimum.as_ref() {
1332 let canonical =
1333 measure_declared_bound_to_canonical(&bound.0, &bound.1, units, type_name, "minimum")?;
1334 sync_measure_units_from_canonical(units, &canonical, UnitConstraintField::Minimum)?;
1335 }
1336 if let Some(bound) = maximum.as_ref() {
1337 let canonical =
1338 measure_declared_bound_to_canonical(&bound.0, &bound.1, units, type_name, "maximum")?;
1339 sync_measure_units_from_canonical(units, &canonical, UnitConstraintField::Maximum)?;
1340 }
1341 if let Some(default) = declared_suggestion {
1342 sync_measure_suggestion_units(units, default, type_name)?;
1343 }
1344
1345 if minimum.is_some() {
1346 for unit in units.iter() {
1347 assert!(
1348 unit.minimum.is_some(),
1349 "BUG: type '{type_name}' has minimum but unit '{}' missing per-unit minimum after finalize",
1350 unit.name
1351 );
1352 }
1353 }
1354 if maximum.is_some() {
1355 for unit in units.iter() {
1356 assert!(
1357 unit.maximum.is_some(),
1358 "BUG: type '{type_name}' has maximum but unit '{}' missing per-unit maximum after finalize",
1359 unit.name
1360 );
1361 }
1362 }
1363 if declared_suggestion.is_some() {
1364 for unit in units.iter() {
1365 assert!(
1366 unit.suggestion_magnitude.is_some(),
1367 "BUG: type '{type_name}' has default but unit '{}' missing per-unit default after finalize",
1368 unit.name
1369 );
1370 }
1371 }
1372
1373 Ok(())
1374}
1375
1376fn sync_ratio_suggestion_units(units: &mut RatioUnits, default: &ValueKind) -> Result<(), String> {
1377 let ValueKind::Ratio(canonical) = default else {
1378 return Ok(());
1379 };
1380 sync_ratio_units_from_canonical(units, canonical, UnitConstraintField::SuggestionMagnitude)
1381}
1382
1383fn option_name(arg: &CommandArg, cmd: &str) -> Result<String, String> {
1389 match arg {
1390 CommandArg::Literal(crate::literals::Value::Text(s)) => Ok(s.clone()),
1391 CommandArg::Label(name) => Ok(name.clone()),
1392 CommandArg::Literal(other) => Err(format!(
1393 "{} requires a text literal or identifier, got {}",
1394 cmd,
1395 value_kind_name(other)
1396 )),
1397 CommandArg::UnitExpr(_) => Err(format!(
1398 "{} requires a text literal or identifier, got a unit expression",
1399 cmd
1400 )),
1401 }
1402}
1403
1404fn label_name(arg: &CommandArg, cmd: &str) -> Result<String, String> {
1405 match arg {
1406 CommandArg::Label(name) => Ok(name.clone()),
1407 CommandArg::Literal(other) => Err(format!(
1408 "{} requires an identifier, got {}",
1409 cmd,
1410 value_kind_name(other)
1411 )),
1412 CommandArg::UnitExpr(_) => Err(format!(
1413 "{} requires an identifier, got a unit expression",
1414 cmd
1415 )),
1416 }
1417}
1418
1419fn measure_trait_name(measure_trait: MeasureTrait) -> &'static str {
1420 match measure_trait {
1421 MeasureTrait::Duration => "duration",
1422 MeasureTrait::Calendar => "calendar",
1423 }
1424}
1425
1426fn parse_measure_trait(args: &[CommandArg]) -> Result<MeasureTrait, String> {
1427 if args.len() != 1 {
1428 return Err("trait requires exactly one identifier argument".to_string());
1429 }
1430 match label_name(&args[0], "trait")?
1431 .trim()
1432 .to_lowercase()
1433 .as_str()
1434 {
1435 "duration" => Ok(MeasureTrait::Duration),
1436 "calendar" => Ok(MeasureTrait::Calendar),
1437 other => Err(format!("Unknown measure trait '{}'", other)),
1438 }
1439}
1440
1441fn validate_calendar_trait_requirements(units: &MeasureUnits) -> Result<(), String> {
1442 let month_unit = units
1443 .iter()
1444 .find(|unit| unit.name == "month")
1445 .ok_or_else(|| {
1446 "trait calendar requires a canonical 'month' unit declared before 'trait calendar'"
1447 .to_string()
1448 })?;
1449 if !month_unit.is_canonical_factor() {
1450 return Err("trait calendar requires unit month 1".to_string());
1451 }
1452 Ok(())
1453}
1454
1455fn validate_duration_trait_requirements(units: &MeasureUnits) -> Result<(), String> {
1456 let second_unit = units
1457 .iter()
1458 .find(|unit| unit.name == "second")
1459 .ok_or_else(|| {
1460 "trait duration requires a canonical 'second' unit declared before 'trait duration'"
1461 .to_string()
1462 })?;
1463 if !second_unit.is_canonical_factor() {
1464 return Err("trait duration requires unit second 1".to_string());
1465 }
1466 Ok(())
1467}
1468
1469fn require_date_literal(args: &[CommandArg], cmd: &str) -> Result<DateTimeValue, String> {
1471 match require_literal(args, cmd)? {
1472 crate::literals::Value::Date(dt) => Ok(dt.clone()),
1473 other => Err(format!(
1474 "{} requires a date literal (e.g. 2024-01-01), got {}",
1475 cmd,
1476 value_kind_name(other)
1477 )),
1478 }
1479}
1480
1481fn require_time_literal(args: &[CommandArg], cmd: &str) -> Result<TimeValue, String> {
1483 match require_literal(args, cmd)? {
1484 crate::literals::Value::Time(t) => Ok(t.clone()),
1485 other => Err(format!(
1486 "{} requires a time literal (e.g. 12:30:00), got {}",
1487 cmd,
1488 value_kind_name(other)
1489 )),
1490 }
1491}
1492
1493#[must_use]
1495pub fn default_help_for_primitive(kind: PrimitiveKind) -> &'static str {
1496 use PrimitiveKind::*;
1497 match kind {
1498 Boolean => "Whether this holds (true or false).",
1499 Number => "A dimensionless number.",
1500 NumberRange => "The lower and upper bound of the number range.",
1501 Text => "A text value.",
1502 Measure => "A numeric amount in one of this type's units.",
1503 MeasureRange => "The lower and upper bound of the measure range.",
1504 Ratio => "A ratio in one of this type's units (e.g. percent).",
1505 RatioRange => "The lower and upper bound of the ratio range.",
1506 Date => "A date, or a date and time with optional timezone.",
1507 DateRange => "The start date and end date of the date range.",
1508 Time => "A time of day, with optional timezone.",
1509 TimeRange => "The start time and end time of the time range.",
1510 }
1511}
1512
1513impl TypeSpecification {
1514 pub fn boolean() -> Self {
1515 TypeSpecification::Boolean {
1516 help: default_help_for_primitive(PrimitiveKind::Boolean).to_string(),
1517 }
1518 }
1519 pub fn measure() -> Self {
1520 TypeSpecification::Measure {
1521 minimum: None,
1522 maximum: None,
1523 decimals: None,
1524 units: MeasureUnits::new(),
1525 traits: Vec::new(),
1526 decomposition: None,
1527 unit: None,
1528 help: default_help_for_primitive(PrimitiveKind::Measure).to_string(),
1529 }
1530 }
1531 pub fn number() -> Self {
1532 TypeSpecification::Number {
1533 minimum: None,
1534 maximum: None,
1535 decimals: None,
1536 help: default_help_for_primitive(PrimitiveKind::Number).to_string(),
1537 }
1538 }
1539 pub fn number_range() -> Self {
1540 TypeSpecification::NumberRange {
1541 lower: None,
1542 upper: None,
1543 minimum: None,
1544 maximum: None,
1545 help: default_help_for_primitive(PrimitiveKind::NumberRange).to_string(),
1546 }
1547 }
1548 pub fn ratio() -> Self {
1549 TypeSpecification::Ratio {
1550 minimum: None,
1551 maximum: None,
1552 decimals: None,
1553 units: RatioUnits(vec![
1554 RatioUnit {
1555 name: "percent".to_string(),
1556 value: crate::computation::rational::rational_new(100, 1),
1557 minimum: None,
1558 maximum: None,
1559 suggestion_magnitude: None,
1560 },
1561 RatioUnit {
1562 name: "permille".to_string(),
1563 value: crate::computation::rational::rational_new(1000, 1),
1564 minimum: None,
1565 maximum: None,
1566 suggestion_magnitude: None,
1567 },
1568 ]),
1569 unit: None,
1570 help: default_help_for_primitive(PrimitiveKind::Ratio).to_string(),
1571 }
1572 }
1573 pub fn ratio_range() -> Self {
1574 TypeSpecification::RatioRange {
1575 lower: None,
1576 upper: None,
1577 minimum: None,
1578 maximum: None,
1579 units: match TypeSpecification::ratio() {
1580 TypeSpecification::Ratio { units, .. } => units,
1581 _ => unreachable!("BUG: ratio constructor must return a ratio type"),
1582 },
1583 help: default_help_for_primitive(PrimitiveKind::RatioRange).to_string(),
1584 }
1585 }
1586 pub fn text() -> Self {
1587 TypeSpecification::Text {
1588 length: None,
1589 options: vec![],
1590 help: default_help_for_primitive(PrimitiveKind::Text).to_string(),
1591 }
1592 }
1593 pub fn date() -> Self {
1594 TypeSpecification::Date {
1595 minimum: None,
1596 maximum: None,
1597 help: default_help_for_primitive(PrimitiveKind::Date).to_string(),
1598 }
1599 }
1600 pub fn date_range() -> Self {
1601 TypeSpecification::DateRange {
1602 lower: None,
1603 upper: None,
1604 minimum: None,
1605 maximum: None,
1606 help: default_help_for_primitive(PrimitiveKind::DateRange).to_string(),
1607 }
1608 }
1609 pub fn time() -> Self {
1610 TypeSpecification::Time {
1611 minimum: None,
1612 maximum: None,
1613 help: default_help_for_primitive(PrimitiveKind::Time).to_string(),
1614 }
1615 }
1616 pub fn time_range() -> Self {
1617 TypeSpecification::TimeRange {
1618 lower: None,
1619 upper: None,
1620 minimum: None,
1621 maximum: None,
1622 help: default_help_for_primitive(PrimitiveKind::TimeRange).to_string(),
1623 }
1624 }
1625 pub fn measure_range() -> Self {
1626 TypeSpecification::MeasureRange {
1627 lower: None,
1628 upper: None,
1629 minimum: None,
1630 maximum: None,
1631 units: MeasureUnits::new(),
1632 decomposition: None,
1633 help: default_help_for_primitive(PrimitiveKind::MeasureRange).to_string(),
1634 }
1635 }
1636
1637 #[must_use]
1639 pub fn element_from_range(&self) -> Option<Self> {
1640 match self {
1641 TypeSpecification::NumberRange { lower, upper, .. } => {
1642 Some(TypeSpecification::Number {
1643 minimum: lower.clone(),
1644 maximum: upper.clone(),
1645 decimals: None,
1646 help: String::new(),
1647 })
1648 }
1649 TypeSpecification::MeasureRange {
1650 lower,
1651 upper,
1652 units,
1653 decomposition,
1654 ..
1655 } => Some(TypeSpecification::Measure {
1656 minimum: lower.clone(),
1657 maximum: upper.clone(),
1658 decimals: None,
1659 units: units.clone(),
1660 traits: Vec::new(),
1661 decomposition: decomposition.clone(),
1662 unit: None,
1663 help: String::new(),
1664 }),
1665 TypeSpecification::DateRange { lower, upper, .. } => Some(TypeSpecification::Date {
1666 minimum: lower.clone(),
1667 maximum: upper.clone(),
1668 help: String::new(),
1669 }),
1670 TypeSpecification::TimeRange { lower, upper, .. } => Some(TypeSpecification::Time {
1671 minimum: lower.clone(),
1672 maximum: upper.clone(),
1673 help: String::new(),
1674 }),
1675 TypeSpecification::RatioRange {
1676 lower,
1677 upper,
1678 units,
1679 ..
1680 } => Some(TypeSpecification::Ratio {
1681 minimum: lower.clone(),
1682 maximum: upper.clone(),
1683 decimals: None,
1684 units: units.clone(),
1685 unit: None,
1686 help: String::new(),
1687 }),
1688 _ => None,
1689 }
1690 }
1691
1692 #[must_use]
1694 pub fn range_from_element(&self) -> Option<Self> {
1695 match self {
1696 TypeSpecification::Number {
1697 minimum, maximum, ..
1698 } => Some(TypeSpecification::NumberRange {
1699 lower: minimum.clone(),
1700 upper: maximum.clone(),
1701 minimum: None,
1702 maximum: None,
1703 help: default_help_for_primitive(PrimitiveKind::NumberRange).to_string(),
1704 }),
1705 TypeSpecification::Measure {
1706 minimum,
1707 maximum,
1708 units,
1709 decomposition,
1710 ..
1711 } => Some(TypeSpecification::MeasureRange {
1712 lower: minimum.clone(),
1713 upper: maximum.clone(),
1714 minimum: None,
1715 maximum: None,
1716 units: units.clone(),
1717 decomposition: decomposition.clone(),
1718 help: default_help_for_primitive(PrimitiveKind::MeasureRange).to_string(),
1719 }),
1720 TypeSpecification::Date {
1721 minimum, maximum, ..
1722 } => Some(TypeSpecification::DateRange {
1723 lower: minimum.clone(),
1724 upper: maximum.clone(),
1725 minimum: None,
1726 maximum: None,
1727 help: default_help_for_primitive(PrimitiveKind::DateRange).to_string(),
1728 }),
1729 TypeSpecification::Time {
1730 minimum, maximum, ..
1731 } => Some(TypeSpecification::TimeRange {
1732 lower: minimum.clone(),
1733 upper: maximum.clone(),
1734 minimum: None,
1735 maximum: None,
1736 help: default_help_for_primitive(PrimitiveKind::TimeRange).to_string(),
1737 }),
1738 TypeSpecification::Ratio {
1739 minimum,
1740 maximum,
1741 units,
1742 ..
1743 } => Some(TypeSpecification::RatioRange {
1744 lower: minimum.clone(),
1745 upper: maximum.clone(),
1746 minimum: None,
1747 maximum: None,
1748 units: units.clone(),
1749 help: default_help_for_primitive(PrimitiveKind::RatioRange).to_string(),
1750 }),
1751 _ => None,
1752 }
1753 }
1754
1755 #[must_use]
1757 pub fn minimum_decimal(&self) -> Option<Decimal> {
1758 match self {
1759 TypeSpecification::Number { minimum, .. }
1760 | TypeSpecification::Ratio { minimum, .. } => minimum.as_ref().map(|bound| {
1761 bound
1762 .try_to_decimal()
1763 .expect("BUG: planned minimum must convert to decimal")
1764 }),
1765 TypeSpecification::Measure { minimum, .. } => minimum.as_ref().map(|(bound, _unit)| {
1766 bound
1767 .try_to_decimal()
1768 .expect("BUG: planned minimum must convert to decimal")
1769 }),
1770 _ => None,
1771 }
1772 }
1773
1774 #[must_use]
1776 pub fn maximum_decimal(&self) -> Option<Decimal> {
1777 match self {
1778 TypeSpecification::Number { maximum, .. }
1779 | TypeSpecification::Ratio { maximum, .. } => maximum.as_ref().map(|bound| {
1780 bound
1781 .try_to_decimal()
1782 .expect("BUG: planned maximum must convert to decimal")
1783 }),
1784 TypeSpecification::Measure { maximum, .. } => maximum.as_ref().map(|(bound, _unit)| {
1785 bound
1786 .try_to_decimal()
1787 .expect("BUG: planned maximum must convert to decimal")
1788 }),
1789 _ => None,
1790 }
1791 }
1792
1793 pub fn veto() -> Self {
1794 TypeSpecification::Veto { message: None }
1795 }
1796
1797 pub fn apply_constraint(
1809 &mut self,
1810 type_name: &str,
1811 command: TypeConstraintCommand,
1812 args: &[CommandArg],
1813 declared_suggestion: &mut Option<RawSuggestion>,
1814 declared_fill: &mut Option<RawSuggestion>,
1815 resolve_unit: Option<&UnitTokenResolver<'_>>,
1816 ) -> Result<(), String> {
1817 if command == TypeConstraintCommand::Trait
1818 && !matches!(&self, TypeSpecification::Measure { .. })
1819 {
1820 return Err("trait command is only valid on measure types".to_string());
1821 }
1822 match self {
1823 TypeSpecification::Boolean { help } => match command {
1824 TypeConstraintCommand::Help => {
1825 apply_type_help_command(help, args)?;
1826 }
1827 TypeConstraintCommand::Suggest | TypeConstraintCommand::Fill => {
1828 let (target, cmd) = match command {
1829 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
1830 TypeConstraintCommand::Fill => (&mut *declared_fill, "fill"),
1831 _ => unreachable!("BUG: only Suggest or Fill in this arm"),
1832 };
1833 let lit = require_literal(args, cmd)?;
1834 reject_calendar_for_suggestion(
1835 lit,
1836 type_name,
1837 SuggestionExpectation::Boolean,
1838 None,
1839 )?;
1840 match lit {
1841 crate::literals::Value::Boolean(bv) => {
1842 *target =
1843 Some(RawSuggestion::Value(ValueKind::Boolean(bool::from(bv))));
1844 }
1845 _ => {
1846 return Err(
1847 "Please provide true or false, for example `-> suggest true`."
1848 .to_string(),
1849 );
1850 }
1851 }
1852 }
1853 other => {
1854 return Err(format!(
1855 "Invalid command '{}' for boolean type. Valid commands: help, suggest, fill",
1856 other
1857 ));
1858 }
1859 },
1860 TypeSpecification::Measure {
1861 decimals,
1862 minimum,
1863 maximum,
1864 units,
1865 traits,
1866 help,
1867 ..
1868 } => match command {
1869 TypeConstraintCommand::Decimals => {
1870 let d = require_decimal_literal(args, "decimals")?;
1871 *decimals = Some(decimal_to_u8(d, "decimals")?);
1872 }
1873 TypeConstraintCommand::Unit => {
1874 let (unit_name, value, derived_measure_factors) = match args {
1875 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
1876 (name.clone(), *v, Vec::new())
1877 }
1878 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Expr(
1879 prefix,
1880 factors,
1881 ))] => {
1882 let raw: Vec<(String, i32)> = factors
1883 .iter()
1884 .map(|f| (f.measure_ref.clone(), f.exp))
1885 .collect();
1886 (name.clone(), *prefix, raw)
1887 }
1888 _ => {
1889 return Err(
1890 "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')"
1891 .to_string(),
1892 );
1893 }
1894 };
1895 if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
1896 let new_factor = crate::computation::rational::decimal_to_rational(value)
1897 .map_err(|failure| failure.to_string())?;
1898 if existing.factor != new_factor
1899 || existing.derived_measure_factors != derived_measure_factors
1900 {
1901 return Err(format!(
1902 "Unit '{unit_name}' is already defined in this type's inherited units; \
1903 cannot change factor or decomposition. Add a new unit name instead."
1904 ));
1905 }
1906 } else {
1907 units.0.push(MeasureUnit::from_decimal_factor(
1908 unit_name,
1909 value,
1910 derived_measure_factors,
1911 )?);
1912 }
1913 }
1914 TypeConstraintCommand::Trait => {
1915 let measure_trait = parse_measure_trait(args)?;
1916 if traits.contains(&measure_trait) {
1917 return Err(format!(
1918 "Duplicate trait '{}' for measure type.",
1919 measure_trait_name(measure_trait)
1920 ));
1921 }
1922 if measure_trait == MeasureTrait::Duration {
1923 validate_duration_trait_requirements(units)?;
1924 }
1925 if measure_trait == MeasureTrait::Calendar {
1926 validate_calendar_trait_requirements(units)?;
1927 }
1928 traits.push(measure_trait);
1929 }
1930 TypeConstraintCommand::Minimum => {
1931 *minimum = Some(parse_measure_declared_bound(
1932 args, "minimum", units, type_name,
1933 )?);
1934 }
1935 TypeConstraintCommand::Maximum => {
1936 *maximum = Some(parse_measure_declared_bound(
1937 args, "maximum", units, type_name,
1938 )?);
1939 }
1940 TypeConstraintCommand::Help => {
1941 apply_type_help_command(help, args)?;
1942 }
1943 TypeConstraintCommand::Suggest | TypeConstraintCommand::Fill => {
1944 let (target, cmd) = match command {
1945 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
1946 TypeConstraintCommand::Fill => (&mut *declared_fill, "fill"),
1947 _ => unreachable!("BUG: only Suggest or Fill in this arm"),
1948 };
1949 let lit = require_literal(args, cmd)?;
1950 if !traits.contains(&MeasureTrait::Calendar) {
1951 reject_calendar_for_suggestion(
1952 lit,
1953 type_name,
1954 SuggestionExpectation::MeasureUnits,
1955 Some(units),
1956 )?;
1957 }
1958 match lit {
1959 crate::literals::Value::NumberWithUnit(_, _) => {
1960 let (magnitude, unit_name) =
1961 parse_measure_declared_bound(args, cmd, units, type_name)?;
1962 *target = Some(RawSuggestion::Measure {
1963 magnitude,
1964 unit_name,
1965 });
1966 }
1967 _ => {
1968 return Err(measure_suggestion_wrong_shape_error(type_name, traits));
1969 }
1970 }
1971 }
1972 _ => {
1973 return Err(format!(
1974 "Invalid command '{}' for measure type. Valid commands: unit, trait, minimum, maximum, decimals, help, suggest, fill",
1975 command
1976 ));
1977 }
1978 },
1979 TypeSpecification::Number {
1980 decimals,
1981 minimum,
1982 maximum,
1983 help,
1984 } => match command {
1985 TypeConstraintCommand::Decimals => {
1986 let d = require_decimal_literal(args, "decimals")?;
1987 *decimals = Some(decimal_to_u8(d, "decimals")?);
1988 }
1989 TypeConstraintCommand::Unit => {
1990 return Err(
1991 "Invalid command 'unit' for number type. Number types are dimensionless and cannot have units. Use 'measure' type instead.".to_string()
1992 );
1993 }
1994 TypeConstraintCommand::Minimum => {
1995 *minimum = Some(require_decimal_literal(args, "minimum")?);
1996 }
1997 TypeConstraintCommand::Maximum => {
1998 *maximum = Some(require_decimal_literal(args, "maximum")?);
1999 }
2000 TypeConstraintCommand::Help => {
2001 apply_type_help_command(help, args)?;
2002 }
2003 TypeConstraintCommand::Suggest | TypeConstraintCommand::Fill => {
2004 let (target, cmd) = match command {
2005 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2006 TypeConstraintCommand::Fill => (&mut *declared_fill, "fill"),
2007 _ => unreachable!("BUG: only Suggest or Fill in this arm"),
2008 };
2009 let lit = require_literal(args, cmd)?;
2010 reject_calendar_for_suggestion(
2011 lit,
2012 type_name,
2013 SuggestionExpectation::Number,
2014 None,
2015 )?;
2016 match lit {
2017 crate::literals::Value::Number(d) => {
2018 *target = Some(RawSuggestion::Value(ValueKind::Number(
2019 lift_parser_decimal(*d)?,
2020 )));
2021 }
2022 _ => {
2023 return Err(
2024 "Please provide a number, for example `-> suggest 42`.".to_string()
2025 );
2026 }
2027 }
2028 }
2029 _ => {
2030 return Err(format!(
2031 "Invalid command '{}' for number type. Valid commands: minimum, maximum, decimals, help, suggest, fill",
2032 command
2033 ));
2034 }
2035 },
2036 TypeSpecification::NumberRange {
2037 lower,
2038 upper,
2039 minimum,
2040 maximum,
2041 help,
2042 } => match command {
2043 TypeConstraintCommand::Lower => {
2044 *lower = Some(require_decimal_literal(args, "lower")?);
2045 }
2046 TypeConstraintCommand::Upper => {
2047 *upper = Some(require_decimal_literal(args, "upper")?);
2048 }
2049 TypeConstraintCommand::Minimum => {
2050 let width = require_decimal_literal(args, "minimum")?;
2051 reject_negative_width_magnitude(&width, "minimum")?;
2052 *minimum = Some(width);
2053 }
2054 TypeConstraintCommand::Maximum => {
2055 let width = require_decimal_literal(args, "maximum")?;
2056 reject_negative_width_magnitude(&width, "maximum")?;
2057 *maximum = Some(width);
2058 }
2059 TypeConstraintCommand::Help => {
2060 apply_type_help_command(help, args)?;
2061 }
2062 TypeConstraintCommand::Suggest | TypeConstraintCommand::Fill => {
2063 let target = match command {
2064 TypeConstraintCommand::Suggest => &mut *declared_suggestion,
2065 TypeConstraintCommand::Fill => &mut *declared_fill,
2066 _ => unreachable!("BUG: only Suggest or Fill in this arm"),
2067 };
2068 let (left, right) = require_suggestion_range_endpoints(
2069 args,
2070 type_name,
2071 SuggestionExpectation::NumberRange,
2072 None,
2073 )?;
2074 let left = literal_value_from_parser_value(left)?;
2075 let right = literal_value_from_parser_value(right)?;
2076 if !left.lemma_type.is_number() || !right.lemma_type.is_number() {
2077 return Err(
2078 "Please provide a number range, for example `-> suggest 10...100`."
2079 .to_string(),
2080 );
2081 }
2082 *target = Some(RawSuggestion::Value(ValueKind::Range(
2083 Box::new(left),
2084 Box::new(right),
2085 )));
2086 }
2087 _ => {
2088 return Err(format!(
2089 "Invalid command '{}' for number range type. Valid commands: lower, upper, minimum, maximum, help, suggest, fill",
2090 command
2091 ));
2092 }
2093 },
2094 TypeSpecification::Ratio {
2095 decimals,
2096 minimum,
2097 maximum,
2098 units,
2099 unit: _,
2100 help,
2101 } => match command {
2102 TypeConstraintCommand::Decimals => {
2103 let d = require_decimal_literal(args, "decimals")?;
2104 *decimals = Some(decimal_to_u8(d, "decimals")?);
2105 }
2106 TypeConstraintCommand::Unit => {
2107 let (unit_name, value_dec) = match args {
2108 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
2109 (name.clone(), *v)
2110 }
2111 _ => {
2112 return Err(
2113 "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."
2114 .to_string(),
2115 );
2116 }
2117 };
2118 let value = crate::computation::rational::decimal_to_rational(value_dec)
2119 .map_err(|failure| {
2120 format!(
2121 "ratio unit value is not exactly representable as a rational: {}",
2122 failure
2123 )
2124 })?;
2125 if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
2126 if existing.value != value {
2127 return Err(format!(
2128 "Unit '{unit_name}' is already defined in this type's inherited units; \
2129 cannot change factor. Add a new unit name instead."
2130 ));
2131 }
2132 } else {
2133 units.0.push(RatioUnit {
2134 name: unit_name,
2135 value,
2136 minimum: None,
2137 maximum: None,
2138 suggestion_magnitude: None,
2139 });
2140 }
2141 }
2142 TypeConstraintCommand::Minimum => {
2143 let canonical = ratio_bound_to_canonical_rational(args, "minimum", units)?;
2144 sync_ratio_units_from_canonical(
2145 units,
2146 &canonical,
2147 UnitConstraintField::Minimum,
2148 )?;
2149 *minimum = Some(canonical);
2150 }
2151 TypeConstraintCommand::Maximum => {
2152 let canonical = ratio_bound_to_canonical_rational(args, "maximum", units)?;
2153 sync_ratio_units_from_canonical(
2154 units,
2155 &canonical,
2156 UnitConstraintField::Maximum,
2157 )?;
2158 *maximum = Some(canonical);
2159 }
2160 TypeConstraintCommand::Help => {
2161 apply_type_help_command(help, args)?;
2162 }
2163 TypeConstraintCommand::Suggest | TypeConstraintCommand::Fill => {
2164 let (target, cmd) = match command {
2165 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2166 TypeConstraintCommand::Fill => (&mut *declared_fill, "fill"),
2167 _ => unreachable!("BUG: only Suggest or Fill in this arm"),
2168 };
2169 let lit = require_literal(args, cmd)?;
2170 reject_calendar_for_suggestion(
2171 lit,
2172 type_name,
2173 SuggestionExpectation::Ratio,
2174 None,
2175 )?;
2176 match lit {
2177 crate::literals::Value::NumberWithUnit(_, unit_name) => {
2178 let element_spec = TypeSpecification::Ratio {
2179 decimals: *decimals,
2180 minimum: minimum.clone(),
2181 maximum: maximum.clone(),
2182 units: units.clone(),
2183 unit: None,
2184 help: help.clone(),
2185 };
2186 let value = parser_value_to_value_kind(lit, &element_spec, None)?;
2187 sync_ratio_suggestion_units(units, &value)?;
2188 *target = Some(RawSuggestion::UnitBound {
2189 value,
2190 unit_name: unit_name.clone(),
2191 });
2192 }
2193 other => {
2194 return Err(format!(
2195 "suggest requires a ratio literal with a unit, got {}. Please provide a ratio value with a unit, for example `-> suggest 25%`.",
2196 value_kind_name(other)
2197 ));
2198 }
2199 }
2200 }
2201 _ => {
2202 return Err(format!(
2203 "Invalid command '{}' for ratio type. Valid commands: unit, minimum, maximum, decimals, help, suggest, fill",
2204 command
2205 ));
2206 }
2207 },
2208 TypeSpecification::RatioRange {
2209 lower,
2210 upper,
2211 minimum,
2212 maximum,
2213 units,
2214 help,
2215 } => match command {
2216 TypeConstraintCommand::Unit => {
2217 let (unit_name, value_dec) = match args {
2218 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
2219 (name.clone(), *v)
2220 }
2221 _ => {
2222 return Err(
2223 "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."
2224 .to_string(),
2225 );
2226 }
2227 };
2228 let value = crate::computation::rational::decimal_to_rational(value_dec)
2229 .map_err(|e| {
2230 format!(
2231 "ratio unit value is not exactly representable as a rational: {e}"
2232 )
2233 })?;
2234 if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
2235 if existing.value != value {
2236 return Err(format!(
2237 "Unit '{unit_name}' is already defined in this type's inherited units; \
2238 cannot change factor. Add a new unit name instead."
2239 ));
2240 }
2241 } else {
2242 units.0.push(RatioUnit {
2243 name: unit_name,
2244 value,
2245 minimum: None,
2246 maximum: None,
2247 suggestion_magnitude: None,
2248 });
2249 }
2250 }
2251 TypeConstraintCommand::Lower => {
2252 *lower = Some(ratio_bound_to_canonical_rational(args, "lower", units)?);
2253 }
2254 TypeConstraintCommand::Upper => {
2255 *upper = Some(ratio_bound_to_canonical_rational(args, "upper", units)?);
2256 }
2257 TypeConstraintCommand::Minimum => {
2258 let width = ratio_bound_to_canonical_rational(args, "minimum", units)?;
2259 reject_negative_width_magnitude(&width, "minimum")?;
2260 *minimum = Some(width);
2261 }
2262 TypeConstraintCommand::Maximum => {
2263 let width = ratio_bound_to_canonical_rational(args, "maximum", units)?;
2264 reject_negative_width_magnitude(&width, "maximum")?;
2265 *maximum = Some(width);
2266 }
2267 TypeConstraintCommand::Help => {
2268 apply_type_help_command(help, args)?;
2269 }
2270 TypeConstraintCommand::Suggest | TypeConstraintCommand::Fill => {
2271 let target = match command {
2272 TypeConstraintCommand::Suggest => &mut *declared_suggestion,
2273 TypeConstraintCommand::Fill => &mut *declared_fill,
2274 _ => unreachable!("BUG: only Suggest or Fill in this arm"),
2275 };
2276 let (left, right) = require_suggestion_range_endpoints(
2277 args,
2278 type_name,
2279 SuggestionExpectation::RatioRange,
2280 None,
2281 )?;
2282 let element_spec = TypeSpecification::RatioRange {
2283 lower: lower.clone(),
2284 upper: upper.clone(),
2285 minimum: minimum.clone(),
2286 maximum: maximum.clone(),
2287 units: units.clone(),
2288 help: help.clone(),
2289 }
2290 .element_from_range()
2291 .expect("BUG: RatioRange must define element_from_range");
2292 let left = lift_range_endpoint(left, &element_spec, resolve_unit)?;
2293 let right = lift_range_endpoint(right, &element_spec, resolve_unit)?;
2294 if !left.lemma_type.is_ratio() || !right.lemma_type.is_ratio() {
2295 return Err(
2296 "Please provide a ratio range, for example `-> suggest 10%...50%`."
2297 .to_string(),
2298 );
2299 }
2300 *target = Some(RawSuggestion::Value(ValueKind::Range(
2301 Box::new(left),
2302 Box::new(right),
2303 )));
2304 }
2305 _ => {
2306 return Err(format!(
2307 "Invalid command '{}' for ratio range type. Valid commands: unit, lower, upper, minimum, maximum, help, suggest, fill",
2308 command
2309 ));
2310 }
2311 },
2312 TypeSpecification::Text {
2313 length,
2314 options,
2315 help,
2316 } => match command {
2317 TypeConstraintCommand::Option => {
2318 if args.len() != 1 {
2319 return Err("option takes exactly one argument".to_string());
2320 }
2321 options.push(option_name(&args[0], "option")?);
2322 }
2323 TypeConstraintCommand::Options => {
2324 let mut collected = Vec::with_capacity(args.len());
2325 for arg in args {
2326 collected.push(option_name(arg, "options")?);
2327 }
2328 *options = collected;
2329 }
2330 TypeConstraintCommand::Length => {
2331 let d = require_decimal_literal(args, "length")?;
2332 *length = Some(decimal_to_usize(d, "length")?);
2333 }
2334 TypeConstraintCommand::Help => {
2335 apply_type_help_command(help, args)?;
2336 }
2337 TypeConstraintCommand::Suggest | TypeConstraintCommand::Fill => {
2338 let (target, cmd) = match command {
2339 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2340 TypeConstraintCommand::Fill => (&mut *declared_fill, "fill"),
2341 _ => unreachable!("BUG: only Suggest or Fill in this arm"),
2342 };
2343 let lit = require_literal(args, cmd)?;
2344 reject_calendar_for_suggestion(
2345 lit,
2346 type_name,
2347 SuggestionExpectation::Text,
2348 None,
2349 )?;
2350 match lit {
2351 crate::literals::Value::Text(s) => {
2352 *target = Some(RawSuggestion::Value(ValueKind::Text(s.clone())));
2353 }
2354 _ => {
2355 return Err(
2356 "Please provide a text value in double quotes, for example `-> suggest \"my default value\"`."
2357 .to_string(),
2358 );
2359 }
2360 }
2361 }
2362 _ => {
2363 return Err(format!(
2364 "Invalid command '{}' for text type. Valid commands: options, length, help, suggest, fill",
2365 command
2366 ));
2367 }
2368 },
2369 TypeSpecification::Date {
2370 minimum,
2371 maximum,
2372 help,
2373 } => match command {
2374 TypeConstraintCommand::Minimum => {
2375 let dt = require_date_literal(args, "minimum")?;
2376 *minimum = Some(dt);
2377 }
2378 TypeConstraintCommand::Maximum => {
2379 let dt = require_date_literal(args, "maximum")?;
2380 *maximum = Some(dt);
2381 }
2382 TypeConstraintCommand::Help => {
2383 apply_type_help_command(help, args)?;
2384 }
2385 TypeConstraintCommand::Suggest | TypeConstraintCommand::Fill => {
2386 let (target, cmd) = match command {
2387 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2388 TypeConstraintCommand::Fill => (&mut *declared_fill, "fill"),
2389 _ => unreachable!("BUG: only Suggest or Fill in this arm"),
2390 };
2391 let lit = require_literal(args, cmd)?;
2392 reject_calendar_for_suggestion(
2393 lit,
2394 type_name,
2395 SuggestionExpectation::Date,
2396 None,
2397 )?;
2398 match lit {
2399 crate::literals::Value::Date(dt) => {
2400 *target = Some(RawSuggestion::Value(ValueKind::Date(
2401 date_time_to_semantic(dt),
2402 )));
2403 }
2404 _ => {
2405 return Err(
2406 "Please provide a date, for example `-> suggest 2024-06-15`."
2407 .to_string(),
2408 );
2409 }
2410 }
2411 }
2412 _ => {
2413 return Err(format!(
2414 "Invalid command '{}' for date type. Valid commands: minimum, maximum, help, suggest, fill",
2415 command
2416 ));
2417 }
2418 },
2419 TypeSpecification::DateRange {
2420 lower,
2421 upper,
2422 minimum,
2423 maximum,
2424 help,
2425 } => match command {
2426 TypeConstraintCommand::Lower => {
2427 *lower = Some(require_date_literal(args, "lower")?);
2428 }
2429 TypeConstraintCommand::Upper => {
2430 *upper = Some(require_date_literal(args, "upper")?);
2431 }
2432 TypeConstraintCommand::Minimum => {
2433 *minimum = Some(parse_unresolved_width_bound(args, "minimum")?);
2434 }
2435 TypeConstraintCommand::Maximum => {
2436 *maximum = Some(parse_unresolved_width_bound(args, "maximum")?);
2437 }
2438 TypeConstraintCommand::Help => {
2439 apply_type_help_command(help, args)?;
2440 }
2441 TypeConstraintCommand::Suggest | TypeConstraintCommand::Fill => {
2442 let target = match command {
2443 TypeConstraintCommand::Suggest => &mut *declared_suggestion,
2444 TypeConstraintCommand::Fill => &mut *declared_fill,
2445 _ => unreachable!("BUG: only Suggest or Fill in this arm"),
2446 };
2447 let (left, right) = require_suggestion_range_endpoints(
2448 args,
2449 type_name,
2450 SuggestionExpectation::DateRange,
2451 None,
2452 )?;
2453 let left = literal_value_from_parser_value(left)?;
2454 let right = literal_value_from_parser_value(right)?;
2455 if !left.lemma_type.is_date() || !right.lemma_type.is_date() {
2456 return Err(
2457 "Please provide a date range, for example `-> suggest 2024-01-01...2024-12-31`."
2458 .to_string(),
2459 );
2460 }
2461 *target = Some(RawSuggestion::Value(ValueKind::Range(
2462 Box::new(left),
2463 Box::new(right),
2464 )));
2465 }
2466 _ => {
2467 return Err(format!(
2468 "Invalid command '{}' for date range type. Valid commands: lower, upper, minimum, maximum, help, suggest, fill",
2469 command
2470 ));
2471 }
2472 },
2473 TypeSpecification::Time {
2474 minimum,
2475 maximum,
2476 help,
2477 } => match command {
2478 TypeConstraintCommand::Minimum => {
2479 let t = require_time_literal(args, "minimum")?;
2480 *minimum = Some(t);
2481 }
2482 TypeConstraintCommand::Maximum => {
2483 let t = require_time_literal(args, "maximum")?;
2484 *maximum = Some(t);
2485 }
2486 TypeConstraintCommand::Help => {
2487 apply_type_help_command(help, args)?;
2488 }
2489 TypeConstraintCommand::Suggest | TypeConstraintCommand::Fill => {
2490 let (target, cmd) = match command {
2491 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2492 TypeConstraintCommand::Fill => (&mut *declared_fill, "fill"),
2493 _ => unreachable!("BUG: only Suggest or Fill in this arm"),
2494 };
2495 let lit = require_literal(args, cmd)?;
2496 reject_calendar_for_suggestion(
2497 lit,
2498 type_name,
2499 SuggestionExpectation::Time,
2500 None,
2501 )?;
2502 match lit {
2503 crate::literals::Value::Time(t) => {
2504 *target =
2505 Some(RawSuggestion::Value(ValueKind::Time(time_to_semantic(t))));
2506 }
2507 _ => {
2508 return Err(
2509 "Please provide a time, for example `-> suggest 09:00:00`."
2510 .to_string(),
2511 );
2512 }
2513 }
2514 }
2515 _ => {
2516 return Err(format!(
2517 "Invalid command '{}' for time type. Valid commands: minimum, maximum, help, suggest, fill",
2518 command
2519 ));
2520 }
2521 },
2522 TypeSpecification::TimeRange {
2523 lower,
2524 upper,
2525 minimum,
2526 maximum,
2527 help,
2528 } => match command {
2529 TypeConstraintCommand::Lower => {
2530 *lower = Some(require_time_literal(args, "lower")?);
2531 }
2532 TypeConstraintCommand::Upper => {
2533 *upper = Some(require_time_literal(args, "upper")?);
2534 }
2535 TypeConstraintCommand::Minimum => {
2536 *minimum = Some(parse_unresolved_width_bound(args, "minimum")?);
2537 }
2538 TypeConstraintCommand::Maximum => {
2539 *maximum = Some(parse_unresolved_width_bound(args, "maximum")?);
2540 }
2541 TypeConstraintCommand::Help => {
2542 apply_type_help_command(help, args)?;
2543 }
2544 TypeConstraintCommand::Suggest | TypeConstraintCommand::Fill => {
2545 let target = match command {
2546 TypeConstraintCommand::Suggest => &mut *declared_suggestion,
2547 TypeConstraintCommand::Fill => &mut *declared_fill,
2548 _ => unreachable!("BUG: only Suggest or Fill in this arm"),
2549 };
2550 let (left, right) = require_suggestion_range_endpoints(
2551 args,
2552 type_name,
2553 SuggestionExpectation::TimeRange,
2554 None,
2555 )?;
2556 let left = literal_value_from_parser_value(left)?;
2557 let right = literal_value_from_parser_value(right)?;
2558 if !left.lemma_type.is_time() || !right.lemma_type.is_time() {
2559 return Err(
2560 "Please provide a time range, for example `-> suggest 09:00...17:00`."
2561 .to_string(),
2562 );
2563 }
2564 *target = Some(RawSuggestion::Value(ValueKind::Range(
2565 Box::new(left),
2566 Box::new(right),
2567 )));
2568 }
2569 _ => {
2570 return Err(format!(
2571 "Invalid command '{}' for time range type. Valid commands: lower, upper, minimum, maximum, help, suggest, fill",
2572 command
2573 ));
2574 }
2575 },
2576 TypeSpecification::MeasureRange {
2577 lower,
2578 upper,
2579 minimum,
2580 maximum,
2581 units,
2582 decomposition,
2583 help,
2584 } => match command {
2585 TypeConstraintCommand::Unit => {
2586 let (unit_name, value, derived_measure_factors) = match args {
2587 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
2588 (name.clone(), *v, Vec::new())
2589 }
2590 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Expr(
2591 prefix,
2592 factors,
2593 ))] => {
2594 let raw: Vec<(String, i32)> = factors
2595 .iter()
2596 .map(|f| (f.measure_ref.clone(), f.exp))
2597 .collect();
2598 (name.clone(), *prefix, raw)
2599 }
2600 _ => {
2601 return Err(
2602 "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')"
2603 .to_string(),
2604 );
2605 }
2606 };
2607 if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
2608 let new_factor = crate::computation::rational::decimal_to_rational(value)
2609 .map_err(|failure| failure.to_string())?;
2610 if existing.factor != new_factor
2611 || existing.derived_measure_factors != derived_measure_factors
2612 {
2613 return Err(format!(
2614 "Unit '{unit_name}' is already defined in this type's inherited units; \
2615 cannot change factor or decomposition. Add a new unit name instead."
2616 ));
2617 }
2618 } else {
2619 units.0.push(MeasureUnit::from_decimal_factor(
2620 unit_name,
2621 value,
2622 derived_measure_factors,
2623 )?);
2624 }
2625 }
2626 TypeConstraintCommand::Lower => {
2627 *lower = Some(parse_measure_declared_bound(
2628 args, "lower", units, type_name,
2629 )?);
2630 }
2631 TypeConstraintCommand::Upper => {
2632 *upper = Some(parse_measure_declared_bound(
2633 args, "upper", units, type_name,
2634 )?);
2635 }
2636 TypeConstraintCommand::Minimum => {
2637 let width = parse_measure_declared_bound(args, "minimum", units, type_name)?;
2638 reject_negative_width_magnitude(&width.0, "minimum")?;
2639 *minimum = Some(width);
2640 }
2641 TypeConstraintCommand::Maximum => {
2642 let width = parse_measure_declared_bound(args, "maximum", units, type_name)?;
2643 reject_negative_width_magnitude(&width.0, "maximum")?;
2644 *maximum = Some(width);
2645 }
2646 TypeConstraintCommand::Help => {
2647 apply_type_help_command(help, args)?;
2648 }
2649 TypeConstraintCommand::Suggest | TypeConstraintCommand::Fill => {
2650 let target = match command {
2651 TypeConstraintCommand::Suggest => &mut *declared_suggestion,
2652 TypeConstraintCommand::Fill => &mut *declared_fill,
2653 _ => unreachable!("BUG: only Suggest or Fill in this arm"),
2654 };
2655 let (left, right) = require_suggestion_range_endpoints(
2656 args,
2657 type_name,
2658 SuggestionExpectation::MeasureRange,
2659 Some(units),
2660 )?;
2661 let element_spec = TypeSpecification::MeasureRange {
2662 lower: lower.clone(),
2663 upper: upper.clone(),
2664 minimum: minimum.clone(),
2665 maximum: maximum.clone(),
2666 units: units.clone(),
2667 decomposition: decomposition.clone(),
2668 help: help.clone(),
2669 }
2670 .element_from_range()
2671 .expect("BUG: MeasureRange must define element_from_range");
2672 let left = lift_range_endpoint(left, &element_spec, resolve_unit)?;
2673 let right = lift_range_endpoint(right, &element_spec, resolve_unit)?;
2674 if !left.lemma_type.is_measure() || !right.lemma_type.is_measure() {
2675 return Err(format!(
2676 "Please provide a range with units valid for '{type_name}', for example `-> suggest 30 kilogram...35 kilogram`."
2677 ));
2678 }
2679 *target = Some(RawSuggestion::Value(ValueKind::Range(
2680 Box::new(left),
2681 Box::new(right),
2682 )));
2683 }
2684 _ => {
2685 return Err(format!(
2686 "Invalid command '{}' for measure range type. Valid commands: unit, lower, upper, minimum, maximum, help, suggest, fill",
2687 command
2688 ));
2689 }
2690 },
2691 TypeSpecification::Veto { .. } => {
2692 return Err(format!(
2693 "Invalid command '{}' for veto type. Veto is not a user-declarable type and cannot have constraints",
2694 command
2695 ));
2696 }
2697 TypeSpecification::Undetermined => {
2698 return Err(format!(
2699 "Invalid command '{}' for undetermined sentinel type. Undetermined is an internal type used during type inference and cannot have constraints",
2700 command
2701 ));
2702 }
2703 TypeSpecification::Spec { .. } => {
2704 return Err(format!(
2705 "Invalid command '{command}' for spec type. A spec instance has no data constraints"
2706 ));
2707 }
2708 }
2709 Ok(())
2710 }
2711}
2712
2713pub fn parse_number_unit(
2716 value_str: &str,
2717 type_spec: &TypeSpecification,
2718) -> Result<crate::parsing::ast::Value, String> {
2719 use crate::literals::{NumberWithUnit, RatioLiteral};
2720 use crate::parsing::ast::Value;
2721
2722 let trimmed = value_str.trim();
2723 match type_spec {
2724 TypeSpecification::Measure { units, .. } => {
2725 if units.is_empty() {
2726 unreachable!(
2727 "BUG: Measure type has no units; should have been validated during planning"
2728 );
2729 }
2730 match trimmed.parse::<NumberWithUnit>() {
2731 Ok(n) => {
2732 let unit = units.get(&n.1).map_err(|e| e.to_string())?;
2733 Ok(Value::NumberWithUnit(n.0, unit.name.clone()))
2734 }
2735 Err(e) => {
2736 if trimmed.split_whitespace().count() == 1 && !trimmed.is_empty() {
2737 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
2738 let example_unit = units
2739 .iter()
2740 .next()
2741 .expect("BUG: units non-empty after guard")
2742 .name
2743 .as_str();
2744 Err(format!(
2745 "Measure value must include a unit, for example: '{} {}'. Valid units: {}.",
2746 trimmed,
2747 example_unit,
2748 valid.join(", ")
2749 ))
2750 } else {
2751 Err(e)
2752 }
2753 }
2754 }
2755 }
2756 TypeSpecification::Ratio { units, .. } => {
2757 if units.is_empty() {
2758 unreachable!(
2759 "BUG: Ratio type has no units; should have been validated during planning"
2760 );
2761 }
2762 match trimmed.parse::<RatioLiteral>()? {
2763 RatioLiteral::Bare(_) => {
2764 Err("Ratio value requires a unit (e.g. '50%', '500 basis_points').".to_string())
2765 }
2766 RatioLiteral::Percent(n) => {
2767 let unit = units.get("percent").map_err(|e| e.to_string())?;
2768 Ok(Value::NumberWithUnit(n, unit.name.clone()))
2769 }
2770 RatioLiteral::Permille(n) => {
2771 let unit = units.get("permille").map_err(|e| e.to_string())?;
2772 Ok(Value::NumberWithUnit(n, unit.name.clone()))
2773 }
2774 RatioLiteral::Named { value, unit } => {
2775 let resolved = units.get(&unit).map_err(|e| e.to_string())?;
2776 Ok(Value::NumberWithUnit(value, resolved.name.clone()))
2777 }
2778 }
2779 }
2780 _ => Err("parse_number_unit only accepts Measure or Ratio type".to_string()),
2781 }
2782}
2783
2784pub fn parse_value_from_string(
2787 value_str: &str,
2788 type_spec: &TypeSpecification,
2789 source: &Source,
2790) -> Result<crate::parsing::ast::Value, Error> {
2791 use crate::parsing::ast::Value;
2792
2793 let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
2794
2795 let parse_range_value = |element_spec: TypeSpecification| -> Result<Value, Error> {
2796 let (left_str, right_str) = value_str.split_once("...").ok_or_else(|| {
2797 to_err("Range value must use '...' between the two endpoints".to_string())
2798 })?;
2799 if left_str.trim().is_empty() || right_str.trim().is_empty() {
2800 return Err(to_err(
2801 "Range value must contain a non-empty left and right endpoint".to_string(),
2802 ));
2803 }
2804 let left = parse_value_from_string(left_str.trim(), &element_spec, source)?;
2805 let right = parse_value_from_string(right_str.trim(), &element_spec, source)?;
2806 Ok(Value::Range(Box::new(left), Box::new(right)))
2807 };
2808
2809 match type_spec {
2810 TypeSpecification::Text { .. } => value_str
2811 .parse::<crate::literals::TextLiteral>()
2812 .map(|t| Value::Text(t.0))
2813 .map_err(to_err),
2814 TypeSpecification::Number { .. } => value_str
2815 .parse::<crate::literals::NumberLiteral>()
2816 .map(|n| Value::Number(n.0))
2817 .map_err(to_err),
2818 TypeSpecification::Measure { .. } => {
2819 parse_number_unit(value_str, type_spec).map_err(to_err)
2820 }
2821 TypeSpecification::Boolean { .. } => value_str
2822 .parse::<BooleanValue>()
2823 .map(Value::Boolean)
2824 .map_err(to_err),
2825 TypeSpecification::Date { .. } => {
2826 let date = value_str.parse::<DateTimeValue>().map_err(to_err)?;
2827 Ok(Value::Date(date))
2828 }
2829 TypeSpecification::Time { .. } => {
2830 let time = value_str.parse::<TimeValue>().map_err(to_err)?;
2831 Ok(Value::Time(time))
2832 }
2833 TypeSpecification::Ratio { .. } => {
2834 parse_number_unit(value_str, type_spec).map_err(to_err)
2835 }
2836 TypeSpecification::NumberRange { .. }
2837 | TypeSpecification::MeasureRange { .. }
2838 | TypeSpecification::DateRange { .. }
2839 | TypeSpecification::TimeRange { .. }
2840 | TypeSpecification::RatioRange { .. } => {
2841 let element_spec = range_element_type_specification(type_spec).unwrap_or_else(|| {
2842 unreachable!("BUG: range_element_type_specification missing arm for known range type")
2843 });
2844 parse_range_value(element_spec)
2845 }
2846 TypeSpecification::Veto { .. } => Err(to_err(
2847 "Veto type cannot be parsed from string".to_string(),
2848 )),
2849 TypeSpecification::Undetermined => unreachable!(
2850 "BUG: parse_value_from_string called with Undetermined sentinel type; this type exists only during type inference"
2851 ),
2852 TypeSpecification::Spec { .. } => Err(to_err(
2853 "spec values are not run-data scalars".to_string(),
2854 )),
2855 }
2856}
2857
2858#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2863#[serde(rename_all = "snake_case")]
2864pub enum SemanticCalendarUnit {
2865 Month,
2866 Year,
2867}
2868
2869impl fmt::Display for SemanticCalendarUnit {
2870 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2871 let s = match self {
2872 SemanticCalendarUnit::Month => "month",
2873 SemanticCalendarUnit::Year => "year",
2874 };
2875 write!(f, "{}", s)
2876 }
2877}
2878
2879pub fn semantic_calendar_unit_from_unit_name(unit_name: &str) -> SemanticCalendarUnit {
2880 match unit_name {
2881 "month" => SemanticCalendarUnit::Month,
2882 "year" => SemanticCalendarUnit::Year,
2883 other => unreachable!(
2884 "BUG: calendar measure signature unit must be month or year, got '{other}'"
2885 ),
2886 }
2887}
2888
2889pub fn semantic_calendar_unit_from_measure_signature(
2890 signature: &[(String, i32)],
2891) -> SemanticCalendarUnit {
2892 let unit_name = signature
2893 .first()
2894 .map(|(name, _)| name.as_str())
2895 .expect("BUG: calendar measure must carry a unit signature");
2896 semantic_calendar_unit_from_unit_name(unit_name)
2897}
2898
2899pub fn semantic_calendar_unit_from_measure_type(lemma_type: &LemmaType) -> SemanticCalendarUnit {
2900 if !lemma_type.is_calendar_like() {
2901 unreachable!(
2902 "BUG: semantic_calendar_unit_from_measure_type called on non-calendar type {}",
2903 lemma_type.name()
2904 );
2905 }
2906 let signature = lemma_type.measure_runtime_signature();
2907 if signature.is_empty() {
2908 return SemanticCalendarUnit::Month;
2909 }
2910 semantic_calendar_unit_from_measure_signature(&signature)
2911}
2912
2913#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2915pub enum SemanticConversionTarget {
2916 Type(PrimitiveKind),
2917 Unit {
2919 unit_name: String,
2920 owning_type: Arc<LemmaType>,
2922 },
2923}
2924
2925impl std::hash::Hash for SemanticConversionTarget {
2926 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
2927 match self {
2928 Self::Type(kind) => {
2929 0u8.hash(state);
2930 kind.hash(state);
2931 }
2932 Self::Unit {
2933 unit_name,
2934 owning_type,
2935 } => {
2936 1u8.hash(state);
2937 unit_name.hash(state);
2938 owning_type.hash(state);
2939 }
2940 }
2941 }
2942}
2943
2944impl SemanticConversionTarget {}
2945
2946impl fmt::Display for SemanticConversionTarget {
2947 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2948 match self {
2949 SemanticConversionTarget::Type(kind) => write!(f, "{kind}"),
2950 SemanticConversionTarget::Unit { unit_name, .. } => write!(f, "{unit_name}"),
2951 }
2952 }
2953}
2954
2955#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2957pub struct SemanticTimezone {
2958 pub offset_hours: i8,
2959 pub offset_minutes: u8,
2960}
2961
2962impl fmt::Display for SemanticTimezone {
2963 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2964 if self.offset_hours == 0 && self.offset_minutes == 0 {
2965 write!(f, "Z")
2966 } else {
2967 let sign = if self.offset_hours >= 0 { "+" } else { "-" };
2968 let hour = self.offset_hours.abs();
2969 write!(f, "{}{:02}:{:02}", sign, hour, self.offset_minutes)
2970 }
2971 }
2972}
2973
2974impl Serialize for SemanticTimezone {
2975 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2976 serializer.serialize_str(&self.to_string())
2977 }
2978}
2979
2980impl<'de> Deserialize<'de> for SemanticTimezone {
2981 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2982 let s = String::deserialize(deserializer)?;
2983 Self::from_str(&s).map_err(serde::de::Error::custom)
2984 }
2985}
2986
2987impl FromStr for SemanticTimezone {
2988 type Err = String;
2989
2990 fn from_str(s: &str) -> Result<Self, Self::Err> {
2991 let tz = TimezoneValue::from_str(s)?;
2992 Ok(Self {
2993 offset_hours: tz.offset_hours,
2994 offset_minutes: tz.offset_minutes,
2995 })
2996 }
2997}
2998
2999#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3001pub struct SemanticTime {
3002 pub hour: u32,
3003 pub minute: u32,
3004 pub second: u32,
3005 pub microsecond: u32,
3006 pub timezone: Option<SemanticTimezone>,
3007}
3008
3009impl fmt::Display for SemanticTime {
3010 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3011 write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
3012 if self.microsecond != 0 {
3013 write!(f, ".{:06}", self.microsecond)?;
3014 }
3015 if let Some(timezone) = &self.timezone {
3016 write!(f, "{}", timezone)?;
3017 }
3018 Ok(())
3019 }
3020}
3021
3022impl Serialize for SemanticTime {
3023 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3024 serializer.serialize_str(&self.to_string())
3025 }
3026}
3027
3028impl<'de> Deserialize<'de> for SemanticTime {
3029 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3030 let s = String::deserialize(deserializer)?;
3031 Self::from_str(&s).map_err(serde::de::Error::custom)
3032 }
3033}
3034
3035impl FromStr for SemanticTime {
3036 type Err = String;
3037
3038 fn from_str(s: &str) -> Result<Self, Self::Err> {
3039 Ok(time_to_semantic(&TimeValue::from_str(s)?))
3040 }
3041}
3042
3043#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
3045pub struct SemanticDateTime {
3046 pub year: i32,
3047 pub month: u32,
3048 pub day: u32,
3049 pub hour: u32,
3050 pub minute: u32,
3051 pub second: u32,
3052 pub microsecond: u32,
3053 pub timezone: Option<SemanticTimezone>,
3054}
3055
3056impl fmt::Display for SemanticDateTime {
3057 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3058 let has_time = self.hour != 0
3059 || self.minute != 0
3060 || self.second != 0
3061 || self.microsecond != 0
3062 || self.timezone.is_some();
3063 if !has_time {
3064 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
3065 } else {
3066 write!(
3067 f,
3068 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
3069 self.year, self.month, self.day, self.hour, self.minute, self.second
3070 )?;
3071 if self.microsecond != 0 {
3072 write!(f, ".{:06}", self.microsecond)?;
3073 }
3074 if let Some(tz) = &self.timezone {
3075 write!(f, "{}", tz)?;
3076 }
3077 Ok(())
3078 }
3079 }
3080}
3081
3082impl Serialize for SemanticDateTime {
3083 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3084 serializer.serialize_str(&self.to_string())
3085 }
3086}
3087
3088impl<'de> Deserialize<'de> for SemanticDateTime {
3089 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3090 let s = String::deserialize(deserializer)?;
3091 Self::from_str(&s).map_err(serde::de::Error::custom)
3092 }
3093}
3094
3095impl FromStr for SemanticDateTime {
3096 type Err = String;
3097
3098 fn from_str(s: &str) -> Result<Self, Self::Err> {
3099 Ok(date_time_to_semantic(&DateTimeValue::from_str(s)?))
3100 }
3101}
3102
3103#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3107pub enum RawSuggestion {
3108 Value(ValueKind),
3109 Measure {
3111 magnitude: RationalInteger,
3112 unit_name: String,
3113 },
3114 UnitBound {
3117 value: ValueKind,
3118 unit_name: String,
3119 },
3120}
3121
3122#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3129pub struct BoundValueKind {
3130 pub value: ValueKind,
3131 pub unit: Option<Arc<str>>,
3132}
3133
3134impl BoundValueKind {
3135 pub fn unbound(value: ValueKind) -> Self {
3136 Self { value, unit: None }
3137 }
3138
3139 pub fn with_binding(value: ValueKind, unit: Option<Arc<str>>) -> Self {
3140 Self { value, unit }
3141 }
3142
3143 pub fn to_literal(&self) -> LiteralValue {
3144 LiteralValue {
3145 value: self.value.clone(),
3146 }
3147 }
3148
3149 #[must_use]
3152 pub fn agree_or_left_binding(
3153 left: Option<&Arc<str>>,
3154 right: Option<&Arc<str>>,
3155 ) -> Option<Arc<str>> {
3156 match (left, right) {
3157 (Some(a), Some(b)) if a.as_ref() == b.as_ref() => Some(Arc::clone(a)),
3158 (Some(a), Some(_)) => Some(Arc::clone(a)),
3159 (Some(a), None) => Some(Arc::clone(a)),
3160 (None, Some(b)) => Some(Arc::clone(b)),
3161 (None, None) => None,
3162 }
3163 }
3164}
3165
3166pub fn bound_value_kind_from_raw_suggestion(
3167 raw: RawSuggestion,
3168 specifications: &TypeSpecification,
3169 type_name: &str,
3170) -> Result<BoundValueKind, String> {
3171 match raw {
3172 RawSuggestion::Value(vk) => Ok(BoundValueKind::unbound(vk)),
3173 RawSuggestion::UnitBound { value, unit_name } => Ok(BoundValueKind {
3174 value,
3175 unit: Some(Arc::from(unit_name)),
3176 }),
3177 RawSuggestion::Measure {
3178 magnitude,
3179 unit_name,
3180 } => {
3181 let TypeSpecification::Measure { units, .. } = specifications else {
3182 return Err(format!(
3183 "BUG: RawSuggestion::Measure for non-measure type '{type_name}'"
3184 ));
3185 };
3186 let canonical = measure_declared_bound_to_canonical(
3187 &magnitude, &unit_name, units, type_name, "suggest",
3188 )?;
3189 Ok(BoundValueKind {
3190 value: ValueKind::Measure(canonical),
3191 unit: Some(Arc::from(unit_name)),
3192 })
3193 }
3194 }
3195}
3196
3197#[must_use]
3201pub fn display_binding_unit(
3202 schema: &LemmaType,
3203 settled: Option<&BoundValueKind>,
3204 fill: Option<&BoundValueKind>,
3205 suggestion: Option<&BoundValueKind>,
3206) -> Option<String> {
3207 if schema.is_measure_range() || schema.is_ratio_range() {
3208 return None;
3209 }
3210 if let Some(unit) = schema.unit() {
3211 return Some(unit.to_string());
3212 }
3213 if let Some(unit) = settled.and_then(|bound| bound.unit.as_ref()) {
3214 return Some(unit.to_string());
3215 }
3216 if let Some(unit) = fill.and_then(|bound| bound.unit.as_ref()) {
3217 return Some(unit.to_string());
3218 }
3219 suggestion
3220 .and_then(|bound| bound.unit.as_ref())
3221 .map(|unit| unit.to_string())
3222}
3223
3224#[must_use]
3229pub fn lemma_type_with_display_binding(
3230 schema: &LemmaType,
3231 settled: Option<&BoundValueKind>,
3232 fill: Option<&BoundValueKind>,
3233 suggestion: Option<&BoundValueKind>,
3234) -> LemmaType {
3235 if schema.is_measure_range() || schema.is_ratio_range() {
3236 return schema.clone();
3237 }
3238 match display_binding_unit(schema, settled, fill, suggestion) {
3239 Some(unit) if schema.unit() != Some(unit.as_str()) => schema.clone().with_unit(unit),
3240 Some(_) | None => schema.clone(),
3241 }
3242}
3243
3244#[must_use]
3246pub fn lemma_type_arc_with_display_binding(
3247 schema: &Arc<LemmaType>,
3248 settled: Option<&BoundValueKind>,
3249 fill: Option<&BoundValueKind>,
3250 suggestion: Option<&BoundValueKind>,
3251) -> Arc<LemmaType> {
3252 if schema.is_measure_range() || schema.is_ratio_range() {
3253 return Arc::clone(schema);
3254 }
3255 match display_binding_unit(schema.as_ref(), settled, fill, suggestion) {
3256 Some(unit) if schema.unit() != Some(unit.as_str()) => {
3257 Arc::new(schema.as_ref().clone().with_unit(unit))
3258 }
3259 Some(_) | None => Arc::clone(schema),
3260 }
3261}
3262
3263#[must_use]
3268pub fn typed_literal_from_bound(
3269 bound: &BoundValueKind,
3270 lemma_type: &Arc<LemmaType>,
3271) -> TypedLiteral {
3272 let lemma_type = match (&bound.unit, &lemma_type.specifications) {
3273 (Some(unit), TypeSpecification::Measure { .. } | TypeSpecification::Ratio { .. })
3274 if lemma_type.unit() != Some(unit.as_ref()) =>
3275 {
3276 Arc::new(lemma_type.as_ref().clone().with_unit(unit.as_ref()))
3277 }
3278 _ => Arc::clone(lemma_type),
3279 };
3280 TypedLiteral {
3281 value: bound.value.clone(),
3282 lemma_type,
3283 }
3284}
3285
3286#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3293pub enum ValueKind {
3294 Number(RationalInteger),
3295 Measure(RationalInteger),
3301 Text(String),
3302 Date(SemanticDateTime),
3303 Time(SemanticTime),
3304 Boolean(bool),
3305 Ratio(RationalInteger),
3307 Range(Box<TypedLiteral>, Box<TypedLiteral>),
3309 Spec(SpecInstance),
3311}
3312
3313impl ValueKind {
3314 pub fn as_decimal_magnitude(&self) -> Result<Decimal, String> {
3316 match self {
3317 ValueKind::Number(n) | ValueKind::Measure(n) | ValueKind::Ratio(n) => {
3318 n.try_to_decimal().map_err(|failure| failure.to_string())
3319 }
3320 other => Err(format!("expected numeric value kind, got {other}")),
3321 }
3322 }
3323
3324 pub fn structural_byte_size(&self) -> usize {
3329 fn rational_byte_estimate(r: &RationalInteger) -> usize {
3330 let numer_bytes = (r.numer_magnitude_bits() as usize).div_ceil(8);
3331 let denom_bytes = (r.denom_magnitude_bits() as usize).div_ceil(8);
3332 numer_bytes.max(1) + denom_bytes.max(1)
3333 }
3334 match self {
3335 ValueKind::Number(r) => rational_byte_estimate(r),
3336 ValueKind::Measure(r) => rational_byte_estimate(r),
3337 ValueKind::Ratio(r) => rational_byte_estimate(r),
3338 ValueKind::Text(s) => s.len(),
3339 ValueKind::Date(_) => 30, ValueKind::Time(_) => 12, ValueKind::Boolean(_) => 5, ValueKind::Range(left, right) => {
3343 left.value.structural_byte_size() + 3 + right.value.structural_byte_size()
3344 }
3345 ValueKind::Spec(instance) => instance.structural_byte_size(),
3346 }
3347 }
3348}
3349
3350fn format_rational_magnitude_for_display(rational: &RationalInteger) -> String {
3351 rational.display_str()
3352}
3353
3354fn format_number_with_unit_for_display(rational: &RationalInteger, unit: &str) -> String {
3355 use crate::parsing::ast::Value;
3356 match rational.try_to_decimal() {
3357 Ok(decimal) => format!("{}", Value::NumberWithUnit(decimal, unit.to_string())),
3358 Err(_) => format!("{} {}", rational.display_str(), unit),
3359 }
3360}
3361
3362impl fmt::Display for ValueKind {
3363 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3364 match self {
3365 ValueKind::Number(rational) => {
3366 write!(f, "{}", format_rational_magnitude_for_display(rational))
3367 }
3368 ValueKind::Measure(rational) => {
3369 write!(f, "{}", format_rational_magnitude_for_display(rational))
3370 }
3371 ValueKind::Text(s) => write!(f, "{}", crate::parsing::ast::Value::Text(s.clone())),
3372 ValueKind::Ratio(rational) => {
3373 write!(f, "{}", format_rational_magnitude_for_display(rational))
3374 }
3375 ValueKind::Date(dt) => write!(f, "{}", dt),
3376 ValueKind::Time(t) => write!(
3377 f,
3378 "{}",
3379 crate::parsing::ast::Value::Time(crate::parsing::ast::TimeValue {
3380 hour: t.hour as u8,
3381 minute: t.minute as u8,
3382 second: t.second as u8,
3383 microsecond: t.microsecond,
3384 timezone: t
3385 .timezone
3386 .as_ref()
3387 .map(|tz| crate::parsing::ast::TimezoneValue {
3388 offset_hours: tz.offset_hours,
3389 offset_minutes: tz.offset_minutes,
3390 }),
3391 })
3392 ),
3393 ValueKind::Boolean(b) => write!(f, "{}", b),
3394 ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
3395 ValueKind::Spec(instance) => write!(f, "{instance}"),
3396 }
3397 }
3398}
3399
3400#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
3411pub struct PathSegment {
3412 pub uses: String,
3414 pub repository: Option<String>,
3417 pub spec: String,
3419}
3420
3421#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3425pub struct SpecInstance {
3426 pub prefix: Vec<PathSegment>,
3427}
3428
3429impl SpecInstance {
3430 pub fn structural_byte_size(&self) -> usize {
3431 self.prefix
3432 .iter()
3433 .map(|segment| {
3434 segment.uses.len()
3435 + segment.spec.len()
3436 + segment.repository.as_ref().map_or(0, String::len)
3437 })
3438 .sum()
3439 }
3440}
3441
3442impl fmt::Display for SpecInstance {
3443 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3444 for (index, segment) in self.prefix.iter().enumerate() {
3445 if index > 0 {
3446 write!(f, ".")?;
3447 }
3448 write!(f, "{}", segment.uses)?;
3449 }
3450 Ok(())
3451 }
3452}
3453
3454#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3456pub enum SpecMemberEnd {
3457 Rule(RulePath),
3458 Data(DataPath),
3459 Instance {
3461 instance: SpecInstance,
3462 identity: SpecIdentity,
3463 },
3464}
3465
3466#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3468pub struct SpecMemberTarget {
3469 pub instance: SpecInstance,
3470 pub end: SpecMemberEnd,
3471}
3472
3473#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
3477pub struct DataPath {
3478 pub segments: Vec<PathSegment>,
3480 pub data: String,
3482}
3483
3484impl DataPath {
3485 pub fn new(segments: Vec<PathSegment>, data: String) -> Self {
3487 Self { segments, data }
3488 }
3489
3490 pub fn local(data: String) -> Self {
3492 Self {
3493 segments: vec![],
3494 data,
3495 }
3496 }
3497
3498 pub fn input_key(&self) -> String {
3501 let mut s = String::new();
3502 for segment in &self.segments {
3503 s.push_str(&segment.uses);
3504 s.push('.');
3505 }
3506 s.push_str(&self.data);
3507 s
3508 }
3509}
3510
3511#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
3515pub struct RulePath {
3516 pub segments: Vec<PathSegment>,
3518 pub rule: String,
3520}
3521
3522impl RulePath {
3523 pub fn new(segments: Vec<PathSegment>, rule: String) -> Self {
3525 Self { segments, rule }
3526 }
3527
3528 pub fn input_key(&self) -> String {
3531 if self.segments.is_empty() {
3532 return self.rule.clone();
3533 }
3534 let mut s = String::new();
3535 for segment in &self.segments {
3536 s.push_str(&segment.uses);
3537 s.push('.');
3538 }
3539 s.push_str(&self.rule);
3540 s
3541 }
3542}
3543
3544#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3553pub struct Expression {
3554 pub kind: ExpressionKind,
3555 pub source_location: Option<Source>,
3556}
3557
3558impl Expression {
3559 pub fn with_source(kind: ExpressionKind, source_location: Option<Source>) -> Self {
3561 Self {
3562 kind,
3563 source_location,
3564 }
3565 }
3566
3567 pub fn collect_data_paths(&self, data: &mut std::collections::HashSet<DataPath>) {
3569 self.kind.collect_data_paths(data);
3570 }
3571}
3572
3573#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3575#[serde(rename_all = "snake_case")]
3576pub enum ExpressionKind {
3577 Literal(Box<TypedLiteral>),
3579 DataPath(DataPath),
3581 RulePath(RulePath),
3583 LogicalAnd(Arc<Expression>, Arc<Expression>),
3584 Arithmetic(Arc<Expression>, ArithmeticComputation, Arc<Expression>),
3585 Comparison(Arc<Expression>, ComparisonComputation, Arc<Expression>),
3586 UnitConversion(Arc<Expression>, SemanticConversionTarget),
3587 LogicalNegation(Arc<Expression>, NegationType),
3588 MathematicalComputation(MathematicalComputation, Arc<Expression>),
3589 Veto(VetoExpression),
3590 Now,
3592 DateRelative(DateRelativeKind, Arc<Expression>),
3594 DateCalendar(DateCalendarKind, CalendarPeriodUnit, Arc<Expression>),
3596 RangeLiteral(Arc<Expression>, Arc<Expression>),
3597 PastFutureRange(DateRelativeKind, Arc<Expression>),
3598 RangeContainment(Arc<Expression>, Arc<Expression>),
3599 RangeBound(RangeBound, Arc<Expression>),
3601 ResultIsVeto(Arc<Expression>),
3603 SpecMember {
3608 base: RulePath,
3609 path: Vec<String>,
3610 name: String,
3611 targets: Vec<SpecMemberTarget>,
3612 },
3613 Piecewise(Vec<(Arc<Expression>, Arc<Expression>)>),
3616}
3617
3618impl ExpressionKind {
3619 pub(crate) fn collect_data_paths(&self, data: &mut std::collections::HashSet<DataPath>) {
3621 match self {
3622 ExpressionKind::DataPath(fp) => {
3623 data.insert(fp.clone());
3624 }
3625 ExpressionKind::LogicalAnd(left, right) => {
3626 left.collect_data_paths(data);
3627 right.collect_data_paths(data);
3628 }
3629 ExpressionKind::Arithmetic(left, _, right)
3630 | ExpressionKind::Comparison(left, _, right)
3631 | ExpressionKind::RangeLiteral(left, right)
3632 | ExpressionKind::RangeContainment(left, right) => {
3633 left.collect_data_paths(data);
3634 right.collect_data_paths(data);
3635 }
3636 ExpressionKind::UnitConversion(inner, _)
3637 | ExpressionKind::LogicalNegation(inner, _)
3638 | ExpressionKind::MathematicalComputation(_, inner)
3639 | ExpressionKind::PastFutureRange(_, inner)
3640 | ExpressionKind::RangeBound(_, inner) => {
3641 inner.collect_data_paths(data);
3642 }
3643 ExpressionKind::DateRelative(_, date_expr) => {
3644 date_expr.collect_data_paths(data);
3645 }
3646 ExpressionKind::DateCalendar(_, _, date_expr) => {
3647 date_expr.collect_data_paths(data);
3648 }
3649 ExpressionKind::Literal(_)
3650 | ExpressionKind::RulePath(_)
3651 | ExpressionKind::Veto(_)
3652 | ExpressionKind::Now
3653 | ExpressionKind::SpecMember { .. } => {}
3654 ExpressionKind::ResultIsVeto(operand) => {
3655 operand.collect_data_paths(data);
3656 }
3657 ExpressionKind::Piecewise(arms) => {
3658 for (condition, result) in arms {
3659 condition.collect_data_paths(data);
3660 result.collect_data_paths(data);
3661 }
3662 }
3663 }
3664 }
3665}
3666
3667#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
3673pub enum TypeDefiningSpec {
3674 Local,
3676 Import,
3678}
3679
3680#[derive(Clone, Debug, Serialize, Deserialize)]
3682pub enum TypeExtends {
3683 Primitive,
3685 Custom {
3688 parent: String,
3689 family: String,
3690 defining_spec: TypeDefiningSpec,
3691 },
3692}
3693
3694impl PartialEq for TypeExtends {
3695 fn eq(&self, other: &Self) -> bool {
3696 match (self, other) {
3697 (TypeExtends::Primitive, TypeExtends::Primitive) => true,
3698 (
3699 TypeExtends::Custom {
3700 parent: lp,
3701 family: lf,
3702 defining_spec: ld,
3703 },
3704 TypeExtends::Custom {
3705 parent: rp,
3706 family: rf,
3707 defining_spec: rd,
3708 },
3709 ) => lp == rp && lf == rf && ld == rd,
3710 _ => false,
3711 }
3712 }
3713}
3714
3715impl Eq for TypeExtends {}
3716
3717impl std::hash::Hash for TypeExtends {
3718 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
3719 match self {
3720 TypeExtends::Primitive => {
3721 0u8.hash(state);
3722 }
3723 TypeExtends::Custom {
3724 parent,
3725 family,
3726 defining_spec,
3727 } => {
3728 1u8.hash(state);
3729 parent.hash(state);
3730 family.hash(state);
3731 defining_spec.hash(state);
3732 }
3733 }
3734 }
3735}
3736
3737impl TypeExtends {
3738 #[must_use]
3740 pub fn custom_local(parent: String, family: String) -> Self {
3741 TypeExtends::Custom {
3742 parent,
3743 family,
3744 defining_spec: TypeDefiningSpec::Local,
3745 }
3746 }
3747
3748 #[must_use]
3750 pub fn parent_name(&self) -> Option<&str> {
3751 match self {
3752 TypeExtends::Primitive => None,
3753 TypeExtends::Custom { parent, .. } => Some(parent.as_str()),
3754 }
3755 }
3756}
3757
3758#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
3763pub struct LemmaType {
3764 pub name: Option<String>,
3766 pub specifications: TypeSpecification,
3768 pub extends: TypeExtends,
3770}
3771
3772impl LemmaType {
3773 pub fn map_measure<F>(self, f: F) -> Self
3777 where
3778 F: FnOnce(
3779 MeasureUnits,
3780 Option<BaseMeasureVector>,
3781 ) -> (MeasureUnits, Option<BaseMeasureVector>),
3782 {
3783 let LemmaType {
3784 name,
3785 specifications,
3786 extends,
3787 } = self;
3788 let specifications = match specifications {
3789 TypeSpecification::Measure {
3790 minimum,
3791 maximum,
3792 decimals,
3793 units,
3794 traits,
3795 decomposition,
3796 unit,
3797 help,
3798 } => {
3799 let (units, decomposition) = f(units, decomposition);
3800 TypeSpecification::Measure {
3801 minimum,
3802 maximum,
3803 decimals,
3804 units,
3805 traits,
3806 decomposition,
3807 unit,
3808 help,
3809 }
3810 }
3811 other => other,
3812 };
3813 LemmaType {
3814 name,
3815 specifications,
3816 extends,
3817 }
3818 }
3819
3820 pub fn new(name: String, specifications: TypeSpecification, extends: TypeExtends) -> Self {
3822 Self {
3823 name: Some(name),
3824 specifications,
3825 extends,
3826 }
3827 }
3828
3829 pub fn without_name(specifications: TypeSpecification, extends: TypeExtends) -> Self {
3831 Self {
3832 name: None,
3833 specifications,
3834 extends,
3835 }
3836 }
3837
3838 pub fn primitive(specifications: TypeSpecification) -> Self {
3840 Self {
3841 name: None,
3842 specifications,
3843 extends: TypeExtends::Primitive,
3844 }
3845 }
3846
3847 #[must_use]
3851 pub fn with_unit(mut self, unit_name: impl Into<String>) -> Self {
3852 let unit_name = unit_name.into();
3853 match &mut self.specifications {
3854 TypeSpecification::Measure { unit, .. } | TypeSpecification::Ratio { unit, .. } => {
3855 *unit = Some(unit_name);
3856 }
3857 other => panic!(
3858 "BUG: with_unit called on non-measure/non-ratio type {}",
3859 other
3860 ),
3861 }
3862 self
3863 }
3864
3865 #[must_use]
3867 pub fn unit(&self) -> Option<&str> {
3868 match &self.specifications {
3869 TypeSpecification::Measure { unit, .. } | TypeSpecification::Ratio { unit, .. } => {
3870 unit.as_deref()
3871 }
3872 _ => None,
3873 }
3874 }
3875
3876 pub fn name(&self) -> String {
3878 self.name
3879 .clone()
3880 .unwrap_or_else(|| self.specifications.to_string())
3881 }
3882
3883 pub fn is_boolean(&self) -> bool {
3885 matches!(&self.specifications, TypeSpecification::Boolean { .. })
3886 }
3887
3888 pub fn matches_primitive_kind(&self, kind: PrimitiveKind) -> bool {
3889 matches!(
3890 (kind, &self.specifications),
3891 (PrimitiveKind::Number, TypeSpecification::Number { .. })
3892 | (PrimitiveKind::Text, TypeSpecification::Text { .. })
3893 | (PrimitiveKind::Boolean, TypeSpecification::Boolean { .. })
3894 | (PrimitiveKind::Date, TypeSpecification::Date { .. })
3895 | (PrimitiveKind::Time, TypeSpecification::Time { .. })
3896 | (PrimitiveKind::Ratio, TypeSpecification::Ratio { .. })
3897 | (PrimitiveKind::Measure, TypeSpecification::Measure { .. })
3898 )
3899 }
3900
3901 pub fn is_measure(&self) -> bool {
3903 matches!(&self.specifications, TypeSpecification::Measure { .. })
3904 }
3905
3906 pub fn is_measure_range(&self) -> bool {
3907 matches!(&self.specifications, TypeSpecification::MeasureRange { .. })
3908 }
3909
3910 pub fn is_number(&self) -> bool {
3912 matches!(&self.specifications, TypeSpecification::Number { .. })
3913 }
3914
3915 pub fn is_number_range(&self) -> bool {
3916 matches!(&self.specifications, TypeSpecification::NumberRange { .. })
3917 }
3918
3919 pub fn is_numeric(&self) -> bool {
3921 matches!(
3922 &self.specifications,
3923 TypeSpecification::Measure { .. } | TypeSpecification::Number { .. }
3924 )
3925 }
3926
3927 pub fn is_text(&self) -> bool {
3929 matches!(&self.specifications, TypeSpecification::Text { .. })
3930 }
3931
3932 pub fn is_date(&self) -> bool {
3934 matches!(&self.specifications, TypeSpecification::Date { .. })
3935 }
3936
3937 pub fn is_date_range(&self) -> bool {
3938 matches!(&self.specifications, TypeSpecification::DateRange { .. })
3939 }
3940
3941 pub fn is_time_range(&self) -> bool {
3942 matches!(&self.specifications, TypeSpecification::TimeRange { .. })
3943 }
3944
3945 pub fn is_time(&self) -> bool {
3947 matches!(&self.specifications, TypeSpecification::Time { .. })
3948 }
3949
3950 pub fn has_trait_duration(&self) -> bool {
3951 match &self.specifications {
3952 TypeSpecification::Measure { traits, .. } => traits.contains(&MeasureTrait::Duration),
3953 _ => false,
3954 }
3955 }
3956
3957 pub fn is_duration_like_measure(&self) -> bool {
3958 if !self.is_measure() {
3959 return false;
3960 }
3961 if self.has_trait_duration() {
3962 return true;
3963 }
3964 self.is_anonymous_measure()
3965 && self
3966 .measure_type_decomposition()
3967 .is_some_and(|d| *d == duration_decomposition())
3968 }
3969
3970 pub fn is_duration_like(&self) -> bool {
3971 self.is_duration_like_measure()
3972 }
3973
3974 pub fn has_trait_calendar(&self) -> bool {
3975 match &self.specifications {
3976 TypeSpecification::Measure { traits, .. } => traits.contains(&MeasureTrait::Calendar),
3977 _ => false,
3978 }
3979 }
3980
3981 pub fn is_calendar_like_measure(&self) -> bool {
3982 if !self.is_measure() {
3983 return false;
3984 }
3985 if self.has_trait_calendar() {
3986 return true;
3987 }
3988 self.is_anonymous_measure()
3989 && self
3990 .measure_type_decomposition()
3991 .is_some_and(|d| *d == calendar_decomposition())
3992 }
3993
3994 pub fn is_calendar_like(&self) -> bool {
3995 self.is_calendar_like_measure()
3996 }
3997
3998 pub fn is_ratio(&self) -> bool {
4000 matches!(&self.specifications, TypeSpecification::Ratio { .. })
4001 }
4002
4003 pub fn is_ratio_range(&self) -> bool {
4004 matches!(&self.specifications, TypeSpecification::RatioRange { .. })
4005 }
4006
4007 pub fn is_calendar_measure_range(&self) -> bool {
4008 matches!(
4009 &self.specifications,
4010 TypeSpecification::MeasureRange { decomposition: Some(decomposition), .. }
4011 if *decomposition == calendar_decomposition()
4012 )
4013 }
4014
4015 pub fn is_calendar_like_range(&self) -> bool {
4016 self.is_calendar_measure_range()
4017 }
4018
4019 pub fn is_range(&self) -> bool {
4020 matches!(
4021 &self.specifications,
4022 TypeSpecification::DateRange { .. }
4023 | TypeSpecification::TimeRange { .. }
4024 | TypeSpecification::NumberRange { .. }
4025 | TypeSpecification::MeasureRange { .. }
4026 | TypeSpecification::RatioRange { .. }
4027 )
4028 }
4029
4030 pub fn is_spec(&self) -> bool {
4032 matches!(&self.specifications, TypeSpecification::Spec { .. })
4033 }
4034
4035 #[must_use]
4037 pub fn spec_identity(&self) -> Option<&SpecIdentity> {
4038 match &self.specifications {
4039 TypeSpecification::Spec { spec } => Some(spec),
4040 _ => None,
4041 }
4042 }
4043
4044 pub fn vetoed(&self) -> bool {
4046 matches!(&self.specifications, TypeSpecification::Veto { .. })
4047 }
4048
4049 pub fn is_undetermined(&self) -> bool {
4051 matches!(&self.specifications, TypeSpecification::Undetermined)
4052 }
4053
4054 pub fn has_same_base_type(&self, other: &LemmaType) -> bool {
4056 use TypeSpecification::*;
4057 matches!(
4058 (&self.specifications, &other.specifications),
4059 (Boolean { .. }, Boolean { .. })
4060 | (Number { .. }, Number { .. })
4061 | (NumberRange { .. }, NumberRange { .. })
4062 | (Measure { .. }, Measure { .. })
4063 | (MeasureRange { .. }, MeasureRange { .. })
4064 | (Text { .. }, Text { .. })
4065 | (Date { .. }, Date { .. })
4066 | (DateRange { .. }, DateRange { .. })
4067 | (Time { .. }, Time { .. })
4068 | (TimeRange { .. }, TimeRange { .. })
4069 | (Ratio { .. }, Ratio { .. })
4070 | (RatioRange { .. }, RatioRange { .. })
4071 | (Veto { .. }, Veto { .. })
4072 | (Undetermined, Undetermined)
4073 | (Spec { .. }, Spec { .. })
4074 )
4075 }
4076
4077 #[must_use]
4079 pub fn measure_family_name(&self) -> Option<&str> {
4080 if !self.is_measure() {
4081 return None;
4082 }
4083 match &self.extends {
4084 TypeExtends::Custom { family, .. } => Some(family.as_str()),
4085 TypeExtends::Primitive => self.name.as_deref(),
4086 }
4087 }
4088
4089 #[must_use]
4091 pub fn ratio_family_name(&self) -> Option<&str> {
4092 if !self.is_ratio() {
4093 return None;
4094 }
4095 match &self.extends {
4096 TypeExtends::Custom { family, .. } => Some(family.as_str()),
4097 TypeExtends::Primitive => self.name.as_deref(),
4098 }
4099 }
4100
4101 #[must_use]
4103 pub(crate) fn unit_family_name(&self) -> Option<&str> {
4104 self.measure_family_name()
4105 .or_else(|| self.ratio_family_name())
4106 }
4107
4108 #[must_use]
4112 pub(crate) fn same_value_type(&self, other: &LemmaType) -> bool {
4113 if !self.has_same_base_type(other) {
4114 return false;
4115 }
4116 if self.is_measure() {
4117 return self.measure_type_decomposition() == other.measure_type_decomposition();
4118 }
4119 if let (
4120 TypeSpecification::Spec { spec: self_spec },
4121 TypeSpecification::Spec { spec: other_spec },
4122 ) = (&self.specifications, &other.specifications)
4123 {
4124 return self_spec == other_spec;
4125 }
4126 if self.is_measure_range() {
4127 let self_element = self
4128 .specifications
4129 .element_from_range()
4130 .expect("BUG: MeasureRange always defines element_from_range");
4131 let other_element = other
4132 .specifications
4133 .element_from_range()
4134 .expect("BUG: MeasureRange always defines element_from_range");
4135 match (&self_element, &other_element) {
4136 (
4137 TypeSpecification::Measure {
4138 decomposition: self_decomp,
4139 ..
4140 },
4141 TypeSpecification::Measure {
4142 decomposition: other_decomp,
4143 ..
4144 },
4145 ) => self_decomp == other_decomp,
4146 _ => unreachable!("BUG: element_from_range of MeasureRange yields Measure"),
4147 }
4148 } else {
4149 true
4150 }
4151 }
4152
4153 #[must_use]
4155 pub fn same_measure_family(&self, other: &LemmaType) -> bool {
4156 if !self.is_measure() || !other.is_measure() {
4157 return false;
4158 }
4159 match (self.measure_family_name(), other.measure_family_name()) {
4160 (Some(self_family), Some(other_family)) => self_family == other_family,
4161 _ => false,
4162 }
4163 }
4164
4165 #[must_use]
4166 pub fn compatible_with_anonymous_measure(&self, other: &LemmaType) -> bool {
4167 if !self.is_measure() || !other.is_measure() {
4168 return false;
4169 }
4170 if !self.is_anonymous_measure() && !other.is_anonymous_measure() {
4171 return false;
4172 }
4173 match (
4174 self.measure_type_decomposition(),
4175 other.measure_type_decomposition(),
4176 ) {
4177 (Some(a), Some(b)) => a == b,
4178 _ => false,
4179 }
4180 }
4181
4182 pub fn veto_type() -> Self {
4184 Self::primitive(TypeSpecification::veto())
4185 }
4186
4187 pub fn undetermined_type() -> Self {
4190 Self::primitive(TypeSpecification::Undetermined)
4191 }
4192
4193 pub fn decimal_places(&self) -> Option<u8> {
4196 match &self.specifications {
4197 TypeSpecification::Number { decimals, .. } => *decimals,
4198 TypeSpecification::Measure { decimals, .. } => *decimals,
4199 TypeSpecification::Ratio { decimals, .. } => *decimals,
4200 _ => None,
4201 }
4202 }
4203
4204 pub fn try_rational_as_decimal(
4209 &self,
4210 magnitude: &crate::computation::rational::RationalInteger,
4211 ) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
4212 let decimal = magnitude.try_to_decimal()?;
4213 Ok(format_decimal_for_api(decimal, self.decimal_places()))
4214 }
4215
4216 pub fn try_measure_canonical_as_decimal_in_unit(
4218 &self,
4219 canonical_magnitude: &crate::computation::rational::RationalInteger,
4220 unit_name: &str,
4221 ) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
4222 use crate::computation::rational::checked_div;
4223 let unit_factor = self.measure_unit_factor(unit_name);
4224 let magnitude_in_unit = checked_div(canonical_magnitude, unit_factor)?;
4225 self.try_rational_as_decimal(&magnitude_in_unit)
4226 }
4227
4228 pub fn try_ratio_canonical_as_decimal_in_unit(
4230 &self,
4231 canonical_magnitude: &crate::computation::rational::RationalInteger,
4232 unit_name: &str,
4233 ) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
4234 use crate::computation::rational::checked_mul;
4235 let units = match &self.specifications {
4236 TypeSpecification::Ratio { units, .. } => units,
4237 _ => unreachable!(
4238 "BUG: try_ratio_canonical_as_decimal_in_unit called on non-ratio type {}",
4239 self.name()
4240 ),
4241 };
4242 let ratio_unit = units
4243 .iter()
4244 .find(|unit| unit.name == unit_name)
4245 .unwrap_or_else(|| {
4246 let valid: Vec<&str> = units.iter().map(|unit| unit.name.as_str()).collect();
4247 unreachable!(
4248 "BUG: unknown ratio unit '{}' for type {} (valid: {}); planning must reject invalid units",
4249 unit_name,
4250 self.name(),
4251 valid.join(", ")
4252 )
4253 });
4254 let magnitude_in_unit = checked_mul(canonical_magnitude, &ratio_unit.value)?;
4255 self.try_rational_as_decimal(&magnitude_in_unit)
4256 }
4257
4258 pub fn example_value(&self) -> &'static str {
4260 match &self.specifications {
4261 TypeSpecification::Text { .. } => "\"hello world\"",
4262 TypeSpecification::Measure { .. } => "12.50 eur",
4263 TypeSpecification::MeasureRange { .. } => "30 kilogram...35 kilogram",
4264 TypeSpecification::Number { .. } => "3.14",
4265 TypeSpecification::NumberRange { .. } => "0...100",
4266 TypeSpecification::Boolean { .. } => "true",
4267 TypeSpecification::Date { .. } => "2023-12-25T14:30:00Z",
4268 TypeSpecification::DateRange { .. } => "2024-01-01...2024-12-31",
4269 TypeSpecification::TimeRange { .. } => "09:00...17:00",
4270 TypeSpecification::Veto { .. } => "veto",
4271 TypeSpecification::Time { .. } => "14:30:00",
4272 TypeSpecification::Ratio { .. } => "50%",
4273 TypeSpecification::RatioRange { .. } => "10%...50%",
4274 TypeSpecification::Undetermined => unreachable!(
4275 "BUG: example_value called on Undetermined sentinel type; this type must never reach user-facing code"
4276 ),
4277 TypeSpecification::Spec { .. } => unreachable!(
4278 "BUG: example_value called on a spec type; spec instances are not prompted data"
4279 ),
4280 }
4281 }
4282
4283 #[must_use]
4287 pub fn measure_type_decomposition(&self) -> Option<&BaseMeasureVector> {
4291 match &self.specifications {
4292 TypeSpecification::Measure { decomposition, .. } => decomposition.as_ref(),
4293 _ => unreachable!(
4294 "BUG: measure_type_decomposition called on non-measure type {}",
4295 self.name()
4296 ),
4297 }
4298 }
4299
4300 #[must_use]
4306 pub fn measure_runtime_signature(&self) -> Vec<(String, i32)> {
4307 if let Some(binding) = self.unit() {
4308 return vec![(binding.to_string(), 1)];
4309 }
4310 match &self.specifications {
4311 TypeSpecification::Measure {
4312 units,
4313 decomposition,
4314 ..
4315 } => {
4316 if let Some(canonical) = units.iter().find(|unit| unit.is_canonical_factor()) {
4317 return vec![(canonical.name.clone(), 1)];
4318 }
4319 if let Some(first) = units.iter().next() {
4320 return vec![(first.name.clone(), 1)];
4321 }
4322 decomposition
4323 .as_ref()
4324 .map(base_measure_vector_as_signature)
4325 .unwrap_or_default()
4326 }
4327 TypeSpecification::MeasureRange {
4328 units,
4329 decomposition,
4330 ..
4331 } => {
4332 if let Some(canonical) = units.iter().find(|unit| unit.is_canonical_factor()) {
4333 return vec![(canonical.name.clone(), 1)];
4334 }
4335 if let Some(first) = units.iter().next() {
4336 return vec![(first.name.clone(), 1)];
4337 }
4338 decomposition
4339 .as_ref()
4340 .map(base_measure_vector_as_signature)
4341 .unwrap_or_default()
4342 }
4343 _ => unreachable!(
4344 "BUG: measure_runtime_signature called on non-measure type {}",
4345 self.name()
4346 ),
4347 }
4348 }
4349
4350 #[must_use]
4352 pub fn ratio_primary_unit(&self) -> Option<&str> {
4353 match &self.specifications {
4354 TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
4355 units.iter().next().map(|unit| unit.name.as_str())
4356 }
4357 TypeSpecification::RatioRange { units, .. } if !units.is_empty() => {
4358 units.iter().next().map(|unit| unit.name.as_str())
4359 }
4360 _ => None,
4361 }
4362 }
4363
4364 pub fn is_anonymous_measure(&self) -> bool {
4367 self.name.is_none() && matches!(&self.specifications, TypeSpecification::Measure { .. })
4368 }
4369
4370 pub fn anonymous_for_decomposition(decomposition: BaseMeasureVector) -> Self {
4375 Self {
4376 name: None,
4377 specifications: TypeSpecification::Measure {
4378 minimum: None,
4379 maximum: None,
4380 decimals: None,
4381 units: crate::literals::MeasureUnits::new(),
4382 traits: Vec::new(),
4383 decomposition: Some(decomposition),
4384 unit: None,
4385 help: String::new(),
4386 },
4387 extends: TypeExtends::Primitive,
4388 }
4389 }
4390
4391 #[must_use]
4393 pub fn ratio_unit_names(&self) -> Option<Vec<&str>> {
4394 match &self.specifications {
4395 TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
4396 Some(units.iter().map(|unit| unit.name.as_str()).collect())
4397 }
4398 TypeSpecification::RatioRange { units, .. } if !units.is_empty() => {
4399 Some(units.iter().map(|unit| unit.name.as_str()).collect())
4400 }
4401 _ => None,
4402 }
4403 }
4404
4405 #[must_use]
4407 pub fn measure_unit_names(&self) -> Option<Vec<&str>> {
4408 match &self.specifications {
4409 TypeSpecification::Measure { units, .. } if !units.is_empty() => {
4410 Some(units.iter().map(|unit| unit.name.as_str()).collect())
4411 }
4412 TypeSpecification::MeasureRange { units, .. } if !units.is_empty() => {
4413 Some(units.iter().map(|unit| unit.name.as_str()).collect())
4414 }
4415 _ => None,
4416 }
4417 }
4418
4419 #[must_use]
4421 pub fn label_for_data_input(&self, input_key: &str) -> String {
4422 let type_label = if let Some(parent) = self.extends.parent_name() {
4423 parent.to_string()
4424 } else {
4425 let type_name = self.name();
4426 if type_name == input_key {
4427 self.specifications.to_string()
4428 } else {
4429 type_name
4430 }
4431 };
4432 if type_label == input_key {
4433 input_key.to_string()
4434 } else {
4435 format!("{input_key} [{type_label}]")
4436 }
4437 }
4438
4439 #[must_use]
4441 pub fn data_veto_message(&self, input_key: &str, detail: &str) -> String {
4442 format!("Data {}: {}", self.label_for_data_input(input_key), detail)
4443 }
4444
4445 #[must_use]
4447 pub fn empty_runtime_input_vetoes(&self) -> bool {
4448 !matches!(self.specifications, TypeSpecification::Text { .. })
4449 }
4450
4451 pub fn measure_unit_factor(
4453 &self,
4454 unit_name: &str,
4455 ) -> &crate::computation::rational::RationalInteger {
4456 let units = match &self.specifications {
4457 TypeSpecification::Measure { units, .. } => units,
4458 TypeSpecification::MeasureRange { units, .. } => units,
4459 _ => unreachable!(
4460 "BUG: measure_unit_factor called with non-measure type {}; only call during evaluation after planning validated measure conversion",
4461 self.name()
4462 ),
4463 };
4464 match units.get(unit_name) {
4465 Ok(MeasureUnit { factor, .. }) => factor,
4466 Err(_) => {
4467 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
4468 unreachable!(
4469 "BUG: unknown unit '{}' for measure type {} (valid: {}); planning must reject invalid conversions with Error",
4470 unit_name,
4471 self.name(),
4472 valid.join(", ")
4473 );
4474 }
4475 }
4476 }
4477
4478 pub fn ratio_unit_factor(
4479 &self,
4480 unit_name: &str,
4481 ) -> &crate::computation::rational::RationalInteger {
4482 let units = match &self.specifications {
4483 TypeSpecification::Ratio { units, .. } => units,
4484 _ => unreachable!(
4485 "BUG: ratio_unit_factor called with non-ratio type {}; only call during evaluation after planning validated ratio conversion",
4486 self.name()
4487 ),
4488 };
4489 match units.get(unit_name) {
4490 Ok(RatioUnit { value, .. }) => value,
4491 Err(_) => {
4492 let valid: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
4493 unreachable!(
4494 "BUG: unknown unit '{}' for ratio type {} (valid: {}); planning must reject invalid conversions with Error",
4495 unit_name,
4496 self.name(),
4497 valid.join(", ")
4498 );
4499 }
4500 }
4501 }
4502
4503 pub(crate) fn measure_literal_unit_map(
4505 &self,
4506 literal: &LiteralValue,
4507 unit_names: &[&str],
4508 factor_source: UnitFactorSource<'_>,
4509 ) -> Result<BTreeMap<String, rust_decimal::Decimal>, LiteralUnitMapFailure> {
4510 use crate::computation::rational::checked_div;
4511
4512 let ValueKind::Measure(magnitude) = &literal.value else {
4513 panic!("BUG: measure_literal_unit_map called with non-measure value");
4514 };
4515 let mut map = BTreeMap::new();
4516 for &unit_name in unit_names {
4517 let unit_factor = factor_source.measure_unit_factor(unit_name);
4518 let magnitude_in_unit = checked_div(magnitude, unit_factor)
4519 .map_err(LiteralUnitMapFailure::UnitConversion)?;
4520 let decimal = self
4521 .try_rational_as_decimal(&magnitude_in_unit)
4522 .map_err(LiteralUnitMapFailure::Commit)?;
4523 map.insert(unit_name.to_string(), decimal);
4524 }
4525 Ok(map)
4526 }
4527
4528 pub(crate) fn ratio_literal_unit_map(
4530 &self,
4531 literal: &LiteralValue,
4532 unit_names: &[&str],
4533 factor_source: UnitFactorSource<'_>,
4534 ) -> Result<BTreeMap<String, rust_decimal::Decimal>, LiteralUnitMapFailure> {
4535 use crate::computation::rational::checked_mul;
4536
4537 let ratio_api_type = match &self.specifications {
4538 TypeSpecification::Ratio { .. } => self,
4539 TypeSpecification::RatioRange { .. } => {
4540 return ratio_element_type_for_api(self).ratio_literal_unit_map(
4541 literal,
4542 unit_names,
4543 factor_source,
4544 );
4545 }
4546 _ => {
4547 panic!(
4548 "BUG: ratio_literal_unit_map called with non-ratio type {}",
4549 self.name()
4550 );
4551 }
4552 };
4553 let ValueKind::Ratio(canonical) = &literal.value else {
4554 panic!("BUG: ratio_literal_unit_map called with non-ratio value");
4555 };
4556 if unit_names.is_empty() {
4557 panic!(
4558 "BUG: ratio literal type '{}' must have at least one unit name",
4559 self.name()
4560 );
4561 }
4562 let mut map = BTreeMap::new();
4563 for &unit_name in unit_names {
4564 let unit_factor = factor_source.ratio_unit_factor(unit_name);
4565 let magnitude_in_unit = checked_mul(canonical, unit_factor)
4566 .map_err(LiteralUnitMapFailure::UnitConversion)?;
4567 let decimal = ratio_api_type
4568 .try_rational_as_decimal(&magnitude_in_unit)
4569 .map_err(LiteralUnitMapFailure::Commit)?;
4570 map.insert(unit_name.to_string(), decimal);
4571 }
4572 Ok(map)
4573 }
4574}
4575
4576pub(crate) enum UnitFactorSource<'a> {
4578 DeclaredOn(&'a LemmaType),
4579 Merged {
4580 measure: Option<&'a MeasureUnits>,
4581 ratio: Option<&'a RatioUnits>,
4582 },
4583}
4584
4585impl UnitFactorSource<'_> {
4586 fn measure_unit_factor(
4587 &self,
4588 unit_name: &str,
4589 ) -> &crate::computation::rational::RationalInteger {
4590 match self {
4591 UnitFactorSource::DeclaredOn(lemma_type) => lemma_type.measure_unit_factor(unit_name),
4592 UnitFactorSource::Merged { measure, .. } => {
4593 let units = measure.unwrap_or_else(|| {
4594 panic!(
4595 "BUG: family measure expansion missing merged table for unit '{unit_name}'"
4596 )
4597 });
4598 &units
4599 .get(unit_name)
4600 .unwrap_or_else(|_| {
4601 panic!("BUG: family unit '{unit_name}' missing from merged measure table")
4602 })
4603 .factor
4604 }
4605 }
4606 }
4607
4608 fn ratio_unit_factor(&self, unit_name: &str) -> &crate::computation::rational::RationalInteger {
4609 match self {
4610 UnitFactorSource::DeclaredOn(lemma_type) => lemma_type.ratio_unit_factor(unit_name),
4611 UnitFactorSource::Merged { ratio, .. } => {
4612 let units = ratio.unwrap_or_else(|| {
4613 panic!(
4614 "BUG: family ratio expansion missing merged table for unit '{unit_name}'"
4615 )
4616 });
4617 &units
4618 .get(unit_name)
4619 .unwrap_or_else(|_| {
4620 panic!("BUG: family unit '{unit_name}' missing from merged ratio table")
4621 })
4622 .value
4623 }
4624 }
4625 }
4626}
4627
4628pub(crate) fn ratio_element_type_for_api(lemma_type: &LemmaType) -> LemmaType {
4630 match &lemma_type.specifications {
4631 TypeSpecification::Ratio { .. } => lemma_type.clone(),
4632 TypeSpecification::RatioRange { .. } => {
4633 let element = range_element_type_specification(&lemma_type.specifications)
4634 .expect("BUG: ratio range type must have ratio element specification");
4635 let TypeSpecification::Ratio {
4636 units, decimals, ..
4637 } = element
4638 else {
4639 panic!("BUG: ratio range element spec must be Ratio");
4640 };
4641 LemmaType::primitive(TypeSpecification::Ratio {
4642 minimum: None,
4643 maximum: None,
4644 decimals,
4645 units,
4646 unit: None,
4647 help: String::new(),
4648 })
4649 }
4650 _ => panic!(
4651 "BUG: ratio_element_type_for_api called with non-ratio type {}",
4652 lemma_type.name()
4653 ),
4654 }
4655}
4656
4657#[derive(Debug, Clone, PartialEq, Eq)]
4659pub(crate) enum LiteralUnitMapFailure {
4660 Commit(crate::computation::rational::NumericFailure),
4661 UnitConversion(crate::computation::rational::NumericFailure),
4662}
4663
4664#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
4667pub struct LiteralValue {
4668 pub value: ValueKind,
4669}
4670
4671impl LiteralValue {
4672 #[inline]
4673 pub fn new(value: ValueKind) -> Self {
4674 Self { value }
4675 }
4676
4677 fn single_measure_signature_unit_name(signature: &[(String, i32)]) -> Option<&str> {
4678 match signature {
4679 [(unit_name, 1)] => Some(unit_name.as_str()),
4680 _ => None,
4681 }
4682 }
4683}
4684
4685#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
4691pub struct TypedLiteral {
4692 pub value: ValueKind,
4693 pub lemma_type: Arc<LemmaType>,
4694}
4695
4696impl TypedLiteral {
4697 #[inline]
4698 pub fn to_literal(&self) -> LiteralValue {
4699 LiteralValue {
4700 value: self.value.clone(),
4701 }
4702 }
4703
4704 #[inline]
4705 pub fn into_literal(self) -> LiteralValue {
4706 LiteralValue { value: self.value }
4707 }
4708}
4709
4710impl From<TypedLiteral> for LiteralValue {
4711 fn from(typed: TypedLiteral) -> Self {
4712 typed.into_literal()
4713 }
4714}
4715
4716impl From<&TypedLiteral> for LiteralValue {
4717 fn from(typed: &TypedLiteral) -> Self {
4718 typed.to_literal()
4719 }
4720}
4721
4722impl LiteralValue {
4723 pub fn text(s: String) -> Self {
4724 Self {
4725 value: ValueKind::Text(s),
4726 }
4727 }
4728
4729 pub fn number(n: RationalInteger) -> Self {
4730 Self {
4731 value: ValueKind::Number(n),
4732 }
4733 }
4734
4735 pub fn number_from_decimal(decimal: Decimal) -> Self {
4736 Self::number(
4737 crate::literals::rational_from_parsed_decimal(decimal)
4738 .expect("BUG: literal number from decimal must lift at boundary"),
4739 )
4740 }
4741
4742 pub fn measure(n: RationalInteger) -> Self {
4743 Self {
4744 value: ValueKind::Measure(n),
4745 }
4746 }
4747
4748 pub fn measure_with_type(n: RationalInteger, _lemma_type: Arc<LemmaType>) -> Self {
4750 Self::measure(n)
4751 }
4752
4753 pub fn measure_with_bound_unit(
4754 n: RationalInteger,
4755 _unit_name: impl Into<String>,
4756 _lemma_type: Arc<LemmaType>,
4757 ) -> Self {
4758 Self::measure(n)
4759 }
4760
4761 pub fn measure_with_signature(n: RationalInteger, _lemma_type: Arc<LemmaType>) -> Self {
4762 Self::measure(n)
4763 }
4764
4765 pub fn number_with_type(n: RationalInteger, _lemma_type: Arc<LemmaType>) -> Self {
4766 Self::number(n)
4767 }
4768
4769 pub fn number_with_type_from_decimal(decimal: Decimal, _lemma_type: Arc<LemmaType>) -> Self {
4770 Self::number_from_decimal(decimal)
4771 }
4772
4773 pub fn ratio_with_type(r: RationalInteger, _lemma_type: Arc<LemmaType>) -> Self {
4774 Self::ratio(r)
4775 }
4776
4777 pub fn ratio_with_bound_unit(
4778 r: RationalInteger,
4779 _unit_name: impl Into<String>,
4780 _lemma_type: Arc<LemmaType>,
4781 ) -> Self {
4782 Self::ratio(r)
4783 }
4784
4785 pub fn text_with_type(s: String, _lemma_type: Arc<LemmaType>) -> Self {
4786 Self::text(s)
4787 }
4788
4789 pub fn date_with_type(dt: SemanticDateTime, _lemma_type: Arc<LemmaType>) -> Self {
4790 Self::date(dt)
4791 }
4792
4793 pub fn time_with_type(t: SemanticTime, _lemma_type: Arc<LemmaType>) -> Self {
4794 Self::time(t)
4795 }
4796
4797 pub fn calendar(
4798 value: RationalInteger,
4799 _unit: SemanticCalendarUnit,
4800 _lemma_type: Arc<LemmaType>,
4801 ) -> Self {
4802 Self::measure(value)
4803 }
4804
4805 pub fn calendar_from_decimal(
4806 value: Decimal,
4807 unit: SemanticCalendarUnit,
4808 lemma_type: Arc<LemmaType>,
4809 ) -> Self {
4810 Self::calendar(
4811 crate::literals::rational_from_parsed_decimal(value)
4812 .expect("BUG: calendar literal from decimal must lift at boundary"),
4813 unit,
4814 lemma_type,
4815 )
4816 }
4817
4818 pub fn calendar_with_type(
4819 value: RationalInteger,
4820 unit: SemanticCalendarUnit,
4821 lemma_type: Arc<LemmaType>,
4822 ) -> Self {
4823 Self::calendar(value, unit, lemma_type)
4824 }
4825
4826 pub fn number_interpreted_as_measure(value: RationalInteger, _unit_name: String) -> Self {
4827 Self::measure(value)
4828 }
4829
4830 pub fn from_bool(b: bool) -> Self {
4831 Self {
4832 value: ValueKind::Boolean(b),
4833 }
4834 }
4835
4836 pub fn from_datetime(dt: &crate::parsing::ast::DateTimeValue) -> Self {
4837 Self::date(date_time_to_semantic(dt))
4838 }
4839
4840 pub fn date(dt: SemanticDateTime) -> Self {
4841 Self {
4842 value: ValueKind::Date(dt),
4843 }
4844 }
4845
4846 pub fn time(t: SemanticTime) -> Self {
4847 Self {
4848 value: ValueKind::Time(t),
4849 }
4850 }
4851
4852 pub fn ratio(r: RationalInteger) -> Self {
4853 Self {
4854 value: ValueKind::Ratio(r),
4855 }
4856 }
4857
4858 pub fn ratio_from_decimal(r: Decimal) -> Self {
4859 Self::ratio(
4860 crate::literals::rational_from_parsed_decimal(r)
4861 .expect("BUG: ratio literal from decimal must lift at boundary"),
4862 )
4863 }
4864
4865 pub fn range(left: TypedLiteral, right: TypedLiteral) -> Self {
4866 Self {
4867 value: ValueKind::Range(Box::new(left), Box::new(right)),
4868 }
4869 }
4870
4871 pub fn display_value(&self) -> String {
4873 match &self.value {
4874 ValueKind::Measure(_) | ValueKind::Ratio(_) => format!("{}", self.value),
4875 ValueKind::Range(left, right) => {
4876 format!("{}...{}", left.display_value(), right.display_value())
4877 }
4878 _ => format!("{}", self.value),
4879 }
4880 }
4881
4882 pub fn display_value_with_type(&self, lemma_type: &LemmaType) -> String {
4884 match &self.value {
4885 ValueKind::Measure(n) => {
4886 let signature = lemma_type.measure_runtime_signature();
4887 format_measure_canonical_for_display(n, lemma_type, &signature)
4888 }
4889 ValueKind::Ratio(n) => format_ratio_canonical_for_display(n, lemma_type),
4890 ValueKind::Range(left, right) => {
4891 format!("{}...{}", left.display_value(), right.display_value())
4892 }
4893 _ => format!("{}", self.value),
4894 }
4895 }
4896
4897 pub fn byte_size(&self) -> usize {
4899 self.value.structural_byte_size()
4900 }
4901
4902 #[must_use]
4904 pub fn magnitude_suggestion_for_decimal_prompt(
4905 &self,
4906 lemma_type: &LemmaType,
4907 ) -> Option<rust_decimal::Decimal> {
4908 match &self.value {
4909 ValueKind::Number(n) => Some(
4910 lemma_type
4911 .try_rational_as_decimal(n)
4912 .expect("BUG: stored number literal must convert to decimal for prompt"),
4913 ),
4914 ValueKind::Measure(n) => {
4915 let signature = lemma_type.measure_runtime_signature();
4916 let unit_name = Self::single_measure_signature_unit_name(&signature).expect(
4917 "BUG: measure prompt requires exactly one signature unit with exponent 1",
4918 );
4919 Some(
4920 lemma_type
4921 .try_measure_canonical_as_decimal_in_unit(n, unit_name)
4922 .expect("BUG: stored measure literal must convert to decimal for prompt"),
4923 )
4924 }
4925 ValueKind::Ratio(n) => {
4926 if let Some(unit_name) = lemma_type.unit() {
4927 Some(
4928 lemma_type
4929 .try_ratio_canonical_as_decimal_in_unit(n, unit_name)
4930 .expect("BUG: stored ratio literal must convert to decimal for prompt"),
4931 )
4932 } else {
4933 Some(lemma_type.try_rational_as_decimal(n).expect(
4934 "BUG: stored bare ratio literal must convert to decimal for prompt",
4935 ))
4936 }
4937 }
4938 _ => None,
4939 }
4940 }
4941
4942 #[must_use]
4944 pub fn measure_units(
4945 &self,
4946 lemma_type: &LemmaType,
4947 ) -> Option<BTreeMap<String, rust_decimal::Decimal>> {
4948 if !matches!(self.value, ValueKind::Measure(_)) {
4949 return None;
4950 }
4951 lemma_type.measure_unit_names()?;
4952 let declared: Vec<&str> = lemma_type
4953 .measure_unit_names()
4954 .expect("BUG: measure_unit_names checked above");
4955 lemma_type
4956 .measure_literal_unit_map(self, &declared, UnitFactorSource::DeclaredOn(lemma_type))
4957 .ok()
4958 }
4959
4960 #[must_use]
4962 pub fn ratio_units(
4963 &self,
4964 lemma_type: &LemmaType,
4965 ) -> Option<BTreeMap<String, rust_decimal::Decimal>> {
4966 if !matches!(self.value, ValueKind::Ratio(_)) {
4967 return None;
4968 }
4969 let has_declared_units = match &lemma_type.specifications {
4970 TypeSpecification::Ratio { units, .. } => !units.is_empty(),
4971 TypeSpecification::RatioRange { .. } => true,
4972 _ => return None,
4973 };
4974 if !has_declared_units {
4975 return None;
4976 }
4977 let declared: Vec<&str> = lemma_type
4978 .ratio_unit_names()
4979 .expect("BUG: ratio units checked above");
4980 lemma_type
4981 .ratio_literal_unit_map(self, &declared, UnitFactorSource::DeclaredOn(lemma_type))
4982 .ok()
4983 }
4984
4985 #[must_use]
4987 pub fn magnitude_in_unit(
4988 &self,
4989 lemma_type: &LemmaType,
4990 unit: &str,
4991 ) -> Option<rust_decimal::Decimal> {
4992 self.measure_units(lemma_type)
4993 .and_then(|map| map.get(unit).copied())
4994 .or_else(|| {
4995 self.ratio_units(lemma_type)
4996 .and_then(|map| map.get(unit).copied())
4997 })
4998 }
4999
5000 pub fn duration_canonical_seconds(&self, lemma_type: &LemmaType) -> RationalInteger {
5002 let ValueKind::Measure(magnitude) = &self.value else {
5003 unreachable!(
5004 "BUG: duration_canonical_seconds called with {:?}",
5005 self.value
5006 );
5007 };
5008 if !lemma_type.is_duration_like_measure() {
5009 unreachable!(
5010 "BUG: duration_canonical_seconds called with type {}",
5011 lemma_type.name()
5012 );
5013 }
5014 let factor = lemma_type.measure_unit_factor("second");
5015 checked_div(magnitude, factor).expect("BUG: duration unit factor cannot be zero")
5016 }
5017
5018 pub fn calendar_canonical_months(&self, lemma_type: &LemmaType) -> RationalInteger {
5020 let ValueKind::Measure(magnitude) = &self.value else {
5021 unreachable!(
5022 "BUG: calendar_canonical_months called with {:?}",
5023 self.value
5024 );
5025 };
5026 if !lemma_type.is_calendar_like() {
5027 unreachable!(
5028 "BUG: calendar_canonical_months called with type {}",
5029 lemma_type.name()
5030 );
5031 }
5032 let factor = lemma_type.measure_unit_factor("month");
5033 checked_div(magnitude, factor).expect("BUG: calendar unit factor cannot be zero")
5034 }
5035}
5036
5037impl TypedLiteral {
5038 pub fn text(s: String) -> Self {
5039 Self {
5040 value: ValueKind::Text(s),
5041 lemma_type: primitive_text_arc().clone(),
5042 }
5043 }
5044
5045 pub fn text_with_type(s: String, lemma_type: Arc<LemmaType>) -> Self {
5046 Self {
5047 value: ValueKind::Text(s),
5048 lemma_type,
5049 }
5050 }
5051
5052 pub fn number(n: RationalInteger) -> Self {
5053 Self {
5054 value: ValueKind::Number(n),
5055 lemma_type: primitive_number_arc().clone(),
5056 }
5057 }
5058
5059 pub fn number_from_decimal(decimal: Decimal) -> Self {
5060 Self::number(
5061 crate::literals::rational_from_parsed_decimal(decimal)
5062 .expect("BUG: literal number from decimal must lift at boundary"),
5063 )
5064 }
5065
5066 pub fn number_with_type(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
5067 Self {
5068 value: ValueKind::Number(n),
5069 lemma_type,
5070 }
5071 }
5072
5073 pub fn number_with_type_from_decimal(decimal: Decimal, lemma_type: Arc<LemmaType>) -> Self {
5074 Self::number_with_type(
5075 crate::literals::rational_from_parsed_decimal(decimal)
5076 .expect("BUG: literal number from decimal must lift at boundary"),
5077 lemma_type,
5078 )
5079 }
5080
5081 pub fn measure_with_bound_unit(
5083 n: RationalInteger,
5084 unit_name: impl Into<String>,
5085 lemma_type: Arc<LemmaType>,
5086 ) -> Self {
5087 Self::measure_with_type(
5088 n,
5089 Arc::new(lemma_type.as_ref().clone().with_unit(unit_name)),
5090 )
5091 }
5092
5093 pub fn measure_with_type(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
5094 Self {
5095 value: ValueKind::Measure(n),
5096 lemma_type,
5097 }
5098 }
5099
5100 pub fn measure_with_signature(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
5101 Self {
5102 value: ValueKind::Measure(n),
5103 lemma_type,
5104 }
5105 }
5106
5107 pub fn number_interpreted_as_measure(value: RationalInteger, unit_name: String) -> Self {
5109 let lemma_type = if unit_name.is_empty() {
5110 Arc::new(anonymous_measure_type())
5111 } else {
5112 let mut decomp = BaseMeasureVector::new();
5113 decomp.insert(unit_name, 1);
5114 Arc::new(LemmaType::anonymous_for_decomposition(decomp))
5115 };
5116 Self {
5117 value: ValueKind::Measure(value),
5118 lemma_type,
5119 }
5120 }
5121
5122 pub fn from_bool(b: bool) -> Self {
5123 Self {
5124 value: ValueKind::Boolean(b),
5125 lemma_type: primitive_boolean_arc().clone(),
5126 }
5127 }
5128
5129 pub fn from_datetime(dt: &crate::parsing::ast::DateTimeValue) -> Self {
5130 Self::date(date_time_to_semantic(dt))
5131 }
5132
5133 pub fn date(dt: SemanticDateTime) -> Self {
5134 Self {
5135 value: ValueKind::Date(dt),
5136 lemma_type: primitive_date_arc().clone(),
5137 }
5138 }
5139
5140 pub fn date_with_type(dt: SemanticDateTime, lemma_type: Arc<LemmaType>) -> Self {
5141 Self {
5142 value: ValueKind::Date(dt),
5143 lemma_type,
5144 }
5145 }
5146
5147 pub fn time(t: SemanticTime) -> Self {
5148 Self {
5149 value: ValueKind::Time(t),
5150 lemma_type: primitive_time_arc().clone(),
5151 }
5152 }
5153
5154 pub fn time_with_type(t: SemanticTime, lemma_type: Arc<LemmaType>) -> Self {
5155 Self {
5156 value: ValueKind::Time(t),
5157 lemma_type,
5158 }
5159 }
5160
5161 pub fn calendar(
5162 value: RationalInteger,
5163 unit: SemanticCalendarUnit,
5164 lemma_type: Arc<LemmaType>,
5165 ) -> Self {
5166 let unit_name = unit.to_string();
5167 debug_assert_eq!(
5168 semantic_calendar_unit_from_unit_name(&unit_name),
5169 unit,
5170 "BUG: calendar unit name must round-trip"
5171 );
5172 Self::measure_with_bound_unit(value, unit_name, lemma_type)
5173 }
5174
5175 pub fn calendar_from_decimal(
5176 value: Decimal,
5177 unit: SemanticCalendarUnit,
5178 lemma_type: Arc<LemmaType>,
5179 ) -> Self {
5180 Self::calendar(
5181 crate::literals::rational_from_parsed_decimal(value)
5182 .expect("BUG: calendar literal from decimal must lift at boundary"),
5183 unit,
5184 lemma_type,
5185 )
5186 }
5187
5188 pub fn calendar_with_type(
5189 value: RationalInteger,
5190 unit: SemanticCalendarUnit,
5191 lemma_type: Arc<LemmaType>,
5192 ) -> Self {
5193 Self::calendar(value, unit, lemma_type)
5194 }
5195
5196 pub fn ratio(r: RationalInteger) -> Self {
5197 Self {
5198 value: ValueKind::Ratio(r),
5199 lemma_type: primitive_ratio_arc().clone(),
5200 }
5201 }
5202
5203 pub fn ratio_from_decimal(r: Decimal) -> Self {
5204 Self::ratio(
5205 crate::literals::rational_from_parsed_decimal(r)
5206 .expect("BUG: ratio literal from decimal must lift at boundary"),
5207 )
5208 }
5209
5210 pub fn ratio_with_type(r: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
5211 Self {
5212 value: ValueKind::Ratio(r),
5213 lemma_type,
5214 }
5215 }
5216
5217 pub fn ratio_with_bound_unit(
5218 r: RationalInteger,
5219 unit_name: impl Into<String>,
5220 lemma_type: Arc<LemmaType>,
5221 ) -> Self {
5222 Self::ratio_with_type(
5223 r,
5224 Arc::new(lemma_type.as_ref().clone().with_unit(unit_name)),
5225 )
5226 }
5227
5228 pub fn range(left: TypedLiteral, right: TypedLiteral) -> Self {
5229 let specifications =
5230 range_type_specification_from_endpoints(&left.lemma_type, &right.lemma_type)
5231 .unwrap_or_else(|| {
5232 unreachable!(
5233 "BUG: attempted to construct a range literal from incompatible endpoint types"
5234 )
5235 });
5236
5237 Self {
5238 value: ValueKind::Range(Box::new(left), Box::new(right)),
5239 lemma_type: Arc::new(LemmaType::primitive(specifications)),
5240 }
5241 }
5242
5243 pub fn display_value(&self) -> String {
5244 self.to_literal().display_value_with_type(&self.lemma_type)
5245 }
5246
5247 pub fn get_type(&self) -> &LemmaType {
5248 &self.lemma_type
5249 }
5250
5251 pub fn byte_size(&self) -> usize {
5252 self.value.structural_byte_size()
5253 }
5254}
5255
5256impl fmt::Display for TypedLiteral {
5257 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5258 write!(f, "{}", self.display_value())
5259 }
5260}
5261
5262#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
5265pub enum ReferenceTarget {
5266 Data(DataPath),
5267 Rule(RulePath),
5268}
5269
5270#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
5274pub(crate) enum ReferenceEnd {
5275 Promptable(DataPath),
5276 Rule(RulePath),
5277 Import,
5278}
5279
5280#[derive(Clone, Debug, Serialize, Deserialize)]
5282pub enum DataDefinition {
5283 Value {
5285 value: LiteralValue,
5286 resolved_type: Arc<LemmaType>,
5287 source: Source,
5288 },
5289 TypeDeclaration {
5295 resolved_type: Arc<LemmaType>,
5296 declared_suggestion: Option<BoundValueKind>,
5297 declared_fill: Option<BoundValueKind>,
5298 source: Source,
5299 },
5300 Import {
5305 target_name: String,
5306 repository: Option<String>,
5307 effective: crate::parsing::ast::EffectiveDate,
5308 source: Source,
5309 },
5310 Reference {
5335 target: ReferenceTarget,
5336 resolved_type: Arc<LemmaType>,
5337 local_constraints: Option<Vec<Constraint>>,
5338 local_suggestion: Option<BoundValueKind>,
5339 local_fill: Option<BoundValueKind>,
5340 source: Source,
5341 },
5342}
5343
5344impl DataDefinition {
5345 pub fn lemma_type(&self) -> Option<&LemmaType> {
5347 self.resolved_type_arc().map(|arc| arc.as_ref())
5348 }
5349
5350 pub fn resolved_type_arc(&self) -> Option<&Arc<LemmaType>> {
5352 match self {
5353 DataDefinition::Value { resolved_type, .. } => Some(resolved_type),
5354 DataDefinition::TypeDeclaration { resolved_type, .. } => Some(resolved_type),
5355 DataDefinition::Reference { resolved_type, .. } => Some(resolved_type),
5356 DataDefinition::Import { .. } => None,
5357 }
5358 }
5359
5360 #[inline]
5362 pub fn schema_type(&self) -> Option<&LemmaType> {
5363 self.lemma_type()
5364 }
5365
5366 pub fn value(&self) -> Option<LiteralValue> {
5370 match self {
5371 DataDefinition::Value { value, .. } => Some(value.clone()),
5372 DataDefinition::TypeDeclaration {
5373 declared_fill: Some(dv),
5374 ..
5375 } => Some(dv.to_literal()),
5376 DataDefinition::Reference {
5377 local_fill: Some(dv),
5378 resolved_type: _,
5379 ..
5380 } => Some(dv.to_literal()),
5381 DataDefinition::TypeDeclaration { .. }
5382 | DataDefinition::Import { .. }
5383 | DataDefinition::Reference { .. } => None,
5384 }
5385 }
5386
5387 pub(crate) fn bound_fill(&self) -> Option<&BoundValueKind> {
5389 match self {
5390 DataDefinition::TypeDeclaration {
5391 declared_fill: Some(dv),
5392 ..
5393 }
5394 | DataDefinition::Reference {
5395 local_fill: Some(dv),
5396 ..
5397 } => Some(dv),
5398 DataDefinition::Value { .. }
5399 | DataDefinition::TypeDeclaration { .. }
5400 | DataDefinition::Reference { .. }
5401 | DataDefinition::Import { .. } => None,
5402 }
5403 }
5404
5405 pub(crate) fn bound_suggestion(&self) -> Option<&BoundValueKind> {
5409 match self {
5410 DataDefinition::TypeDeclaration {
5411 declared_suggestion: Some(dv),
5412 ..
5413 }
5414 | DataDefinition::Reference {
5415 local_suggestion: Some(dv),
5416 ..
5417 } => Some(dv),
5418 DataDefinition::Value { .. }
5419 | DataDefinition::TypeDeclaration { .. }
5420 | DataDefinition::Reference { .. }
5421 | DataDefinition::Import { .. } => None,
5422 }
5423 }
5424
5425 pub fn source(&self) -> &Source {
5427 match self {
5428 DataDefinition::Value { source, .. } => source,
5429 DataDefinition::TypeDeclaration { source, .. } => source,
5430 DataDefinition::Import { source, .. } => source,
5431 DataDefinition::Reference { source, .. } => source,
5432 }
5433 }
5434}
5435
5436pub fn number_with_unit_to_value_kind(
5438 magnitude: rust_decimal::Decimal,
5439 unit_name: &str,
5440 lemma_type: &LemmaType,
5441) -> Result<ValueKind, String> {
5442 match &lemma_type.specifications {
5443 TypeSpecification::Ratio { units, .. } => {
5444 use crate::computation::rational::{checked_div, decimal_to_rational};
5445 let unit = units.get(unit_name)?;
5446 let magnitude_rational = decimal_to_rational(magnitude)
5447 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
5448 let canonical_rational = checked_div(&magnitude_rational, &unit.value)
5449 .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
5450 Ok(ValueKind::Ratio(canonical_rational))
5451 }
5452 TypeSpecification::Measure { units, .. } => {
5453 use crate::computation::rational::checked_mul;
5454 let rational = lift_parser_decimal(magnitude)?;
5455 let unit = units.get(unit_name)?;
5456 let canonical = checked_mul(&rational, &unit.factor)
5457 .map_err(|failure| format!("measure canonicalization overflow: {failure}"))?;
5458 Ok(ValueKind::Measure(canonical))
5459 }
5460 _ => Err(format!(
5461 "Unit '{}' is defined on type '{}' which is not measure or ratio",
5462 unit_name,
5463 lemma_type.name()
5464 )),
5465 }
5466}
5467
5468pub(crate) fn value_kind_matches_spec(value: &ValueKind, type_spec: &TypeSpecification) -> bool {
5471 matches!(
5472 (type_spec, value),
5473 (TypeSpecification::Number { .. }, ValueKind::Number(_))
5474 | (TypeSpecification::Text { .. }, ValueKind::Text(_))
5475 | (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
5476 | (TypeSpecification::Date { .. }, ValueKind::Date(_))
5477 | (TypeSpecification::Time { .. }, ValueKind::Time(_))
5478 | (TypeSpecification::Measure { .. }, ValueKind::Measure(_))
5479 | (TypeSpecification::Ratio { .. }, ValueKind::Ratio(_))
5480 | (TypeSpecification::Ratio { .. }, ValueKind::Number(_))
5481 | (
5482 TypeSpecification::NumberRange { .. },
5483 ValueKind::Range(_, _)
5484 )
5485 | (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
5486 | (TypeSpecification::TimeRange { .. }, ValueKind::Range(_, _))
5487 | (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
5488 | (
5489 TypeSpecification::MeasureRange { .. },
5490 ValueKind::Range(_, _)
5491 )
5492 | (TypeSpecification::Veto { .. }, _)
5493 | (TypeSpecification::Undetermined, _)
5494 | (TypeSpecification::Spec { .. }, ValueKind::Spec(_))
5495 )
5496}
5497
5498fn parser_value_type_mismatch(
5499 value: &crate::literals::Value,
5500 type_spec: &TypeSpecification,
5501) -> String {
5502 use crate::parsing::ast::AsLemmaSource;
5503 let value_str = format!("{}", AsLemmaSource(value));
5504 match type_spec {
5505 TypeSpecification::Measure { units, .. } => {
5506 let unit_hint = units
5507 .iter()
5508 .find(|u| u.factor == crate::computation::rational::rational_one())
5509 .map(|u| u.name.as_str())
5510 .or_else(|| units.iter().next().map(|u| u.name.as_str()))
5511 .unwrap_or("unit");
5512 format!("cannot use {value_str} as {type_spec}: expected `<n> {unit_hint}`")
5513 }
5514 TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
5515 let unit_hint = units
5516 .iter()
5517 .next()
5518 .map(|u| u.name.as_str())
5519 .unwrap_or("unit");
5520 format!(
5521 "cannot use {value_str} as {type_spec}: expected `<n> {unit_hint}` or bare ratio"
5522 )
5523 }
5524 _ => format!("cannot use {value_str} as {type_spec}"),
5525 }
5526}
5527
5528pub fn refresh_measure_literal_canonical_magnitude(
5533 lit: &mut LiteralValue,
5534 previous_type: &LemmaType,
5535 resolved_type: &LemmaType,
5536) -> Result<(), crate::computation::rational::NumericFailure> {
5537 use crate::computation::rational::{checked_div, checked_mul};
5538 let ValueKind::Measure(magnitude) = &mut lit.value else {
5539 return Ok(());
5540 };
5541 let signature = previous_type.measure_runtime_signature();
5545 let Some((unit_name, 1)) = signature.first().map(|(n, e)| (n.as_str(), *e)) else {
5546 return Ok(());
5547 };
5548 if signature.len() != 1 {
5549 return Ok(());
5550 }
5551 let stored_factor = previous_type.measure_unit_factor(unit_name);
5552 let resolved_factor = resolved_type.measure_unit_factor(unit_name);
5553 if stored_factor == resolved_factor {
5554 return Ok(());
5555 }
5556 let scaled = checked_mul(magnitude, resolved_factor)?;
5557 *magnitude = checked_div(&scaled, stored_factor)?;
5558 Ok(())
5559}
5560
5561pub fn parser_value_to_value_kind(
5566 value: &crate::literals::Value,
5567 type_spec: &TypeSpecification,
5568 resolve_unit: Option<&UnitTokenResolver<'_>>,
5569) -> Result<ValueKind, String> {
5570 use crate::computation::rational::decimal_to_rational;
5571 use crate::literals::Value;
5572 match (value, type_spec) {
5573 (Value::NumberWithUnit(magnitude, unit_name), TypeSpecification::Ratio { units, .. }) => {
5574 use crate::computation::rational::checked_div;
5575 let unit = units.get(unit_name.as_str())?;
5576 let magnitude_rational = decimal_to_rational(*magnitude)
5577 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
5578 let canonical_rational = checked_div(&magnitude_rational, &unit.value)
5579 .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
5580 Ok(ValueKind::Ratio(canonical_rational))
5581 }
5582 (Value::NumberWithUnit(magnitude, unit_name), TypeSpecification::Measure { units, .. }) => {
5583 use crate::computation::rational::checked_mul;
5584 let rational = lift_parser_decimal(*magnitude)?;
5585 let unit = units.get(unit_name.as_str())?;
5586 let canonical = checked_mul(&rational, &unit.factor)
5587 .map_err(|failure| format!("measure canonicalization overflow: {failure}"))?;
5588 Ok(ValueKind::Measure(canonical))
5589 }
5590 (Value::NumberWithUnit(_, _), _) => {
5591 Err("number_with_unit literal requires a measure or ratio type".to_string())
5592 }
5593 (Value::Number(n), TypeSpecification::Number { .. }) => {
5594 Ok(ValueKind::Number(lift_parser_decimal(*n)?))
5595 }
5596 (Value::Number(n), TypeSpecification::Ratio { .. }) => {
5597 let r = decimal_to_rational(*n)
5598 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
5599 Ok(ValueKind::Ratio(r))
5600 }
5601 (Value::Text(s), TypeSpecification::Text { .. }) => Ok(ValueKind::Text(s.clone())),
5602 (Value::Boolean(b), TypeSpecification::Boolean { .. }) => Ok(ValueKind::Boolean(b.into())),
5603 (Value::Date(dt), TypeSpecification::Date { .. }) => {
5604 Ok(ValueKind::Date(date_time_to_semantic(dt)))
5605 }
5606 (Value::Time(t), TypeSpecification::Time { .. }) => {
5607 Ok(ValueKind::Time(time_to_semantic(t)))
5608 }
5609 (
5610 Value::Range(left, right),
5611 range_spec @ (TypeSpecification::NumberRange { .. }
5612 | TypeSpecification::DateRange { .. }
5613 | TypeSpecification::TimeRange { .. }
5614 | TypeSpecification::RatioRange { .. }
5615 | TypeSpecification::MeasureRange { .. }),
5616 ) => {
5617 let endpoint = range_element_type_specification(range_spec).ok_or_else(|| {
5618 "BUG: range_element_type_specification missing arm for range type".to_string()
5619 })?;
5620 let left_lit = lift_range_endpoint(left, &endpoint, resolve_unit)?;
5621 let right_lit = lift_range_endpoint(right, &endpoint, resolve_unit)?;
5622 Ok(ValueKind::Range(Box::new(left_lit), Box::new(right_lit)))
5623 }
5624 (value, type_spec) => Err(parser_value_type_mismatch(value, type_spec)),
5625 }
5626}
5627
5628pub fn value_to_semantic(value: &crate::parsing::ast::Value) -> Result<ValueKind, String> {
5632 use crate::parsing::ast::Value;
5633 Ok(match value {
5634 Value::Number(n) => ValueKind::Number(lift_parser_decimal(*n)?),
5635 Value::Text(s) => ValueKind::Text(s.clone()),
5636 Value::Boolean(b) => ValueKind::Boolean(bool::from(*b)),
5637 Value::Date(dt) => ValueKind::Date(date_time_to_semantic(dt)),
5638 Value::Time(t) => ValueKind::Time(time_to_semantic(t)),
5639 Value::NumberWithUnit(_, _) => {
5640 return Err(
5641 "number_with_unit literal requires type context (measure or ratio)".to_string(),
5642 );
5643 }
5644 Value::Range(_, _) => literal_value_from_parser_value(value)?.value,
5645 })
5646}
5647
5648pub(crate) fn date_time_to_semantic(dt: &crate::parsing::ast::DateTimeValue) -> SemanticDateTime {
5650 SemanticDateTime {
5651 year: dt.year,
5652 month: dt.month,
5653 day: dt.day,
5654 hour: dt.hour,
5655 minute: dt.minute,
5656 second: dt.second,
5657 microsecond: dt.microsecond,
5658 timezone: dt.timezone.as_ref().map(|tz| SemanticTimezone {
5659 offset_hours: tz.offset_hours,
5660 offset_minutes: tz.offset_minutes,
5661 }),
5662 }
5663}
5664
5665pub(crate) fn time_to_semantic(t: &crate::parsing::ast::TimeValue) -> SemanticTime {
5667 SemanticTime {
5668 hour: t.hour.into(),
5669 minute: t.minute.into(),
5670 second: t.second.into(),
5671 microsecond: t.microsecond,
5672 timezone: t.timezone.as_ref().map(|tz| SemanticTimezone {
5673 offset_hours: tz.offset_hours,
5674 offset_minutes: tz.offset_minutes,
5675 }),
5676 }
5677}
5678
5679pub(crate) fn compare_semantic_dates(
5683 left: &SemanticDateTime,
5684 right: &SemanticDateTime,
5685) -> std::cmp::Ordering {
5686 left.year
5687 .cmp(&right.year)
5688 .then_with(|| left.month.cmp(&right.month))
5689 .then_with(|| left.day.cmp(&right.day))
5690 .then_with(|| left.hour.cmp(&right.hour))
5691 .then_with(|| left.minute.cmp(&right.minute))
5692 .then_with(|| left.second.cmp(&right.second))
5693 .then_with(|| left.microsecond.cmp(&right.microsecond))
5694}
5695
5696pub(crate) fn compare_semantic_times(
5699 left: &SemanticTime,
5700 right: &SemanticTime,
5701) -> std::cmp::Ordering {
5702 left.hour
5703 .cmp(&right.hour)
5704 .then_with(|| left.minute.cmp(&right.minute))
5705 .then_with(|| left.second.cmp(&right.second))
5706 .then_with(|| left.microsecond.cmp(&right.microsecond))
5707}
5708
5709pub fn conversion_target_to_semantic(
5711 ct: &ConversionTarget,
5712 unit_index: Option<&crate::planning::unit_index::UnitIndex>,
5713 resolved_types: Option<&IndexMap<String, Arc<LemmaType>>>,
5714) -> Result<SemanticConversionTarget, String> {
5715 match ct {
5716 ConversionTarget::Type(kind) => Ok(SemanticConversionTarget::Type(*kind)),
5717 ConversionTarget::Unit { unit_name } => {
5718 let index = unit_index.ok_or_else(|| format!("Unknown unit '{unit_name}'."))?;
5719 let (bare, owning_type) = match resolved_types {
5720 Some(resolved) => index.resolve_with_named_types(unit_name, resolved)?,
5721 None => index.resolve(unit_name)?,
5722 };
5723 Ok(SemanticConversionTarget::Unit {
5724 unit_name: bare,
5725 owning_type,
5726 })
5727 }
5728 }
5729}
5730
5731static PRIMITIVE_BOOLEAN: OnceLock<Arc<LemmaType>> = OnceLock::new();
5737static PRIMITIVE_NUMBER: OnceLock<Arc<LemmaType>> = OnceLock::new();
5738static PRIMITIVE_TEXT: OnceLock<Arc<LemmaType>> = OnceLock::new();
5739static PRIMITIVE_DATE: OnceLock<Arc<LemmaType>> = OnceLock::new();
5740static PRIMITIVE_DATE_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
5741static PRIMITIVE_TIME: OnceLock<Arc<LemmaType>> = OnceLock::new();
5742static PRIMITIVE_RATIO: OnceLock<Arc<LemmaType>> = OnceLock::new();
5743
5744#[must_use]
5745pub fn primitive_boolean_arc() -> &'static Arc<LemmaType> {
5746 PRIMITIVE_BOOLEAN.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::boolean())))
5747}
5748
5749#[must_use]
5750pub fn primitive_number_arc() -> &'static Arc<LemmaType> {
5751 PRIMITIVE_NUMBER.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::number())))
5752}
5753
5754#[must_use]
5755pub fn primitive_text_arc() -> &'static Arc<LemmaType> {
5756 PRIMITIVE_TEXT.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::text())))
5757}
5758
5759#[must_use]
5760pub fn primitive_date_arc() -> &'static Arc<LemmaType> {
5761 PRIMITIVE_DATE.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::date())))
5762}
5763
5764#[must_use]
5765pub fn primitive_date_range_arc() -> &'static Arc<LemmaType> {
5766 PRIMITIVE_DATE_RANGE
5767 .get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::date_range())))
5768}
5769
5770#[must_use]
5771pub fn primitive_time_arc() -> &'static Arc<LemmaType> {
5772 PRIMITIVE_TIME.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::time())))
5773}
5774
5775#[must_use]
5776pub fn primitive_ratio_arc() -> &'static Arc<LemmaType> {
5777 PRIMITIVE_RATIO.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::ratio())))
5778}
5779
5780#[must_use]
5782pub fn type_spec_for_primitive(kind: PrimitiveKind) -> TypeSpecification {
5783 match kind {
5784 PrimitiveKind::Boolean => TypeSpecification::boolean(),
5785 PrimitiveKind::Measure => TypeSpecification::measure(),
5786 PrimitiveKind::MeasureRange => TypeSpecification::measure_range(),
5787 PrimitiveKind::Number => TypeSpecification::number(),
5788 PrimitiveKind::NumberRange => TypeSpecification::number_range(),
5789 PrimitiveKind::Ratio => TypeSpecification::ratio(),
5790 PrimitiveKind::RatioRange => TypeSpecification::ratio_range(),
5791 PrimitiveKind::Text => TypeSpecification::text(),
5792 PrimitiveKind::Date => TypeSpecification::date(),
5793 PrimitiveKind::DateRange => TypeSpecification::date_range(),
5794 PrimitiveKind::Time => TypeSpecification::time(),
5795 PrimitiveKind::TimeRange => TypeSpecification::time_range(),
5796 }
5797}
5798
5799impl fmt::Display for PathSegment {
5804 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5805 match &self.repository {
5806 Some(repository) => write!(f, "{} → {} {}", self.uses, repository, self.spec),
5807 None => write!(f, "{} → {}", self.uses, self.spec),
5808 }
5809 }
5810}
5811
5812impl fmt::Display for DataPath {
5813 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5814 write!(f, "{}", self.input_key())
5815 }
5816}
5817
5818impl fmt::Display for RulePath {
5819 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5820 write!(f, "{}", self.input_key())
5821 }
5822}
5823
5824impl fmt::Display for LemmaType {
5825 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5826 write!(f, "{}", self.name())
5827 }
5828}
5829
5830pub(crate) fn format_decimal_for_api(
5831 decimal: rust_decimal::Decimal,
5832 decimal_places: Option<u8>,
5833) -> rust_decimal::Decimal {
5834 match decimal_places {
5835 Some(decimal_places) => {
5836 let places = u32::from(decimal_places);
5837 let mut rounded = decimal.round_dp(places);
5838 rounded.rescale(places);
5839 rounded
5840 }
5841 None => {
5842 let normalized = decimal.normalize();
5843 if normalized.fract().is_zero() {
5844 normalized.trunc()
5845 } else {
5846 normalized
5847 }
5848 }
5849 }
5850}
5851
5852fn format_decimal_for_human_display(
5853 decimal: rust_decimal::Decimal,
5854 decimal_places: Option<u8>,
5855) -> String {
5856 match decimal_places {
5857 Some(decimal_places) => {
5858 let rounded = decimal.round_dp(u32::from(decimal_places));
5859 format!("{:.prec$}", rounded, prec = decimal_places as usize)
5860 }
5861 None => decimal.normalize().to_string(),
5862 }
5863}
5864
5865fn format_rational_for_human_display(
5866 magnitude: &crate::computation::rational::RationalInteger,
5867 decimal_places: Option<u8>,
5868) -> String {
5869 match magnitude.try_to_decimal() {
5870 Ok(decimal) => format_decimal_for_human_display(decimal, decimal_places),
5871 Err(crate::computation::rational::NumericFailure::Overflow) => magnitude.display_str(),
5872 Err(_) => magnitude.display_str(),
5873 }
5874}
5875
5876fn format_measure_canonical_for_display(
5877 canonical: &crate::computation::rational::RationalInteger,
5878 lemma_type: &LemmaType,
5879 signature: &[(String, i32)],
5880) -> String {
5881 use crate::computation::rational::checked_div;
5882
5883 let decimals = lemma_type.decimal_places();
5884
5885 if let TypeSpecification::Measure { units, .. } = &lemma_type.specifications {
5886 if !units.is_empty() {
5887 let unit = match lemma_type.unit() {
5889 Some(binding) => units
5890 .iter()
5891 .find(|unit| unit.name == binding)
5892 .unwrap_or_else(|| {
5893 panic!(
5894 "BUG: measure display binding '{binding}' is not a declared unit on type {}",
5895 lemma_type.name()
5896 )
5897 }),
5898 None => units
5899 .iter()
5900 .next()
5901 .expect("BUG: measure type with non-empty units must have a first unit"),
5902 };
5903 let in_unit = checked_div(canonical, &unit.factor)
5904 .expect("BUG: de-canonicalization for measure display must not fail");
5905 let formatted = format_rational_for_human_display(&in_unit, decimals);
5906 return format!("{} {}", formatted, unit.name);
5907 }
5908 }
5909
5910 let unit_label = match signature {
5911 [] => String::new(),
5912 [(name, 1)] => name.clone(),
5913 _ => format_signature_operator_style(signature),
5914 };
5915 let formatted = format_rational_for_human_display(canonical, decimals);
5916 if unit_label.is_empty() {
5917 formatted
5918 } else {
5919 format!("{formatted} {unit_label}")
5920 }
5921}
5922
5923fn format_ratio_canonical_for_display(
5924 canonical: &crate::computation::rational::RationalInteger,
5925 lemma_type: &LemmaType,
5926) -> String {
5927 use crate::computation::rational::{checked_mul, rational_new};
5928
5929 let display_unit = lemma_type
5930 .unit()
5931 .or_else(|| lemma_type.ratio_primary_unit());
5932
5933 match display_unit {
5934 Some("percent") => match checked_mul(canonical, &rational_new(100, 1)) {
5935 Ok(scaled) => format_number_with_unit_for_display(&scaled, "percent"),
5936 Err(_) => format!("{} percent", canonical.display_str()),
5937 },
5938 Some("permille") => match checked_mul(canonical, &rational_new(1000, 1)) {
5939 Ok(scaled) => format_number_with_unit_for_display(&scaled, "permille"),
5940 Err(_) => format!("{} permille", canonical.display_str()),
5941 },
5942 Some(unit_name) => format_number_with_unit_for_display(canonical, unit_name),
5943 None => format_rational_magnitude_for_display(canonical),
5944 }
5945}
5946
5947impl fmt::Display for LiteralValue {
5948 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5949 match &self.value {
5953 ValueKind::Measure(_) | ValueKind::Ratio(_) => {
5954 write!(f, "{}", self.value)
5955 }
5956 ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
5957 _ => write!(f, "{}", self.value),
5958 }
5959 }
5960}
5961
5962#[cfg(test)]
5967pub(crate) mod tests {
5968 use super::*;
5969 use crate::computation::rational::decimal_to_rational;
5970 use crate::literals::DateGranularity;
5971 use crate::literals::Value;
5972 use crate::parsing::ast::{BooleanValue, DateTimeValue, PrimitiveKind, TimeValue};
5973 use rust_decimal::Decimal;
5974 use std::str::FromStr;
5975 use std::sync::{Arc, OnceLock};
5976
5977 static PRIMITIVE_MEASURE: OnceLock<Arc<LemmaType>> = OnceLock::new();
5978
5979 #[must_use]
5980 pub(crate) fn primitive_measure_arc() -> &'static Arc<LemmaType> {
5981 PRIMITIVE_MEASURE
5982 .get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::measure())))
5983 }
5984
5985 #[must_use]
5986 pub(crate) fn primitive_measure() -> &'static LemmaType {
5987 primitive_measure_arc().as_ref()
5988 }
5989
5990 #[test]
5991 fn default_primitive_help_is_goal_oriented() {
5992 let kinds = [
5993 PrimitiveKind::Boolean,
5994 PrimitiveKind::Measure,
5995 PrimitiveKind::MeasureRange,
5996 PrimitiveKind::Number,
5997 PrimitiveKind::NumberRange,
5998 PrimitiveKind::Ratio,
5999 PrimitiveKind::RatioRange,
6000 PrimitiveKind::Text,
6001 PrimitiveKind::Date,
6002 PrimitiveKind::DateRange,
6003 PrimitiveKind::Time,
6004 PrimitiveKind::TimeRange,
6005 ];
6006 for kind in kinds {
6007 let spec = type_spec_for_primitive(kind);
6008 let help = match &spec {
6009 TypeSpecification::Boolean { help, .. }
6010 | TypeSpecification::Number { help, .. }
6011 | TypeSpecification::NumberRange { help, .. }
6012 | TypeSpecification::Text { help, .. }
6013 | TypeSpecification::Measure { help, .. }
6014 | TypeSpecification::MeasureRange { help, .. }
6015 | TypeSpecification::Ratio { help, .. }
6016 | TypeSpecification::RatioRange { help, .. }
6017 | TypeSpecification::Date { help, .. }
6018 | TypeSpecification::DateRange { help, .. }
6019 | TypeSpecification::TimeRange { help, .. }
6020 | TypeSpecification::Time { help, .. } => help,
6021 TypeSpecification::Veto { .. }
6022 | TypeSpecification::Undetermined
6023 | TypeSpecification::Spec { .. } => {
6024 unreachable!(
6025 "BUG: primitive kind {:?} mapped to non-primitive spec",
6026 kind
6027 )
6028 }
6029 };
6030 assert!(!help.is_empty(), "help for {:?}", kind);
6031 assert!(
6032 !help.to_ascii_lowercase().contains("format:"),
6033 "help for {:?} must not describe syntax: {:?}",
6034 kind,
6035 help
6036 );
6037 assert_eq!(help, default_help_for_primitive(kind));
6038 }
6039 }
6040
6041 #[test]
6042 fn data_path_and_rule_path_display_are_input_key() {
6043 let implicit = PathSegment {
6044 uses: "bag".to_string(),
6045 repository: None,
6046 spec: "bag".to_string(),
6047 };
6048 let explicit = PathSegment {
6049 uses: "bag".to_string(),
6050 repository: None,
6051 spec: "nut_bag".to_string(),
6052 };
6053 let named_repo = PathSegment {
6054 uses: "calc".to_string(),
6055 repository: Some("alpha".to_string()),
6056 spec: "pricing".to_string(),
6057 };
6058
6059 assert_eq!(
6060 DataPath::new(vec![implicit.clone()], "weight".to_string()).to_string(),
6061 "bag.weight"
6062 );
6063 assert_eq!(
6064 DataPath::new(vec![explicit.clone()], "weight".to_string()).to_string(),
6065 "bag.weight"
6066 );
6067 assert_eq!(
6068 DataPath::new(vec![named_repo.clone()], "price".to_string()).to_string(),
6069 "calc.price"
6070 );
6071 assert_eq!(
6072 RulePath::new(vec![implicit], "cost".to_string()).to_string(),
6073 "bag.cost"
6074 );
6075 assert_eq!(
6076 RulePath::new(vec![explicit], "cost".to_string()).to_string(),
6077 "bag.cost"
6078 );
6079 assert_eq!(
6080 RulePath::new(vec![named_repo], "price".to_string()).to_string(),
6081 "calc.price"
6082 );
6083
6084 assert_eq!(
6085 PathSegment {
6086 uses: "bag".to_string(),
6087 repository: None,
6088 spec: "nut_bag".to_string(),
6089 }
6090 .to_string(),
6091 "bag → nut_bag"
6092 );
6093 assert_eq!(
6094 PathSegment {
6095 uses: "calc".to_string(),
6096 repository: Some("alpha".to_string()),
6097 spec: "pricing".to_string(),
6098 }
6099 .to_string(),
6100 "calc → alpha pricing"
6101 );
6102 }
6103
6104 #[test]
6105 fn test_negated_comparison() {
6106 assert_eq!(
6107 negated_comparison(ComparisonComputation::LessThan),
6108 ComparisonComputation::GreaterThanOrEqual
6109 );
6110 assert_eq!(
6111 negated_comparison(ComparisonComputation::GreaterThanOrEqual),
6112 ComparisonComputation::LessThan
6113 );
6114 assert_eq!(
6115 negated_comparison(ComparisonComputation::Is),
6116 ComparisonComputation::IsNot
6117 );
6118 assert_eq!(
6119 negated_comparison(ComparisonComputation::IsNot),
6120 ComparisonComputation::Is
6121 );
6122 }
6123
6124 #[test]
6125 fn value_to_semantic_number_is_decimal() {
6126 let kind = value_to_semantic(&Value::Number(Decimal::from(42))).unwrap();
6127 assert!(matches!(kind, ValueKind::Number(d) if d == rational_new(42, 1)));
6128 }
6129
6130 #[test]
6131 fn value_kind_measure_serializes_magnitude_only() {
6132 let kind =
6133 ValueKind::Measure(decimal_to_rational(Decimal::from_str("99.50").unwrap()).unwrap());
6134 let json = serde_json::to_value(crate::api::ValueKind::from(&kind)).unwrap();
6135 assert_eq!(json["measure"]["value"], "99.5");
6136 assert!(json["measure"].get("signature").is_none());
6137 }
6138
6139 #[test]
6140 fn value_kind_measure_roundtrips() {
6141 let original =
6142 ValueKind::Measure(decimal_to_rational(Decimal::from_str("4800").unwrap()).unwrap());
6143 let api = crate::api::ValueKind::from(&original);
6144 let json = serde_json::to_string(&api).unwrap();
6145 let parsed: crate::api::ValueKind = serde_json::from_str(&json).unwrap();
6146 assert_eq!(api, parsed);
6147 }
6148
6149 #[test]
6150 fn value_kind_measure_empty_roundtrips() {
6151 let original =
6152 ValueKind::Measure(decimal_to_rational(Decimal::from_str("12.5").unwrap()).unwrap());
6153 let api = crate::api::ValueKind::from(&original);
6154 let json = serde_json::to_string(&api).unwrap();
6155 let parsed: crate::api::ValueKind = serde_json::from_str(&json).unwrap();
6156 assert_eq!(api, parsed);
6157 }
6158
6159 #[test]
6160 fn literal_value_number_serde_not_rational_array() {
6161 let lit = LiteralValue::number_from_decimal(Decimal::from(20));
6162 let json = serde_json::to_value(crate::api::LiteralValue::from(&lit)).unwrap();
6163 let number = json
6164 .get("value")
6165 .and_then(|v| v.get("number"))
6166 .expect("number field");
6167 assert!(number.is_string());
6168 assert_eq!(number.as_str(), Some("20"));
6169 assert!(
6170 !number.is_array(),
6171 "stored number must not serialize as [n,d]"
6172 );
6173 }
6174
6175 #[test]
6176 fn test_literal_value_to_primitive_type() {
6177 let one = rational_new(1, 1);
6178
6179 assert_eq!(TypedLiteral::text("".to_string()).lemma_type.name(), "text");
6180 assert_eq!(
6181 TypedLiteral::number(one.clone()).lemma_type.name(),
6182 "number"
6183 );
6184 assert_eq!(
6185 TypedLiteral::from_bool(bool::from(BooleanValue::True))
6186 .lemma_type
6187 .name(),
6188 "boolean"
6189 );
6190
6191 let dt = DateTimeValue {
6192 year: 2024,
6193 month: 1,
6194 day: 1,
6195 hour: 0,
6196 minute: 0,
6197 second: 0,
6198 microsecond: 0,
6199 timezone: None,
6200
6201 granularity: DateGranularity::Full,
6202 };
6203 assert_eq!(
6204 TypedLiteral::date(date_time_to_semantic(&dt))
6205 .lemma_type
6206 .name(),
6207 "date"
6208 );
6209 assert_eq!(
6210 TypedLiteral::ratio_from_decimal(Decimal::new(1, 2))
6211 .lemma_type
6212 .name(),
6213 "ratio"
6214 );
6215 let dur_type = LemmaType::new(
6216 "duration".to_string(),
6217 TypeSpecification::Measure {
6218 minimum: None,
6219 maximum: None,
6220 decimals: None,
6221 units: MeasureUnits::from(vec![MeasureUnit {
6222 name: "second".to_string(),
6223 factor: crate::computation::rational::rational_one(),
6224 derived_measure_factors: Vec::new(),
6225 decomposition: BaseMeasureVector::new(),
6226 minimum: None,
6227 maximum: None,
6228 suggestion_magnitude: None,
6229 }]),
6230 traits: vec![MeasureTrait::Duration],
6231 decomposition: None,
6232 unit: None,
6233 help: String::new(),
6234 },
6235 TypeExtends::Primitive,
6236 );
6237 assert_eq!(
6238 TypedLiteral::measure_with_type(one.clone(), Arc::new(dur_type))
6239 .lemma_type
6240 .name(),
6241 "duration"
6242 );
6243 }
6244
6245 #[test]
6246 fn test_type_display() {
6247 let specs = TypeSpecification::text();
6248 let lemma_type = LemmaType::new("name".to_string(), specs, TypeExtends::Primitive);
6249 assert_eq!(format!("{}", lemma_type), "name");
6250 }
6251
6252 #[test]
6253 fn test_type_serialization() {
6254 let specs = TypeSpecification::number();
6255 let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
6256 let api = crate::api::LemmaType::from(&lemma_type);
6257 let serialized = serde_json::to_string(&api).unwrap();
6258 let deserialized: crate::api::LemmaType = serde_json::from_str(&serialized).unwrap();
6259 assert_eq!(api, deserialized);
6260 }
6261
6262 #[test]
6263 fn test_literal_value_display_value() {
6264 let ten = rational_new(10, 1);
6265
6266 assert_eq!(
6267 LiteralValue::text("hello".to_string()).display_value(),
6268 "hello"
6269 );
6270 assert_eq!(LiteralValue::number(ten).display_value(), "10");
6271 assert_eq!(LiteralValue::from_bool(true).display_value(), "true");
6272 assert_eq!(LiteralValue::from_bool(false).display_value(), "false");
6273
6274 let ratio_ty = Arc::new(primitive_ratio_arc().as_ref().clone().with_unit("percent"));
6276 let ten_percent_ratio = LiteralValue::ratio(
6277 crate::literals::rational_from_parsed_decimal(Decimal::new(1, 1))
6278 .expect("ratio decimal"),
6279 );
6280 assert_eq!(
6281 ten_percent_ratio.display_value_with_type(ratio_ty.as_ref()),
6282 "10%"
6283 );
6284
6285 let time = TimeValue {
6286 hour: 14,
6287 minute: 30,
6288 second: 0,
6289 microsecond: 0,
6290 timezone: None,
6291 };
6292 let time_display = LiteralValue::time(time_to_semantic(&time)).display_value();
6293 assert!(time_display.contains("14"));
6294 assert!(time_display.contains("30"));
6295 }
6296
6297 #[test]
6298 fn test_measure_display_respects_type_decimals() {
6299 let money_type = LemmaType {
6300 name: Some("money".to_string()),
6301 specifications: TypeSpecification::Measure {
6302 minimum: None,
6303 maximum: None,
6304 decimals: Some(2),
6305 units: MeasureUnits::from(vec![MeasureUnit {
6306 name: "eur".to_string(),
6307 factor: crate::computation::rational::rational_one(),
6308 derived_measure_factors: Vec::new(),
6309 decomposition: BaseMeasureVector::new(),
6310 minimum: None,
6311 maximum: None,
6312 suggestion_magnitude: None,
6313 }]),
6314 traits: Vec::new(),
6315 decomposition: None,
6316 unit: None,
6317 help: String::new(),
6318 },
6319 extends: TypeExtends::Primitive,
6320 };
6321 let money_type = Arc::new(money_type);
6322 let val = LiteralValue::measure_with_type(
6323 decimal_to_rational(Decimal::from_str("1.8").unwrap()).unwrap(),
6324 money_type.clone(),
6325 );
6326 assert_eq!(val.display_value_with_type(money_type.as_ref()), "1.80 eur");
6327 let more_precision = LiteralValue::measure_with_type(
6328 decimal_to_rational(Decimal::from_str("1.80000").unwrap()).unwrap(),
6329 money_type.clone(),
6330 );
6331 assert_eq!(
6332 more_precision.display_value_with_type(money_type.as_ref()),
6333 "1.80 eur"
6334 );
6335 let measure_no_decimals = Arc::new(LemmaType {
6336 name: Some("count".to_string()),
6337 specifications: TypeSpecification::Measure {
6338 minimum: None,
6339 maximum: None,
6340 decimals: None,
6341 units: MeasureUnits::from(vec![MeasureUnit {
6342 name: "items".to_string(),
6343 factor: crate::computation::rational::rational_one(),
6344 derived_measure_factors: Vec::new(),
6345 decomposition: BaseMeasureVector::new(),
6346 minimum: None,
6347 maximum: None,
6348 suggestion_magnitude: None,
6349 }]),
6350 traits: Vec::new(),
6351 decomposition: None,
6352 unit: None,
6353 help: String::new(),
6354 },
6355 extends: TypeExtends::Primitive,
6356 });
6357 let val_any = LiteralValue::measure_with_type(
6358 decimal_to_rational(Decimal::from_str("42.50").unwrap()).unwrap(),
6359 Arc::clone(&measure_no_decimals),
6360 );
6361 assert_eq!(
6362 val_any.display_value_with_type(measure_no_decimals.as_ref()),
6363 "42.5 items"
6364 );
6365 }
6366
6367 #[test]
6368 fn test_literal_value_time_type() {
6369 let time = TimeValue {
6370 hour: 14,
6371 minute: 30,
6372 second: 0,
6373 microsecond: 0,
6374 timezone: None,
6375 };
6376 let lit = TypedLiteral::time(time_to_semantic(&time));
6377 assert_eq!(lit.lemma_type.name(), "time");
6378 }
6379
6380 #[test]
6381 fn test_measure_family_name_primitive_root() {
6382 let measure_spec = TypeSpecification::measure();
6383 let money_primitive = LemmaType::new(
6384 "money".to_string(),
6385 measure_spec.clone(),
6386 TypeExtends::Primitive,
6387 );
6388 assert_eq!(money_primitive.measure_family_name(), Some("money"));
6389 }
6390
6391 #[test]
6392 fn test_measure_family_name_custom() {
6393 let measure_spec = TypeSpecification::measure();
6394 let money_custom = LemmaType::new(
6395 "money".to_string(),
6396 measure_spec,
6397 TypeExtends::custom_local("money".to_string(), "money".to_string()),
6398 );
6399 assert_eq!(money_custom.measure_family_name(), Some("money"));
6400 }
6401
6402 #[test]
6403 fn test_same_measure_family_same_name_different_extends() {
6404 let measure_spec = TypeSpecification::measure();
6405 let money_primitive = LemmaType::new(
6406 "money".to_string(),
6407 measure_spec.clone(),
6408 TypeExtends::Primitive,
6409 );
6410 let money_custom = LemmaType::new(
6411 "money".to_string(),
6412 measure_spec,
6413 TypeExtends::custom_local("money".to_string(), "money".to_string()),
6414 );
6415 assert!(money_primitive.same_measure_family(&money_custom));
6416 assert!(money_custom.same_measure_family(&money_primitive));
6417 }
6418
6419 #[test]
6420 fn test_same_measure_family_parent_and_child() {
6421 let measure_spec = TypeSpecification::measure();
6422 let type_x = LemmaType::new(
6423 "x".to_string(),
6424 measure_spec.clone(),
6425 TypeExtends::Primitive,
6426 );
6427 let type_x2 = LemmaType::new(
6428 "x2".to_string(),
6429 measure_spec,
6430 TypeExtends::custom_local("x".to_string(), "x".to_string()),
6431 );
6432 assert_eq!(type_x.measure_family_name(), Some("x"));
6433 assert_eq!(type_x2.measure_family_name(), Some("x"));
6434 assert!(type_x.same_measure_family(&type_x2));
6435 assert!(type_x2.same_measure_family(&type_x));
6436 }
6437
6438 #[test]
6439 fn test_same_measure_family_siblings() {
6440 let measure_spec = TypeSpecification::measure();
6441 let type_x2_a = LemmaType::new(
6442 "x2a".to_string(),
6443 measure_spec.clone(),
6444 TypeExtends::custom_local("x".to_string(), "x".to_string()),
6445 );
6446 let type_x2_b = LemmaType::new(
6447 "x2b".to_string(),
6448 measure_spec,
6449 TypeExtends::custom_local("x".to_string(), "x".to_string()),
6450 );
6451 assert!(type_x2_a.same_measure_family(&type_x2_b));
6452 }
6453
6454 #[test]
6455 fn test_same_measure_family_different_families() {
6456 let measure_spec = TypeSpecification::measure();
6457 let money = LemmaType::new(
6458 "money".to_string(),
6459 measure_spec.clone(),
6460 TypeExtends::Primitive,
6461 );
6462 let temperature = LemmaType::new(
6463 "temperature".to_string(),
6464 measure_spec,
6465 TypeExtends::Primitive,
6466 );
6467 assert!(!money.same_measure_family(&temperature));
6468 assert!(!temperature.same_measure_family(&money));
6469 }
6470
6471 #[test]
6472 fn test_same_measure_family_measure_vs_non_measure() {
6473 let measure_spec = TypeSpecification::measure();
6474 let number_spec = TypeSpecification::number();
6475 let measure_type =
6476 LemmaType::new("money".to_string(), measure_spec, TypeExtends::Primitive);
6477 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
6478 assert!(!measure_type.same_measure_family(&number_type));
6479 assert!(!number_type.same_measure_family(&measure_type));
6480 }
6481
6482 #[test]
6483 fn test_same_measure_family_anonymous_measures_are_not_family_compatible() {
6484 let left = LemmaType::anonymous_for_decomposition(duration_decomposition());
6485 let right = LemmaType::anonymous_for_decomposition(duration_decomposition());
6486
6487 assert!(!left.same_measure_family(&right));
6488 assert!(left.compatible_with_anonymous_measure(&right));
6489 }
6490
6491 #[test]
6492 fn test_measure_family_name_non_measure_returns_none() {
6493 let number_spec = TypeSpecification::number();
6494 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
6495 assert_eq!(number_type.measure_family_name(), None);
6496 }
6497
6498 #[test]
6499 fn test_lemma_type_inequality_local_vs_import_same_shape() {
6500 let measure_spec = TypeSpecification::measure();
6501 let local = LemmaType::new(
6502 "t".to_string(),
6503 measure_spec.clone(),
6504 TypeExtends::custom_local("money".to_string(), "money".to_string()),
6505 );
6506 let imported = LemmaType::new(
6507 "t".to_string(),
6508 measure_spec,
6509 TypeExtends::Custom {
6510 parent: "money".to_string(),
6511 family: "money".to_string(),
6512 defining_spec: TypeDefiningSpec::Import,
6513 },
6514 );
6515 assert_ne!(local, imported);
6516 }
6517
6518 #[test]
6519 fn test_lemma_type_equality_import_unit_variant() {
6520 let measure_spec = TypeSpecification::measure();
6521 let left = LemmaType::new(
6522 "t".to_string(),
6523 measure_spec.clone(),
6524 TypeExtends::Custom {
6525 parent: "money".to_string(),
6526 family: "money".to_string(),
6527 defining_spec: TypeDefiningSpec::Import,
6528 },
6529 );
6530 let right = LemmaType::new(
6531 "t".to_string(),
6532 measure_spec,
6533 TypeExtends::Custom {
6534 parent: "money".to_string(),
6535 family: "money".to_string(),
6536 defining_spec: TypeDefiningSpec::Import,
6537 },
6538 );
6539 assert_eq!(left, right);
6540 }
6541
6542 fn month_suggestion_arg() -> CommandArg {
6543 CommandArg::Literal(crate::literals::Value::NumberWithUnit(
6544 Decimal::ONE,
6545 "month".to_string(),
6546 ))
6547 }
6548
6549 fn unit_factor_arg(name: &str, factor: i64) -> [CommandArg; 2] {
6550 [
6551 CommandArg::Label(name.to_string()),
6552 CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::from(factor))),
6553 ]
6554 }
6555
6556 #[test]
6557 fn default_calendar_on_text_reports_hint() {
6558 let mut specs = TypeSpecification::text();
6559 let mut default = None;
6560 let err = specs
6561 .apply_constraint(
6562 "notes",
6563 TypeConstraintCommand::Suggest,
6564 &[month_suggestion_arg()],
6565 &mut default,
6566 &mut None,
6567 None,
6568 )
6569 .unwrap_err();
6570 assert!(err.contains("Unit 'month' is for calendar data"));
6571 assert!(err.contains("double quotes"));
6572 }
6573
6574 #[test]
6575 fn default_calendar_on_duration_reports_valid_units() {
6576 let mut specs = TypeSpecification::measure();
6577 specs
6578 .apply_constraint(
6579 "duration",
6580 TypeConstraintCommand::Unit,
6581 &unit_factor_arg("second", 1),
6582 &mut None,
6583 &mut None,
6584 None,
6585 )
6586 .unwrap();
6587 specs
6588 .apply_constraint(
6589 "duration",
6590 TypeConstraintCommand::Unit,
6591 &unit_factor_arg("week", 604_800),
6592 &mut None,
6593 &mut None,
6594 None,
6595 )
6596 .unwrap();
6597 specs
6598 .apply_constraint(
6599 "duration",
6600 TypeConstraintCommand::Trait,
6601 &[CommandArg::Label("duration".to_string())],
6602 &mut None,
6603 &mut None,
6604 None,
6605 )
6606 .unwrap();
6607 let mut default = None;
6608 let err = specs
6609 .apply_constraint(
6610 "duration",
6611 TypeConstraintCommand::Suggest,
6612 &[month_suggestion_arg()],
6613 &mut default,
6614 &mut None,
6615 None,
6616 )
6617 .unwrap_err();
6618 assert!(err.contains("Unit 'month' is for calendar data"));
6619 assert!(err.contains("Valid 'duration' units are"));
6620 assert!(err.contains("week"));
6621 }
6622
6623 #[test]
6624 fn default_valid_duration_weeks_accepted() {
6625 let mut specs = TypeSpecification::measure();
6626 specs
6627 .apply_constraint(
6628 "duration",
6629 TypeConstraintCommand::Unit,
6630 &unit_factor_arg("second", 1),
6631 &mut None,
6632 &mut None,
6633 None,
6634 )
6635 .unwrap();
6636 specs
6637 .apply_constraint(
6638 "duration",
6639 TypeConstraintCommand::Unit,
6640 &unit_factor_arg("week", 604_800),
6641 &mut None,
6642 &mut None,
6643 None,
6644 )
6645 .unwrap();
6646 specs
6647 .apply_constraint(
6648 "duration",
6649 TypeConstraintCommand::Trait,
6650 &[CommandArg::Label("duration".to_string())],
6651 &mut None,
6652 &mut None,
6653 None,
6654 )
6655 .unwrap();
6656 let mut default = None;
6657 specs
6658 .apply_constraint(
6659 "duration",
6660 TypeConstraintCommand::Suggest,
6661 &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
6662 Decimal::from(4),
6663 "week".to_string(),
6664 ))],
6665 &mut default,
6666 &mut None,
6667 None,
6668 )
6669 .unwrap();
6670 assert!(matches!(
6671 default,
6672 Some(RawSuggestion::Measure {
6673 unit_name,
6674 ..
6675 }) if unit_name == "week"
6676 ));
6677 }
6678
6679 #[test]
6680 fn default_unknown_unit_on_duration_lists_valid_units() {
6681 let mut specs = TypeSpecification::measure();
6682 specs
6683 .apply_constraint(
6684 "duration",
6685 TypeConstraintCommand::Unit,
6686 &unit_factor_arg("second", 1),
6687 &mut None,
6688 &mut None,
6689 None,
6690 )
6691 .unwrap();
6692 specs
6693 .apply_constraint(
6694 "duration",
6695 TypeConstraintCommand::Trait,
6696 &[CommandArg::Label("duration".to_string())],
6697 &mut None,
6698 &mut None,
6699 None,
6700 )
6701 .unwrap();
6702 let mut default = None;
6703 let err = specs
6704 .apply_constraint(
6705 "duration",
6706 TypeConstraintCommand::Suggest,
6707 &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
6708 Decimal::ONE,
6709 "fortnight".to_string(),
6710 ))],
6711 &mut default,
6712 &mut None,
6713 None,
6714 )
6715 .unwrap_err();
6716 assert!(err.contains("fortnight"));
6717 assert!(err.contains("not defined on 'duration'"));
6718 assert!(err.contains("Valid units are"));
6719 }
6720
6721 fn money_measure_type() -> LemmaType {
6722 LemmaType::new(
6723 "Money".to_string(),
6724 TypeSpecification::Measure {
6725 minimum: None,
6726 maximum: None,
6727 decimals: None,
6728 units: MeasureUnits::from(vec![
6729 MeasureUnit {
6730 name: "eur".to_string(),
6731 factor: crate::computation::rational::rational_one(),
6732 derived_measure_factors: Vec::new(),
6733 decomposition: BaseMeasureVector::new(),
6734 minimum: None,
6735 maximum: None,
6736 suggestion_magnitude: None,
6737 },
6738 MeasureUnit {
6739 name: "usd".to_string(),
6740 factor: crate::computation::rational::decimal_to_rational(Decimal::new(
6741 91, 2,
6742 ))
6743 .expect("factor"),
6744 derived_measure_factors: Vec::new(),
6745 decomposition: BaseMeasureVector::new(),
6746 minimum: None,
6747 maximum: None,
6748 suggestion_magnitude: None,
6749 },
6750 ]),
6751 traits: Vec::new(),
6752 decomposition: None,
6753 unit: None,
6754 help: String::new(),
6755 },
6756 TypeExtends::Primitive,
6757 )
6758 }
6759
6760 #[test]
6761 fn measure_unit_names_for_named_measure() {
6762 let money = money_measure_type();
6763 assert_eq!(money.measure_unit_names(), Some(vec!["eur", "usd"]));
6764 }
6765
6766 fn sig(pairs: &[(&str, i32)]) -> Vec<(String, i32)> {
6771 pairs.iter().map(|(s, e)| (s.to_string(), *e)).collect()
6772 }
6773
6774 #[test]
6775 fn combine_signatures_multiply_adds_exponents() {
6776 let left = sig(&[("eur", 1)]);
6777 let right = sig(&[("hour", -1)]);
6778 let result = combine_signatures(&left, &right, true);
6779 assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
6780 }
6781
6782 #[test]
6783 fn combine_signatures_divide_subtracts_exponents() {
6784 let left = sig(&[("eur", 1)]);
6785 let right = sig(&[("hour", 1)]);
6786 let result = combine_signatures(&left, &right, false);
6787 assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
6788 }
6789
6790 #[test]
6791 fn combine_signatures_cancels_to_empty() {
6792 let left = sig(&[("ce", 1), ("minute", -1)]);
6793 let right = sig(&[("minute", 1)]);
6794 let result = combine_signatures(&left, &right, true);
6795 assert_eq!(result, sig(&[("ce", 1)]));
6797 }
6798
6799 #[test]
6800 fn combine_signatures_output_is_canonical_form() {
6801 let left = sig(&[("eur", 1), ("hour", 1)]);
6802 let right = sig(&[("minute", 1)]);
6803 let result = combine_signatures(&left, &right, false); let expected = sig(&[("eur", 1), ("hour", 1), ("minute", -1)]);
6806 assert_eq!(result, expected);
6807 }
6808
6809 #[test]
6810 fn canonicalize_signature_drops_zero_exponents() {
6811 let sig_with_zero = sig(&[("eur", 1), ("hour", 0), ("minute", -1)]);
6812 let result = canonicalize_signature(&sig_with_zero);
6813 assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
6814 }
6815
6816 #[test]
6817 fn canonicalize_signature_sorts_by_name() {
6818 let unsorted = sig(&[("minute", -1), ("eur", 1)]);
6819 let result = canonicalize_signature(&unsorted);
6820 assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
6821 }
6822
6823 #[test]
6829 fn format_signature_operator_style_numerator_only() {
6830 let signature = sig(&[("eur", 1)]);
6831 let result = format_signature_operator_style(&signature);
6832 assert_eq!(result, "eur");
6833 }
6834
6835 #[test]
6836 fn format_signature_operator_style_with_denominator() {
6837 let signature = sig(&[("eur", 1), ("hour", -1)]);
6838 let result = format_signature_operator_style(&signature);
6839 assert_eq!(result, "eur/hour");
6840 }
6841
6842 #[test]
6843 fn format_signature_operator_style_denominator_only() {
6844 let signature = sig(&[("meter", -1)]);
6845 let result = format_signature_operator_style(&signature);
6846 assert_eq!(result, "1/meter");
6847 }
6848
6849 #[test]
6850 fn format_signature_operator_style_with_exponents() {
6851 let signature = sig(&[("meter", 2), ("second", -2)]);
6852 let result = format_signature_operator_style(&signature);
6853 assert_eq!(result, "meter^2/second^2");
6854 }
6855
6856 #[test]
6861 fn calendar_unit_factor_table_completeness() {
6862 for unit in &[SemanticCalendarUnit::Month, SemanticCalendarUnit::Year] {
6865 let name = unit.to_string();
6866 assert!(
6867 calendar_unit_factor(&name).is_some(),
6868 "calendar_unit_factor('{}') must return Some",
6869 name
6870 );
6871 }
6872 }
6873
6874 #[test]
6875 fn semantic_calendar_unit_display_returns_singular() {
6876 assert_eq!(SemanticCalendarUnit::Month.to_string(), "month");
6879 assert_eq!(SemanticCalendarUnit::Year.to_string(), "year");
6880 }
6881
6882 #[test]
6887 fn signature_factor_with_calendar_units() {
6888 let calendar = test_calendar_type_for_signature_factor();
6889 let unit_index = crate::planning::unit_index::UnitIndex::new();
6890 let sig_month_per_year = sig(&[("month", 1), ("year", -1)]);
6893 let factor = signature_factor(&sig_month_per_year, &unit_index, Some(&calendar))
6894 .expect("must not overflow");
6895 let expected = rational_new(1, 12);
6896 assert_eq!(factor, expected, "month/year factor must be 1/12");
6897 }
6898
6899 fn test_calendar_type_for_signature_factor() -> LemmaType {
6900 use crate::computation::rational::{decimal_to_rational, rational_one};
6901 use crate::literals::{MeasureUnit, MeasureUnits};
6902 use rust_decimal::Decimal;
6903 LemmaType::new(
6904 "calendar".to_string(),
6905 TypeSpecification::Measure {
6906 minimum: None,
6907 maximum: None,
6908 decimals: None,
6909 units: MeasureUnits::from(vec![
6910 MeasureUnit {
6911 name: "month".to_string(),
6912 factor: rational_one(),
6913 minimum: None,
6914 maximum: None,
6915 suggestion_magnitude: None,
6916 decomposition: calendar_decomposition(),
6917 derived_measure_factors: Vec::new(),
6918 },
6919 MeasureUnit {
6920 name: "year".to_string(),
6921 factor: decimal_to_rational(Decimal::from(12)).expect("year factor"),
6922 minimum: None,
6923 maximum: None,
6924 suggestion_magnitude: None,
6925 decomposition: calendar_decomposition(),
6926 derived_measure_factors: Vec::new(),
6927 },
6928 ]),
6929 traits: vec![MeasureTrait::Calendar],
6930 decomposition: Some(calendar_decomposition()),
6931 unit: None,
6932 help: String::new(),
6933 },
6934 TypeExtends::Primitive,
6935 )
6936 }
6937
6938 #[test]
6939 #[should_panic(expected = "BUG: signature_factor called with unresolved unit name")]
6940 fn signature_factor_panics_on_unresolved_name() {
6941 let unit_index = crate::planning::unit_index::UnitIndex::new();
6942 let bad_sig = sig(&[("nonexistent_unit_xyz", 1)]);
6943 let _ = signature_factor(&bad_sig, &unit_index, None);
6944 }
6945
6946 #[test]
6947 fn signature_factor_uses_owner_when_expression_index_empty() {
6948 let money = test_money_type_for_signature_factor();
6949 let expression_units = crate::planning::unit_index::UnitIndex::new();
6950 let sig_usd = sig(&[("usd", 1)]);
6951 let factor =
6952 signature_factor(&sig_usd, &expression_units, Some(&money)).expect("must not overflow");
6953 assert_eq!(factor, rational_new(91, 100));
6954 }
6955
6956 fn test_money_type_for_signature_factor() -> LemmaType {
6957 use crate::computation::rational::decimal_to_rational;
6958 use crate::literals::{MeasureUnit, MeasureUnits};
6959 use rust_decimal::Decimal;
6960 LemmaType::new(
6961 "money".to_string(),
6962 TypeSpecification::Measure {
6963 minimum: None,
6964 maximum: None,
6965 decimals: Some(2),
6966 units: MeasureUnits::from(vec![
6967 MeasureUnit {
6968 name: "eur".to_string(),
6969 factor: crate::computation::rational::rational_one(),
6970 minimum: None,
6971 maximum: None,
6972 suggestion_magnitude: None,
6973 decomposition: BaseMeasureVector::new(),
6974 derived_measure_factors: Vec::new(),
6975 },
6976 MeasureUnit {
6977 name: "usd".to_string(),
6978 factor: decimal_to_rational(Decimal::new(91, 2)).expect("usd factor"),
6979 minimum: None,
6980 maximum: None,
6981 suggestion_magnitude: None,
6982 decomposition: BaseMeasureVector::new(),
6983 derived_measure_factors: Vec::new(),
6984 },
6985 ]),
6986 traits: Vec::new(),
6987 decomposition: None,
6988 unit: None,
6989 help: String::new(),
6990 },
6991 TypeExtends::Primitive,
6992 )
6993 }
6994
6995 fn measure_type_with_kilogram() -> TypeSpecification {
6996 use crate::computation::rational::rational_one;
6997 use crate::literals::{MeasureUnit, MeasureUnits};
6998 let mut units = MeasureUnits::new();
6999 units.push(MeasureUnit {
7000 name: "kilogram".to_string(),
7001 factor: rational_one(),
7002 minimum: None,
7003 maximum: None,
7004 suggestion_magnitude: None,
7005 decomposition: BaseMeasureVector::new(),
7006 derived_measure_factors: Vec::new(),
7007 });
7008 TypeSpecification::Measure {
7009 minimum: None,
7010 maximum: None,
7011 decimals: None,
7012 units,
7013 traits: Vec::new(),
7014 decomposition: None,
7015 unit: None,
7016 help: String::new(),
7017 }
7018 }
7019
7020 #[test]
7021 fn parser_value_to_value_kind_rejects_bare_number_for_measure() {
7022 let ten = Value::Number(Decimal::from(10));
7023 let err = parser_value_to_value_kind(&ten, &measure_type_with_kilogram(), None)
7024 .expect_err("bare number must not bind to measure");
7025 assert!(
7026 err.contains("kilogram"),
7027 "error must hint expected unit, got: {err}"
7028 );
7029 }
7030
7031 #[test]
7032 fn parser_value_to_value_kind_accepts_number_with_unit_for_measure() {
7033 let ten_kg = Value::NumberWithUnit(Decimal::from(10), "kilogram".to_string());
7034 let kind = parser_value_to_value_kind(&ten_kg, &measure_type_with_kilogram(), None)
7035 .expect("10 kilogram must bind to measure");
7036 assert!(matches!(kind, ValueKind::Measure(_)));
7037 }
7038
7039 #[test]
7040 fn parser_value_to_value_kind_accepts_bare_number_for_ratio() {
7041 let ten = Value::Number(Decimal::from(10));
7042 let kind = parser_value_to_value_kind(&ten, &TypeSpecification::ratio(), None)
7043 .expect("number -> ratio");
7044 assert!(matches!(kind, ValueKind::Ratio(_)));
7045 }
7046
7047 #[test]
7048 fn value_kind_matches_spec_rejects_number_for_measure() {
7049 let n = ValueKind::Number(rational_new(10, 1));
7050 assert!(!value_kind_matches_spec(&n, &measure_type_with_kilogram()));
7051 }
7052
7053 #[test]
7054 fn apply_constraint_rejects_inherited_unit_factor_change() {
7055 let mut specs = TypeSpecification::measure();
7056 specs
7057 .apply_constraint(
7058 "money",
7059 TypeConstraintCommand::Unit,
7060 &unit_factor_arg("eur", 1),
7061 &mut None,
7062 &mut None,
7063 None,
7064 )
7065 .expect("seed eur");
7066 let err = specs
7067 .apply_constraint(
7068 "money",
7069 TypeConstraintCommand::Unit,
7070 &[
7071 CommandArg::Label("eur".to_string()),
7072 CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::new(11, 1))),
7073 ],
7074 &mut None,
7075 &mut None,
7076 None,
7077 )
7078 .expect_err("must not change inherited unit factor");
7079 assert!(err.contains("eur"), "error must name unit, got: {err}");
7080 assert!(
7081 err.contains("inherited") || err.contains("cannot change"),
7082 "error must reject factor change, got: {err}"
7083 );
7084 }
7085
7086 #[test]
7087 fn apply_constraint_allows_additive_unit_on_inherited_spec() {
7088 let mut specs = TypeSpecification::measure();
7089 specs
7090 .apply_constraint(
7091 "money",
7092 TypeConstraintCommand::Unit,
7093 &unit_factor_arg("eur", 1),
7094 &mut None,
7095 &mut None,
7096 None,
7097 )
7098 .expect("seed eur");
7099 specs
7100 .apply_constraint(
7101 "money",
7102 TypeConstraintCommand::Unit,
7103 &unit_factor_arg("usd", 1),
7104 &mut None,
7105 &mut None,
7106 None,
7107 )
7108 .expect("add usd");
7109 match &specs {
7110 TypeSpecification::Measure { units, .. } => assert_eq!(units.len(), 2),
7111 other => panic!("expected Measure, got {other:?}"),
7112 }
7113 }
7114
7115 #[test]
7116 fn apply_constraint_idempotent_inherited_unit_redeclare() {
7117 let mut specs = TypeSpecification::measure();
7118 specs
7119 .apply_constraint(
7120 "money",
7121 TypeConstraintCommand::Unit,
7122 &unit_factor_arg("eur", 1),
7123 &mut None,
7124 &mut None,
7125 None,
7126 )
7127 .expect("seed eur");
7128 specs
7129 .apply_constraint(
7130 "money",
7131 TypeConstraintCommand::Unit,
7132 &unit_factor_arg("eur", 1),
7133 &mut None,
7134 &mut None,
7135 None,
7136 )
7137 .expect("idempotent eur");
7138 match &specs {
7139 TypeSpecification::Measure { units, .. } => {
7140 assert_eq!(units.len(), 1);
7141 assert_eq!(
7142 units.iter().find(|u| u.name == "eur").expect("eur").factor,
7143 crate::computation::rational::rational_one()
7144 );
7145 }
7146 other => panic!("expected Measure, got {other:?}"),
7147 }
7148 }
7149
7150 #[test]
7151 fn element_from_range_returns_element_for_every_range_primitive() {
7152 type RangeElementMatcher = fn(&TypeSpecification) -> bool;
7153 let cases: [(PrimitiveKind, RangeElementMatcher); 5] = [
7154 (PrimitiveKind::NumberRange, |element| {
7155 matches!(element, TypeSpecification::Number { .. })
7156 }),
7157 (PrimitiveKind::MeasureRange, |element| {
7158 matches!(element, TypeSpecification::Measure { .. })
7159 }),
7160 (PrimitiveKind::RatioRange, |element| {
7161 matches!(element, TypeSpecification::Ratio { .. })
7162 }),
7163 (PrimitiveKind::DateRange, |element| {
7164 matches!(element, TypeSpecification::Date { .. })
7165 }),
7166 (PrimitiveKind::TimeRange, |element| {
7167 matches!(element, TypeSpecification::Time { .. })
7168 }),
7169 ];
7170 for (kind, matches_element) in cases {
7171 let range_spec = type_spec_for_primitive(kind);
7172 let element = range_spec
7173 .element_from_range()
7174 .unwrap_or_else(|| panic!("{kind:?} must define element_from_range"));
7175 assert!(
7176 matches_element(&element),
7177 "{kind:?} element must match documented mapping, got {element:?}"
7178 );
7179 }
7180 }
7181
7182 #[test]
7183 fn element_from_range_returns_none_for_non_range_primitives() {
7184 let non_range = [
7185 type_spec_for_primitive(PrimitiveKind::Boolean),
7186 type_spec_for_primitive(PrimitiveKind::Measure),
7187 TypeSpecification::Undetermined,
7188 TypeSpecification::veto(),
7189 ];
7190 for spec in non_range {
7191 assert!(
7192 spec.element_from_range().is_none(),
7193 "{spec:?} must not define element_from_range"
7194 );
7195 }
7196 }
7197
7198 fn spec_type(name: &str, effective: crate::parsing::ast::EffectiveDate) -> LemmaType {
7199 LemmaType::primitive(TypeSpecification::Spec {
7200 spec: SpecIdentity {
7201 repository: None,
7202 spec: name.to_string(),
7203 effective,
7204 },
7205 })
7206 }
7207
7208 #[test]
7209 fn same_value_type_spec_identity() {
7210 use crate::parsing::ast::EffectiveDate;
7211 let origin = EffectiveDate::Origin;
7212 let dated = EffectiveDate::DateTimeValue(crate::DateTimeValue {
7213 year: 2026,
7214 month: 1,
7215 day: 1,
7216 hour: 0,
7217 minute: 0,
7218 second: 0,
7219 microsecond: 0,
7220 timezone: None,
7221 granularity: crate::literals::DateGranularity::Full,
7222 });
7223 let tax = spec_type("tax_bracket", origin.clone());
7224 let tax_again = spec_type("tax_bracket", origin.clone());
7225 let other = spec_type("menu_item", origin);
7226 let tax_dated = spec_type("tax_bracket", dated);
7227 assert!(tax.same_value_type(&tax_again));
7228 assert!(!tax.same_value_type(&other));
7229 assert!(!tax.same_value_type(&tax_dated));
7230 let mut other_repo = tax.clone();
7231 match &mut other_repo.specifications {
7232 TypeSpecification::Spec { spec } => spec.repository = Some("@jack/finance".to_string()),
7233 other => panic!("expected Spec, got {other:?}"),
7234 }
7235 assert!(!tax.same_value_type(&other_repo));
7236 }
7237
7238 #[test]
7239 fn piecewise_type_keeps_spec_identity() {
7240 use crate::parsing::ast::EffectiveDate;
7241 let tax = Arc::new(spec_type("tax_bracket", EffectiveDate::Origin));
7242 let picked = crate::planning::typing::piecewise_type([
7243 Arc::new(LemmaType::veto_type()),
7244 Arc::clone(&tax),
7245 ]);
7246 assert!(picked.same_value_type(&tax));
7247 }
7248}