1pub use crate::parsing::ast::{
9 ArithmeticComputation, ComparisonComputation, MathematicalComputation, NegationType,
10 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 help: String,
561 },
562 Number {
563 minimum: Option<RationalInteger>,
564 maximum: Option<RationalInteger>,
565 decimals: Option<u8>,
566 help: String,
567 },
568 NumberRange {
569 lower: Option<RationalInteger>,
570 upper: Option<RationalInteger>,
571 minimum: Option<RationalInteger>,
572 maximum: Option<RationalInteger>,
573 help: String,
574 },
575 Ratio {
576 minimum: Option<RationalInteger>,
577 maximum: Option<RationalInteger>,
578 decimals: Option<u8>,
579 units: RatioUnits,
580 help: String,
581 },
582 RatioRange {
583 lower: Option<RationalInteger>,
584 upper: Option<RationalInteger>,
585 minimum: Option<RationalInteger>,
586 maximum: Option<RationalInteger>,
587 units: RatioUnits,
588 help: String,
589 },
590 Text {
591 length: Option<usize>,
592 options: Vec<String>,
593 help: String,
594 },
595 Date {
596 minimum: Option<DateTimeValue>,
597 maximum: Option<DateTimeValue>,
598 help: String,
599 },
600 DateRange {
601 lower: Option<DateTimeValue>,
602 upper: Option<DateTimeValue>,
603 minimum: Option<(RationalInteger, String)>,
604 maximum: Option<(RationalInteger, String)>,
605 help: String,
606 },
607 Time {
608 minimum: Option<TimeValue>,
609 maximum: Option<TimeValue>,
610 help: String,
611 },
612 TimeRange {
613 lower: Option<TimeValue>,
614 upper: Option<TimeValue>,
615 minimum: Option<(RationalInteger, String)>,
616 maximum: Option<(RationalInteger, String)>,
617 help: String,
618 },
619 MeasureRange {
620 lower: Option<(RationalInteger, String)>,
621 upper: Option<(RationalInteger, String)>,
622 minimum: Option<(RationalInteger, String)>,
623 maximum: Option<(RationalInteger, String)>,
624 units: MeasureUnits,
625 decomposition: Option<BaseMeasureVector>,
626 help: String,
627 },
628 Veto {
629 message: Option<String>,
630 },
631 Undetermined,
635}
636
637impl std::fmt::Display for TypeSpecification {
638 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
639 let label = match self {
640 Self::Boolean { .. } => "boolean",
641 Self::Measure { .. } => "measure",
642 Self::MeasureRange { .. } => "measure range",
643 Self::Number { .. } => "number",
644 Self::NumberRange { .. } => "number range",
645 Self::Text { .. } => "text",
646 Self::Date { .. } => "date",
647 Self::DateRange { .. } => "date range",
648 Self::Time { .. } => "time",
649 Self::TimeRange { .. } => "time range",
650 Self::Ratio { .. } => "ratio",
651 Self::RatioRange { .. } => "ratio range",
652 Self::Veto { .. } => "veto",
653 Self::Undetermined => "undetermined",
654 };
655 f.write_str(label)
656 }
657}
658
659impl TypeSpecification {
660 pub fn help(&self) -> &str {
662 match self {
663 Self::Boolean { help, .. }
664 | Self::Measure { help, .. }
665 | Self::Number { help, .. }
666 | Self::NumberRange { help, .. }
667 | Self::Text { help, .. }
668 | Self::Date { help, .. }
669 | Self::DateRange { help, .. }
670 | Self::Time { help, .. }
671 | Self::TimeRange { help, .. }
672 | Self::Ratio { help, .. }
673 | Self::RatioRange { help, .. }
674 | Self::MeasureRange { help, .. } => help.as_str(),
675 Self::Veto { .. } | Self::Undetermined => "",
676 }
677 }
678}
679
680fn require_literal<'a>(
686 args: &'a [CommandArg],
687 cmd: &str,
688) -> Result<&'a crate::literals::Value, String> {
689 let arg = args
690 .first()
691 .ok_or_else(|| format!("{} requires an argument", cmd))?;
692 match arg {
693 CommandArg::Literal(v) => Ok(v),
694 CommandArg::Label(name) => Err(format!(
695 "{} requires a literal value, got identifier '{}'",
696 cmd, name
697 )),
698 CommandArg::UnitExpr(_) => Err(format!(
699 "{} requires a literal value, got a unit expression (only valid for 'unit' command)",
700 cmd
701 )),
702 }
703}
704
705fn apply_type_help_command(help: &mut String, args: &[CommandArg]) -> Result<(), String> {
706 match require_literal(args, "help")? {
707 crate::literals::Value::Text(s) => {
708 *help = s.clone();
709 Ok(())
710 }
711 other => Err(format!(
712 "help requires a text literal (quoted string), got {}",
713 value_kind_name(other)
714 )),
715 }
716}
717
718fn format_measure_units_list(units: &MeasureUnits) -> String {
719 units
720 .iter()
721 .map(|u| u.name.as_str())
722 .collect::<Vec<_>>()
723 .join(", ")
724}
725
726#[derive(Debug, Clone, Copy, PartialEq, Eq)]
728pub(crate) enum SuggestionExpectation {
729 MeasureUnits,
730 Text,
731 Number,
732 Boolean,
733 Date,
734 Time,
735 Ratio,
736 NumberRange,
737 DateRange,
738 TimeRange,
739 MeasureRange,
740 RatioRange,
741}
742
743pub(crate) fn suggestion_value_mismatch_error(
744 calendar_unit: &str,
745 type_name: &str,
746 expectation: SuggestionExpectation,
747 measure_units: Option<&MeasureUnits>,
748) -> String {
749 let unit_label = calendar_unit;
750 let first = format!("Unit '{unit_label}' is for calendar data.");
751 match expectation {
752 SuggestionExpectation::MeasureUnits => {
753 let list = measure_units
754 .map(format_measure_units_list)
755 .unwrap_or_default();
756 format!("{first} Valid '{type_name}' units are: {list}.")
757 }
758 SuggestionExpectation::Text => format!(
759 "{first} Please provide a text value in double quotes, for example `-> suggest \"my default value\"`."
760 ),
761 SuggestionExpectation::Number => format!(
762 "{first} Please provide a number, for example `-> suggest 42`."
763 ),
764 SuggestionExpectation::Boolean => format!(
765 "{first} Please provide true or false, for example `-> suggest true`."
766 ),
767 SuggestionExpectation::Date => format!(
768 "{first} Please provide a date, for example `-> suggest 2024-06-15`."
769 ),
770 SuggestionExpectation::Time => format!(
771 "{first} Please provide a time, for example `-> suggest 09:00:00`."
772 ),
773 SuggestionExpectation::Ratio | SuggestionExpectation::RatioRange => format!(
774 "{first} Please provide a ratio, for example `-> suggest 25%`."
775 ),
776 SuggestionExpectation::NumberRange => format!(
777 "{first} Please provide a number range, for example `-> suggest 10...100`."
778 ),
779 SuggestionExpectation::DateRange => format!(
780 "{first} Please provide a date range, for example `-> suggest 2024-01-01...2024-12-31`."
781 ),
782 SuggestionExpectation::TimeRange => format!(
783 "{first} Please provide a time range, for example `-> suggest 09:00...17:00`."
784 ),
785 SuggestionExpectation::MeasureRange => format!(
786 "{first} Please provide a range with units valid for '{type_name}', for example `-> suggest 30 kilogram...35 kilogram`."
787 ),
788 }
789}
790
791fn measure_suggestion_wrong_shape_error(type_name: &str, traits: &[MeasureTrait]) -> String {
792 let example = if traits.contains(&MeasureTrait::Duration) {
793 "4 week"
794 } else if traits.contains(&MeasureTrait::Calendar) {
795 "3 month"
796 } else {
797 "30 kilogram"
798 };
799 format!(
800 "Please provide a value with a unit valid for '{type_name}', for example `-> suggest {example}`."
801 )
802}
803
804fn reject_calendar_for_suggestion(
805 value: &crate::literals::Value,
806 type_name: &str,
807 expectation: SuggestionExpectation,
808 measure_units: Option<&MeasureUnits>,
809) -> Result<(), String> {
810 if let crate::literals::Value::NumberWithUnit(_, unit) = value {
811 if calendar_unit_factor(unit).is_some() {
812 return Err(suggestion_value_mismatch_error(
813 unit,
814 type_name,
815 expectation,
816 measure_units,
817 ));
818 }
819 }
820 Ok(())
821}
822
823fn value_kind_name(v: &crate::literals::Value) -> &'static str {
825 use crate::literals::Value;
826 match v {
827 Value::Number(_) => "number",
828 Value::NumberWithUnit(_, _) => "number_with_unit",
829 Value::Text(_) => "text",
830 Value::Date(_) => "date",
831 Value::Time(_) => "time",
832 Value::Boolean(_) => "boolean",
833 Value::Range(_, _) => "range",
834 }
835}
836
837fn require_suggestion_range_endpoints<'a>(
838 args: &'a [CommandArg],
839 type_name: &str,
840 expectation: SuggestionExpectation,
841 measure_units: Option<&MeasureUnits>,
842) -> Result<(&'a crate::literals::Value, &'a crate::literals::Value), String> {
843 match require_literal(args, "suggest")? {
844 crate::literals::Value::NumberWithUnit(_, unit)
845 if calendar_unit_factor(unit).is_some() =>
846 {
847 Err(suggestion_value_mismatch_error(
848 unit,
849 type_name,
850 expectation,
851 measure_units,
852 ))
853 }
854 crate::literals::Value::Range(left, right) => Ok((left.as_ref(), right.as_ref())),
855 _ => Err(match expectation {
856 SuggestionExpectation::NumberRange => {
857 "Please provide a number range, for example `-> suggest 10...100`.".to_string()
858 }
859 SuggestionExpectation::DateRange => {
860 "Please provide a date range, for example `-> suggest 2024-01-01...2024-12-31`."
861 .to_string()
862 }
863 SuggestionExpectation::RatioRange => {
864 "Please provide a ratio range, for example `-> suggest 10%...50%`.".to_string()
865 }
866 SuggestionExpectation::MeasureRange => format!(
867 "Please provide a range with units valid for '{type_name}', for example `-> suggest 30 kilogram...35 kilogram`."
868 ),
869 _ => unreachable!("BUG: require_suggestion_range_endpoints called with non-range expectation"),
870 }),
871 }
872}
873
874fn lift_parser_decimal(decimal: rust_decimal::Decimal) -> Result<RationalInteger, String> {
875 crate::computation::rational::decimal_to_rational(decimal)
876 .map_err(|failure| format!("literal failed rational lift: {failure}"))
877}
878
879pub fn range_element_type_specification(
881 range_spec: &TypeSpecification,
882) -> Option<TypeSpecification> {
883 range_spec.element_from_range()
884}
885
886fn range_endpoints_compatible(left: &LemmaType, right: &LemmaType) -> bool {
887 match (&left.specifications, &right.specifications) {
888 (TypeSpecification::Date { .. }, TypeSpecification::Date { .. }) => true,
889 (TypeSpecification::Time { .. }, TypeSpecification::Time { .. }) => true,
890 (TypeSpecification::Number { .. }, TypeSpecification::Number { .. }) => true,
891 (TypeSpecification::Measure { .. }, TypeSpecification::Measure { .. }) => {
892 left.same_measure_family(right)
893 || left.compatible_with_anonymous_measure(right)
894 || right.compatible_with_anonymous_measure(left)
895 }
896 (TypeSpecification::Ratio { .. }, TypeSpecification::Ratio { .. }) => true,
897 _ => false,
898 }
899}
900
901pub fn range_type_specification_from_endpoints(
903 left: &LemmaType,
904 right: &LemmaType,
905) -> Option<TypeSpecification> {
906 if !range_endpoints_compatible(left, right) {
907 return None;
908 }
909 left.specifications.range_from_element()
910}
911
912fn lift_range_endpoint(
916 value: &crate::parsing::ast::Value,
917 element_spec: &TypeSpecification,
918) -> Result<TypedLiteral, String> {
919 use crate::parsing::ast::Value;
920 match value {
921 Value::NumberWithUnit(_, unit_name) => {
922 let kind = parser_value_to_value_kind(value, element_spec)?;
923 let lemma_type = match &kind {
924 ValueKind::Measure(_) | ValueKind::Ratio(_) => Arc::new(
925 LemmaType::primitive(element_spec.clone())
926 .with_measure_binding_unit(unit_name.clone()),
927 ),
928 _ => Arc::new(LemmaType::primitive(element_spec.clone())),
929 };
930 Ok(TypedLiteral {
931 value: kind,
932 lemma_type,
933 })
934 }
935 _ => {
936 let kind = value_to_semantic(value)?;
937 Ok(TypedLiteral {
938 value: kind,
939 lemma_type: Arc::new(LemmaType::primitive(element_spec.clone())),
940 })
941 }
942 }
943}
944
945fn literal_value_from_parser_value(
946 value: &crate::parsing::ast::Value,
947) -> Result<TypedLiteral, String> {
948 use crate::parsing::ast::Value;
949
950 match value {
951 Value::Number(n) => Ok(TypedLiteral::number(lift_parser_decimal(*n)?)),
952 Value::Text(s) => Ok(TypedLiteral::text(s.clone())),
953 Value::Date(dt) => Ok(TypedLiteral::date(date_time_to_semantic(dt))),
954 Value::Time(t) => Ok(TypedLiteral::time(time_to_semantic(t))),
955 Value::Boolean(b) => Ok(TypedLiteral::from_bool(bool::from(*b))),
956 Value::NumberWithUnit(n, unit) => Ok(TypedLiteral::number_interpreted_as_measure(
957 lift_parser_decimal(*n)?,
958 unit.clone(),
959 )),
960 Value::Range(left, right) => {
961 let left = literal_value_from_parser_value(left)?;
962 let right = literal_value_from_parser_value(right)?;
963 let compatible = match (
964 &left.lemma_type.specifications,
965 &right.lemma_type.specifications,
966 ) {
967 (TypeSpecification::Date { .. }, TypeSpecification::Date { .. }) => true,
968 (TypeSpecification::Time { .. }, TypeSpecification::Time { .. }) => true,
969 (TypeSpecification::Number { .. }, TypeSpecification::Number { .. }) => true,
970 (TypeSpecification::Measure { .. }, TypeSpecification::Measure { .. }) => {
971 left.lemma_type.same_measure_family(&right.lemma_type)
972 || left
973 .lemma_type
974 .compatible_with_anonymous_measure(&right.lemma_type)
975 || right
976 .lemma_type
977 .compatible_with_anonymous_measure(&left.lemma_type)
978 }
979 (TypeSpecification::Ratio { .. }, TypeSpecification::Ratio { .. }) => true,
980 _ => false,
981 };
982 if !compatible {
983 return Err(format!(
984 "range endpoints must have the same supported base type, got {} and {}",
985 left.lemma_type.name(),
986 right.lemma_type.name()
987 ));
988 }
989 Ok(TypedLiteral::range(left, right))
990 }
991 }
992}
993
994fn decimal_to_u8(d: RationalInteger, ctx: &str) -> Result<u8, String> {
996 if !d.is_integer() {
997 return Err(format!(
998 "{} requires a whole number, got fractional value",
999 ctx
1000 ));
1001 }
1002 d.numer_to_u8()
1003 .ok_or_else(|| format!("{} value out of range for u8", ctx))
1004}
1005
1006fn decimal_to_usize(d: RationalInteger, ctx: &str) -> Result<usize, String> {
1008 if !d.is_integer() {
1009 return Err(format!(
1010 "{} requires a whole number, got fractional value",
1011 ctx
1012 ));
1013 }
1014 d.numer_to_usize()
1015 .ok_or_else(|| format!("{} value out of range for usize", ctx))
1016}
1017
1018fn ratio_bound_to_canonical_rational(
1024 args: &[CommandArg],
1025 cmd: &str,
1026 units: &RatioUnits,
1027) -> Result<RationalInteger, String> {
1028 use crate::computation::rational::{checked_div, decimal_to_rational};
1029 let lit = require_literal(args, cmd)?;
1030 match lit {
1031 crate::literals::Value::NumberWithUnit(magnitude, unit_name) => {
1032 let unit = units.get(unit_name.as_str())?;
1033 let magnitude_rational = decimal_to_rational(*magnitude)
1034 .map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
1035 checked_div(&magnitude_rational, &unit.value)
1036 .map_err(|failure| format!("{cmd}: unit conversion failed: {failure}"))
1037 }
1038 other => Err(format!(
1039 "{cmd} requires a ratio literal with a unit, got {}",
1040 value_kind_name(other)
1041 )),
1042 }
1043}
1044
1045fn require_decimal_literal(args: &[CommandArg], cmd: &str) -> Result<RationalInteger, String> {
1046 use crate::computation::rational::decimal_to_rational;
1047 match require_literal(args, cmd)? {
1048 crate::literals::Value::Number(d) => decimal_to_rational(*d)
1049 .map_err(|failure| format!("{} literal failed rational lift: {}", cmd, failure)),
1050 other => Err(format!(
1051 "{} requires a number literal, got {}",
1052 cmd,
1053 value_kind_name(other)
1054 )),
1055 }
1056}
1057
1058enum UnitConstraintField {
1059 Minimum,
1060 Maximum,
1061 SuggestionMagnitude,
1062}
1063
1064pub(crate) fn measure_declared_bound_to_canonical(
1065 magnitude: &RationalInteger,
1066 unit_name: &str,
1067 units: &MeasureUnits,
1068 type_name: &str,
1069 command: &str,
1070) -> Result<RationalInteger, String> {
1071 use crate::computation::rational::checked_mul;
1072 let unit = units.get(unit_name).map_err(|_| {
1073 format!(
1074 "Unit '{unit_name}' is not defined on '{type_name}'. Valid units are: {}.",
1075 format_measure_units_list(units)
1076 )
1077 })?;
1078 checked_mul(magnitude, &unit.factor)
1079 .map_err(|failure| format!("{command}: unit conversion overflow: {failure}"))
1080}
1081
1082fn parse_measure_declared_bound(
1083 args: &[CommandArg],
1084 cmd: &str,
1085 units: &MeasureUnits,
1086 type_name: &str,
1087) -> Result<(RationalInteger, String), String> {
1088 use crate::computation::rational::decimal_to_rational;
1089 let lit = require_literal(args, cmd)?;
1090 let (magnitude, unit_name) = match lit {
1091 crate::literals::Value::NumberWithUnit(n, unit) => (*n, unit.clone()),
1092 other => {
1093 return Err(format!(
1094 "{cmd} requires a measure literal with a unit, got {}",
1095 value_kind_name(other)
1096 ));
1097 }
1098 };
1099 units.get(unit_name.as_str()).map_err(|_| {
1100 format!(
1101 "Unit '{unit_name}' is not defined on '{type_name}'. Valid units are: {}.",
1102 format_measure_units_list(units)
1103 )
1104 })?;
1105 let magnitude_rational = decimal_to_rational(magnitude)
1106 .map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
1107 Ok((magnitude_rational, unit_name))
1108}
1109
1110fn sync_measure_units_from_canonical(
1111 units: &mut MeasureUnits,
1112 canonical: &RationalInteger,
1113 field: UnitConstraintField,
1114) -> Result<(), String> {
1115 use crate::computation::rational::checked_div;
1116 for unit in &mut units.0 {
1117 let magnitude = checked_div(canonical, &unit.factor).map_err(|failure| {
1118 format!(
1119 "cannot derive per-unit constraint for unit '{}': {failure}",
1120 unit.name
1121 )
1122 })?;
1123 match field {
1124 UnitConstraintField::Minimum => unit.minimum = Some(magnitude),
1125 UnitConstraintField::Maximum => unit.maximum = Some(magnitude),
1126 UnitConstraintField::SuggestionMagnitude => unit.suggestion_magnitude = Some(magnitude),
1127 }
1128 }
1129 Ok(())
1130}
1131
1132fn sync_ratio_units_from_canonical(
1133 units: &mut RatioUnits,
1134 canonical: &RationalInteger,
1135 field: UnitConstraintField,
1136) -> Result<(), String> {
1137 use crate::computation::rational::checked_mul;
1138 for unit in &mut units.0 {
1139 let magnitude = checked_mul(canonical, &unit.value).map_err(|failure| {
1140 format!(
1141 "cannot derive per-unit constraint for ratio unit '{}': {failure}",
1142 unit.name
1143 )
1144 })?;
1145 match field {
1146 UnitConstraintField::Minimum => unit.minimum = Some(magnitude),
1147 UnitConstraintField::Maximum => unit.maximum = Some(magnitude),
1148 UnitConstraintField::SuggestionMagnitude => unit.suggestion_magnitude = Some(magnitude),
1149 }
1150 }
1151 Ok(())
1152}
1153
1154fn sync_measure_suggestion_units(
1155 units: &mut MeasureUnits,
1156 default: &ValueKind,
1157 type_name: &str,
1158) -> Result<(), String> {
1159 let ValueKind::Measure(magnitude) = default else {
1160 return Ok(());
1161 };
1162 let unit_name = units
1163 .iter()
1164 .find(|unit| unit.is_canonical_factor())
1165 .or_else(|| units.iter().next())
1166 .map(|unit| unit.name.as_str())
1167 .expect(
1168 "BUG: Measure suggestion value requires at least one declared unit on the measure type",
1169 );
1170 units.get(unit_name).map_err(|_| {
1171 format!("Suggestion unit '{unit_name}' is not defined on measure type '{type_name}'.")
1172 })?;
1173 sync_measure_units_from_canonical(units, magnitude, UnitConstraintField::SuggestionMagnitude)
1174}
1175
1176pub(crate) fn finalize_measure_unit_constraint_magnitudes(
1177 specification: &mut TypeSpecification,
1178 declared_suggestion: Option<&ValueKind>,
1179 type_name: &str,
1180) -> Result<(), String> {
1181 let TypeSpecification::Measure {
1182 minimum,
1183 maximum,
1184 units,
1185 ..
1186 } = specification
1187 else {
1188 return Ok(());
1189 };
1190
1191 if let Some(bound) = minimum.as_ref() {
1192 let canonical =
1193 measure_declared_bound_to_canonical(&bound.0, &bound.1, units, type_name, "minimum")?;
1194 sync_measure_units_from_canonical(units, &canonical, UnitConstraintField::Minimum)?;
1195 }
1196 if let Some(bound) = maximum.as_ref() {
1197 let canonical =
1198 measure_declared_bound_to_canonical(&bound.0, &bound.1, units, type_name, "maximum")?;
1199 sync_measure_units_from_canonical(units, &canonical, UnitConstraintField::Maximum)?;
1200 }
1201 if let Some(default) = declared_suggestion {
1202 sync_measure_suggestion_units(units, default, type_name)?;
1203 }
1204
1205 if minimum.is_some() {
1206 for unit in units.iter() {
1207 assert!(
1208 unit.minimum.is_some(),
1209 "BUG: type '{type_name}' has minimum but unit '{}' missing per-unit minimum after finalize",
1210 unit.name
1211 );
1212 }
1213 }
1214 if maximum.is_some() {
1215 for unit in units.iter() {
1216 assert!(
1217 unit.maximum.is_some(),
1218 "BUG: type '{type_name}' has maximum but unit '{}' missing per-unit maximum after finalize",
1219 unit.name
1220 );
1221 }
1222 }
1223 if declared_suggestion.is_some() {
1224 for unit in units.iter() {
1225 assert!(
1226 unit.suggestion_magnitude.is_some(),
1227 "BUG: type '{type_name}' has default but unit '{}' missing per-unit default after finalize",
1228 unit.name
1229 );
1230 }
1231 }
1232
1233 Ok(())
1234}
1235
1236fn sync_ratio_suggestion_units(units: &mut RatioUnits, default: &ValueKind) -> Result<(), String> {
1237 let ValueKind::Ratio(canonical) = default else {
1238 return Ok(());
1239 };
1240 sync_ratio_units_from_canonical(units, canonical, UnitConstraintField::SuggestionMagnitude)
1241}
1242
1243fn option_name(arg: &CommandArg, cmd: &str) -> Result<String, String> {
1249 match arg {
1250 CommandArg::Literal(crate::literals::Value::Text(s)) => Ok(s.clone()),
1251 CommandArg::Label(name) => Ok(name.clone()),
1252 CommandArg::Literal(other) => Err(format!(
1253 "{} requires a text literal or identifier, got {}",
1254 cmd,
1255 value_kind_name(other)
1256 )),
1257 CommandArg::UnitExpr(_) => Err(format!(
1258 "{} requires a text literal or identifier, got a unit expression",
1259 cmd
1260 )),
1261 }
1262}
1263
1264fn label_name(arg: &CommandArg, cmd: &str) -> Result<String, String> {
1265 match arg {
1266 CommandArg::Label(name) => Ok(name.clone()),
1267 CommandArg::Literal(other) => Err(format!(
1268 "{} requires an identifier, got {}",
1269 cmd,
1270 value_kind_name(other)
1271 )),
1272 CommandArg::UnitExpr(_) => Err(format!(
1273 "{} requires an identifier, got a unit expression",
1274 cmd
1275 )),
1276 }
1277}
1278
1279fn measure_trait_name(measure_trait: MeasureTrait) -> &'static str {
1280 match measure_trait {
1281 MeasureTrait::Duration => "duration",
1282 MeasureTrait::Calendar => "calendar",
1283 }
1284}
1285
1286fn parse_measure_trait(args: &[CommandArg]) -> Result<MeasureTrait, String> {
1287 if args.len() != 1 {
1288 return Err("trait requires exactly one identifier argument".to_string());
1289 }
1290 match label_name(&args[0], "trait")?
1291 .trim()
1292 .to_lowercase()
1293 .as_str()
1294 {
1295 "duration" => Ok(MeasureTrait::Duration),
1296 "calendar" => Ok(MeasureTrait::Calendar),
1297 other => Err(format!("Unknown measure trait '{}'", other)),
1298 }
1299}
1300
1301fn validate_calendar_trait_requirements(units: &MeasureUnits) -> Result<(), String> {
1302 let month_unit = units
1303 .iter()
1304 .find(|unit| unit.name == "month")
1305 .ok_or_else(|| {
1306 "trait calendar requires a canonical 'month' unit declared before 'trait calendar'"
1307 .to_string()
1308 })?;
1309 if !month_unit.is_canonical_factor() {
1310 return Err("trait calendar requires unit month 1".to_string());
1311 }
1312 Ok(())
1313}
1314
1315fn validate_duration_trait_requirements(units: &MeasureUnits) -> Result<(), String> {
1316 let second_unit = units
1317 .iter()
1318 .find(|unit| unit.name == "second")
1319 .ok_or_else(|| {
1320 "trait duration requires a canonical 'second' unit declared before 'trait duration'"
1321 .to_string()
1322 })?;
1323 if !second_unit.is_canonical_factor() {
1324 return Err("trait duration requires unit second 1".to_string());
1325 }
1326 Ok(())
1327}
1328
1329fn require_date_literal(args: &[CommandArg], cmd: &str) -> Result<DateTimeValue, String> {
1331 match require_literal(args, cmd)? {
1332 crate::literals::Value::Date(dt) => Ok(dt.clone()),
1333 other => Err(format!(
1334 "{} requires a date literal (e.g. 2024-01-01), got {}",
1335 cmd,
1336 value_kind_name(other)
1337 )),
1338 }
1339}
1340
1341fn require_time_literal(args: &[CommandArg], cmd: &str) -> Result<TimeValue, String> {
1343 match require_literal(args, cmd)? {
1344 crate::literals::Value::Time(t) => Ok(t.clone()),
1345 other => Err(format!(
1346 "{} requires a time literal (e.g. 12:30:00), got {}",
1347 cmd,
1348 value_kind_name(other)
1349 )),
1350 }
1351}
1352
1353#[must_use]
1355pub fn default_help_for_primitive(kind: PrimitiveKind) -> &'static str {
1356 use PrimitiveKind::*;
1357 match kind {
1358 Boolean => "Whether this holds (true or false).",
1359 Number => "A dimensionless number.",
1360 NumberRange => "The lower and upper bound of the number range.",
1361 Text => "A text value.",
1362 Measure => "A numeric amount in one of this type's units.",
1363 MeasureRange => "The lower and upper bound of the measure range in the same unit.",
1364 Ratio => "A ratio in one of this type's units (e.g. percent).",
1365 RatioRange => "The lower and upper bound of the ratio range.",
1366 Date => "A date, or a date and time with optional timezone.",
1367 DateRange => "The start date and end date of the date range.",
1368 Time => "A time of day, with optional timezone.",
1369 TimeRange => "The start time and end time of the time range.",
1370 }
1371}
1372
1373impl TypeSpecification {
1374 pub fn boolean() -> Self {
1375 TypeSpecification::Boolean {
1376 help: default_help_for_primitive(PrimitiveKind::Boolean).to_string(),
1377 }
1378 }
1379 pub fn measure() -> Self {
1380 TypeSpecification::Measure {
1381 minimum: None,
1382 maximum: None,
1383 decimals: None,
1384 units: MeasureUnits::new(),
1385 traits: Vec::new(),
1386 decomposition: None,
1387 help: default_help_for_primitive(PrimitiveKind::Measure).to_string(),
1388 }
1389 }
1390 pub fn number() -> Self {
1391 TypeSpecification::Number {
1392 minimum: None,
1393 maximum: None,
1394 decimals: None,
1395 help: default_help_for_primitive(PrimitiveKind::Number).to_string(),
1396 }
1397 }
1398 pub fn number_range() -> Self {
1399 TypeSpecification::NumberRange {
1400 lower: None,
1401 upper: None,
1402 minimum: None,
1403 maximum: None,
1404 help: default_help_for_primitive(PrimitiveKind::NumberRange).to_string(),
1405 }
1406 }
1407 pub fn ratio() -> Self {
1408 TypeSpecification::Ratio {
1409 minimum: None,
1410 maximum: None,
1411 decimals: None,
1412 units: RatioUnits(vec![
1413 RatioUnit {
1414 name: "percent".to_string(),
1415 value: crate::computation::rational::rational_new(100, 1),
1416 minimum: None,
1417 maximum: None,
1418 suggestion_magnitude: None,
1419 },
1420 RatioUnit {
1421 name: "permille".to_string(),
1422 value: crate::computation::rational::rational_new(1000, 1),
1423 minimum: None,
1424 maximum: None,
1425 suggestion_magnitude: None,
1426 },
1427 ]),
1428 help: default_help_for_primitive(PrimitiveKind::Ratio).to_string(),
1429 }
1430 }
1431 pub fn ratio_range() -> Self {
1432 TypeSpecification::RatioRange {
1433 lower: None,
1434 upper: None,
1435 minimum: None,
1436 maximum: None,
1437 units: match TypeSpecification::ratio() {
1438 TypeSpecification::Ratio { units, .. } => units,
1439 _ => unreachable!("BUG: ratio constructor must return a ratio type"),
1440 },
1441 help: default_help_for_primitive(PrimitiveKind::RatioRange).to_string(),
1442 }
1443 }
1444 pub fn text() -> Self {
1445 TypeSpecification::Text {
1446 length: None,
1447 options: vec![],
1448 help: default_help_for_primitive(PrimitiveKind::Text).to_string(),
1449 }
1450 }
1451 pub fn date() -> Self {
1452 TypeSpecification::Date {
1453 minimum: None,
1454 maximum: None,
1455 help: default_help_for_primitive(PrimitiveKind::Date).to_string(),
1456 }
1457 }
1458 pub fn date_range() -> Self {
1459 TypeSpecification::DateRange {
1460 lower: None,
1461 upper: None,
1462 minimum: None,
1463 maximum: None,
1464 help: default_help_for_primitive(PrimitiveKind::DateRange).to_string(),
1465 }
1466 }
1467 pub fn time() -> Self {
1468 TypeSpecification::Time {
1469 minimum: None,
1470 maximum: None,
1471 help: default_help_for_primitive(PrimitiveKind::Time).to_string(),
1472 }
1473 }
1474 pub fn time_range() -> Self {
1475 TypeSpecification::TimeRange {
1476 lower: None,
1477 upper: None,
1478 minimum: None,
1479 maximum: None,
1480 help: default_help_for_primitive(PrimitiveKind::TimeRange).to_string(),
1481 }
1482 }
1483 pub fn measure_range() -> Self {
1484 TypeSpecification::MeasureRange {
1485 lower: None,
1486 upper: None,
1487 minimum: None,
1488 maximum: None,
1489 units: MeasureUnits::new(),
1490 decomposition: None,
1491 help: default_help_for_primitive(PrimitiveKind::MeasureRange).to_string(),
1492 }
1493 }
1494
1495 #[must_use]
1497 pub fn element_from_range(&self) -> Option<Self> {
1498 match self {
1499 TypeSpecification::NumberRange { lower, upper, .. } => {
1500 Some(TypeSpecification::Number {
1501 minimum: lower.clone(),
1502 maximum: upper.clone(),
1503 decimals: None,
1504 help: String::new(),
1505 })
1506 }
1507 TypeSpecification::MeasureRange {
1508 lower,
1509 upper,
1510 units,
1511 decomposition,
1512 ..
1513 } => Some(TypeSpecification::Measure {
1514 minimum: lower.clone(),
1515 maximum: upper.clone(),
1516 decimals: None,
1517 units: units.clone(),
1518 traits: Vec::new(),
1519 decomposition: decomposition.clone(),
1520 help: String::new(),
1521 }),
1522 TypeSpecification::DateRange { lower, upper, .. } => Some(TypeSpecification::Date {
1523 minimum: lower.clone(),
1524 maximum: upper.clone(),
1525 help: String::new(),
1526 }),
1527 TypeSpecification::TimeRange { lower, upper, .. } => Some(TypeSpecification::Time {
1528 minimum: lower.clone(),
1529 maximum: upper.clone(),
1530 help: String::new(),
1531 }),
1532 TypeSpecification::RatioRange {
1533 lower,
1534 upper,
1535 units,
1536 ..
1537 } => Some(TypeSpecification::Ratio {
1538 minimum: lower.clone(),
1539 maximum: upper.clone(),
1540 decimals: None,
1541 units: units.clone(),
1542 help: String::new(),
1543 }),
1544 _ => None,
1545 }
1546 }
1547
1548 #[must_use]
1550 pub fn range_from_element(&self) -> Option<Self> {
1551 match self {
1552 TypeSpecification::Number {
1553 minimum, maximum, ..
1554 } => Some(TypeSpecification::NumberRange {
1555 lower: minimum.clone(),
1556 upper: maximum.clone(),
1557 minimum: None,
1558 maximum: None,
1559 help: default_help_for_primitive(PrimitiveKind::NumberRange).to_string(),
1560 }),
1561 TypeSpecification::Measure {
1562 minimum,
1563 maximum,
1564 units,
1565 decomposition,
1566 ..
1567 } => Some(TypeSpecification::MeasureRange {
1568 lower: minimum.clone(),
1569 upper: maximum.clone(),
1570 minimum: None,
1571 maximum: None,
1572 units: units.clone(),
1573 decomposition: decomposition.clone(),
1574 help: default_help_for_primitive(PrimitiveKind::MeasureRange).to_string(),
1575 }),
1576 TypeSpecification::Date {
1577 minimum, maximum, ..
1578 } => Some(TypeSpecification::DateRange {
1579 lower: minimum.clone(),
1580 upper: maximum.clone(),
1581 minimum: None,
1582 maximum: None,
1583 help: default_help_for_primitive(PrimitiveKind::DateRange).to_string(),
1584 }),
1585 TypeSpecification::Time {
1586 minimum, maximum, ..
1587 } => Some(TypeSpecification::TimeRange {
1588 lower: minimum.clone(),
1589 upper: maximum.clone(),
1590 minimum: None,
1591 maximum: None,
1592 help: default_help_for_primitive(PrimitiveKind::TimeRange).to_string(),
1593 }),
1594 TypeSpecification::Ratio {
1595 minimum,
1596 maximum,
1597 units,
1598 ..
1599 } => Some(TypeSpecification::RatioRange {
1600 lower: minimum.clone(),
1601 upper: maximum.clone(),
1602 minimum: None,
1603 maximum: None,
1604 units: units.clone(),
1605 help: default_help_for_primitive(PrimitiveKind::RatioRange).to_string(),
1606 }),
1607 _ => None,
1608 }
1609 }
1610
1611 #[must_use]
1613 pub fn minimum_decimal(&self) -> Option<Decimal> {
1614 match self {
1615 TypeSpecification::Number { minimum, .. }
1616 | TypeSpecification::Ratio { minimum, .. } => minimum.as_ref().map(|bound| {
1617 bound
1618 .try_to_decimal()
1619 .expect("BUG: planned minimum must convert to decimal")
1620 }),
1621 TypeSpecification::Measure { minimum, .. } => minimum.as_ref().map(|(bound, _unit)| {
1622 bound
1623 .try_to_decimal()
1624 .expect("BUG: planned minimum must convert to decimal")
1625 }),
1626 _ => None,
1627 }
1628 }
1629
1630 #[must_use]
1632 pub fn maximum_decimal(&self) -> Option<Decimal> {
1633 match self {
1634 TypeSpecification::Number { maximum, .. }
1635 | TypeSpecification::Ratio { maximum, .. } => maximum.as_ref().map(|bound| {
1636 bound
1637 .try_to_decimal()
1638 .expect("BUG: planned maximum must convert to decimal")
1639 }),
1640 TypeSpecification::Measure { maximum, .. } => maximum.as_ref().map(|(bound, _unit)| {
1641 bound
1642 .try_to_decimal()
1643 .expect("BUG: planned maximum must convert to decimal")
1644 }),
1645 _ => None,
1646 }
1647 }
1648
1649 pub fn veto() -> Self {
1650 TypeSpecification::Veto { message: None }
1651 }
1652
1653 pub fn apply_constraint(
1665 &mut self,
1666 type_name: &str,
1667 command: TypeConstraintCommand,
1668 args: &[CommandArg],
1669 declared_suggestion: &mut Option<RawSuggestion>,
1670 declared_fill: &mut Option<RawSuggestion>,
1671 ) -> Result<(), String> {
1672 if command == TypeConstraintCommand::Trait
1673 && !matches!(&self, TypeSpecification::Measure { .. })
1674 {
1675 return Err("trait command is only valid on measure types".to_string());
1676 }
1677 match self {
1678 TypeSpecification::Boolean { help } => match command {
1679 TypeConstraintCommand::Help => {
1680 apply_type_help_command(help, args)?;
1681 }
1682 TypeConstraintCommand::Suggest | TypeConstraintCommand::Fill => {
1683 let (target, cmd) = match command {
1684 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
1685 TypeConstraintCommand::Fill => (&mut *declared_fill, "fill"),
1686 _ => unreachable!("BUG: only Suggest or Fill in this arm"),
1687 };
1688 let lit = require_literal(args, cmd)?;
1689 reject_calendar_for_suggestion(
1690 lit,
1691 type_name,
1692 SuggestionExpectation::Boolean,
1693 None,
1694 )?;
1695 match lit {
1696 crate::literals::Value::Boolean(bv) => {
1697 *target =
1698 Some(RawSuggestion::Value(ValueKind::Boolean(bool::from(bv))));
1699 }
1700 _ => {
1701 return Err(
1702 "Please provide true or false, for example `-> suggest true`."
1703 .to_string(),
1704 );
1705 }
1706 }
1707 }
1708 other => {
1709 return Err(format!(
1710 "Invalid command '{}' for boolean type. Valid commands: help, suggest, fill",
1711 other
1712 ));
1713 }
1714 },
1715 TypeSpecification::Measure {
1716 decimals,
1717 minimum,
1718 maximum,
1719 units,
1720 traits,
1721 help,
1722 ..
1723 } => match command {
1724 TypeConstraintCommand::Decimals => {
1725 let d = require_decimal_literal(args, "decimals")?;
1726 *decimals = Some(decimal_to_u8(d, "decimals")?);
1727 }
1728 TypeConstraintCommand::Unit => {
1729 let (unit_name, value, derived_measure_factors) = match args {
1730 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
1731 (name.clone(), *v, Vec::new())
1732 }
1733 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Expr(
1734 prefix,
1735 factors,
1736 ))] => {
1737 let raw: Vec<(String, i32)> = factors
1738 .iter()
1739 .map(|f| (f.measure_ref.clone(), f.exp))
1740 .collect();
1741 (name.clone(), *prefix, raw)
1742 }
1743 _ => {
1744 return Err(
1745 "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')"
1746 .to_string(),
1747 );
1748 }
1749 };
1750 if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
1751 let new_factor = crate::computation::rational::decimal_to_rational(value)
1752 .map_err(|failure| failure.to_string())?;
1753 if existing.factor != new_factor
1754 || existing.derived_measure_factors != derived_measure_factors
1755 {
1756 return Err(format!(
1757 "Unit '{unit_name}' is already defined in this type's inherited units; \
1758 cannot change factor or decomposition. Add a new unit name instead."
1759 ));
1760 }
1761 } else {
1762 units.0.push(MeasureUnit::from_decimal_factor(
1763 unit_name,
1764 value,
1765 derived_measure_factors,
1766 )?);
1767 }
1768 }
1769 TypeConstraintCommand::Trait => {
1770 let measure_trait = parse_measure_trait(args)?;
1771 if traits.contains(&measure_trait) {
1772 return Err(format!(
1773 "Duplicate trait '{}' for measure type.",
1774 measure_trait_name(measure_trait)
1775 ));
1776 }
1777 if measure_trait == MeasureTrait::Duration {
1778 validate_duration_trait_requirements(units)?;
1779 }
1780 if measure_trait == MeasureTrait::Calendar {
1781 validate_calendar_trait_requirements(units)?;
1782 }
1783 traits.push(measure_trait);
1784 }
1785 TypeConstraintCommand::Minimum => {
1786 *minimum = Some(parse_measure_declared_bound(
1787 args, "minimum", units, type_name,
1788 )?);
1789 }
1790 TypeConstraintCommand::Maximum => {
1791 *maximum = Some(parse_measure_declared_bound(
1792 args, "maximum", units, type_name,
1793 )?);
1794 }
1795 TypeConstraintCommand::Help => {
1796 apply_type_help_command(help, args)?;
1797 }
1798 TypeConstraintCommand::Suggest | TypeConstraintCommand::Fill => {
1799 let (target, cmd) = match command {
1800 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
1801 TypeConstraintCommand::Fill => (&mut *declared_fill, "fill"),
1802 _ => unreachable!("BUG: only Suggest or Fill in this arm"),
1803 };
1804 let lit = require_literal(args, cmd)?;
1805 if !traits.contains(&MeasureTrait::Calendar) {
1806 reject_calendar_for_suggestion(
1807 lit,
1808 type_name,
1809 SuggestionExpectation::MeasureUnits,
1810 Some(units),
1811 )?;
1812 }
1813 match lit {
1814 crate::literals::Value::NumberWithUnit(_, _) => {
1815 let (magnitude, unit_name) =
1816 parse_measure_declared_bound(args, cmd, units, type_name)?;
1817 *target = Some(RawSuggestion::Measure {
1818 magnitude,
1819 unit_name,
1820 });
1821 }
1822 _ => {
1823 return Err(measure_suggestion_wrong_shape_error(type_name, traits));
1824 }
1825 }
1826 }
1827 _ => {
1828 return Err(format!(
1829 "Invalid command '{}' for measure type. Valid commands: unit, trait, minimum, maximum, decimals, help, suggest, fill",
1830 command
1831 ));
1832 }
1833 },
1834 TypeSpecification::Number {
1835 decimals,
1836 minimum,
1837 maximum,
1838 help,
1839 } => match command {
1840 TypeConstraintCommand::Decimals => {
1841 let d = require_decimal_literal(args, "decimals")?;
1842 *decimals = Some(decimal_to_u8(d, "decimals")?);
1843 }
1844 TypeConstraintCommand::Unit => {
1845 return Err(
1846 "Invalid command 'unit' for number type. Number types are dimensionless and cannot have units. Use 'measure' type instead.".to_string()
1847 );
1848 }
1849 TypeConstraintCommand::Minimum => {
1850 *minimum = Some(require_decimal_literal(args, "minimum")?);
1851 }
1852 TypeConstraintCommand::Maximum => {
1853 *maximum = Some(require_decimal_literal(args, "maximum")?);
1854 }
1855 TypeConstraintCommand::Help => {
1856 apply_type_help_command(help, args)?;
1857 }
1858 TypeConstraintCommand::Suggest | TypeConstraintCommand::Fill => {
1859 let (target, cmd) = match command {
1860 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
1861 TypeConstraintCommand::Fill => (&mut *declared_fill, "fill"),
1862 _ => unreachable!("BUG: only Suggest or Fill in this arm"),
1863 };
1864 let lit = require_literal(args, cmd)?;
1865 reject_calendar_for_suggestion(
1866 lit,
1867 type_name,
1868 SuggestionExpectation::Number,
1869 None,
1870 )?;
1871 match lit {
1872 crate::literals::Value::Number(d) => {
1873 *target = Some(RawSuggestion::Value(ValueKind::Number(
1874 lift_parser_decimal(*d)?,
1875 )));
1876 }
1877 _ => {
1878 return Err(
1879 "Please provide a number, for example `-> suggest 42`.".to_string()
1880 );
1881 }
1882 }
1883 }
1884 _ => {
1885 return Err(format!(
1886 "Invalid command '{}' for number type. Valid commands: minimum, maximum, decimals, help, suggest, fill",
1887 command
1888 ));
1889 }
1890 },
1891 TypeSpecification::NumberRange {
1892 lower,
1893 upper,
1894 minimum,
1895 maximum,
1896 help,
1897 } => match command {
1898 TypeConstraintCommand::Lower => {
1899 *lower = Some(require_decimal_literal(args, "lower")?);
1900 }
1901 TypeConstraintCommand::Upper => {
1902 *upper = Some(require_decimal_literal(args, "upper")?);
1903 }
1904 TypeConstraintCommand::Minimum => {
1905 let width = require_decimal_literal(args, "minimum")?;
1906 reject_negative_width_magnitude(&width, "minimum")?;
1907 *minimum = Some(width);
1908 }
1909 TypeConstraintCommand::Maximum => {
1910 let width = require_decimal_literal(args, "maximum")?;
1911 reject_negative_width_magnitude(&width, "maximum")?;
1912 *maximum = Some(width);
1913 }
1914 TypeConstraintCommand::Help => {
1915 apply_type_help_command(help, args)?;
1916 }
1917 TypeConstraintCommand::Suggest | TypeConstraintCommand::Fill => {
1918 let target = match command {
1919 TypeConstraintCommand::Suggest => &mut *declared_suggestion,
1920 TypeConstraintCommand::Fill => &mut *declared_fill,
1921 _ => unreachable!("BUG: only Suggest or Fill in this arm"),
1922 };
1923 let (left, right) = require_suggestion_range_endpoints(
1924 args,
1925 type_name,
1926 SuggestionExpectation::NumberRange,
1927 None,
1928 )?;
1929 let left = literal_value_from_parser_value(left)?;
1930 let right = literal_value_from_parser_value(right)?;
1931 if !left.lemma_type.is_number() || !right.lemma_type.is_number() {
1932 return Err(
1933 "Please provide a number range, for example `-> suggest 10...100`."
1934 .to_string(),
1935 );
1936 }
1937 *target = Some(RawSuggestion::Value(ValueKind::Range(
1938 Box::new(left.to_literal()),
1939 Box::new(right.to_literal()),
1940 )));
1941 }
1942 _ => {
1943 return Err(format!(
1944 "Invalid command '{}' for number range type. Valid commands: lower, upper, minimum, maximum, help, suggest, fill",
1945 command
1946 ));
1947 }
1948 },
1949 TypeSpecification::Ratio {
1950 decimals,
1951 minimum,
1952 maximum,
1953 units,
1954 help,
1955 } => match command {
1956 TypeConstraintCommand::Decimals => {
1957 let d = require_decimal_literal(args, "decimals")?;
1958 *decimals = Some(decimal_to_u8(d, "decimals")?);
1959 }
1960 TypeConstraintCommand::Unit => {
1961 let (unit_name, value_dec) = match args {
1962 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
1963 (name.clone(), *v)
1964 }
1965 _ => {
1966 return Err(
1967 "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."
1968 .to_string(),
1969 );
1970 }
1971 };
1972 let value = crate::computation::rational::decimal_to_rational(value_dec)
1973 .map_err(|failure| {
1974 format!(
1975 "ratio unit value is not exactly representable as a rational: {}",
1976 failure
1977 )
1978 })?;
1979 if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
1980 if existing.value != value {
1981 return Err(format!(
1982 "Unit '{unit_name}' is already defined in this type's inherited units; \
1983 cannot change factor. Add a new unit name instead."
1984 ));
1985 }
1986 } else {
1987 units.0.push(RatioUnit {
1988 name: unit_name,
1989 value,
1990 minimum: None,
1991 maximum: None,
1992 suggestion_magnitude: None,
1993 });
1994 }
1995 }
1996 TypeConstraintCommand::Minimum => {
1997 let canonical = ratio_bound_to_canonical_rational(args, "minimum", units)?;
1998 sync_ratio_units_from_canonical(
1999 units,
2000 &canonical,
2001 UnitConstraintField::Minimum,
2002 )?;
2003 *minimum = Some(canonical);
2004 }
2005 TypeConstraintCommand::Maximum => {
2006 let canonical = ratio_bound_to_canonical_rational(args, "maximum", units)?;
2007 sync_ratio_units_from_canonical(
2008 units,
2009 &canonical,
2010 UnitConstraintField::Maximum,
2011 )?;
2012 *maximum = Some(canonical);
2013 }
2014 TypeConstraintCommand::Help => {
2015 apply_type_help_command(help, args)?;
2016 }
2017 TypeConstraintCommand::Suggest | TypeConstraintCommand::Fill => {
2018 let (target, cmd) = match command {
2019 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2020 TypeConstraintCommand::Fill => (&mut *declared_fill, "fill"),
2021 _ => unreachable!("BUG: only Suggest or Fill in this arm"),
2022 };
2023 let lit = require_literal(args, cmd)?;
2024 reject_calendar_for_suggestion(
2025 lit,
2026 type_name,
2027 SuggestionExpectation::Ratio,
2028 None,
2029 )?;
2030 match lit {
2031 crate::literals::Value::NumberWithUnit(_, unit_name) => {
2032 let element_spec = TypeSpecification::Ratio {
2033 decimals: *decimals,
2034 minimum: minimum.clone(),
2035 maximum: maximum.clone(),
2036 units: units.clone(),
2037 help: help.clone(),
2038 };
2039 let value = parser_value_to_value_kind(lit, &element_spec)?;
2040 sync_ratio_suggestion_units(units, &value)?;
2041 *target = Some(RawSuggestion::UnitBound {
2042 value,
2043 unit_name: unit_name.clone(),
2044 });
2045 }
2046 other => {
2047 return Err(format!(
2048 "suggest requires a ratio literal with a unit, got {}. Please provide a ratio value with a unit, for example `-> suggest 25%`.",
2049 value_kind_name(other)
2050 ));
2051 }
2052 }
2053 }
2054 _ => {
2055 return Err(format!(
2056 "Invalid command '{}' for ratio type. Valid commands: unit, minimum, maximum, decimals, help, suggest, fill",
2057 command
2058 ));
2059 }
2060 },
2061 TypeSpecification::RatioRange {
2062 lower,
2063 upper,
2064 minimum,
2065 maximum,
2066 units,
2067 help,
2068 } => match command {
2069 TypeConstraintCommand::Unit => {
2070 let (unit_name, value_dec) = match args {
2071 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
2072 (name.clone(), *v)
2073 }
2074 _ => {
2075 return Err(
2076 "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."
2077 .to_string(),
2078 );
2079 }
2080 };
2081 let value = crate::computation::rational::decimal_to_rational(value_dec)
2082 .map_err(|e| {
2083 format!(
2084 "ratio unit value is not exactly representable as a rational: {e}"
2085 )
2086 })?;
2087 if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
2088 if existing.value != value {
2089 return Err(format!(
2090 "Unit '{unit_name}' is already defined in this type's inherited units; \
2091 cannot change factor. Add a new unit name instead."
2092 ));
2093 }
2094 } else {
2095 units.0.push(RatioUnit {
2096 name: unit_name,
2097 value,
2098 minimum: None,
2099 maximum: None,
2100 suggestion_magnitude: None,
2101 });
2102 }
2103 }
2104 TypeConstraintCommand::Lower => {
2105 *lower = Some(ratio_bound_to_canonical_rational(args, "lower", units)?);
2106 }
2107 TypeConstraintCommand::Upper => {
2108 *upper = Some(ratio_bound_to_canonical_rational(args, "upper", units)?);
2109 }
2110 TypeConstraintCommand::Minimum => {
2111 let width = ratio_bound_to_canonical_rational(args, "minimum", units)?;
2112 reject_negative_width_magnitude(&width, "minimum")?;
2113 *minimum = Some(width);
2114 }
2115 TypeConstraintCommand::Maximum => {
2116 let width = ratio_bound_to_canonical_rational(args, "maximum", units)?;
2117 reject_negative_width_magnitude(&width, "maximum")?;
2118 *maximum = Some(width);
2119 }
2120 TypeConstraintCommand::Help => {
2121 apply_type_help_command(help, args)?;
2122 }
2123 TypeConstraintCommand::Suggest | TypeConstraintCommand::Fill => {
2124 let target = match command {
2125 TypeConstraintCommand::Suggest => &mut *declared_suggestion,
2126 TypeConstraintCommand::Fill => &mut *declared_fill,
2127 _ => unreachable!("BUG: only Suggest or Fill in this arm"),
2128 };
2129 let (left, right) = require_suggestion_range_endpoints(
2130 args,
2131 type_name,
2132 SuggestionExpectation::RatioRange,
2133 None,
2134 )?;
2135 let element_spec = TypeSpecification::RatioRange {
2136 lower: lower.clone(),
2137 upper: upper.clone(),
2138 minimum: minimum.clone(),
2139 maximum: maximum.clone(),
2140 units: units.clone(),
2141 help: help.clone(),
2142 }
2143 .element_from_range()
2144 .expect("BUG: RatioRange must define element_from_range");
2145 let left = lift_range_endpoint(left, &element_spec)?;
2146 let right = lift_range_endpoint(right, &element_spec)?;
2147 if !left.lemma_type.is_ratio() || !right.lemma_type.is_ratio() {
2148 return Err(
2149 "Please provide a ratio range, for example `-> suggest 10%...50%`."
2150 .to_string(),
2151 );
2152 }
2153 let value =
2154 ValueKind::Range(Box::new(left.to_literal()), Box::new(right.to_literal()));
2155 *target = match (
2156 left.lemma_type.measure_binding_unit.as_ref(),
2157 right.lemma_type.measure_binding_unit.as_ref(),
2158 ) {
2159 (Some(left_unit), Some(right_unit)) if left_unit == right_unit => {
2160 Some(RawSuggestion::UnitBound {
2161 value,
2162 unit_name: left_unit.clone(),
2163 })
2164 }
2165 _ => Some(RawSuggestion::Value(value)),
2166 };
2167 }
2168 _ => {
2169 return Err(format!(
2170 "Invalid command '{}' for ratio range type. Valid commands: unit, lower, upper, minimum, maximum, help, suggest, fill",
2171 command
2172 ));
2173 }
2174 },
2175 TypeSpecification::Text {
2176 length,
2177 options,
2178 help,
2179 } => match command {
2180 TypeConstraintCommand::Option => {
2181 if args.len() != 1 {
2182 return Err("option takes exactly one argument".to_string());
2183 }
2184 options.push(option_name(&args[0], "option")?);
2185 }
2186 TypeConstraintCommand::Options => {
2187 let mut collected = Vec::with_capacity(args.len());
2188 for arg in args {
2189 collected.push(option_name(arg, "options")?);
2190 }
2191 *options = collected;
2192 }
2193 TypeConstraintCommand::Length => {
2194 let d = require_decimal_literal(args, "length")?;
2195 *length = Some(decimal_to_usize(d, "length")?);
2196 }
2197 TypeConstraintCommand::Help => {
2198 apply_type_help_command(help, args)?;
2199 }
2200 TypeConstraintCommand::Suggest | TypeConstraintCommand::Fill => {
2201 let (target, cmd) = match command {
2202 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2203 TypeConstraintCommand::Fill => (&mut *declared_fill, "fill"),
2204 _ => unreachable!("BUG: only Suggest or Fill in this arm"),
2205 };
2206 let lit = require_literal(args, cmd)?;
2207 reject_calendar_for_suggestion(
2208 lit,
2209 type_name,
2210 SuggestionExpectation::Text,
2211 None,
2212 )?;
2213 match lit {
2214 crate::literals::Value::Text(s) => {
2215 *target = Some(RawSuggestion::Value(ValueKind::Text(s.clone())));
2216 }
2217 _ => {
2218 return Err(
2219 "Please provide a text value in double quotes, for example `-> suggest \"my default value\"`."
2220 .to_string(),
2221 );
2222 }
2223 }
2224 }
2225 _ => {
2226 return Err(format!(
2227 "Invalid command '{}' for text type. Valid commands: options, length, help, suggest, fill",
2228 command
2229 ));
2230 }
2231 },
2232 TypeSpecification::Date {
2233 minimum,
2234 maximum,
2235 help,
2236 } => match command {
2237 TypeConstraintCommand::Minimum => {
2238 let dt = require_date_literal(args, "minimum")?;
2239 *minimum = Some(dt);
2240 }
2241 TypeConstraintCommand::Maximum => {
2242 let dt = require_date_literal(args, "maximum")?;
2243 *maximum = Some(dt);
2244 }
2245 TypeConstraintCommand::Help => {
2246 apply_type_help_command(help, args)?;
2247 }
2248 TypeConstraintCommand::Suggest | TypeConstraintCommand::Fill => {
2249 let (target, cmd) = match command {
2250 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2251 TypeConstraintCommand::Fill => (&mut *declared_fill, "fill"),
2252 _ => unreachable!("BUG: only Suggest or Fill in this arm"),
2253 };
2254 let lit = require_literal(args, cmd)?;
2255 reject_calendar_for_suggestion(
2256 lit,
2257 type_name,
2258 SuggestionExpectation::Date,
2259 None,
2260 )?;
2261 match lit {
2262 crate::literals::Value::Date(dt) => {
2263 *target = Some(RawSuggestion::Value(ValueKind::Date(
2264 date_time_to_semantic(dt),
2265 )));
2266 }
2267 _ => {
2268 return Err(
2269 "Please provide a date, for example `-> suggest 2024-06-15`."
2270 .to_string(),
2271 );
2272 }
2273 }
2274 }
2275 _ => {
2276 return Err(format!(
2277 "Invalid command '{}' for date type. Valid commands: minimum, maximum, help, suggest, fill",
2278 command
2279 ));
2280 }
2281 },
2282 TypeSpecification::DateRange {
2283 lower,
2284 upper,
2285 minimum,
2286 maximum,
2287 help,
2288 } => match command {
2289 TypeConstraintCommand::Lower => {
2290 *lower = Some(require_date_literal(args, "lower")?);
2291 }
2292 TypeConstraintCommand::Upper => {
2293 *upper = Some(require_date_literal(args, "upper")?);
2294 }
2295 TypeConstraintCommand::Minimum => {
2296 *minimum = Some(parse_unresolved_width_bound(args, "minimum")?);
2297 }
2298 TypeConstraintCommand::Maximum => {
2299 *maximum = Some(parse_unresolved_width_bound(args, "maximum")?);
2300 }
2301 TypeConstraintCommand::Help => {
2302 apply_type_help_command(help, args)?;
2303 }
2304 TypeConstraintCommand::Suggest | TypeConstraintCommand::Fill => {
2305 let target = match command {
2306 TypeConstraintCommand::Suggest => &mut *declared_suggestion,
2307 TypeConstraintCommand::Fill => &mut *declared_fill,
2308 _ => unreachable!("BUG: only Suggest or Fill in this arm"),
2309 };
2310 let (left, right) = require_suggestion_range_endpoints(
2311 args,
2312 type_name,
2313 SuggestionExpectation::DateRange,
2314 None,
2315 )?;
2316 let left = literal_value_from_parser_value(left)?;
2317 let right = literal_value_from_parser_value(right)?;
2318 if !left.lemma_type.is_date() || !right.lemma_type.is_date() {
2319 return Err(
2320 "Please provide a date range, for example `-> suggest 2024-01-01...2024-12-31`."
2321 .to_string(),
2322 );
2323 }
2324 *target = Some(RawSuggestion::Value(ValueKind::Range(
2325 Box::new(left.to_literal()),
2326 Box::new(right.to_literal()),
2327 )));
2328 }
2329 _ => {
2330 return Err(format!(
2331 "Invalid command '{}' for date range type. Valid commands: lower, upper, minimum, maximum, help, suggest, fill",
2332 command
2333 ));
2334 }
2335 },
2336 TypeSpecification::Time {
2337 minimum,
2338 maximum,
2339 help,
2340 } => match command {
2341 TypeConstraintCommand::Minimum => {
2342 let t = require_time_literal(args, "minimum")?;
2343 *minimum = Some(t);
2344 }
2345 TypeConstraintCommand::Maximum => {
2346 let t = require_time_literal(args, "maximum")?;
2347 *maximum = Some(t);
2348 }
2349 TypeConstraintCommand::Help => {
2350 apply_type_help_command(help, args)?;
2351 }
2352 TypeConstraintCommand::Suggest | TypeConstraintCommand::Fill => {
2353 let (target, cmd) = match command {
2354 TypeConstraintCommand::Suggest => (&mut *declared_suggestion, "suggest"),
2355 TypeConstraintCommand::Fill => (&mut *declared_fill, "fill"),
2356 _ => unreachable!("BUG: only Suggest or Fill in this arm"),
2357 };
2358 let lit = require_literal(args, cmd)?;
2359 reject_calendar_for_suggestion(
2360 lit,
2361 type_name,
2362 SuggestionExpectation::Time,
2363 None,
2364 )?;
2365 match lit {
2366 crate::literals::Value::Time(t) => {
2367 *target =
2368 Some(RawSuggestion::Value(ValueKind::Time(time_to_semantic(t))));
2369 }
2370 _ => {
2371 return Err(
2372 "Please provide a time, for example `-> suggest 09:00:00`."
2373 .to_string(),
2374 );
2375 }
2376 }
2377 }
2378 _ => {
2379 return Err(format!(
2380 "Invalid command '{}' for time type. Valid commands: minimum, maximum, help, suggest, fill",
2381 command
2382 ));
2383 }
2384 },
2385 TypeSpecification::TimeRange {
2386 lower,
2387 upper,
2388 minimum,
2389 maximum,
2390 help,
2391 } => match command {
2392 TypeConstraintCommand::Lower => {
2393 *lower = Some(require_time_literal(args, "lower")?);
2394 }
2395 TypeConstraintCommand::Upper => {
2396 *upper = Some(require_time_literal(args, "upper")?);
2397 }
2398 TypeConstraintCommand::Minimum => {
2399 *minimum = Some(parse_unresolved_width_bound(args, "minimum")?);
2400 }
2401 TypeConstraintCommand::Maximum => {
2402 *maximum = Some(parse_unresolved_width_bound(args, "maximum")?);
2403 }
2404 TypeConstraintCommand::Help => {
2405 apply_type_help_command(help, args)?;
2406 }
2407 TypeConstraintCommand::Suggest | TypeConstraintCommand::Fill => {
2408 let target = match command {
2409 TypeConstraintCommand::Suggest => &mut *declared_suggestion,
2410 TypeConstraintCommand::Fill => &mut *declared_fill,
2411 _ => unreachable!("BUG: only Suggest or Fill in this arm"),
2412 };
2413 let (left, right) = require_suggestion_range_endpoints(
2414 args,
2415 type_name,
2416 SuggestionExpectation::TimeRange,
2417 None,
2418 )?;
2419 let left = literal_value_from_parser_value(left)?;
2420 let right = literal_value_from_parser_value(right)?;
2421 if !left.lemma_type.is_time() || !right.lemma_type.is_time() {
2422 return Err(
2423 "Please provide a time range, for example `-> suggest 09:00...17:00`."
2424 .to_string(),
2425 );
2426 }
2427 *target = Some(RawSuggestion::Value(ValueKind::Range(
2428 Box::new(left.to_literal()),
2429 Box::new(right.to_literal()),
2430 )));
2431 }
2432 _ => {
2433 return Err(format!(
2434 "Invalid command '{}' for time range type. Valid commands: lower, upper, minimum, maximum, help, suggest, fill",
2435 command
2436 ));
2437 }
2438 },
2439 TypeSpecification::MeasureRange {
2440 lower,
2441 upper,
2442 minimum,
2443 maximum,
2444 units,
2445 decomposition,
2446 help,
2447 } => match command {
2448 TypeConstraintCommand::Unit => {
2449 let (unit_name, value, derived_measure_factors) = match args {
2450 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
2451 (name.clone(), *v, Vec::new())
2452 }
2453 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Expr(
2454 prefix,
2455 factors,
2456 ))] => {
2457 let raw: Vec<(String, i32)> = factors
2458 .iter()
2459 .map(|f| (f.measure_ref.clone(), f.exp))
2460 .collect();
2461 (name.clone(), *prefix, raw)
2462 }
2463 _ => {
2464 return Err(
2465 "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')"
2466 .to_string(),
2467 );
2468 }
2469 };
2470 if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
2471 let new_factor = crate::computation::rational::decimal_to_rational(value)
2472 .map_err(|failure| failure.to_string())?;
2473 if existing.factor != new_factor
2474 || existing.derived_measure_factors != derived_measure_factors
2475 {
2476 return Err(format!(
2477 "Unit '{unit_name}' is already defined in this type's inherited units; \
2478 cannot change factor or decomposition. Add a new unit name instead."
2479 ));
2480 }
2481 } else {
2482 units.0.push(MeasureUnit::from_decimal_factor(
2483 unit_name,
2484 value,
2485 derived_measure_factors,
2486 )?);
2487 }
2488 }
2489 TypeConstraintCommand::Lower => {
2490 *lower = Some(parse_measure_declared_bound(
2491 args, "lower", units, type_name,
2492 )?);
2493 }
2494 TypeConstraintCommand::Upper => {
2495 *upper = Some(parse_measure_declared_bound(
2496 args, "upper", units, type_name,
2497 )?);
2498 }
2499 TypeConstraintCommand::Minimum => {
2500 let width = parse_measure_declared_bound(args, "minimum", units, type_name)?;
2501 reject_negative_width_magnitude(&width.0, "minimum")?;
2502 *minimum = Some(width);
2503 }
2504 TypeConstraintCommand::Maximum => {
2505 let width = parse_measure_declared_bound(args, "maximum", units, type_name)?;
2506 reject_negative_width_magnitude(&width.0, "maximum")?;
2507 *maximum = Some(width);
2508 }
2509 TypeConstraintCommand::Help => {
2510 apply_type_help_command(help, args)?;
2511 }
2512 TypeConstraintCommand::Suggest | TypeConstraintCommand::Fill => {
2513 let target = match command {
2514 TypeConstraintCommand::Suggest => &mut *declared_suggestion,
2515 TypeConstraintCommand::Fill => &mut *declared_fill,
2516 _ => unreachable!("BUG: only Suggest or Fill in this arm"),
2517 };
2518 let (left, right) = require_suggestion_range_endpoints(
2519 args,
2520 type_name,
2521 SuggestionExpectation::MeasureRange,
2522 Some(units),
2523 )?;
2524 let element_spec = TypeSpecification::MeasureRange {
2525 lower: lower.clone(),
2526 upper: upper.clone(),
2527 minimum: minimum.clone(),
2528 maximum: maximum.clone(),
2529 units: units.clone(),
2530 decomposition: decomposition.clone(),
2531 help: help.clone(),
2532 }
2533 .element_from_range()
2534 .expect("BUG: MeasureRange must define element_from_range");
2535 let left = lift_range_endpoint(left, &element_spec)?;
2536 let right = lift_range_endpoint(right, &element_spec)?;
2537 if !left.lemma_type.is_measure() || !right.lemma_type.is_measure() {
2538 return Err(format!(
2539 "Please provide a range with units valid for '{type_name}', for example `-> suggest 30 kilogram...35 kilogram`."
2540 ));
2541 }
2542 *target = Some(RawSuggestion::Value(ValueKind::Range(
2543 Box::new(left.to_literal()),
2544 Box::new(right.to_literal()),
2545 )));
2546 }
2547 _ => {
2548 return Err(format!(
2549 "Invalid command '{}' for measure range type. Valid commands: unit, lower, upper, minimum, maximum, help, suggest, fill",
2550 command
2551 ));
2552 }
2553 },
2554 TypeSpecification::Veto { .. } => {
2555 return Err(format!(
2556 "Invalid command '{}' for veto type. Veto is not a user-declarable type and cannot have constraints",
2557 command
2558 ));
2559 }
2560 TypeSpecification::Undetermined => {
2561 return Err(format!(
2562 "Invalid command '{}' for undetermined sentinel type. Undetermined is an internal type used during type inference and cannot have constraints",
2563 command
2564 ));
2565 }
2566 }
2567 Ok(())
2568 }
2569}
2570
2571pub fn parse_number_unit(
2574 value_str: &str,
2575 type_spec: &TypeSpecification,
2576) -> Result<crate::parsing::ast::Value, String> {
2577 use crate::literals::{NumberWithUnit, RatioLiteral};
2578 use crate::parsing::ast::Value;
2579
2580 let trimmed = value_str.trim();
2581 match type_spec {
2582 TypeSpecification::Measure { units, .. } => {
2583 if units.is_empty() {
2584 unreachable!(
2585 "BUG: Measure type has no units; should have been validated during planning"
2586 );
2587 }
2588 match trimmed.parse::<NumberWithUnit>() {
2589 Ok(n) => {
2590 let unit = units.get(&n.1).map_err(|e| e.to_string())?;
2591 Ok(Value::NumberWithUnit(n.0, unit.name.clone()))
2592 }
2593 Err(e) => {
2594 if trimmed.split_whitespace().count() == 1 && !trimmed.is_empty() {
2595 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
2596 let example_unit = units
2597 .iter()
2598 .next()
2599 .expect("BUG: units non-empty after guard")
2600 .name
2601 .as_str();
2602 Err(format!(
2603 "Measure value must include a unit, for example: '{} {}'. Valid units: {}.",
2604 trimmed,
2605 example_unit,
2606 valid.join(", ")
2607 ))
2608 } else {
2609 Err(e)
2610 }
2611 }
2612 }
2613 }
2614 TypeSpecification::Ratio { units, .. } => {
2615 if units.is_empty() {
2616 unreachable!(
2617 "BUG: Ratio type has no units; should have been validated during planning"
2618 );
2619 }
2620 match trimmed.parse::<RatioLiteral>()? {
2621 RatioLiteral::Bare(_) => {
2622 Err("Ratio value requires a unit (e.g. '50%', '500 basis_points').".to_string())
2623 }
2624 RatioLiteral::Percent(n) => {
2625 let unit = units.get("percent").map_err(|e| e.to_string())?;
2626 Ok(Value::NumberWithUnit(n, unit.name.clone()))
2627 }
2628 RatioLiteral::Permille(n) => {
2629 let unit = units.get("permille").map_err(|e| e.to_string())?;
2630 Ok(Value::NumberWithUnit(n, unit.name.clone()))
2631 }
2632 RatioLiteral::Named { value, unit } => {
2633 let resolved = units.get(&unit).map_err(|e| e.to_string())?;
2634 Ok(Value::NumberWithUnit(value, resolved.name.clone()))
2635 }
2636 }
2637 }
2638 _ => Err("parse_number_unit only accepts Measure or Ratio type".to_string()),
2639 }
2640}
2641
2642pub fn parse_value_from_string(
2645 value_str: &str,
2646 type_spec: &TypeSpecification,
2647 source: &Source,
2648) -> Result<crate::parsing::ast::Value, Error> {
2649 use crate::parsing::ast::Value;
2650
2651 let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
2652
2653 let parse_range_value = |element_spec: TypeSpecification| -> Result<Value, Error> {
2654 let (left_str, right_str) = value_str.split_once("...").ok_or_else(|| {
2655 to_err("Range value must use '...' between the two endpoints".to_string())
2656 })?;
2657 if left_str.trim().is_empty() || right_str.trim().is_empty() {
2658 return Err(to_err(
2659 "Range value must contain a non-empty left and right endpoint".to_string(),
2660 ));
2661 }
2662 let left = parse_value_from_string(left_str.trim(), &element_spec, source)?;
2663 let right = parse_value_from_string(right_str.trim(), &element_spec, source)?;
2664 Ok(Value::Range(Box::new(left), Box::new(right)))
2665 };
2666
2667 match type_spec {
2668 TypeSpecification::Text { .. } => value_str
2669 .parse::<crate::literals::TextLiteral>()
2670 .map(|t| Value::Text(t.0))
2671 .map_err(to_err),
2672 TypeSpecification::Number { .. } => value_str
2673 .parse::<crate::literals::NumberLiteral>()
2674 .map(|n| Value::Number(n.0))
2675 .map_err(to_err),
2676 TypeSpecification::Measure { .. } => {
2677 parse_number_unit(value_str, type_spec).map_err(to_err)
2678 }
2679 TypeSpecification::Boolean { .. } => value_str
2680 .parse::<BooleanValue>()
2681 .map(Value::Boolean)
2682 .map_err(to_err),
2683 TypeSpecification::Date { .. } => {
2684 let date = value_str.parse::<DateTimeValue>().map_err(to_err)?;
2685 Ok(Value::Date(date))
2686 }
2687 TypeSpecification::Time { .. } => {
2688 let time = value_str.parse::<TimeValue>().map_err(to_err)?;
2689 Ok(Value::Time(time))
2690 }
2691 TypeSpecification::Ratio { .. } => {
2692 parse_number_unit(value_str, type_spec).map_err(to_err)
2693 }
2694 TypeSpecification::NumberRange { .. }
2695 | TypeSpecification::MeasureRange { .. }
2696 | TypeSpecification::DateRange { .. }
2697 | TypeSpecification::TimeRange { .. }
2698 | TypeSpecification::RatioRange { .. } => {
2699 let element_spec = range_element_type_specification(type_spec).unwrap_or_else(|| {
2700 unreachable!("BUG: range_element_type_specification missing arm for known range type")
2701 });
2702 parse_range_value(element_spec)
2703 }
2704 TypeSpecification::Veto { .. } => Err(to_err(
2705 "Veto type cannot be parsed from string".to_string(),
2706 )),
2707 TypeSpecification::Undetermined => unreachable!(
2708 "BUG: parse_value_from_string called with Undetermined sentinel type; this type exists only during type inference"
2709 ),
2710 }
2711}
2712
2713#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2718#[serde(rename_all = "snake_case")]
2719pub enum SemanticCalendarUnit {
2720 Month,
2721 Year,
2722}
2723
2724impl fmt::Display for SemanticCalendarUnit {
2725 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2726 let s = match self {
2727 SemanticCalendarUnit::Month => "month",
2728 SemanticCalendarUnit::Year => "year",
2729 };
2730 write!(f, "{}", s)
2731 }
2732}
2733
2734pub fn semantic_calendar_unit_from_unit_name(unit_name: &str) -> SemanticCalendarUnit {
2735 match unit_name {
2736 "month" => SemanticCalendarUnit::Month,
2737 "year" => SemanticCalendarUnit::Year,
2738 other => unreachable!(
2739 "BUG: calendar measure signature unit must be month or year, got '{other}'"
2740 ),
2741 }
2742}
2743
2744pub fn semantic_calendar_unit_from_measure_signature(
2745 signature: &[(String, i32)],
2746) -> SemanticCalendarUnit {
2747 let unit_name = signature
2748 .first()
2749 .map(|(name, _)| name.as_str())
2750 .expect("BUG: calendar measure must carry a unit signature");
2751 semantic_calendar_unit_from_unit_name(unit_name)
2752}
2753
2754pub fn semantic_calendar_unit_from_measure_type(lemma_type: &LemmaType) -> SemanticCalendarUnit {
2755 if !lemma_type.is_calendar_like() {
2756 unreachable!(
2757 "BUG: semantic_calendar_unit_from_measure_type called on non-calendar type {}",
2758 lemma_type.name()
2759 );
2760 }
2761 let signature = lemma_type.measure_runtime_signature();
2762 if signature.is_empty() {
2763 return SemanticCalendarUnit::Month;
2764 }
2765 semantic_calendar_unit_from_measure_signature(&signature)
2766}
2767
2768#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2770pub enum SemanticConversionTarget {
2771 Type(PrimitiveKind),
2772 Unit {
2774 unit_name: String,
2775 owning_type: Arc<LemmaType>,
2777 },
2778}
2779
2780impl std::hash::Hash for SemanticConversionTarget {
2781 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
2782 match self {
2783 Self::Type(kind) => {
2784 0u8.hash(state);
2785 kind.hash(state);
2786 }
2787 Self::Unit {
2788 unit_name,
2789 owning_type,
2790 } => {
2791 1u8.hash(state);
2792 unit_name.hash(state);
2793 owning_type.hash(state);
2794 }
2795 }
2796 }
2797}
2798
2799impl SemanticConversionTarget {}
2800
2801impl fmt::Display for SemanticConversionTarget {
2802 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2803 match self {
2804 SemanticConversionTarget::Type(kind) => write!(f, "{kind}"),
2805 SemanticConversionTarget::Unit { unit_name, .. } => write!(f, "{unit_name}"),
2806 }
2807 }
2808}
2809
2810#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2812pub struct SemanticTimezone {
2813 pub offset_hours: i8,
2814 pub offset_minutes: u8,
2815}
2816
2817impl fmt::Display for SemanticTimezone {
2818 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2819 if self.offset_hours == 0 && self.offset_minutes == 0 {
2820 write!(f, "Z")
2821 } else {
2822 let sign = if self.offset_hours >= 0 { "+" } else { "-" };
2823 let hour = self.offset_hours.abs();
2824 write!(f, "{}{:02}:{:02}", sign, hour, self.offset_minutes)
2825 }
2826 }
2827}
2828
2829impl Serialize for SemanticTimezone {
2830 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2831 serializer.serialize_str(&self.to_string())
2832 }
2833}
2834
2835impl<'de> Deserialize<'de> for SemanticTimezone {
2836 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2837 let s = String::deserialize(deserializer)?;
2838 Self::from_str(&s).map_err(serde::de::Error::custom)
2839 }
2840}
2841
2842impl FromStr for SemanticTimezone {
2843 type Err = String;
2844
2845 fn from_str(s: &str) -> Result<Self, Self::Err> {
2846 let tz = TimezoneValue::from_str(s)?;
2847 Ok(Self {
2848 offset_hours: tz.offset_hours,
2849 offset_minutes: tz.offset_minutes,
2850 })
2851 }
2852}
2853
2854#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2856pub struct SemanticTime {
2857 pub hour: u32,
2858 pub minute: u32,
2859 pub second: u32,
2860 pub microsecond: u32,
2861 pub timezone: Option<SemanticTimezone>,
2862}
2863
2864impl fmt::Display for SemanticTime {
2865 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2866 write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
2867 if self.microsecond != 0 {
2868 write!(f, ".{:06}", self.microsecond)?;
2869 }
2870 if let Some(timezone) = &self.timezone {
2871 write!(f, "{}", timezone)?;
2872 }
2873 Ok(())
2874 }
2875}
2876
2877impl Serialize for SemanticTime {
2878 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2879 serializer.serialize_str(&self.to_string())
2880 }
2881}
2882
2883impl<'de> Deserialize<'de> for SemanticTime {
2884 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2885 let s = String::deserialize(deserializer)?;
2886 Self::from_str(&s).map_err(serde::de::Error::custom)
2887 }
2888}
2889
2890impl FromStr for SemanticTime {
2891 type Err = String;
2892
2893 fn from_str(s: &str) -> Result<Self, Self::Err> {
2894 Ok(time_to_semantic(&TimeValue::from_str(s)?))
2895 }
2896}
2897
2898#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2900pub struct SemanticDateTime {
2901 pub year: i32,
2902 pub month: u32,
2903 pub day: u32,
2904 pub hour: u32,
2905 pub minute: u32,
2906 pub second: u32,
2907 pub microsecond: u32,
2908 pub timezone: Option<SemanticTimezone>,
2909}
2910
2911impl fmt::Display for SemanticDateTime {
2912 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2913 let has_time = self.hour != 0
2914 || self.minute != 0
2915 || self.second != 0
2916 || self.microsecond != 0
2917 || self.timezone.is_some();
2918 if !has_time {
2919 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
2920 } else {
2921 write!(
2922 f,
2923 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
2924 self.year, self.month, self.day, self.hour, self.minute, self.second
2925 )?;
2926 if self.microsecond != 0 {
2927 write!(f, ".{:06}", self.microsecond)?;
2928 }
2929 if let Some(tz) = &self.timezone {
2930 write!(f, "{}", tz)?;
2931 }
2932 Ok(())
2933 }
2934 }
2935}
2936
2937impl Serialize for SemanticDateTime {
2938 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2939 serializer.serialize_str(&self.to_string())
2940 }
2941}
2942
2943impl<'de> Deserialize<'de> for SemanticDateTime {
2944 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2945 let s = String::deserialize(deserializer)?;
2946 Self::from_str(&s).map_err(serde::de::Error::custom)
2947 }
2948}
2949
2950impl FromStr for SemanticDateTime {
2951 type Err = String;
2952
2953 fn from_str(s: &str) -> Result<Self, Self::Err> {
2954 Ok(date_time_to_semantic(&DateTimeValue::from_str(s)?))
2955 }
2956}
2957
2958#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2962pub enum RawSuggestion {
2963 Value(ValueKind),
2964 Measure {
2966 magnitude: RationalInteger,
2967 unit_name: String,
2968 },
2969 UnitBound {
2972 value: ValueKind,
2973 unit_name: String,
2974 },
2975}
2976
2977#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2983pub struct BoundValueKind {
2984 pub value: ValueKind,
2985 pub measure_binding_unit: Option<Arc<str>>,
2986}
2987
2988impl BoundValueKind {
2989 pub fn unbound(value: ValueKind) -> Self {
2990 Self {
2991 value,
2992 measure_binding_unit: None,
2993 }
2994 }
2995
2996 pub fn with_binding(value: ValueKind, measure_binding_unit: Option<Arc<str>>) -> Self {
2997 Self {
2998 value,
2999 measure_binding_unit,
3000 }
3001 }
3002
3003 pub fn to_literal(&self) -> LiteralValue {
3004 LiteralValue {
3005 value: self.value.clone(),
3006 }
3007 }
3008
3009 #[must_use]
3012 pub fn agree_or_left_binding(
3013 left: Option<&Arc<str>>,
3014 right: Option<&Arc<str>>,
3015 ) -> Option<Arc<str>> {
3016 match (left, right) {
3017 (Some(a), Some(b)) if a.as_ref() == b.as_ref() => Some(Arc::clone(a)),
3018 (Some(a), Some(_)) => Some(Arc::clone(a)),
3019 (Some(a), None) => Some(Arc::clone(a)),
3020 (None, Some(b)) => Some(Arc::clone(b)),
3021 (None, None) => None,
3022 }
3023 }
3024}
3025
3026pub fn bound_value_kind_from_raw_suggestion(
3027 raw: RawSuggestion,
3028 specifications: &TypeSpecification,
3029 type_name: &str,
3030) -> Result<BoundValueKind, String> {
3031 match raw {
3032 RawSuggestion::Value(vk) => Ok(BoundValueKind::unbound(vk)),
3033 RawSuggestion::UnitBound { value, unit_name } => Ok(BoundValueKind {
3034 value,
3035 measure_binding_unit: Some(Arc::from(unit_name)),
3036 }),
3037 RawSuggestion::Measure {
3038 magnitude,
3039 unit_name,
3040 } => {
3041 let TypeSpecification::Measure { units, .. } = specifications else {
3042 return Err(format!(
3043 "BUG: RawSuggestion::Measure for non-measure type '{type_name}'"
3044 ));
3045 };
3046 let canonical = measure_declared_bound_to_canonical(
3047 &magnitude, &unit_name, units, type_name, "suggest",
3048 )?;
3049 Ok(BoundValueKind {
3050 value: ValueKind::Measure(canonical),
3051 measure_binding_unit: Some(Arc::from(unit_name)),
3052 })
3053 }
3054 }
3055}
3056
3057#[must_use]
3059pub fn display_binding_unit(
3060 schema: &LemmaType,
3061 settled: Option<&BoundValueKind>,
3062 fill: Option<&BoundValueKind>,
3063 suggestion: Option<&BoundValueKind>,
3064) -> Option<String> {
3065 if let Some(unit) = schema.measure_binding_unit.clone() {
3066 return Some(unit);
3067 }
3068 if let Some(unit) = settled.and_then(|bound| bound.measure_binding_unit.as_ref()) {
3069 return Some(unit.to_string());
3070 }
3071 if let Some(unit) = fill.and_then(|bound| bound.measure_binding_unit.as_ref()) {
3072 return Some(unit.to_string());
3073 }
3074 suggestion
3075 .and_then(|bound| bound.measure_binding_unit.as_ref())
3076 .map(|unit| unit.to_string())
3077}
3078
3079#[must_use]
3083pub fn lemma_type_with_display_binding(
3084 schema: &LemmaType,
3085 settled: Option<&BoundValueKind>,
3086 fill: Option<&BoundValueKind>,
3087 suggestion: Option<&BoundValueKind>,
3088) -> LemmaType {
3089 match display_binding_unit(schema, settled, fill, suggestion) {
3090 Some(unit) if schema.measure_binding_unit.as_deref() != Some(unit.as_str()) => {
3091 schema.clone().with_measure_binding_unit(unit)
3092 }
3093 Some(_) | None => schema.clone(),
3094 }
3095}
3096
3097#[must_use]
3099pub fn lemma_type_arc_with_display_binding(
3100 schema: &Arc<LemmaType>,
3101 settled: Option<&BoundValueKind>,
3102 fill: Option<&BoundValueKind>,
3103 suggestion: Option<&BoundValueKind>,
3104) -> Arc<LemmaType> {
3105 match display_binding_unit(schema.as_ref(), settled, fill, suggestion) {
3106 Some(unit) if schema.measure_binding_unit.as_deref() != Some(unit.as_str()) => {
3107 Arc::new(schema.as_ref().clone().with_measure_binding_unit(unit))
3108 }
3109 Some(_) | None => Arc::clone(schema),
3110 }
3111}
3112
3113#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3116pub enum ValueKind {
3117 Number(RationalInteger),
3118 Measure(RationalInteger),
3124 Text(String),
3125 Date(SemanticDateTime),
3126 Time(SemanticTime),
3127 Boolean(bool),
3128 Ratio(RationalInteger),
3130 Range(Box<LiteralValue>, Box<LiteralValue>),
3131}
3132
3133impl ValueKind {
3134 pub fn as_decimal_magnitude(&self) -> Result<Decimal, String> {
3136 match self {
3137 ValueKind::Number(n) | ValueKind::Measure(n) | ValueKind::Ratio(n) => {
3138 n.try_to_decimal().map_err(|failure| failure.to_string())
3139 }
3140 other => Err(format!("expected numeric value kind, got {other}")),
3141 }
3142 }
3143
3144 pub fn structural_byte_size(&self) -> usize {
3149 fn rational_byte_estimate(r: &RationalInteger) -> usize {
3150 let numer_bytes = (r.numer_magnitude_bits() as usize).div_ceil(8);
3151 let denom_bytes = (r.denom_magnitude_bits() as usize).div_ceil(8);
3152 numer_bytes.max(1) + denom_bytes.max(1)
3153 }
3154 match self {
3155 ValueKind::Number(r) => rational_byte_estimate(r),
3156 ValueKind::Measure(r) => rational_byte_estimate(r),
3157 ValueKind::Ratio(r) => rational_byte_estimate(r),
3158 ValueKind::Text(s) => s.len(),
3159 ValueKind::Date(_) => 30, ValueKind::Time(_) => 12, ValueKind::Boolean(_) => 5, ValueKind::Range(left, right) => {
3163 left.value.structural_byte_size() + 3 + right.value.structural_byte_size()
3164 }
3165 }
3166 }
3167}
3168
3169fn format_rational_magnitude_for_display(rational: &RationalInteger) -> String {
3170 rational.display_str()
3171}
3172
3173fn format_number_with_unit_for_display(rational: &RationalInteger, unit: &str) -> String {
3174 use crate::parsing::ast::Value;
3175 match rational.try_to_decimal() {
3176 Ok(decimal) => format!("{}", Value::NumberWithUnit(decimal, unit.to_string())),
3177 Err(_) => format!("{} {}", rational.display_str(), unit),
3178 }
3179}
3180
3181impl fmt::Display for ValueKind {
3182 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3183 match self {
3184 ValueKind::Number(rational) => {
3185 write!(f, "{}", format_rational_magnitude_for_display(rational))
3186 }
3187 ValueKind::Measure(rational) => {
3188 write!(f, "{}", format_rational_magnitude_for_display(rational))
3189 }
3190 ValueKind::Text(s) => write!(f, "{}", crate::parsing::ast::Value::Text(s.clone())),
3191 ValueKind::Ratio(rational) => {
3192 write!(f, "{}", format_rational_magnitude_for_display(rational))
3193 }
3194 ValueKind::Date(dt) => write!(f, "{}", dt),
3195 ValueKind::Time(t) => write!(
3196 f,
3197 "{}",
3198 crate::parsing::ast::Value::Time(crate::parsing::ast::TimeValue {
3199 hour: t.hour as u8,
3200 minute: t.minute as u8,
3201 second: t.second as u8,
3202 microsecond: t.microsecond,
3203 timezone: t
3204 .timezone
3205 .as_ref()
3206 .map(|tz| crate::parsing::ast::TimezoneValue {
3207 offset_hours: tz.offset_hours,
3208 offset_minutes: tz.offset_minutes,
3209 }),
3210 })
3211 ),
3212 ValueKind::Boolean(b) => write!(f, "{}", b),
3213 ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
3214 }
3215 }
3216}
3217
3218#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
3229pub struct PathSegment {
3230 pub uses: String,
3232 pub repository: Option<String>,
3235 pub spec: String,
3237}
3238
3239#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
3243pub struct DataPath {
3244 pub segments: Vec<PathSegment>,
3246 pub data: String,
3248}
3249
3250impl DataPath {
3251 pub fn new(segments: Vec<PathSegment>, data: String) -> Self {
3253 Self { segments, data }
3254 }
3255
3256 pub fn local(data: String) -> Self {
3258 Self {
3259 segments: vec![],
3260 data,
3261 }
3262 }
3263
3264 pub fn input_key(&self) -> String {
3267 let mut s = String::new();
3268 for segment in &self.segments {
3269 s.push_str(&segment.uses);
3270 s.push('.');
3271 }
3272 s.push_str(&self.data);
3273 s
3274 }
3275}
3276
3277#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
3281pub struct RulePath {
3282 pub segments: Vec<PathSegment>,
3284 pub rule: String,
3286}
3287
3288impl RulePath {
3289 pub fn new(segments: Vec<PathSegment>, rule: String) -> Self {
3291 Self { segments, rule }
3292 }
3293
3294 pub fn input_key(&self) -> String {
3297 if self.segments.is_empty() {
3298 return self.rule.clone();
3299 }
3300 let mut s = String::new();
3301 for segment in &self.segments {
3302 s.push_str(&segment.uses);
3303 s.push('.');
3304 }
3305 s.push_str(&self.rule);
3306 s
3307 }
3308}
3309
3310#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3319pub struct Expression {
3320 pub kind: ExpressionKind,
3321 pub source_location: Option<Source>,
3322}
3323
3324impl Expression {
3325 pub fn with_source(kind: ExpressionKind, source_location: Option<Source>) -> Self {
3327 Self {
3328 kind,
3329 source_location,
3330 }
3331 }
3332
3333 pub fn collect_data_paths(&self, data: &mut std::collections::HashSet<DataPath>) {
3335 self.kind.collect_data_paths(data);
3336 }
3337}
3338
3339#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3341#[serde(rename_all = "snake_case")]
3342pub enum ExpressionKind {
3343 Literal(Box<TypedLiteral>),
3345 DataPath(DataPath),
3347 RulePath(RulePath),
3349 LogicalAnd(Arc<Expression>, Arc<Expression>),
3350 Arithmetic(Arc<Expression>, ArithmeticComputation, Arc<Expression>),
3351 Comparison(Arc<Expression>, ComparisonComputation, Arc<Expression>),
3352 UnitConversion(Arc<Expression>, SemanticConversionTarget),
3353 LogicalNegation(Arc<Expression>, NegationType),
3354 MathematicalComputation(MathematicalComputation, Arc<Expression>),
3355 Veto(VetoExpression),
3356 Now,
3358 DateRelative(DateRelativeKind, Arc<Expression>),
3360 DateCalendar(DateCalendarKind, CalendarPeriodUnit, Arc<Expression>),
3362 RangeLiteral(Arc<Expression>, Arc<Expression>),
3363 PastFutureRange(DateRelativeKind, Arc<Expression>),
3364 RangeContainment(Arc<Expression>, Arc<Expression>),
3365 ResultIsVeto(Arc<Expression>),
3367 Piecewise(Vec<(Arc<Expression>, Arc<Expression>)>),
3370}
3371
3372impl ExpressionKind {
3373 pub(crate) fn collect_data_paths(&self, data: &mut std::collections::HashSet<DataPath>) {
3375 match self {
3376 ExpressionKind::DataPath(fp) => {
3377 data.insert(fp.clone());
3378 }
3379 ExpressionKind::LogicalAnd(left, right) => {
3380 left.collect_data_paths(data);
3381 right.collect_data_paths(data);
3382 }
3383 ExpressionKind::Arithmetic(left, _, right)
3384 | ExpressionKind::Comparison(left, _, right)
3385 | ExpressionKind::RangeLiteral(left, right)
3386 | ExpressionKind::RangeContainment(left, right) => {
3387 left.collect_data_paths(data);
3388 right.collect_data_paths(data);
3389 }
3390 ExpressionKind::UnitConversion(inner, _)
3391 | ExpressionKind::LogicalNegation(inner, _)
3392 | ExpressionKind::MathematicalComputation(_, inner)
3393 | ExpressionKind::PastFutureRange(_, inner) => {
3394 inner.collect_data_paths(data);
3395 }
3396 ExpressionKind::DateRelative(_, date_expr) => {
3397 date_expr.collect_data_paths(data);
3398 }
3399 ExpressionKind::DateCalendar(_, _, date_expr) => {
3400 date_expr.collect_data_paths(data);
3401 }
3402 ExpressionKind::Literal(_)
3403 | ExpressionKind::RulePath(_)
3404 | ExpressionKind::Veto(_)
3405 | ExpressionKind::Now => {}
3406 ExpressionKind::ResultIsVeto(operand) => {
3407 operand.collect_data_paths(data);
3408 }
3409 ExpressionKind::Piecewise(arms) => {
3410 for (condition, result) in arms {
3411 condition.collect_data_paths(data);
3412 result.collect_data_paths(data);
3413 }
3414 }
3415 }
3416 }
3417}
3418
3419#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
3425pub enum TypeDefiningSpec {
3426 Local,
3428 Import,
3430}
3431
3432#[derive(Clone, Debug, Serialize, Deserialize)]
3434pub enum TypeExtends {
3435 Primitive,
3437 Custom {
3440 parent: String,
3441 family: String,
3442 defining_spec: TypeDefiningSpec,
3443 },
3444}
3445
3446impl PartialEq for TypeExtends {
3447 fn eq(&self, other: &Self) -> bool {
3448 match (self, other) {
3449 (TypeExtends::Primitive, TypeExtends::Primitive) => true,
3450 (
3451 TypeExtends::Custom {
3452 parent: lp,
3453 family: lf,
3454 defining_spec: ld,
3455 },
3456 TypeExtends::Custom {
3457 parent: rp,
3458 family: rf,
3459 defining_spec: rd,
3460 },
3461 ) => lp == rp && lf == rf && ld == rd,
3462 _ => false,
3463 }
3464 }
3465}
3466
3467impl Eq for TypeExtends {}
3468
3469impl std::hash::Hash for TypeExtends {
3470 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
3471 match self {
3472 TypeExtends::Primitive => {
3473 0u8.hash(state);
3474 }
3475 TypeExtends::Custom {
3476 parent,
3477 family,
3478 defining_spec,
3479 } => {
3480 1u8.hash(state);
3481 parent.hash(state);
3482 family.hash(state);
3483 defining_spec.hash(state);
3484 }
3485 }
3486 }
3487}
3488
3489impl TypeExtends {
3490 #[must_use]
3492 pub fn custom_local(parent: String, family: String) -> Self {
3493 TypeExtends::Custom {
3494 parent,
3495 family,
3496 defining_spec: TypeDefiningSpec::Local,
3497 }
3498 }
3499
3500 #[must_use]
3502 pub fn parent_name(&self) -> Option<&str> {
3503 match self {
3504 TypeExtends::Primitive => None,
3505 TypeExtends::Custom { parent, .. } => Some(parent.as_str()),
3506 }
3507 }
3508}
3509
3510#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
3515pub struct LemmaType {
3516 pub name: Option<String>,
3518 pub specifications: TypeSpecification,
3520 pub extends: TypeExtends,
3522 pub measure_binding_unit: Option<String>,
3526}
3527
3528impl LemmaType {
3529 pub fn map_measure<F>(self, f: F) -> Self
3533 where
3534 F: FnOnce(
3535 MeasureUnits,
3536 Option<BaseMeasureVector>,
3537 ) -> (MeasureUnits, Option<BaseMeasureVector>),
3538 {
3539 let LemmaType {
3540 name,
3541 specifications,
3542 extends,
3543 measure_binding_unit,
3544 } = self;
3545 let specifications = match specifications {
3546 TypeSpecification::Measure {
3547 minimum,
3548 maximum,
3549 decimals,
3550 units,
3551 traits,
3552 decomposition,
3553 help,
3554 } => {
3555 let (units, decomposition) = f(units, decomposition);
3556 TypeSpecification::Measure {
3557 minimum,
3558 maximum,
3559 decimals,
3560 units,
3561 traits,
3562 decomposition,
3563 help,
3564 }
3565 }
3566 other => other,
3567 };
3568 LemmaType {
3569 name,
3570 specifications,
3571 extends,
3572 measure_binding_unit,
3573 }
3574 }
3575
3576 pub fn new(name: String, specifications: TypeSpecification, extends: TypeExtends) -> Self {
3578 Self {
3579 name: Some(name),
3580 specifications,
3581 extends,
3582 measure_binding_unit: None,
3583 }
3584 }
3585
3586 pub fn without_name(specifications: TypeSpecification, extends: TypeExtends) -> Self {
3588 Self {
3589 name: None,
3590 specifications,
3591 extends,
3592 measure_binding_unit: None,
3593 }
3594 }
3595
3596 pub fn primitive(specifications: TypeSpecification) -> Self {
3598 Self {
3599 name: None,
3600 specifications,
3601 extends: TypeExtends::Primitive,
3602 measure_binding_unit: None,
3603 }
3604 }
3605
3606 #[must_use]
3608 pub fn with_measure_binding_unit(mut self, unit_name: impl Into<String>) -> Self {
3609 self.measure_binding_unit = Some(unit_name.into());
3610 self
3611 }
3612
3613 pub fn name(&self) -> String {
3615 self.name
3616 .clone()
3617 .unwrap_or_else(|| self.specifications.to_string())
3618 }
3619
3620 pub fn is_boolean(&self) -> bool {
3622 matches!(&self.specifications, TypeSpecification::Boolean { .. })
3623 }
3624
3625 pub fn matches_primitive_kind(&self, kind: PrimitiveKind) -> bool {
3626 matches!(
3627 (kind, &self.specifications),
3628 (PrimitiveKind::Number, TypeSpecification::Number { .. })
3629 | (PrimitiveKind::Text, TypeSpecification::Text { .. })
3630 | (PrimitiveKind::Boolean, TypeSpecification::Boolean { .. })
3631 | (PrimitiveKind::Date, TypeSpecification::Date { .. })
3632 | (PrimitiveKind::Time, TypeSpecification::Time { .. })
3633 | (PrimitiveKind::Ratio, TypeSpecification::Ratio { .. })
3634 | (PrimitiveKind::Measure, TypeSpecification::Measure { .. })
3635 )
3636 }
3637
3638 pub fn is_measure(&self) -> bool {
3640 matches!(&self.specifications, TypeSpecification::Measure { .. })
3641 }
3642
3643 pub fn is_measure_range(&self) -> bool {
3644 matches!(&self.specifications, TypeSpecification::MeasureRange { .. })
3645 }
3646
3647 pub fn is_number(&self) -> bool {
3649 matches!(&self.specifications, TypeSpecification::Number { .. })
3650 }
3651
3652 pub fn is_number_range(&self) -> bool {
3653 matches!(&self.specifications, TypeSpecification::NumberRange { .. })
3654 }
3655
3656 pub fn is_numeric(&self) -> bool {
3658 matches!(
3659 &self.specifications,
3660 TypeSpecification::Measure { .. } | TypeSpecification::Number { .. }
3661 )
3662 }
3663
3664 pub fn is_text(&self) -> bool {
3666 matches!(&self.specifications, TypeSpecification::Text { .. })
3667 }
3668
3669 pub fn is_date(&self) -> bool {
3671 matches!(&self.specifications, TypeSpecification::Date { .. })
3672 }
3673
3674 pub fn is_date_range(&self) -> bool {
3675 matches!(&self.specifications, TypeSpecification::DateRange { .. })
3676 }
3677
3678 pub fn is_time_range(&self) -> bool {
3679 matches!(&self.specifications, TypeSpecification::TimeRange { .. })
3680 }
3681
3682 pub fn is_time(&self) -> bool {
3684 matches!(&self.specifications, TypeSpecification::Time { .. })
3685 }
3686
3687 pub fn has_trait_duration(&self) -> bool {
3688 match &self.specifications {
3689 TypeSpecification::Measure { traits, .. } => traits.contains(&MeasureTrait::Duration),
3690 _ => false,
3691 }
3692 }
3693
3694 pub fn is_duration_like_measure(&self) -> bool {
3695 if !self.is_measure() {
3696 return false;
3697 }
3698 if self.has_trait_duration() {
3699 return true;
3700 }
3701 self.is_anonymous_measure()
3702 && self
3703 .measure_type_decomposition()
3704 .is_some_and(|d| *d == duration_decomposition())
3705 }
3706
3707 pub fn is_duration_like(&self) -> bool {
3708 self.is_duration_like_measure()
3709 }
3710
3711 pub fn has_trait_calendar(&self) -> bool {
3712 match &self.specifications {
3713 TypeSpecification::Measure { traits, .. } => traits.contains(&MeasureTrait::Calendar),
3714 _ => false,
3715 }
3716 }
3717
3718 pub fn is_calendar_like_measure(&self) -> bool {
3719 if !self.is_measure() {
3720 return false;
3721 }
3722 if self.has_trait_calendar() {
3723 return true;
3724 }
3725 self.is_anonymous_measure()
3726 && self
3727 .measure_type_decomposition()
3728 .is_some_and(|d| *d == calendar_decomposition())
3729 }
3730
3731 pub fn is_calendar_like(&self) -> bool {
3732 self.is_calendar_like_measure()
3733 }
3734
3735 pub fn is_ratio(&self) -> bool {
3737 matches!(&self.specifications, TypeSpecification::Ratio { .. })
3738 }
3739
3740 pub fn is_ratio_range(&self) -> bool {
3741 matches!(&self.specifications, TypeSpecification::RatioRange { .. })
3742 }
3743
3744 pub fn is_calendar_measure_range(&self) -> bool {
3745 matches!(
3746 &self.specifications,
3747 TypeSpecification::MeasureRange { decomposition: Some(decomposition), .. }
3748 if *decomposition == calendar_decomposition()
3749 )
3750 }
3751
3752 pub fn is_calendar_like_range(&self) -> bool {
3753 self.is_calendar_measure_range()
3754 }
3755
3756 pub fn is_range(&self) -> bool {
3757 matches!(
3758 &self.specifications,
3759 TypeSpecification::DateRange { .. }
3760 | TypeSpecification::TimeRange { .. }
3761 | TypeSpecification::NumberRange { .. }
3762 | TypeSpecification::MeasureRange { .. }
3763 | TypeSpecification::RatioRange { .. }
3764 )
3765 }
3766
3767 pub fn vetoed(&self) -> bool {
3769 matches!(&self.specifications, TypeSpecification::Veto { .. })
3770 }
3771
3772 pub fn is_undetermined(&self) -> bool {
3774 matches!(&self.specifications, TypeSpecification::Undetermined)
3775 }
3776
3777 pub fn has_same_base_type(&self, other: &LemmaType) -> bool {
3779 use TypeSpecification::*;
3780 matches!(
3781 (&self.specifications, &other.specifications),
3782 (Boolean { .. }, Boolean { .. })
3783 | (Number { .. }, Number { .. })
3784 | (NumberRange { .. }, NumberRange { .. })
3785 | (Measure { .. }, Measure { .. })
3786 | (MeasureRange { .. }, MeasureRange { .. })
3787 | (Text { .. }, Text { .. })
3788 | (Date { .. }, Date { .. })
3789 | (DateRange { .. }, DateRange { .. })
3790 | (Time { .. }, Time { .. })
3791 | (TimeRange { .. }, TimeRange { .. })
3792 | (Ratio { .. }, Ratio { .. })
3793 | (RatioRange { .. }, RatioRange { .. })
3794 | (Veto { .. }, Veto { .. })
3795 | (Undetermined, Undetermined)
3796 )
3797 }
3798
3799 #[must_use]
3801 pub fn measure_family_name(&self) -> Option<&str> {
3802 if !self.is_measure() {
3803 return None;
3804 }
3805 match &self.extends {
3806 TypeExtends::Custom { family, .. } => Some(family.as_str()),
3807 TypeExtends::Primitive => self.name.as_deref(),
3808 }
3809 }
3810
3811 #[must_use]
3813 pub fn ratio_family_name(&self) -> Option<&str> {
3814 if !self.is_ratio() {
3815 return None;
3816 }
3817 match &self.extends {
3818 TypeExtends::Custom { family, .. } => Some(family.as_str()),
3819 TypeExtends::Primitive => self.name.as_deref(),
3820 }
3821 }
3822
3823 #[must_use]
3825 pub(crate) fn unit_family_name(&self) -> Option<&str> {
3826 self.measure_family_name()
3827 .or_else(|| self.ratio_family_name())
3828 }
3829
3830 #[must_use]
3834 pub(crate) fn same_value_type(&self, other: &LemmaType) -> bool {
3835 if !self.has_same_base_type(other) {
3836 return false;
3837 }
3838 if self.is_measure() {
3839 return self.measure_type_decomposition() == other.measure_type_decomposition();
3840 }
3841 if self.is_measure_range() {
3842 let self_element = self
3843 .specifications
3844 .element_from_range()
3845 .expect("BUG: MeasureRange always defines element_from_range");
3846 let other_element = other
3847 .specifications
3848 .element_from_range()
3849 .expect("BUG: MeasureRange always defines element_from_range");
3850 match (&self_element, &other_element) {
3851 (
3852 TypeSpecification::Measure {
3853 decomposition: self_decomp,
3854 ..
3855 },
3856 TypeSpecification::Measure {
3857 decomposition: other_decomp,
3858 ..
3859 },
3860 ) => self_decomp == other_decomp,
3861 _ => unreachable!("BUG: element_from_range of MeasureRange yields Measure"),
3862 }
3863 } else {
3864 true
3865 }
3866 }
3867
3868 #[must_use]
3870 pub fn same_measure_family(&self, other: &LemmaType) -> bool {
3871 if !self.is_measure() || !other.is_measure() {
3872 return false;
3873 }
3874 match (self.measure_family_name(), other.measure_family_name()) {
3875 (Some(self_family), Some(other_family)) => self_family == other_family,
3876 _ => false,
3877 }
3878 }
3879
3880 #[must_use]
3881 pub fn compatible_with_anonymous_measure(&self, other: &LemmaType) -> bool {
3882 if !self.is_measure() || !other.is_measure() {
3883 return false;
3884 }
3885 if !self.is_anonymous_measure() && !other.is_anonymous_measure() {
3886 return false;
3887 }
3888 match (
3889 self.measure_type_decomposition(),
3890 other.measure_type_decomposition(),
3891 ) {
3892 (Some(a), Some(b)) => a == b,
3893 _ => false,
3894 }
3895 }
3896
3897 pub fn veto_type() -> Self {
3899 Self::primitive(TypeSpecification::veto())
3900 }
3901
3902 pub fn undetermined_type() -> Self {
3905 Self::primitive(TypeSpecification::Undetermined)
3906 }
3907
3908 pub fn decimal_places(&self) -> Option<u8> {
3911 match &self.specifications {
3912 TypeSpecification::Number { decimals, .. } => *decimals,
3913 TypeSpecification::Measure { decimals, .. } => *decimals,
3914 TypeSpecification::Ratio { decimals, .. } => *decimals,
3915 _ => None,
3916 }
3917 }
3918
3919 pub fn try_rational_as_decimal(
3924 &self,
3925 magnitude: &crate::computation::rational::RationalInteger,
3926 ) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
3927 let decimal = magnitude.try_to_decimal()?;
3928 Ok(format_decimal_for_api(decimal, self.decimal_places()))
3929 }
3930
3931 pub fn try_measure_canonical_as_decimal_in_unit(
3933 &self,
3934 canonical_magnitude: &crate::computation::rational::RationalInteger,
3935 unit_name: &str,
3936 ) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
3937 use crate::computation::rational::checked_div;
3938 let unit_factor = self.measure_unit_factor(unit_name);
3939 let magnitude_in_unit = checked_div(canonical_magnitude, unit_factor)?;
3940 self.try_rational_as_decimal(&magnitude_in_unit)
3941 }
3942
3943 pub fn try_ratio_canonical_as_decimal_in_unit(
3945 &self,
3946 canonical_magnitude: &crate::computation::rational::RationalInteger,
3947 unit_name: &str,
3948 ) -> Result<rust_decimal::Decimal, crate::computation::rational::NumericFailure> {
3949 use crate::computation::rational::checked_mul;
3950 let units = match &self.specifications {
3951 TypeSpecification::Ratio { units, .. } => units,
3952 _ => unreachable!(
3953 "BUG: try_ratio_canonical_as_decimal_in_unit called on non-ratio type {}",
3954 self.name()
3955 ),
3956 };
3957 let ratio_unit = units
3958 .iter()
3959 .find(|unit| unit.name == unit_name)
3960 .unwrap_or_else(|| {
3961 let valid: Vec<&str> = units.iter().map(|unit| unit.name.as_str()).collect();
3962 unreachable!(
3963 "BUG: unknown ratio unit '{}' for type {} (valid: {}); planning must reject invalid units",
3964 unit_name,
3965 self.name(),
3966 valid.join(", ")
3967 )
3968 });
3969 let magnitude_in_unit = checked_mul(canonical_magnitude, &ratio_unit.value)?;
3970 self.try_rational_as_decimal(&magnitude_in_unit)
3971 }
3972
3973 pub fn example_value(&self) -> &'static str {
3975 match &self.specifications {
3976 TypeSpecification::Text { .. } => "\"hello world\"",
3977 TypeSpecification::Measure { .. } => "12.50 eur",
3978 TypeSpecification::MeasureRange { .. } => "30 kilogram...35 kilogram",
3979 TypeSpecification::Number { .. } => "3.14",
3980 TypeSpecification::NumberRange { .. } => "0...100",
3981 TypeSpecification::Boolean { .. } => "true",
3982 TypeSpecification::Date { .. } => "2023-12-25T14:30:00Z",
3983 TypeSpecification::DateRange { .. } => "2024-01-01...2024-12-31",
3984 TypeSpecification::TimeRange { .. } => "09:00...17:00",
3985 TypeSpecification::Veto { .. } => "veto",
3986 TypeSpecification::Time { .. } => "14:30:00",
3987 TypeSpecification::Ratio { .. } => "50%",
3988 TypeSpecification::RatioRange { .. } => "10%...50%",
3989 TypeSpecification::Undetermined => unreachable!(
3990 "BUG: example_value called on Undetermined sentinel type; this type must never reach user-facing code"
3991 ),
3992 }
3993 }
3994
3995 #[must_use]
3999 pub fn measure_type_decomposition(&self) -> Option<&BaseMeasureVector> {
4003 match &self.specifications {
4004 TypeSpecification::Measure { decomposition, .. } => decomposition.as_ref(),
4005 _ => unreachable!(
4006 "BUG: measure_type_decomposition called on non-measure type {}",
4007 self.name()
4008 ),
4009 }
4010 }
4011
4012 #[must_use]
4018 pub fn measure_runtime_signature(&self) -> Vec<(String, i32)> {
4019 if let Some(binding) = &self.measure_binding_unit {
4020 return vec![(binding.clone(), 1)];
4021 }
4022 match &self.specifications {
4023 TypeSpecification::Measure {
4024 units,
4025 decomposition,
4026 ..
4027 } => {
4028 if let Some(canonical) = units.iter().find(|unit| unit.is_canonical_factor()) {
4029 return vec![(canonical.name.clone(), 1)];
4030 }
4031 if let Some(first) = units.iter().next() {
4032 return vec![(first.name.clone(), 1)];
4033 }
4034 decomposition
4035 .as_ref()
4036 .map(base_measure_vector_as_signature)
4037 .unwrap_or_default()
4038 }
4039 TypeSpecification::MeasureRange {
4040 units,
4041 decomposition,
4042 ..
4043 } => {
4044 if let Some(canonical) = units.iter().find(|unit| unit.is_canonical_factor()) {
4045 return vec![(canonical.name.clone(), 1)];
4046 }
4047 if let Some(first) = units.iter().next() {
4048 return vec![(first.name.clone(), 1)];
4049 }
4050 decomposition
4051 .as_ref()
4052 .map(base_measure_vector_as_signature)
4053 .unwrap_or_default()
4054 }
4055 _ => unreachable!(
4056 "BUG: measure_runtime_signature called on non-measure type {}",
4057 self.name()
4058 ),
4059 }
4060 }
4061
4062 #[must_use]
4064 pub fn ratio_primary_unit(&self) -> Option<&str> {
4065 match &self.specifications {
4066 TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
4067 units.iter().next().map(|unit| unit.name.as_str())
4068 }
4069 TypeSpecification::RatioRange { units, .. } if !units.is_empty() => {
4070 units.iter().next().map(|unit| unit.name.as_str())
4071 }
4072 _ => None,
4073 }
4074 }
4075
4076 pub fn is_anonymous_measure(&self) -> bool {
4079 self.name.is_none() && matches!(&self.specifications, TypeSpecification::Measure { .. })
4080 }
4081
4082 pub fn anonymous_for_decomposition(decomposition: BaseMeasureVector) -> Self {
4087 Self {
4088 name: None,
4089 specifications: TypeSpecification::Measure {
4090 minimum: None,
4091 maximum: None,
4092 decimals: None,
4093 units: crate::literals::MeasureUnits::new(),
4094 traits: Vec::new(),
4095 decomposition: Some(decomposition),
4096 help: String::new(),
4097 },
4098 extends: TypeExtends::Primitive,
4099 measure_binding_unit: None,
4100 }
4101 }
4102
4103 #[must_use]
4105 pub fn ratio_unit_names(&self) -> Option<Vec<&str>> {
4106 match &self.specifications {
4107 TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
4108 Some(units.iter().map(|unit| unit.name.as_str()).collect())
4109 }
4110 TypeSpecification::RatioRange { units, .. } if !units.is_empty() => {
4111 Some(units.iter().map(|unit| unit.name.as_str()).collect())
4112 }
4113 _ => None,
4114 }
4115 }
4116
4117 #[must_use]
4119 pub fn measure_unit_names(&self) -> Option<Vec<&str>> {
4120 match &self.specifications {
4121 TypeSpecification::Measure { units, .. } if !units.is_empty() => {
4122 Some(units.iter().map(|unit| unit.name.as_str()).collect())
4123 }
4124 TypeSpecification::MeasureRange { units, .. } if !units.is_empty() => {
4125 Some(units.iter().map(|unit| unit.name.as_str()).collect())
4126 }
4127 _ => None,
4128 }
4129 }
4130
4131 #[must_use]
4133 pub fn label_for_data_input(&self, input_key: &str) -> String {
4134 let type_label = if let Some(parent) = self.extends.parent_name() {
4135 parent.to_string()
4136 } else {
4137 let type_name = self.name();
4138 if type_name == input_key {
4139 self.specifications.to_string()
4140 } else {
4141 type_name
4142 }
4143 };
4144 if type_label == input_key {
4145 input_key.to_string()
4146 } else {
4147 format!("{input_key} [{type_label}]")
4148 }
4149 }
4150
4151 #[must_use]
4153 pub fn data_veto_message(&self, input_key: &str, detail: &str) -> String {
4154 format!("Data {}: {}", self.label_for_data_input(input_key), detail)
4155 }
4156
4157 #[must_use]
4159 pub fn empty_runtime_input_vetoes(&self) -> bool {
4160 !matches!(self.specifications, TypeSpecification::Text { .. })
4161 }
4162
4163 pub fn measure_unit_factor(
4165 &self,
4166 unit_name: &str,
4167 ) -> &crate::computation::rational::RationalInteger {
4168 let units = match &self.specifications {
4169 TypeSpecification::Measure { units, .. } => units,
4170 TypeSpecification::MeasureRange { units, .. } => units,
4171 _ => unreachable!(
4172 "BUG: measure_unit_factor called with non-measure type {}; only call during evaluation after planning validated measure conversion",
4173 self.name()
4174 ),
4175 };
4176 match units.get(unit_name) {
4177 Ok(MeasureUnit { factor, .. }) => factor,
4178 Err(_) => {
4179 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
4180 unreachable!(
4181 "BUG: unknown unit '{}' for measure type {} (valid: {}); planning must reject invalid conversions with Error",
4182 unit_name,
4183 self.name(),
4184 valid.join(", ")
4185 );
4186 }
4187 }
4188 }
4189
4190 pub fn ratio_unit_factor(
4191 &self,
4192 unit_name: &str,
4193 ) -> &crate::computation::rational::RationalInteger {
4194 let units = match &self.specifications {
4195 TypeSpecification::Ratio { units, .. } => units,
4196 _ => unreachable!(
4197 "BUG: ratio_unit_factor called with non-ratio type {}; only call during evaluation after planning validated ratio conversion",
4198 self.name()
4199 ),
4200 };
4201 match units.get(unit_name) {
4202 Ok(RatioUnit { value, .. }) => value,
4203 Err(_) => {
4204 let valid: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
4205 unreachable!(
4206 "BUG: unknown unit '{}' for ratio type {} (valid: {}); planning must reject invalid conversions with Error",
4207 unit_name,
4208 self.name(),
4209 valid.join(", ")
4210 );
4211 }
4212 }
4213 }
4214
4215 pub(crate) fn measure_literal_unit_map(
4217 &self,
4218 literal: &LiteralValue,
4219 unit_names: &[&str],
4220 factor_source: UnitFactorSource<'_>,
4221 ) -> Result<BTreeMap<String, rust_decimal::Decimal>, LiteralUnitMapFailure> {
4222 use crate::computation::rational::checked_div;
4223
4224 let ValueKind::Measure(magnitude) = &literal.value else {
4225 panic!("BUG: measure_literal_unit_map called with non-measure value");
4226 };
4227 let mut map = BTreeMap::new();
4228 for &unit_name in unit_names {
4229 let unit_factor = factor_source.measure_unit_factor(unit_name);
4230 let magnitude_in_unit = checked_div(magnitude, unit_factor)
4231 .map_err(LiteralUnitMapFailure::UnitConversion)?;
4232 let decimal = self
4233 .try_rational_as_decimal(&magnitude_in_unit)
4234 .map_err(LiteralUnitMapFailure::Commit)?;
4235 map.insert(unit_name.to_string(), decimal);
4236 }
4237 Ok(map)
4238 }
4239
4240 pub(crate) fn ratio_literal_unit_map(
4242 &self,
4243 literal: &LiteralValue,
4244 unit_names: &[&str],
4245 factor_source: UnitFactorSource<'_>,
4246 ) -> Result<BTreeMap<String, rust_decimal::Decimal>, LiteralUnitMapFailure> {
4247 use crate::computation::rational::checked_mul;
4248
4249 let ratio_api_type = match &self.specifications {
4250 TypeSpecification::Ratio { .. } => self,
4251 TypeSpecification::RatioRange { .. } => {
4252 return ratio_element_type_for_api(self).ratio_literal_unit_map(
4253 literal,
4254 unit_names,
4255 factor_source,
4256 );
4257 }
4258 _ => {
4259 panic!(
4260 "BUG: ratio_literal_unit_map called with non-ratio type {}",
4261 self.name()
4262 );
4263 }
4264 };
4265 let ValueKind::Ratio(canonical) = &literal.value else {
4266 panic!("BUG: ratio_literal_unit_map called with non-ratio value");
4267 };
4268 if unit_names.is_empty() {
4269 panic!(
4270 "BUG: ratio literal type '{}' must have at least one unit name",
4271 self.name()
4272 );
4273 }
4274 let mut map = BTreeMap::new();
4275 for &unit_name in unit_names {
4276 let unit_factor = factor_source.ratio_unit_factor(unit_name);
4277 let magnitude_in_unit = checked_mul(canonical, unit_factor)
4278 .map_err(LiteralUnitMapFailure::UnitConversion)?;
4279 let decimal = ratio_api_type
4280 .try_rational_as_decimal(&magnitude_in_unit)
4281 .map_err(LiteralUnitMapFailure::Commit)?;
4282 map.insert(unit_name.to_string(), decimal);
4283 }
4284 Ok(map)
4285 }
4286}
4287
4288pub(crate) enum UnitFactorSource<'a> {
4290 DeclaredOn(&'a LemmaType),
4291 Merged {
4292 measure: Option<&'a MeasureUnits>,
4293 ratio: Option<&'a RatioUnits>,
4294 },
4295}
4296
4297impl UnitFactorSource<'_> {
4298 fn measure_unit_factor(
4299 &self,
4300 unit_name: &str,
4301 ) -> &crate::computation::rational::RationalInteger {
4302 match self {
4303 UnitFactorSource::DeclaredOn(lemma_type) => lemma_type.measure_unit_factor(unit_name),
4304 UnitFactorSource::Merged { measure, .. } => {
4305 let units = measure.unwrap_or_else(|| {
4306 panic!(
4307 "BUG: family measure expansion missing merged table for unit '{unit_name}'"
4308 )
4309 });
4310 &units
4311 .get(unit_name)
4312 .unwrap_or_else(|_| {
4313 panic!("BUG: family unit '{unit_name}' missing from merged measure table")
4314 })
4315 .factor
4316 }
4317 }
4318 }
4319
4320 fn ratio_unit_factor(&self, unit_name: &str) -> &crate::computation::rational::RationalInteger {
4321 match self {
4322 UnitFactorSource::DeclaredOn(lemma_type) => lemma_type.ratio_unit_factor(unit_name),
4323 UnitFactorSource::Merged { ratio, .. } => {
4324 let units = ratio.unwrap_or_else(|| {
4325 panic!(
4326 "BUG: family ratio expansion missing merged table for unit '{unit_name}'"
4327 )
4328 });
4329 &units
4330 .get(unit_name)
4331 .unwrap_or_else(|_| {
4332 panic!("BUG: family unit '{unit_name}' missing from merged ratio table")
4333 })
4334 .value
4335 }
4336 }
4337 }
4338}
4339
4340pub(crate) fn ratio_element_type_for_api(lemma_type: &LemmaType) -> LemmaType {
4342 match &lemma_type.specifications {
4343 TypeSpecification::Ratio { .. } => lemma_type.clone(),
4344 TypeSpecification::RatioRange { .. } => {
4345 let element = range_element_type_specification(&lemma_type.specifications)
4346 .expect("BUG: ratio range type must have ratio element specification");
4347 let TypeSpecification::Ratio {
4348 units, decimals, ..
4349 } = element
4350 else {
4351 panic!("BUG: ratio range element spec must be Ratio");
4352 };
4353 LemmaType::primitive(TypeSpecification::Ratio {
4354 minimum: None,
4355 maximum: None,
4356 decimals,
4357 units,
4358 help: String::new(),
4359 })
4360 }
4361 _ => panic!(
4362 "BUG: ratio_element_type_for_api called with non-ratio type {}",
4363 lemma_type.name()
4364 ),
4365 }
4366}
4367
4368#[derive(Debug, Clone, PartialEq, Eq)]
4370pub(crate) enum LiteralUnitMapFailure {
4371 Commit(crate::computation::rational::NumericFailure),
4372 UnitConversion(crate::computation::rational::NumericFailure),
4373}
4374
4375#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
4378pub struct LiteralValue {
4379 pub value: ValueKind,
4380}
4381
4382impl LiteralValue {
4383 #[inline]
4384 pub fn new(value: ValueKind) -> Self {
4385 Self { value }
4386 }
4387
4388 fn single_measure_signature_unit_name(signature: &[(String, i32)]) -> Option<&str> {
4389 match signature {
4390 [(unit_name, 1)] => Some(unit_name.as_str()),
4391 _ => None,
4392 }
4393 }
4394}
4395
4396#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
4402pub struct TypedLiteral {
4403 pub value: ValueKind,
4404 pub lemma_type: Arc<LemmaType>,
4405}
4406
4407impl TypedLiteral {
4408 #[inline]
4409 pub fn to_literal(&self) -> LiteralValue {
4410 LiteralValue {
4411 value: self.value.clone(),
4412 }
4413 }
4414
4415 #[inline]
4416 pub fn into_literal(self) -> LiteralValue {
4417 LiteralValue { value: self.value }
4418 }
4419}
4420
4421impl From<TypedLiteral> for LiteralValue {
4422 fn from(typed: TypedLiteral) -> Self {
4423 typed.into_literal()
4424 }
4425}
4426
4427impl From<&TypedLiteral> for LiteralValue {
4428 fn from(typed: &TypedLiteral) -> Self {
4429 typed.to_literal()
4430 }
4431}
4432
4433impl LiteralValue {
4434 pub fn text(s: String) -> Self {
4435 Self {
4436 value: ValueKind::Text(s),
4437 }
4438 }
4439
4440 pub fn number(n: RationalInteger) -> Self {
4441 Self {
4442 value: ValueKind::Number(n),
4443 }
4444 }
4445
4446 pub fn number_from_decimal(decimal: Decimal) -> Self {
4447 Self::number(
4448 crate::literals::rational_from_parsed_decimal(decimal)
4449 .expect("BUG: literal number from decimal must lift at boundary"),
4450 )
4451 }
4452
4453 pub fn measure(n: RationalInteger) -> Self {
4454 Self {
4455 value: ValueKind::Measure(n),
4456 }
4457 }
4458
4459 pub fn measure_with_type(n: RationalInteger, _lemma_type: Arc<LemmaType>) -> Self {
4461 Self::measure(n)
4462 }
4463
4464 pub fn measure_with_bound_unit(
4465 n: RationalInteger,
4466 _unit_name: impl Into<String>,
4467 _lemma_type: Arc<LemmaType>,
4468 ) -> Self {
4469 Self::measure(n)
4470 }
4471
4472 pub fn measure_with_signature(n: RationalInteger, _lemma_type: Arc<LemmaType>) -> Self {
4473 Self::measure(n)
4474 }
4475
4476 pub fn number_with_type(n: RationalInteger, _lemma_type: Arc<LemmaType>) -> Self {
4477 Self::number(n)
4478 }
4479
4480 pub fn number_with_type_from_decimal(decimal: Decimal, _lemma_type: Arc<LemmaType>) -> Self {
4481 Self::number_from_decimal(decimal)
4482 }
4483
4484 pub fn ratio_with_type(r: RationalInteger, _lemma_type: Arc<LemmaType>) -> Self {
4485 Self::ratio(r)
4486 }
4487
4488 pub fn ratio_with_bound_unit(
4489 r: RationalInteger,
4490 _unit_name: impl Into<String>,
4491 _lemma_type: Arc<LemmaType>,
4492 ) -> Self {
4493 Self::ratio(r)
4494 }
4495
4496 pub fn text_with_type(s: String, _lemma_type: Arc<LemmaType>) -> Self {
4497 Self::text(s)
4498 }
4499
4500 pub fn date_with_type(dt: SemanticDateTime, _lemma_type: Arc<LemmaType>) -> Self {
4501 Self::date(dt)
4502 }
4503
4504 pub fn time_with_type(t: SemanticTime, _lemma_type: Arc<LemmaType>) -> Self {
4505 Self::time(t)
4506 }
4507
4508 pub fn calendar(
4509 value: RationalInteger,
4510 _unit: SemanticCalendarUnit,
4511 _lemma_type: Arc<LemmaType>,
4512 ) -> Self {
4513 Self::measure(value)
4514 }
4515
4516 pub fn calendar_from_decimal(
4517 value: Decimal,
4518 unit: SemanticCalendarUnit,
4519 lemma_type: Arc<LemmaType>,
4520 ) -> Self {
4521 Self::calendar(
4522 crate::literals::rational_from_parsed_decimal(value)
4523 .expect("BUG: calendar literal from decimal must lift at boundary"),
4524 unit,
4525 lemma_type,
4526 )
4527 }
4528
4529 pub fn calendar_with_type(
4530 value: RationalInteger,
4531 unit: SemanticCalendarUnit,
4532 lemma_type: Arc<LemmaType>,
4533 ) -> Self {
4534 Self::calendar(value, unit, lemma_type)
4535 }
4536
4537 pub fn number_interpreted_as_measure(value: RationalInteger, _unit_name: String) -> Self {
4538 Self::measure(value)
4539 }
4540
4541 pub fn from_bool(b: bool) -> Self {
4542 Self {
4543 value: ValueKind::Boolean(b),
4544 }
4545 }
4546
4547 pub fn from_datetime(dt: &crate::parsing::ast::DateTimeValue) -> Self {
4548 Self::date(date_time_to_semantic(dt))
4549 }
4550
4551 pub fn date(dt: SemanticDateTime) -> Self {
4552 Self {
4553 value: ValueKind::Date(dt),
4554 }
4555 }
4556
4557 pub fn time(t: SemanticTime) -> Self {
4558 Self {
4559 value: ValueKind::Time(t),
4560 }
4561 }
4562
4563 pub fn ratio(r: RationalInteger) -> Self {
4564 Self {
4565 value: ValueKind::Ratio(r),
4566 }
4567 }
4568
4569 pub fn ratio_from_decimal(r: Decimal) -> Self {
4570 Self::ratio(
4571 crate::literals::rational_from_parsed_decimal(r)
4572 .expect("BUG: ratio literal from decimal must lift at boundary"),
4573 )
4574 }
4575
4576 pub fn range(left: LiteralValue, right: LiteralValue) -> Self {
4577 Self {
4578 value: ValueKind::Range(Box::new(left), Box::new(right)),
4579 }
4580 }
4581
4582 pub fn display_value(&self) -> String {
4584 match &self.value {
4585 ValueKind::Measure(_) | ValueKind::Ratio(_) => format!("{}", self.value),
4586 ValueKind::Range(left, right) => {
4587 format!("{}...{}", left.display_value(), right.display_value())
4588 }
4589 _ => format!("{}", self.value),
4590 }
4591 }
4592
4593 pub fn display_value_with_type(&self, lemma_type: &LemmaType) -> String {
4595 match &self.value {
4596 ValueKind::Measure(n) => {
4597 let signature = lemma_type.measure_runtime_signature();
4598 format_measure_canonical_for_display(n, lemma_type, &signature)
4599 }
4600 ValueKind::Ratio(n) => format_ratio_canonical_for_display(n, lemma_type),
4601 ValueKind::Range(left, right) => {
4602 let endpoint_ty = range_element_type_specification(&lemma_type.specifications)
4603 .map(LemmaType::primitive)
4604 .unwrap_or_else(|| lemma_type.clone());
4605 format!(
4606 "{}...{}",
4607 left.display_value_with_type(&endpoint_ty),
4608 right.display_value_with_type(&endpoint_ty)
4609 )
4610 }
4611 _ => format!("{}", self.value),
4612 }
4613 }
4614
4615 pub fn byte_size(&self) -> usize {
4617 self.value.structural_byte_size()
4618 }
4619
4620 #[must_use]
4622 pub fn magnitude_suggestion_for_decimal_prompt(
4623 &self,
4624 lemma_type: &LemmaType,
4625 ) -> Option<rust_decimal::Decimal> {
4626 match &self.value {
4627 ValueKind::Number(n) => Some(
4628 lemma_type
4629 .try_rational_as_decimal(n)
4630 .expect("BUG: stored number literal must convert to decimal for prompt"),
4631 ),
4632 ValueKind::Measure(n) => {
4633 let signature = lemma_type.measure_runtime_signature();
4634 let unit_name = Self::single_measure_signature_unit_name(&signature).expect(
4635 "BUG: measure prompt requires exactly one signature unit with exponent 1",
4636 );
4637 Some(
4638 lemma_type
4639 .try_measure_canonical_as_decimal_in_unit(n, unit_name)
4640 .expect("BUG: stored measure literal must convert to decimal for prompt"),
4641 )
4642 }
4643 ValueKind::Ratio(n) => {
4644 if let Some(unit_name) = lemma_type.measure_binding_unit.as_deref() {
4645 Some(
4646 lemma_type
4647 .try_ratio_canonical_as_decimal_in_unit(n, unit_name)
4648 .expect("BUG: stored ratio literal must convert to decimal for prompt"),
4649 )
4650 } else {
4651 Some(lemma_type.try_rational_as_decimal(n).expect(
4652 "BUG: stored bare ratio literal must convert to decimal for prompt",
4653 ))
4654 }
4655 }
4656 _ => None,
4657 }
4658 }
4659
4660 #[must_use]
4662 pub fn measure_units(
4663 &self,
4664 lemma_type: &LemmaType,
4665 ) -> Option<BTreeMap<String, rust_decimal::Decimal>> {
4666 if !matches!(self.value, ValueKind::Measure(_)) {
4667 return None;
4668 }
4669 lemma_type.measure_unit_names()?;
4670 let declared: Vec<&str> = lemma_type
4671 .measure_unit_names()
4672 .expect("BUG: measure_unit_names checked above");
4673 lemma_type
4674 .measure_literal_unit_map(self, &declared, UnitFactorSource::DeclaredOn(lemma_type))
4675 .ok()
4676 }
4677
4678 #[must_use]
4680 pub fn ratio_units(
4681 &self,
4682 lemma_type: &LemmaType,
4683 ) -> Option<BTreeMap<String, rust_decimal::Decimal>> {
4684 if !matches!(self.value, ValueKind::Ratio(_)) {
4685 return None;
4686 }
4687 let has_declared_units = match &lemma_type.specifications {
4688 TypeSpecification::Ratio { units, .. } => !units.is_empty(),
4689 TypeSpecification::RatioRange { .. } => true,
4690 _ => return None,
4691 };
4692 if !has_declared_units {
4693 return None;
4694 }
4695 let declared: Vec<&str> = lemma_type
4696 .ratio_unit_names()
4697 .expect("BUG: ratio units checked above");
4698 lemma_type
4699 .ratio_literal_unit_map(self, &declared, UnitFactorSource::DeclaredOn(lemma_type))
4700 .ok()
4701 }
4702
4703 #[must_use]
4705 pub fn magnitude_in_unit(
4706 &self,
4707 lemma_type: &LemmaType,
4708 unit: &str,
4709 ) -> Option<rust_decimal::Decimal> {
4710 self.measure_units(lemma_type)
4711 .and_then(|map| map.get(unit).copied())
4712 .or_else(|| {
4713 self.ratio_units(lemma_type)
4714 .and_then(|map| map.get(unit).copied())
4715 })
4716 }
4717
4718 pub fn duration_canonical_seconds(&self, lemma_type: &LemmaType) -> RationalInteger {
4720 let ValueKind::Measure(magnitude) = &self.value else {
4721 unreachable!(
4722 "BUG: duration_canonical_seconds called with {:?}",
4723 self.value
4724 );
4725 };
4726 if !lemma_type.is_duration_like_measure() {
4727 unreachable!(
4728 "BUG: duration_canonical_seconds called with type {}",
4729 lemma_type.name()
4730 );
4731 }
4732 let factor = lemma_type.measure_unit_factor("second");
4733 checked_div(magnitude, factor).expect("BUG: duration unit factor cannot be zero")
4734 }
4735
4736 pub fn calendar_canonical_months(&self, lemma_type: &LemmaType) -> RationalInteger {
4738 let ValueKind::Measure(magnitude) = &self.value else {
4739 unreachable!(
4740 "BUG: calendar_canonical_months called with {:?}",
4741 self.value
4742 );
4743 };
4744 if !lemma_type.is_calendar_like() {
4745 unreachable!(
4746 "BUG: calendar_canonical_months called with type {}",
4747 lemma_type.name()
4748 );
4749 }
4750 let factor = lemma_type.measure_unit_factor("month");
4751 checked_div(magnitude, factor).expect("BUG: calendar unit factor cannot be zero")
4752 }
4753}
4754
4755impl TypedLiteral {
4756 pub fn text(s: String) -> Self {
4757 Self {
4758 value: ValueKind::Text(s),
4759 lemma_type: primitive_text_arc().clone(),
4760 }
4761 }
4762
4763 pub fn text_with_type(s: String, lemma_type: Arc<LemmaType>) -> Self {
4764 Self {
4765 value: ValueKind::Text(s),
4766 lemma_type,
4767 }
4768 }
4769
4770 pub fn number(n: RationalInteger) -> Self {
4771 Self {
4772 value: ValueKind::Number(n),
4773 lemma_type: primitive_number_arc().clone(),
4774 }
4775 }
4776
4777 pub fn number_from_decimal(decimal: Decimal) -> Self {
4778 Self::number(
4779 crate::literals::rational_from_parsed_decimal(decimal)
4780 .expect("BUG: literal number from decimal must lift at boundary"),
4781 )
4782 }
4783
4784 pub fn number_with_type(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
4785 Self {
4786 value: ValueKind::Number(n),
4787 lemma_type,
4788 }
4789 }
4790
4791 pub fn number_with_type_from_decimal(decimal: Decimal, lemma_type: Arc<LemmaType>) -> Self {
4792 Self::number_with_type(
4793 crate::literals::rational_from_parsed_decimal(decimal)
4794 .expect("BUG: literal number from decimal must lift at boundary"),
4795 lemma_type,
4796 )
4797 }
4798
4799 pub fn measure_with_bound_unit(
4801 n: RationalInteger,
4802 unit_name: impl Into<String>,
4803 lemma_type: Arc<LemmaType>,
4804 ) -> Self {
4805 Self::measure_with_type(
4806 n,
4807 Arc::new(
4808 lemma_type
4809 .as_ref()
4810 .clone()
4811 .with_measure_binding_unit(unit_name),
4812 ),
4813 )
4814 }
4815
4816 pub fn measure_with_type(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
4817 Self {
4818 value: ValueKind::Measure(n),
4819 lemma_type,
4820 }
4821 }
4822
4823 pub fn measure_with_signature(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
4824 Self {
4825 value: ValueKind::Measure(n),
4826 lemma_type,
4827 }
4828 }
4829
4830 pub fn number_interpreted_as_measure(value: RationalInteger, unit_name: String) -> Self {
4832 let lemma_type = if unit_name.is_empty() {
4833 Arc::new(anonymous_measure_type())
4834 } else {
4835 let mut decomp = BaseMeasureVector::new();
4836 decomp.insert(unit_name, 1);
4837 Arc::new(LemmaType::anonymous_for_decomposition(decomp))
4838 };
4839 Self {
4840 value: ValueKind::Measure(value),
4841 lemma_type,
4842 }
4843 }
4844
4845 pub fn from_bool(b: bool) -> Self {
4846 Self {
4847 value: ValueKind::Boolean(b),
4848 lemma_type: primitive_boolean_arc().clone(),
4849 }
4850 }
4851
4852 pub fn from_datetime(dt: &crate::parsing::ast::DateTimeValue) -> Self {
4853 Self::date(date_time_to_semantic(dt))
4854 }
4855
4856 pub fn date(dt: SemanticDateTime) -> Self {
4857 Self {
4858 value: ValueKind::Date(dt),
4859 lemma_type: primitive_date_arc().clone(),
4860 }
4861 }
4862
4863 pub fn date_with_type(dt: SemanticDateTime, lemma_type: Arc<LemmaType>) -> Self {
4864 Self {
4865 value: ValueKind::Date(dt),
4866 lemma_type,
4867 }
4868 }
4869
4870 pub fn time(t: SemanticTime) -> Self {
4871 Self {
4872 value: ValueKind::Time(t),
4873 lemma_type: primitive_time_arc().clone(),
4874 }
4875 }
4876
4877 pub fn time_with_type(t: SemanticTime, lemma_type: Arc<LemmaType>) -> Self {
4878 Self {
4879 value: ValueKind::Time(t),
4880 lemma_type,
4881 }
4882 }
4883
4884 pub fn calendar(
4885 value: RationalInteger,
4886 unit: SemanticCalendarUnit,
4887 lemma_type: Arc<LemmaType>,
4888 ) -> Self {
4889 let unit_name = unit.to_string();
4890 debug_assert_eq!(
4891 semantic_calendar_unit_from_unit_name(&unit_name),
4892 unit,
4893 "BUG: calendar unit name must round-trip"
4894 );
4895 Self::measure_with_bound_unit(value, unit_name, lemma_type)
4896 }
4897
4898 pub fn calendar_from_decimal(
4899 value: Decimal,
4900 unit: SemanticCalendarUnit,
4901 lemma_type: Arc<LemmaType>,
4902 ) -> Self {
4903 Self::calendar(
4904 crate::literals::rational_from_parsed_decimal(value)
4905 .expect("BUG: calendar literal from decimal must lift at boundary"),
4906 unit,
4907 lemma_type,
4908 )
4909 }
4910
4911 pub fn calendar_with_type(
4912 value: RationalInteger,
4913 unit: SemanticCalendarUnit,
4914 lemma_type: Arc<LemmaType>,
4915 ) -> Self {
4916 Self::calendar(value, unit, lemma_type)
4917 }
4918
4919 pub fn ratio(r: RationalInteger) -> Self {
4920 Self {
4921 value: ValueKind::Ratio(r),
4922 lemma_type: primitive_ratio_arc().clone(),
4923 }
4924 }
4925
4926 pub fn ratio_from_decimal(r: Decimal) -> Self {
4927 Self::ratio(
4928 crate::literals::rational_from_parsed_decimal(r)
4929 .expect("BUG: ratio literal from decimal must lift at boundary"),
4930 )
4931 }
4932
4933 pub fn ratio_with_type(r: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
4934 Self {
4935 value: ValueKind::Ratio(r),
4936 lemma_type,
4937 }
4938 }
4939
4940 pub fn ratio_with_bound_unit(
4941 r: RationalInteger,
4942 unit_name: impl Into<String>,
4943 lemma_type: Arc<LemmaType>,
4944 ) -> Self {
4945 Self::ratio_with_type(
4946 r,
4947 Arc::new(
4948 lemma_type
4949 .as_ref()
4950 .clone()
4951 .with_measure_binding_unit(unit_name),
4952 ),
4953 )
4954 }
4955
4956 pub fn range(left: TypedLiteral, right: TypedLiteral) -> Self {
4957 let specifications =
4958 range_type_specification_from_endpoints(&left.lemma_type, &right.lemma_type)
4959 .unwrap_or_else(|| {
4960 unreachable!(
4961 "BUG: attempted to construct a range literal from incompatible endpoint types"
4962 )
4963 });
4964
4965 Self {
4966 value: ValueKind::Range(Box::new(left.to_literal()), Box::new(right.to_literal())),
4967 lemma_type: Arc::new(LemmaType::primitive(specifications)),
4968 }
4969 }
4970
4971 pub fn display_value(&self) -> String {
4972 self.to_literal().display_value_with_type(&self.lemma_type)
4973 }
4974
4975 pub fn get_type(&self) -> &LemmaType {
4976 &self.lemma_type
4977 }
4978
4979 pub fn byte_size(&self) -> usize {
4980 self.value.structural_byte_size()
4981 }
4982}
4983
4984impl fmt::Display for TypedLiteral {
4985 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4986 write!(f, "{}", self.display_value())
4987 }
4988}
4989
4990#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
4993pub enum ReferenceTarget {
4994 Data(DataPath),
4995 Rule(RulePath),
4996}
4997
4998#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
5002pub(crate) enum ReferenceEnd {
5003 Promptable(DataPath),
5004 Rule(RulePath),
5005 Import,
5006}
5007
5008#[derive(Clone, Debug, Serialize, Deserialize)]
5010pub enum DataDefinition {
5011 Value {
5013 value: LiteralValue,
5014 resolved_type: Arc<LemmaType>,
5015 source: Source,
5016 },
5017 TypeDeclaration {
5023 resolved_type: Arc<LemmaType>,
5024 declared_suggestion: Option<BoundValueKind>,
5025 declared_fill: Option<BoundValueKind>,
5026 source: Source,
5027 },
5028 Import { target_name: String, source: Source },
5030 Reference {
5055 target: ReferenceTarget,
5056 resolved_type: Arc<LemmaType>,
5057 local_constraints: Option<Vec<Constraint>>,
5058 local_suggestion: Option<BoundValueKind>,
5059 local_fill: Option<BoundValueKind>,
5060 source: Source,
5061 },
5062}
5063
5064impl DataDefinition {
5065 pub fn lemma_type(&self) -> Option<&LemmaType> {
5067 self.resolved_type_arc().map(|arc| arc.as_ref())
5068 }
5069
5070 pub fn resolved_type_arc(&self) -> Option<&Arc<LemmaType>> {
5072 match self {
5073 DataDefinition::Value { resolved_type, .. } => Some(resolved_type),
5074 DataDefinition::TypeDeclaration { resolved_type, .. } => Some(resolved_type),
5075 DataDefinition::Reference { resolved_type, .. } => Some(resolved_type),
5076 DataDefinition::Import { .. } => None,
5077 }
5078 }
5079
5080 #[inline]
5082 pub fn schema_type(&self) -> Option<&LemmaType> {
5083 self.lemma_type()
5084 }
5085
5086 pub fn value(&self) -> Option<LiteralValue> {
5090 match self {
5091 DataDefinition::Value { value, .. } => Some(value.clone()),
5092 DataDefinition::TypeDeclaration {
5093 declared_fill: Some(dv),
5094 ..
5095 } => Some(dv.to_literal()),
5096 DataDefinition::Reference {
5097 local_fill: Some(dv),
5098 resolved_type: _,
5099 ..
5100 } => Some(dv.to_literal()),
5101 DataDefinition::TypeDeclaration { .. }
5102 | DataDefinition::Import { .. }
5103 | DataDefinition::Reference { .. } => None,
5104 }
5105 }
5106
5107 pub(crate) fn bound_fill(&self) -> Option<&BoundValueKind> {
5109 match self {
5110 DataDefinition::TypeDeclaration {
5111 declared_fill: Some(dv),
5112 ..
5113 }
5114 | DataDefinition::Reference {
5115 local_fill: Some(dv),
5116 ..
5117 } => Some(dv),
5118 DataDefinition::Value { .. }
5119 | DataDefinition::TypeDeclaration { .. }
5120 | DataDefinition::Reference { .. }
5121 | DataDefinition::Import { .. } => None,
5122 }
5123 }
5124
5125 pub(crate) fn bound_suggestion(&self) -> Option<&BoundValueKind> {
5129 match self {
5130 DataDefinition::TypeDeclaration {
5131 declared_suggestion: Some(dv),
5132 ..
5133 }
5134 | DataDefinition::Reference {
5135 local_suggestion: Some(dv),
5136 ..
5137 } => Some(dv),
5138 DataDefinition::Value { .. }
5139 | DataDefinition::TypeDeclaration { .. }
5140 | DataDefinition::Reference { .. }
5141 | DataDefinition::Import { .. } => None,
5142 }
5143 }
5144
5145 pub fn source(&self) -> &Source {
5147 match self {
5148 DataDefinition::Value { source, .. } => source,
5149 DataDefinition::TypeDeclaration { source, .. } => source,
5150 DataDefinition::Import { source, .. } => source,
5151 DataDefinition::Reference { source, .. } => source,
5152 }
5153 }
5154}
5155
5156pub fn number_with_unit_to_value_kind(
5158 magnitude: rust_decimal::Decimal,
5159 unit_name: &str,
5160 lemma_type: &LemmaType,
5161) -> Result<ValueKind, String> {
5162 match &lemma_type.specifications {
5163 TypeSpecification::Ratio { units, .. } => {
5164 use crate::computation::rational::{checked_div, decimal_to_rational};
5165 let unit = units.get(unit_name)?;
5166 let magnitude_rational = decimal_to_rational(magnitude)
5167 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
5168 let canonical_rational = checked_div(&magnitude_rational, &unit.value)
5169 .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
5170 Ok(ValueKind::Ratio(canonical_rational))
5171 }
5172 TypeSpecification::Measure { units, .. } => {
5173 use crate::computation::rational::checked_mul;
5174 let rational = lift_parser_decimal(magnitude)?;
5175 let unit = units.get(unit_name)?;
5176 let canonical = checked_mul(&rational, &unit.factor)
5177 .map_err(|failure| format!("measure canonicalization overflow: {failure}"))?;
5178 Ok(ValueKind::Measure(canonical))
5179 }
5180 _ => Err(format!(
5181 "Unit '{}' is defined on type '{}' which is not measure or ratio",
5182 unit_name,
5183 lemma_type.name()
5184 )),
5185 }
5186}
5187
5188pub(crate) fn value_kind_matches_spec(value: &ValueKind, type_spec: &TypeSpecification) -> bool {
5191 matches!(
5192 (type_spec, value),
5193 (TypeSpecification::Number { .. }, ValueKind::Number(_))
5194 | (TypeSpecification::Text { .. }, ValueKind::Text(_))
5195 | (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
5196 | (TypeSpecification::Date { .. }, ValueKind::Date(_))
5197 | (TypeSpecification::Time { .. }, ValueKind::Time(_))
5198 | (TypeSpecification::Measure { .. }, ValueKind::Measure(_))
5199 | (TypeSpecification::Ratio { .. }, ValueKind::Ratio(_))
5200 | (TypeSpecification::Ratio { .. }, ValueKind::Number(_))
5201 | (
5202 TypeSpecification::NumberRange { .. },
5203 ValueKind::Range(_, _)
5204 )
5205 | (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
5206 | (TypeSpecification::TimeRange { .. }, ValueKind::Range(_, _))
5207 | (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
5208 | (
5209 TypeSpecification::MeasureRange { .. },
5210 ValueKind::Range(_, _)
5211 )
5212 | (TypeSpecification::Veto { .. }, _)
5213 | (TypeSpecification::Undetermined, _)
5214 )
5215}
5216
5217fn parser_value_type_mismatch(
5218 value: &crate::literals::Value,
5219 type_spec: &TypeSpecification,
5220) -> String {
5221 use crate::parsing::ast::AsLemmaSource;
5222 let value_str = format!("{}", AsLemmaSource(value));
5223 match type_spec {
5224 TypeSpecification::Measure { units, .. } => {
5225 let unit_hint = units
5226 .iter()
5227 .find(|u| u.factor == crate::computation::rational::rational_one())
5228 .map(|u| u.name.as_str())
5229 .or_else(|| units.iter().next().map(|u| u.name.as_str()))
5230 .unwrap_or("unit");
5231 format!("cannot use {value_str} as {type_spec}: expected `<n> {unit_hint}`")
5232 }
5233 TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
5234 let unit_hint = units
5235 .iter()
5236 .next()
5237 .map(|u| u.name.as_str())
5238 .unwrap_or("unit");
5239 format!(
5240 "cannot use {value_str} as {type_spec}: expected `<n> {unit_hint}` or bare ratio"
5241 )
5242 }
5243 _ => format!("cannot use {value_str} as {type_spec}"),
5244 }
5245}
5246
5247pub fn refresh_measure_literal_canonical_magnitude(
5252 lit: &mut LiteralValue,
5253 previous_type: &LemmaType,
5254 resolved_type: &LemmaType,
5255) -> Result<(), crate::computation::rational::NumericFailure> {
5256 use crate::computation::rational::{checked_div, checked_mul};
5257 let ValueKind::Measure(magnitude) = &mut lit.value else {
5258 return Ok(());
5259 };
5260 let signature = previous_type.measure_runtime_signature();
5264 let Some((unit_name, 1)) = signature.first().map(|(n, e)| (n.as_str(), *e)) else {
5265 return Ok(());
5266 };
5267 if signature.len() != 1 {
5268 return Ok(());
5269 }
5270 let stored_factor = previous_type.measure_unit_factor(unit_name);
5271 let resolved_factor = resolved_type.measure_unit_factor(unit_name);
5272 if stored_factor == resolved_factor {
5273 return Ok(());
5274 }
5275 let scaled = checked_mul(magnitude, resolved_factor)?;
5276 *magnitude = checked_div(&scaled, stored_factor)?;
5277 Ok(())
5278}
5279
5280pub fn parser_value_to_value_kind(
5282 value: &crate::literals::Value,
5283 type_spec: &TypeSpecification,
5284) -> Result<ValueKind, String> {
5285 use crate::computation::rational::decimal_to_rational;
5286 use crate::literals::Value;
5287 match (value, type_spec) {
5288 (Value::NumberWithUnit(magnitude, unit_name), TypeSpecification::Ratio { units, .. }) => {
5289 use crate::computation::rational::checked_div;
5290 let unit = units.get(unit_name.as_str())?;
5291 let magnitude_rational = decimal_to_rational(*magnitude)
5292 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
5293 let canonical_rational = checked_div(&magnitude_rational, &unit.value)
5294 .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
5295 Ok(ValueKind::Ratio(canonical_rational))
5296 }
5297 (Value::NumberWithUnit(magnitude, unit_name), TypeSpecification::Measure { units, .. }) => {
5298 use crate::computation::rational::checked_mul;
5299 let rational = lift_parser_decimal(*magnitude)?;
5300 let unit = units.get(unit_name.as_str())?;
5301 let canonical = checked_mul(&rational, &unit.factor)
5302 .map_err(|failure| format!("measure canonicalization overflow: {failure}"))?;
5303 Ok(ValueKind::Measure(canonical))
5304 }
5305 (Value::NumberWithUnit(_, _), _) => {
5306 Err("number_with_unit literal requires a measure or ratio type".to_string())
5307 }
5308 (Value::Number(n), TypeSpecification::Number { .. }) => {
5309 Ok(ValueKind::Number(lift_parser_decimal(*n)?))
5310 }
5311 (Value::Number(n), TypeSpecification::Ratio { .. }) => {
5312 let r = decimal_to_rational(*n)
5313 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
5314 Ok(ValueKind::Ratio(r))
5315 }
5316 (Value::Text(s), TypeSpecification::Text { .. }) => Ok(ValueKind::Text(s.clone())),
5317 (Value::Boolean(b), TypeSpecification::Boolean { .. }) => Ok(ValueKind::Boolean(b.into())),
5318 (Value::Date(dt), TypeSpecification::Date { .. }) => {
5319 Ok(ValueKind::Date(date_time_to_semantic(dt)))
5320 }
5321 (Value::Time(t), TypeSpecification::Time { .. }) => {
5322 Ok(ValueKind::Time(time_to_semantic(t)))
5323 }
5324 (
5325 Value::Range(left, right),
5326 range_spec @ (TypeSpecification::NumberRange { .. }
5327 | TypeSpecification::DateRange { .. }
5328 | TypeSpecification::TimeRange { .. }
5329 | TypeSpecification::RatioRange { .. }
5330 | TypeSpecification::MeasureRange { .. }),
5331 ) => {
5332 let endpoint = range_element_type_specification(range_spec).ok_or_else(|| {
5333 "BUG: range_element_type_specification missing arm for range type".to_string()
5334 })?;
5335 let left_lit = lift_range_endpoint(left, &endpoint)?;
5336 let right_lit = lift_range_endpoint(right, &endpoint)?;
5337 Ok(ValueKind::Range(
5338 Box::new(left_lit.to_literal()),
5339 Box::new(right_lit.to_literal()),
5340 ))
5341 }
5342 (value, type_spec) => Err(parser_value_type_mismatch(value, type_spec)),
5343 }
5344}
5345
5346pub fn value_to_semantic(value: &crate::parsing::ast::Value) -> Result<ValueKind, String> {
5350 use crate::parsing::ast::Value;
5351 Ok(match value {
5352 Value::Number(n) => ValueKind::Number(lift_parser_decimal(*n)?),
5353 Value::Text(s) => ValueKind::Text(s.clone()),
5354 Value::Boolean(b) => ValueKind::Boolean(bool::from(*b)),
5355 Value::Date(dt) => ValueKind::Date(date_time_to_semantic(dt)),
5356 Value::Time(t) => ValueKind::Time(time_to_semantic(t)),
5357 Value::NumberWithUnit(_, _) => {
5358 return Err(
5359 "number_with_unit literal requires type context (measure or ratio)".to_string(),
5360 );
5361 }
5362 Value::Range(_, _) => literal_value_from_parser_value(value)?.value,
5363 })
5364}
5365
5366pub(crate) fn date_time_to_semantic(dt: &crate::parsing::ast::DateTimeValue) -> SemanticDateTime {
5368 SemanticDateTime {
5369 year: dt.year,
5370 month: dt.month,
5371 day: dt.day,
5372 hour: dt.hour,
5373 minute: dt.minute,
5374 second: dt.second,
5375 microsecond: dt.microsecond,
5376 timezone: dt.timezone.as_ref().map(|tz| SemanticTimezone {
5377 offset_hours: tz.offset_hours,
5378 offset_minutes: tz.offset_minutes,
5379 }),
5380 }
5381}
5382
5383pub(crate) fn time_to_semantic(t: &crate::parsing::ast::TimeValue) -> SemanticTime {
5385 SemanticTime {
5386 hour: t.hour.into(),
5387 minute: t.minute.into(),
5388 second: t.second.into(),
5389 microsecond: t.microsecond,
5390 timezone: t.timezone.as_ref().map(|tz| SemanticTimezone {
5391 offset_hours: tz.offset_hours,
5392 offset_minutes: tz.offset_minutes,
5393 }),
5394 }
5395}
5396
5397pub(crate) fn compare_semantic_dates(
5401 left: &SemanticDateTime,
5402 right: &SemanticDateTime,
5403) -> std::cmp::Ordering {
5404 left.year
5405 .cmp(&right.year)
5406 .then_with(|| left.month.cmp(&right.month))
5407 .then_with(|| left.day.cmp(&right.day))
5408 .then_with(|| left.hour.cmp(&right.hour))
5409 .then_with(|| left.minute.cmp(&right.minute))
5410 .then_with(|| left.second.cmp(&right.second))
5411 .then_with(|| left.microsecond.cmp(&right.microsecond))
5412}
5413
5414pub(crate) fn compare_semantic_times(
5417 left: &SemanticTime,
5418 right: &SemanticTime,
5419) -> std::cmp::Ordering {
5420 left.hour
5421 .cmp(&right.hour)
5422 .then_with(|| left.minute.cmp(&right.minute))
5423 .then_with(|| left.second.cmp(&right.second))
5424 .then_with(|| left.microsecond.cmp(&right.microsecond))
5425}
5426
5427pub fn conversion_target_to_semantic(
5429 ct: &ConversionTarget,
5430 unit_index: Option<&crate::planning::unit_index::UnitIndex>,
5431 resolved_types: Option<&IndexMap<String, Arc<LemmaType>>>,
5432) -> Result<SemanticConversionTarget, String> {
5433 match ct {
5434 ConversionTarget::Type(kind) => Ok(SemanticConversionTarget::Type(*kind)),
5435 ConversionTarget::Unit { unit_name } => {
5436 let index = unit_index.ok_or_else(|| format!("Unknown unit '{unit_name}'."))?;
5437 let (bare, owning_type) = match resolved_types {
5438 Some(resolved) => index.resolve_with_named_types(unit_name, resolved)?,
5439 None => index.resolve(unit_name)?,
5440 };
5441 Ok(SemanticConversionTarget::Unit {
5442 unit_name: bare,
5443 owning_type,
5444 })
5445 }
5446 }
5447}
5448
5449static PRIMITIVE_BOOLEAN: OnceLock<Arc<LemmaType>> = OnceLock::new();
5455static PRIMITIVE_NUMBER: OnceLock<Arc<LemmaType>> = OnceLock::new();
5456static PRIMITIVE_TEXT: OnceLock<Arc<LemmaType>> = OnceLock::new();
5457static PRIMITIVE_DATE: OnceLock<Arc<LemmaType>> = OnceLock::new();
5458static PRIMITIVE_DATE_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
5459static PRIMITIVE_TIME: OnceLock<Arc<LemmaType>> = OnceLock::new();
5460static PRIMITIVE_RATIO: OnceLock<Arc<LemmaType>> = OnceLock::new();
5461
5462#[must_use]
5463pub fn primitive_boolean_arc() -> &'static Arc<LemmaType> {
5464 PRIMITIVE_BOOLEAN.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::boolean())))
5465}
5466
5467#[must_use]
5468pub fn primitive_number_arc() -> &'static Arc<LemmaType> {
5469 PRIMITIVE_NUMBER.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::number())))
5470}
5471
5472#[must_use]
5473pub fn primitive_text_arc() -> &'static Arc<LemmaType> {
5474 PRIMITIVE_TEXT.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::text())))
5475}
5476
5477#[must_use]
5478pub fn primitive_date_arc() -> &'static Arc<LemmaType> {
5479 PRIMITIVE_DATE.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::date())))
5480}
5481
5482#[must_use]
5483pub fn primitive_date_range_arc() -> &'static Arc<LemmaType> {
5484 PRIMITIVE_DATE_RANGE
5485 .get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::date_range())))
5486}
5487
5488#[must_use]
5489pub fn primitive_time_arc() -> &'static Arc<LemmaType> {
5490 PRIMITIVE_TIME.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::time())))
5491}
5492
5493#[must_use]
5494pub fn primitive_ratio_arc() -> &'static Arc<LemmaType> {
5495 PRIMITIVE_RATIO.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::ratio())))
5496}
5497
5498#[must_use]
5500pub fn type_spec_for_primitive(kind: PrimitiveKind) -> TypeSpecification {
5501 match kind {
5502 PrimitiveKind::Boolean => TypeSpecification::boolean(),
5503 PrimitiveKind::Measure => TypeSpecification::measure(),
5504 PrimitiveKind::MeasureRange => TypeSpecification::measure_range(),
5505 PrimitiveKind::Number => TypeSpecification::number(),
5506 PrimitiveKind::NumberRange => TypeSpecification::number_range(),
5507 PrimitiveKind::Ratio => TypeSpecification::ratio(),
5508 PrimitiveKind::RatioRange => TypeSpecification::ratio_range(),
5509 PrimitiveKind::Text => TypeSpecification::text(),
5510 PrimitiveKind::Date => TypeSpecification::date(),
5511 PrimitiveKind::DateRange => TypeSpecification::date_range(),
5512 PrimitiveKind::Time => TypeSpecification::time(),
5513 PrimitiveKind::TimeRange => TypeSpecification::time_range(),
5514 }
5515}
5516
5517impl fmt::Display for PathSegment {
5522 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5523 match &self.repository {
5524 Some(repository) => write!(f, "{} → {} {}", self.uses, repository, self.spec),
5525 None => write!(f, "{} → {}", self.uses, self.spec),
5526 }
5527 }
5528}
5529
5530impl fmt::Display for DataPath {
5531 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5532 write!(f, "{}", self.input_key())
5533 }
5534}
5535
5536impl fmt::Display for RulePath {
5537 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5538 write!(f, "{}", self.input_key())
5539 }
5540}
5541
5542impl fmt::Display for LemmaType {
5543 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5544 write!(f, "{}", self.name())
5545 }
5546}
5547
5548pub(crate) fn format_decimal_for_api(
5549 decimal: rust_decimal::Decimal,
5550 decimal_places: Option<u8>,
5551) -> rust_decimal::Decimal {
5552 match decimal_places {
5553 Some(decimal_places) => {
5554 let places = u32::from(decimal_places);
5555 let mut rounded = decimal.round_dp(places);
5556 rounded.rescale(places);
5557 rounded
5558 }
5559 None => {
5560 let normalized = decimal.normalize();
5561 if normalized.fract().is_zero() {
5562 normalized.trunc()
5563 } else {
5564 normalized
5565 }
5566 }
5567 }
5568}
5569
5570fn format_decimal_for_human_display(
5571 decimal: rust_decimal::Decimal,
5572 decimal_places: Option<u8>,
5573) -> String {
5574 match decimal_places {
5575 Some(decimal_places) => {
5576 let rounded = decimal.round_dp(u32::from(decimal_places));
5577 format!("{:.prec$}", rounded, prec = decimal_places as usize)
5578 }
5579 None => decimal.normalize().to_string(),
5580 }
5581}
5582
5583fn format_rational_for_human_display(
5584 magnitude: &crate::computation::rational::RationalInteger,
5585 decimal_places: Option<u8>,
5586) -> String {
5587 match magnitude.try_to_decimal() {
5588 Ok(decimal) => format_decimal_for_human_display(decimal, decimal_places),
5589 Err(crate::computation::rational::NumericFailure::Overflow) => magnitude.display_str(),
5590 Err(_) => magnitude.display_str(),
5591 }
5592}
5593
5594fn format_measure_canonical_for_display(
5595 canonical: &crate::computation::rational::RationalInteger,
5596 lemma_type: &LemmaType,
5597 signature: &[(String, i32)],
5598) -> String {
5599 use crate::computation::rational::checked_div;
5600
5601 let decimals = lemma_type.decimal_places();
5602
5603 if let TypeSpecification::Measure { units, .. } = &lemma_type.specifications {
5604 if !units.is_empty() {
5605 let unit = match lemma_type.measure_binding_unit.as_deref() {
5607 Some(binding) => units
5608 .iter()
5609 .find(|unit| unit.name == binding)
5610 .unwrap_or_else(|| {
5611 panic!(
5612 "BUG: measure display binding '{binding}' is not a declared unit on type {}",
5613 lemma_type.name()
5614 )
5615 }),
5616 None => units
5617 .iter()
5618 .next()
5619 .expect("BUG: measure type with non-empty units must have a first unit"),
5620 };
5621 let in_unit = checked_div(canonical, &unit.factor)
5622 .expect("BUG: de-canonicalization for measure display must not fail");
5623 let formatted = format_rational_for_human_display(&in_unit, decimals);
5624 return format!("{} {}", formatted, unit.name);
5625 }
5626 }
5627
5628 let unit_label = match signature {
5629 [] => String::new(),
5630 [(name, 1)] => name.clone(),
5631 _ => format_signature_operator_style(signature),
5632 };
5633 let formatted = format_rational_for_human_display(canonical, decimals);
5634 if unit_label.is_empty() {
5635 formatted
5636 } else {
5637 format!("{formatted} {unit_label}")
5638 }
5639}
5640
5641fn format_ratio_canonical_for_display(
5642 canonical: &crate::computation::rational::RationalInteger,
5643 lemma_type: &LemmaType,
5644) -> String {
5645 use crate::computation::rational::{checked_mul, rational_new};
5646
5647 let display_unit = lemma_type
5648 .measure_binding_unit
5649 .as_deref()
5650 .or_else(|| lemma_type.ratio_primary_unit());
5651
5652 match display_unit {
5653 Some("percent") => match checked_mul(canonical, &rational_new(100, 1)) {
5654 Ok(scaled) => format_number_with_unit_for_display(&scaled, "percent"),
5655 Err(_) => format!("{} percent", canonical.display_str()),
5656 },
5657 Some("permille") => match checked_mul(canonical, &rational_new(1000, 1)) {
5658 Ok(scaled) => format_number_with_unit_for_display(&scaled, "permille"),
5659 Err(_) => format!("{} permille", canonical.display_str()),
5660 },
5661 Some(unit_name) => format_number_with_unit_for_display(canonical, unit_name),
5662 None => format_rational_magnitude_for_display(canonical),
5663 }
5664}
5665
5666impl fmt::Display for LiteralValue {
5667 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5668 match &self.value {
5672 ValueKind::Measure(_) | ValueKind::Ratio(_) => {
5673 write!(f, "{}", self.value)
5674 }
5675 ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
5676 _ => write!(f, "{}", self.value),
5677 }
5678 }
5679}
5680
5681#[cfg(test)]
5686pub(crate) mod tests {
5687 use super::*;
5688 use crate::computation::rational::decimal_to_rational;
5689 use crate::literals::DateGranularity;
5690 use crate::literals::Value;
5691 use crate::parsing::ast::{BooleanValue, DateTimeValue, PrimitiveKind, TimeValue};
5692 use rust_decimal::Decimal;
5693 use std::str::FromStr;
5694 use std::sync::{Arc, OnceLock};
5695
5696 static PRIMITIVE_MEASURE: OnceLock<Arc<LemmaType>> = OnceLock::new();
5697
5698 #[must_use]
5699 pub(crate) fn primitive_measure_arc() -> &'static Arc<LemmaType> {
5700 PRIMITIVE_MEASURE
5701 .get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::measure())))
5702 }
5703
5704 #[must_use]
5705 pub(crate) fn primitive_measure() -> &'static LemmaType {
5706 primitive_measure_arc().as_ref()
5707 }
5708
5709 #[test]
5710 fn default_primitive_help_is_goal_oriented() {
5711 let kinds = [
5712 PrimitiveKind::Boolean,
5713 PrimitiveKind::Measure,
5714 PrimitiveKind::MeasureRange,
5715 PrimitiveKind::Number,
5716 PrimitiveKind::NumberRange,
5717 PrimitiveKind::Ratio,
5718 PrimitiveKind::RatioRange,
5719 PrimitiveKind::Text,
5720 PrimitiveKind::Date,
5721 PrimitiveKind::DateRange,
5722 PrimitiveKind::Time,
5723 PrimitiveKind::TimeRange,
5724 ];
5725 for kind in kinds {
5726 let spec = type_spec_for_primitive(kind);
5727 let help = match &spec {
5728 TypeSpecification::Boolean { help, .. }
5729 | TypeSpecification::Number { help, .. }
5730 | TypeSpecification::NumberRange { help, .. }
5731 | TypeSpecification::Text { help, .. }
5732 | TypeSpecification::Measure { help, .. }
5733 | TypeSpecification::MeasureRange { help, .. }
5734 | TypeSpecification::Ratio { help, .. }
5735 | TypeSpecification::RatioRange { help, .. }
5736 | TypeSpecification::Date { help, .. }
5737 | TypeSpecification::DateRange { help, .. }
5738 | TypeSpecification::TimeRange { help, .. }
5739 | TypeSpecification::Time { help, .. } => help,
5740 TypeSpecification::Veto { .. } | TypeSpecification::Undetermined => {
5741 unreachable!(
5742 "BUG: primitive kind {:?} mapped to non-primitive spec",
5743 kind
5744 )
5745 }
5746 };
5747 assert!(!help.is_empty(), "help for {:?}", kind);
5748 assert!(
5749 !help.to_ascii_lowercase().contains("format:"),
5750 "help for {:?} must not describe syntax: {:?}",
5751 kind,
5752 help
5753 );
5754 assert_eq!(help, default_help_for_primitive(kind));
5755 }
5756 }
5757
5758 #[test]
5759 fn data_path_and_rule_path_display_are_input_key() {
5760 let implicit = PathSegment {
5761 uses: "bag".to_string(),
5762 repository: None,
5763 spec: "bag".to_string(),
5764 };
5765 let explicit = PathSegment {
5766 uses: "bag".to_string(),
5767 repository: None,
5768 spec: "nut_bag".to_string(),
5769 };
5770 let named_repo = PathSegment {
5771 uses: "calc".to_string(),
5772 repository: Some("alpha".to_string()),
5773 spec: "pricing".to_string(),
5774 };
5775
5776 assert_eq!(
5777 DataPath::new(vec![implicit.clone()], "weight".to_string()).to_string(),
5778 "bag.weight"
5779 );
5780 assert_eq!(
5781 DataPath::new(vec![explicit.clone()], "weight".to_string()).to_string(),
5782 "bag.weight"
5783 );
5784 assert_eq!(
5785 DataPath::new(vec![named_repo.clone()], "price".to_string()).to_string(),
5786 "calc.price"
5787 );
5788 assert_eq!(
5789 RulePath::new(vec![implicit], "cost".to_string()).to_string(),
5790 "bag.cost"
5791 );
5792 assert_eq!(
5793 RulePath::new(vec![explicit], "cost".to_string()).to_string(),
5794 "bag.cost"
5795 );
5796 assert_eq!(
5797 RulePath::new(vec![named_repo], "price".to_string()).to_string(),
5798 "calc.price"
5799 );
5800
5801 assert_eq!(
5802 PathSegment {
5803 uses: "bag".to_string(),
5804 repository: None,
5805 spec: "nut_bag".to_string(),
5806 }
5807 .to_string(),
5808 "bag → nut_bag"
5809 );
5810 assert_eq!(
5811 PathSegment {
5812 uses: "calc".to_string(),
5813 repository: Some("alpha".to_string()),
5814 spec: "pricing".to_string(),
5815 }
5816 .to_string(),
5817 "calc → alpha pricing"
5818 );
5819 }
5820
5821 #[test]
5822 fn test_negated_comparison() {
5823 assert_eq!(
5824 negated_comparison(ComparisonComputation::LessThan),
5825 ComparisonComputation::GreaterThanOrEqual
5826 );
5827 assert_eq!(
5828 negated_comparison(ComparisonComputation::GreaterThanOrEqual),
5829 ComparisonComputation::LessThan
5830 );
5831 assert_eq!(
5832 negated_comparison(ComparisonComputation::Is),
5833 ComparisonComputation::IsNot
5834 );
5835 assert_eq!(
5836 negated_comparison(ComparisonComputation::IsNot),
5837 ComparisonComputation::Is
5838 );
5839 }
5840
5841 #[test]
5842 fn value_to_semantic_number_is_decimal() {
5843 let kind = value_to_semantic(&Value::Number(Decimal::from(42))).unwrap();
5844 assert!(matches!(kind, ValueKind::Number(d) if d == rational_new(42, 1)));
5845 }
5846
5847 #[test]
5848 fn value_kind_measure_serializes_magnitude_only() {
5849 let kind =
5850 ValueKind::Measure(decimal_to_rational(Decimal::from_str("99.50").unwrap()).unwrap());
5851 let json = serde_json::to_value(crate::api::ValueKind::from(&kind)).unwrap();
5852 assert_eq!(json["measure"]["value"], "99.5");
5853 assert!(json["measure"].get("signature").is_none());
5854 }
5855
5856 #[test]
5857 fn value_kind_measure_roundtrips() {
5858 let original =
5859 ValueKind::Measure(decimal_to_rational(Decimal::from_str("4800").unwrap()).unwrap());
5860 let api = crate::api::ValueKind::from(&original);
5861 let json = serde_json::to_string(&api).unwrap();
5862 let parsed: crate::api::ValueKind = serde_json::from_str(&json).unwrap();
5863 assert_eq!(api, parsed);
5864 }
5865
5866 #[test]
5867 fn value_kind_measure_empty_roundtrips() {
5868 let original =
5869 ValueKind::Measure(decimal_to_rational(Decimal::from_str("12.5").unwrap()).unwrap());
5870 let api = crate::api::ValueKind::from(&original);
5871 let json = serde_json::to_string(&api).unwrap();
5872 let parsed: crate::api::ValueKind = serde_json::from_str(&json).unwrap();
5873 assert_eq!(api, parsed);
5874 }
5875
5876 #[test]
5877 fn literal_value_number_serde_not_rational_array() {
5878 let lit = LiteralValue::number_from_decimal(Decimal::from(20));
5879 let json = serde_json::to_value(crate::api::LiteralValue::from(&lit)).unwrap();
5880 let number = json
5881 .get("value")
5882 .and_then(|v| v.get("number"))
5883 .expect("number field");
5884 assert!(number.is_string());
5885 assert_eq!(number.as_str(), Some("20"));
5886 assert!(
5887 !number.is_array(),
5888 "stored number must not serialize as [n,d]"
5889 );
5890 }
5891
5892 #[test]
5893 fn test_literal_value_to_primitive_type() {
5894 let one = rational_new(1, 1);
5895
5896 assert_eq!(TypedLiteral::text("".to_string()).lemma_type.name(), "text");
5897 assert_eq!(
5898 TypedLiteral::number(one.clone()).lemma_type.name(),
5899 "number"
5900 );
5901 assert_eq!(
5902 TypedLiteral::from_bool(bool::from(BooleanValue::True))
5903 .lemma_type
5904 .name(),
5905 "boolean"
5906 );
5907
5908 let dt = DateTimeValue {
5909 year: 2024,
5910 month: 1,
5911 day: 1,
5912 hour: 0,
5913 minute: 0,
5914 second: 0,
5915 microsecond: 0,
5916 timezone: None,
5917
5918 granularity: DateGranularity::Full,
5919 };
5920 assert_eq!(
5921 TypedLiteral::date(date_time_to_semantic(&dt))
5922 .lemma_type
5923 .name(),
5924 "date"
5925 );
5926 assert_eq!(
5927 TypedLiteral::ratio_from_decimal(Decimal::new(1, 2))
5928 .lemma_type
5929 .name(),
5930 "ratio"
5931 );
5932 let dur_type = LemmaType::new(
5933 "duration".to_string(),
5934 TypeSpecification::Measure {
5935 minimum: None,
5936 maximum: None,
5937 decimals: None,
5938 units: MeasureUnits::from(vec![MeasureUnit {
5939 name: "second".to_string(),
5940 factor: crate::computation::rational::rational_one(),
5941 derived_measure_factors: Vec::new(),
5942 decomposition: BaseMeasureVector::new(),
5943 minimum: None,
5944 maximum: None,
5945 suggestion_magnitude: None,
5946 }]),
5947 traits: vec![MeasureTrait::Duration],
5948 decomposition: None,
5949 help: String::new(),
5950 },
5951 TypeExtends::Primitive,
5952 );
5953 assert_eq!(
5954 TypedLiteral::measure_with_type(one.clone(), Arc::new(dur_type))
5955 .lemma_type
5956 .name(),
5957 "duration"
5958 );
5959 }
5960
5961 #[test]
5962 fn test_type_display() {
5963 let specs = TypeSpecification::text();
5964 let lemma_type = LemmaType::new("name".to_string(), specs, TypeExtends::Primitive);
5965 assert_eq!(format!("{}", lemma_type), "name");
5966 }
5967
5968 #[test]
5969 fn test_type_serialization() {
5970 let specs = TypeSpecification::number();
5971 let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
5972 let api = crate::api::LemmaType::from(&lemma_type);
5973 let serialized = serde_json::to_string(&api).unwrap();
5974 let deserialized: crate::api::LemmaType = serde_json::from_str(&serialized).unwrap();
5975 assert_eq!(api, deserialized);
5976 }
5977
5978 #[test]
5979 fn test_literal_value_display_value() {
5980 let ten = rational_new(10, 1);
5981
5982 assert_eq!(
5983 LiteralValue::text("hello".to_string()).display_value(),
5984 "hello"
5985 );
5986 assert_eq!(LiteralValue::number(ten).display_value(), "10");
5987 assert_eq!(LiteralValue::from_bool(true).display_value(), "true");
5988 assert_eq!(LiteralValue::from_bool(false).display_value(), "false");
5989
5990 let ratio_ty = Arc::new(
5992 primitive_ratio_arc()
5993 .as_ref()
5994 .clone()
5995 .with_measure_binding_unit("percent"),
5996 );
5997 let ten_percent_ratio = LiteralValue::ratio(
5998 crate::literals::rational_from_parsed_decimal(Decimal::new(1, 1))
5999 .expect("ratio decimal"),
6000 );
6001 assert_eq!(
6002 ten_percent_ratio.display_value_with_type(ratio_ty.as_ref()),
6003 "10%"
6004 );
6005
6006 let time = TimeValue {
6007 hour: 14,
6008 minute: 30,
6009 second: 0,
6010 microsecond: 0,
6011 timezone: None,
6012 };
6013 let time_display = LiteralValue::time(time_to_semantic(&time)).display_value();
6014 assert!(time_display.contains("14"));
6015 assert!(time_display.contains("30"));
6016 }
6017
6018 #[test]
6019 fn test_measure_display_respects_type_decimals() {
6020 let money_type = LemmaType {
6021 name: Some("money".to_string()),
6022 specifications: TypeSpecification::Measure {
6023 minimum: None,
6024 maximum: None,
6025 decimals: Some(2),
6026 units: MeasureUnits::from(vec![MeasureUnit {
6027 name: "eur".to_string(),
6028 factor: crate::computation::rational::rational_one(),
6029 derived_measure_factors: Vec::new(),
6030 decomposition: BaseMeasureVector::new(),
6031 minimum: None,
6032 maximum: None,
6033 suggestion_magnitude: None,
6034 }]),
6035 traits: Vec::new(),
6036 decomposition: None,
6037 help: String::new(),
6038 },
6039 extends: TypeExtends::Primitive,
6040 measure_binding_unit: None,
6041 };
6042 let money_type = Arc::new(money_type);
6043 let val = LiteralValue::measure_with_type(
6044 decimal_to_rational(Decimal::from_str("1.8").unwrap()).unwrap(),
6045 money_type.clone(),
6046 );
6047 assert_eq!(val.display_value_with_type(money_type.as_ref()), "1.80 eur");
6048 let more_precision = LiteralValue::measure_with_type(
6049 decimal_to_rational(Decimal::from_str("1.80000").unwrap()).unwrap(),
6050 money_type.clone(),
6051 );
6052 assert_eq!(
6053 more_precision.display_value_with_type(money_type.as_ref()),
6054 "1.80 eur"
6055 );
6056 let measure_no_decimals = Arc::new(LemmaType {
6057 name: Some("count".to_string()),
6058 specifications: TypeSpecification::Measure {
6059 minimum: None,
6060 maximum: None,
6061 decimals: None,
6062 units: MeasureUnits::from(vec![MeasureUnit {
6063 name: "items".to_string(),
6064 factor: crate::computation::rational::rational_one(),
6065 derived_measure_factors: Vec::new(),
6066 decomposition: BaseMeasureVector::new(),
6067 minimum: None,
6068 maximum: None,
6069 suggestion_magnitude: None,
6070 }]),
6071 traits: Vec::new(),
6072 decomposition: None,
6073 help: String::new(),
6074 },
6075 extends: TypeExtends::Primitive,
6076 measure_binding_unit: None,
6077 });
6078 let val_any = LiteralValue::measure_with_type(
6079 decimal_to_rational(Decimal::from_str("42.50").unwrap()).unwrap(),
6080 Arc::clone(&measure_no_decimals),
6081 );
6082 assert_eq!(
6083 val_any.display_value_with_type(measure_no_decimals.as_ref()),
6084 "42.5 items"
6085 );
6086 }
6087
6088 #[test]
6089 fn test_literal_value_time_type() {
6090 let time = TimeValue {
6091 hour: 14,
6092 minute: 30,
6093 second: 0,
6094 microsecond: 0,
6095 timezone: None,
6096 };
6097 let lit = TypedLiteral::time(time_to_semantic(&time));
6098 assert_eq!(lit.lemma_type.name(), "time");
6099 }
6100
6101 #[test]
6102 fn test_measure_family_name_primitive_root() {
6103 let measure_spec = TypeSpecification::measure();
6104 let money_primitive = LemmaType::new(
6105 "money".to_string(),
6106 measure_spec.clone(),
6107 TypeExtends::Primitive,
6108 );
6109 assert_eq!(money_primitive.measure_family_name(), Some("money"));
6110 }
6111
6112 #[test]
6113 fn test_measure_family_name_custom() {
6114 let measure_spec = TypeSpecification::measure();
6115 let money_custom = LemmaType::new(
6116 "money".to_string(),
6117 measure_spec,
6118 TypeExtends::custom_local("money".to_string(), "money".to_string()),
6119 );
6120 assert_eq!(money_custom.measure_family_name(), Some("money"));
6121 }
6122
6123 #[test]
6124 fn test_same_measure_family_same_name_different_extends() {
6125 let measure_spec = TypeSpecification::measure();
6126 let money_primitive = LemmaType::new(
6127 "money".to_string(),
6128 measure_spec.clone(),
6129 TypeExtends::Primitive,
6130 );
6131 let money_custom = LemmaType::new(
6132 "money".to_string(),
6133 measure_spec,
6134 TypeExtends::custom_local("money".to_string(), "money".to_string()),
6135 );
6136 assert!(money_primitive.same_measure_family(&money_custom));
6137 assert!(money_custom.same_measure_family(&money_primitive));
6138 }
6139
6140 #[test]
6141 fn test_same_measure_family_parent_and_child() {
6142 let measure_spec = TypeSpecification::measure();
6143 let type_x = LemmaType::new(
6144 "x".to_string(),
6145 measure_spec.clone(),
6146 TypeExtends::Primitive,
6147 );
6148 let type_x2 = LemmaType::new(
6149 "x2".to_string(),
6150 measure_spec,
6151 TypeExtends::custom_local("x".to_string(), "x".to_string()),
6152 );
6153 assert_eq!(type_x.measure_family_name(), Some("x"));
6154 assert_eq!(type_x2.measure_family_name(), Some("x"));
6155 assert!(type_x.same_measure_family(&type_x2));
6156 assert!(type_x2.same_measure_family(&type_x));
6157 }
6158
6159 #[test]
6160 fn test_same_measure_family_siblings() {
6161 let measure_spec = TypeSpecification::measure();
6162 let type_x2_a = LemmaType::new(
6163 "x2a".to_string(),
6164 measure_spec.clone(),
6165 TypeExtends::custom_local("x".to_string(), "x".to_string()),
6166 );
6167 let type_x2_b = LemmaType::new(
6168 "x2b".to_string(),
6169 measure_spec,
6170 TypeExtends::custom_local("x".to_string(), "x".to_string()),
6171 );
6172 assert!(type_x2_a.same_measure_family(&type_x2_b));
6173 }
6174
6175 #[test]
6176 fn test_same_measure_family_different_families() {
6177 let measure_spec = TypeSpecification::measure();
6178 let money = LemmaType::new(
6179 "money".to_string(),
6180 measure_spec.clone(),
6181 TypeExtends::Primitive,
6182 );
6183 let temperature = LemmaType::new(
6184 "temperature".to_string(),
6185 measure_spec,
6186 TypeExtends::Primitive,
6187 );
6188 assert!(!money.same_measure_family(&temperature));
6189 assert!(!temperature.same_measure_family(&money));
6190 }
6191
6192 #[test]
6193 fn test_same_measure_family_measure_vs_non_measure() {
6194 let measure_spec = TypeSpecification::measure();
6195 let number_spec = TypeSpecification::number();
6196 let measure_type =
6197 LemmaType::new("money".to_string(), measure_spec, TypeExtends::Primitive);
6198 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
6199 assert!(!measure_type.same_measure_family(&number_type));
6200 assert!(!number_type.same_measure_family(&measure_type));
6201 }
6202
6203 #[test]
6204 fn test_same_measure_family_anonymous_measures_are_not_family_compatible() {
6205 let left = LemmaType::anonymous_for_decomposition(duration_decomposition());
6206 let right = LemmaType::anonymous_for_decomposition(duration_decomposition());
6207
6208 assert!(!left.same_measure_family(&right));
6209 assert!(left.compatible_with_anonymous_measure(&right));
6210 }
6211
6212 #[test]
6213 fn test_measure_family_name_non_measure_returns_none() {
6214 let number_spec = TypeSpecification::number();
6215 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
6216 assert_eq!(number_type.measure_family_name(), None);
6217 }
6218
6219 #[test]
6220 fn test_lemma_type_inequality_local_vs_import_same_shape() {
6221 let measure_spec = TypeSpecification::measure();
6222 let local = LemmaType::new(
6223 "t".to_string(),
6224 measure_spec.clone(),
6225 TypeExtends::custom_local("money".to_string(), "money".to_string()),
6226 );
6227 let imported = LemmaType::new(
6228 "t".to_string(),
6229 measure_spec,
6230 TypeExtends::Custom {
6231 parent: "money".to_string(),
6232 family: "money".to_string(),
6233 defining_spec: TypeDefiningSpec::Import,
6234 },
6235 );
6236 assert_ne!(local, imported);
6237 }
6238
6239 #[test]
6240 fn test_lemma_type_equality_import_unit_variant() {
6241 let measure_spec = TypeSpecification::measure();
6242 let left = LemmaType::new(
6243 "t".to_string(),
6244 measure_spec.clone(),
6245 TypeExtends::Custom {
6246 parent: "money".to_string(),
6247 family: "money".to_string(),
6248 defining_spec: TypeDefiningSpec::Import,
6249 },
6250 );
6251 let right = LemmaType::new(
6252 "t".to_string(),
6253 measure_spec,
6254 TypeExtends::Custom {
6255 parent: "money".to_string(),
6256 family: "money".to_string(),
6257 defining_spec: TypeDefiningSpec::Import,
6258 },
6259 );
6260 assert_eq!(left, right);
6261 }
6262
6263 fn month_suggestion_arg() -> CommandArg {
6264 CommandArg::Literal(crate::literals::Value::NumberWithUnit(
6265 Decimal::ONE,
6266 "month".to_string(),
6267 ))
6268 }
6269
6270 fn unit_factor_arg(name: &str, factor: i64) -> [CommandArg; 2] {
6271 [
6272 CommandArg::Label(name.to_string()),
6273 CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::from(factor))),
6274 ]
6275 }
6276
6277 #[test]
6278 fn default_calendar_on_text_reports_hint() {
6279 let mut specs = TypeSpecification::text();
6280 let mut default = None;
6281 let err = specs
6282 .apply_constraint(
6283 "notes",
6284 TypeConstraintCommand::Suggest,
6285 &[month_suggestion_arg()],
6286 &mut default,
6287 &mut None,
6288 )
6289 .unwrap_err();
6290 assert!(err.contains("Unit 'month' is for calendar data"));
6291 assert!(err.contains("double quotes"));
6292 }
6293
6294 #[test]
6295 fn default_calendar_on_duration_reports_valid_units() {
6296 let mut specs = TypeSpecification::measure();
6297 specs
6298 .apply_constraint(
6299 "duration",
6300 TypeConstraintCommand::Unit,
6301 &unit_factor_arg("second", 1),
6302 &mut None,
6303 &mut None,
6304 )
6305 .unwrap();
6306 specs
6307 .apply_constraint(
6308 "duration",
6309 TypeConstraintCommand::Unit,
6310 &unit_factor_arg("week", 604_800),
6311 &mut None,
6312 &mut None,
6313 )
6314 .unwrap();
6315 specs
6316 .apply_constraint(
6317 "duration",
6318 TypeConstraintCommand::Trait,
6319 &[CommandArg::Label("duration".to_string())],
6320 &mut None,
6321 &mut None,
6322 )
6323 .unwrap();
6324 let mut default = None;
6325 let err = specs
6326 .apply_constraint(
6327 "duration",
6328 TypeConstraintCommand::Suggest,
6329 &[month_suggestion_arg()],
6330 &mut default,
6331 &mut None,
6332 )
6333 .unwrap_err();
6334 assert!(err.contains("Unit 'month' is for calendar data"));
6335 assert!(err.contains("Valid 'duration' units are"));
6336 assert!(err.contains("week"));
6337 }
6338
6339 #[test]
6340 fn default_valid_duration_weeks_accepted() {
6341 let mut specs = TypeSpecification::measure();
6342 specs
6343 .apply_constraint(
6344 "duration",
6345 TypeConstraintCommand::Unit,
6346 &unit_factor_arg("second", 1),
6347 &mut None,
6348 &mut None,
6349 )
6350 .unwrap();
6351 specs
6352 .apply_constraint(
6353 "duration",
6354 TypeConstraintCommand::Unit,
6355 &unit_factor_arg("week", 604_800),
6356 &mut None,
6357 &mut None,
6358 )
6359 .unwrap();
6360 specs
6361 .apply_constraint(
6362 "duration",
6363 TypeConstraintCommand::Trait,
6364 &[CommandArg::Label("duration".to_string())],
6365 &mut None,
6366 &mut None,
6367 )
6368 .unwrap();
6369 let mut default = None;
6370 specs
6371 .apply_constraint(
6372 "duration",
6373 TypeConstraintCommand::Suggest,
6374 &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
6375 Decimal::from(4),
6376 "week".to_string(),
6377 ))],
6378 &mut default,
6379 &mut None,
6380 )
6381 .unwrap();
6382 assert!(matches!(
6383 default,
6384 Some(RawSuggestion::Measure {
6385 unit_name,
6386 ..
6387 }) if unit_name == "week"
6388 ));
6389 }
6390
6391 #[test]
6392 fn default_unknown_unit_on_duration_lists_valid_units() {
6393 let mut specs = TypeSpecification::measure();
6394 specs
6395 .apply_constraint(
6396 "duration",
6397 TypeConstraintCommand::Unit,
6398 &unit_factor_arg("second", 1),
6399 &mut None,
6400 &mut None,
6401 )
6402 .unwrap();
6403 specs
6404 .apply_constraint(
6405 "duration",
6406 TypeConstraintCommand::Trait,
6407 &[CommandArg::Label("duration".to_string())],
6408 &mut None,
6409 &mut None,
6410 )
6411 .unwrap();
6412 let mut default = None;
6413 let err = specs
6414 .apply_constraint(
6415 "duration",
6416 TypeConstraintCommand::Suggest,
6417 &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
6418 Decimal::ONE,
6419 "fortnight".to_string(),
6420 ))],
6421 &mut default,
6422 &mut None,
6423 )
6424 .unwrap_err();
6425 assert!(err.contains("fortnight"));
6426 assert!(err.contains("not defined on 'duration'"));
6427 assert!(err.contains("Valid units are"));
6428 }
6429
6430 fn money_measure_type() -> LemmaType {
6431 LemmaType::new(
6432 "Money".to_string(),
6433 TypeSpecification::Measure {
6434 minimum: None,
6435 maximum: None,
6436 decimals: None,
6437 units: MeasureUnits::from(vec![
6438 MeasureUnit {
6439 name: "eur".to_string(),
6440 factor: crate::computation::rational::rational_one(),
6441 derived_measure_factors: Vec::new(),
6442 decomposition: BaseMeasureVector::new(),
6443 minimum: None,
6444 maximum: None,
6445 suggestion_magnitude: None,
6446 },
6447 MeasureUnit {
6448 name: "usd".to_string(),
6449 factor: crate::computation::rational::decimal_to_rational(Decimal::new(
6450 91, 2,
6451 ))
6452 .expect("factor"),
6453 derived_measure_factors: Vec::new(),
6454 decomposition: BaseMeasureVector::new(),
6455 minimum: None,
6456 maximum: None,
6457 suggestion_magnitude: None,
6458 },
6459 ]),
6460 traits: Vec::new(),
6461 decomposition: None,
6462 help: String::new(),
6463 },
6464 TypeExtends::Primitive,
6465 )
6466 }
6467
6468 #[test]
6469 fn measure_unit_names_for_named_measure() {
6470 let money = money_measure_type();
6471 assert_eq!(money.measure_unit_names(), Some(vec!["eur", "usd"]));
6472 }
6473
6474 fn sig(pairs: &[(&str, i32)]) -> Vec<(String, i32)> {
6479 pairs.iter().map(|(s, e)| (s.to_string(), *e)).collect()
6480 }
6481
6482 #[test]
6483 fn combine_signatures_multiply_adds_exponents() {
6484 let left = sig(&[("eur", 1)]);
6485 let right = sig(&[("hour", -1)]);
6486 let result = combine_signatures(&left, &right, true);
6487 assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
6488 }
6489
6490 #[test]
6491 fn combine_signatures_divide_subtracts_exponents() {
6492 let left = sig(&[("eur", 1)]);
6493 let right = sig(&[("hour", 1)]);
6494 let result = combine_signatures(&left, &right, false);
6495 assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
6496 }
6497
6498 #[test]
6499 fn combine_signatures_cancels_to_empty() {
6500 let left = sig(&[("ce", 1), ("minute", -1)]);
6501 let right = sig(&[("minute", 1)]);
6502 let result = combine_signatures(&left, &right, true);
6503 assert_eq!(result, sig(&[("ce", 1)]));
6505 }
6506
6507 #[test]
6508 fn combine_signatures_output_is_canonical_form() {
6509 let left = sig(&[("eur", 1), ("hour", 1)]);
6510 let right = sig(&[("minute", 1)]);
6511 let result = combine_signatures(&left, &right, false); let expected = sig(&[("eur", 1), ("hour", 1), ("minute", -1)]);
6514 assert_eq!(result, expected);
6515 }
6516
6517 #[test]
6518 fn canonicalize_signature_drops_zero_exponents() {
6519 let sig_with_zero = sig(&[("eur", 1), ("hour", 0), ("minute", -1)]);
6520 let result = canonicalize_signature(&sig_with_zero);
6521 assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
6522 }
6523
6524 #[test]
6525 fn canonicalize_signature_sorts_by_name() {
6526 let unsorted = sig(&[("minute", -1), ("eur", 1)]);
6527 let result = canonicalize_signature(&unsorted);
6528 assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
6529 }
6530
6531 #[test]
6537 fn format_signature_operator_style_numerator_only() {
6538 let signature = sig(&[("eur", 1)]);
6539 let result = format_signature_operator_style(&signature);
6540 assert_eq!(result, "eur");
6541 }
6542
6543 #[test]
6544 fn format_signature_operator_style_with_denominator() {
6545 let signature = sig(&[("eur", 1), ("hour", -1)]);
6546 let result = format_signature_operator_style(&signature);
6547 assert_eq!(result, "eur/hour");
6548 }
6549
6550 #[test]
6551 fn format_signature_operator_style_denominator_only() {
6552 let signature = sig(&[("meter", -1)]);
6553 let result = format_signature_operator_style(&signature);
6554 assert_eq!(result, "1/meter");
6555 }
6556
6557 #[test]
6558 fn format_signature_operator_style_with_exponents() {
6559 let signature = sig(&[("meter", 2), ("second", -2)]);
6560 let result = format_signature_operator_style(&signature);
6561 assert_eq!(result, "meter^2/second^2");
6562 }
6563
6564 #[test]
6569 fn calendar_unit_factor_table_completeness() {
6570 for unit in &[SemanticCalendarUnit::Month, SemanticCalendarUnit::Year] {
6573 let name = unit.to_string();
6574 assert!(
6575 calendar_unit_factor(&name).is_some(),
6576 "calendar_unit_factor('{}') must return Some",
6577 name
6578 );
6579 }
6580 }
6581
6582 #[test]
6583 fn semantic_calendar_unit_display_returns_singular() {
6584 assert_eq!(SemanticCalendarUnit::Month.to_string(), "month");
6587 assert_eq!(SemanticCalendarUnit::Year.to_string(), "year");
6588 }
6589
6590 #[test]
6595 fn signature_factor_with_calendar_units() {
6596 let calendar = test_calendar_type_for_signature_factor();
6597 let unit_index = crate::planning::unit_index::UnitIndex::new();
6598 let sig_month_per_year = sig(&[("month", 1), ("year", -1)]);
6601 let factor = signature_factor(&sig_month_per_year, &unit_index, Some(&calendar))
6602 .expect("must not overflow");
6603 let expected = rational_new(1, 12);
6604 assert_eq!(factor, expected, "month/year factor must be 1/12");
6605 }
6606
6607 fn test_calendar_type_for_signature_factor() -> LemmaType {
6608 use crate::computation::rational::{decimal_to_rational, rational_one};
6609 use crate::literals::{MeasureUnit, MeasureUnits};
6610 use rust_decimal::Decimal;
6611 LemmaType::new(
6612 "calendar".to_string(),
6613 TypeSpecification::Measure {
6614 minimum: None,
6615 maximum: None,
6616 decimals: None,
6617 units: MeasureUnits::from(vec![
6618 MeasureUnit {
6619 name: "month".to_string(),
6620 factor: rational_one(),
6621 minimum: None,
6622 maximum: None,
6623 suggestion_magnitude: None,
6624 decomposition: calendar_decomposition(),
6625 derived_measure_factors: Vec::new(),
6626 },
6627 MeasureUnit {
6628 name: "year".to_string(),
6629 factor: decimal_to_rational(Decimal::from(12)).expect("year factor"),
6630 minimum: None,
6631 maximum: None,
6632 suggestion_magnitude: None,
6633 decomposition: calendar_decomposition(),
6634 derived_measure_factors: Vec::new(),
6635 },
6636 ]),
6637 traits: vec![MeasureTrait::Calendar],
6638 decomposition: Some(calendar_decomposition()),
6639 help: String::new(),
6640 },
6641 TypeExtends::Primitive,
6642 )
6643 }
6644
6645 #[test]
6646 #[should_panic(expected = "BUG: signature_factor called with unresolved unit name")]
6647 fn signature_factor_panics_on_unresolved_name() {
6648 let unit_index = crate::planning::unit_index::UnitIndex::new();
6649 let bad_sig = sig(&[("nonexistent_unit_xyz", 1)]);
6650 let _ = signature_factor(&bad_sig, &unit_index, None);
6651 }
6652
6653 #[test]
6654 fn signature_factor_uses_owner_when_expression_index_empty() {
6655 let money = test_money_type_for_signature_factor();
6656 let expression_units = crate::planning::unit_index::UnitIndex::new();
6657 let sig_usd = sig(&[("usd", 1)]);
6658 let factor =
6659 signature_factor(&sig_usd, &expression_units, Some(&money)).expect("must not overflow");
6660 assert_eq!(factor, rational_new(91, 100));
6661 }
6662
6663 fn test_money_type_for_signature_factor() -> LemmaType {
6664 use crate::computation::rational::decimal_to_rational;
6665 use crate::literals::{MeasureUnit, MeasureUnits};
6666 use rust_decimal::Decimal;
6667 LemmaType::new(
6668 "money".to_string(),
6669 TypeSpecification::Measure {
6670 minimum: None,
6671 maximum: None,
6672 decimals: Some(2),
6673 units: MeasureUnits::from(vec![
6674 MeasureUnit {
6675 name: "eur".to_string(),
6676 factor: crate::computation::rational::rational_one(),
6677 minimum: None,
6678 maximum: None,
6679 suggestion_magnitude: None,
6680 decomposition: BaseMeasureVector::new(),
6681 derived_measure_factors: Vec::new(),
6682 },
6683 MeasureUnit {
6684 name: "usd".to_string(),
6685 factor: decimal_to_rational(Decimal::new(91, 2)).expect("usd factor"),
6686 minimum: None,
6687 maximum: None,
6688 suggestion_magnitude: None,
6689 decomposition: BaseMeasureVector::new(),
6690 derived_measure_factors: Vec::new(),
6691 },
6692 ]),
6693 traits: Vec::new(),
6694 decomposition: None,
6695 help: String::new(),
6696 },
6697 TypeExtends::Primitive,
6698 )
6699 }
6700
6701 fn measure_type_with_kilogram() -> TypeSpecification {
6702 use crate::computation::rational::rational_one;
6703 use crate::literals::{MeasureUnit, MeasureUnits};
6704 let mut units = MeasureUnits::new();
6705 units.push(MeasureUnit {
6706 name: "kilogram".to_string(),
6707 factor: rational_one(),
6708 minimum: None,
6709 maximum: None,
6710 suggestion_magnitude: None,
6711 decomposition: BaseMeasureVector::new(),
6712 derived_measure_factors: Vec::new(),
6713 });
6714 TypeSpecification::Measure {
6715 minimum: None,
6716 maximum: None,
6717 decimals: None,
6718 units,
6719 traits: Vec::new(),
6720 decomposition: None,
6721 help: String::new(),
6722 }
6723 }
6724
6725 #[test]
6726 fn parser_value_to_value_kind_rejects_bare_number_for_measure() {
6727 let ten = Value::Number(Decimal::from(10));
6728 let err = parser_value_to_value_kind(&ten, &measure_type_with_kilogram())
6729 .expect_err("bare number must not bind to measure");
6730 assert!(
6731 err.contains("kilogram"),
6732 "error must hint expected unit, got: {err}"
6733 );
6734 }
6735
6736 #[test]
6737 fn parser_value_to_value_kind_accepts_number_with_unit_for_measure() {
6738 let ten_kg = Value::NumberWithUnit(Decimal::from(10), "kilogram".to_string());
6739 let kind = parser_value_to_value_kind(&ten_kg, &measure_type_with_kilogram())
6740 .expect("10 kilogram must bind to measure");
6741 assert!(matches!(kind, ValueKind::Measure(_)));
6742 }
6743
6744 #[test]
6745 fn parser_value_to_value_kind_accepts_bare_number_for_ratio() {
6746 let ten = Value::Number(Decimal::from(10));
6747 let kind =
6748 parser_value_to_value_kind(&ten, &TypeSpecification::ratio()).expect("number -> ratio");
6749 assert!(matches!(kind, ValueKind::Ratio(_)));
6750 }
6751
6752 #[test]
6753 fn value_kind_matches_spec_rejects_number_for_measure() {
6754 let n = ValueKind::Number(rational_new(10, 1));
6755 assert!(!value_kind_matches_spec(&n, &measure_type_with_kilogram()));
6756 }
6757
6758 #[test]
6759 fn apply_constraint_rejects_inherited_unit_factor_change() {
6760 let mut specs = TypeSpecification::measure();
6761 specs
6762 .apply_constraint(
6763 "money",
6764 TypeConstraintCommand::Unit,
6765 &unit_factor_arg("eur", 1),
6766 &mut None,
6767 &mut None,
6768 )
6769 .expect("seed eur");
6770 let err = specs
6771 .apply_constraint(
6772 "money",
6773 TypeConstraintCommand::Unit,
6774 &[
6775 CommandArg::Label("eur".to_string()),
6776 CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::new(11, 1))),
6777 ],
6778 &mut None,
6779 &mut None,
6780 )
6781 .expect_err("must not change inherited unit factor");
6782 assert!(err.contains("eur"), "error must name unit, got: {err}");
6783 assert!(
6784 err.contains("inherited") || err.contains("cannot change"),
6785 "error must reject factor change, got: {err}"
6786 );
6787 }
6788
6789 #[test]
6790 fn apply_constraint_allows_additive_unit_on_inherited_spec() {
6791 let mut specs = TypeSpecification::measure();
6792 specs
6793 .apply_constraint(
6794 "money",
6795 TypeConstraintCommand::Unit,
6796 &unit_factor_arg("eur", 1),
6797 &mut None,
6798 &mut None,
6799 )
6800 .expect("seed eur");
6801 specs
6802 .apply_constraint(
6803 "money",
6804 TypeConstraintCommand::Unit,
6805 &unit_factor_arg("usd", 1),
6806 &mut None,
6807 &mut None,
6808 )
6809 .expect("add usd");
6810 match &specs {
6811 TypeSpecification::Measure { units, .. } => assert_eq!(units.len(), 2),
6812 other => panic!("expected Measure, got {other:?}"),
6813 }
6814 }
6815
6816 #[test]
6817 fn apply_constraint_idempotent_inherited_unit_redeclare() {
6818 let mut specs = TypeSpecification::measure();
6819 specs
6820 .apply_constraint(
6821 "money",
6822 TypeConstraintCommand::Unit,
6823 &unit_factor_arg("eur", 1),
6824 &mut None,
6825 &mut None,
6826 )
6827 .expect("seed eur");
6828 specs
6829 .apply_constraint(
6830 "money",
6831 TypeConstraintCommand::Unit,
6832 &unit_factor_arg("eur", 1),
6833 &mut None,
6834 &mut None,
6835 )
6836 .expect("idempotent eur");
6837 match &specs {
6838 TypeSpecification::Measure { units, .. } => {
6839 assert_eq!(units.len(), 1);
6840 assert_eq!(
6841 units.iter().find(|u| u.name == "eur").expect("eur").factor,
6842 crate::computation::rational::rational_one()
6843 );
6844 }
6845 other => panic!("expected Measure, got {other:?}"),
6846 }
6847 }
6848
6849 #[test]
6850 fn element_from_range_returns_element_for_every_range_primitive() {
6851 type RangeElementMatcher = fn(&TypeSpecification) -> bool;
6852 let cases: [(PrimitiveKind, RangeElementMatcher); 5] = [
6853 (PrimitiveKind::NumberRange, |element| {
6854 matches!(element, TypeSpecification::Number { .. })
6855 }),
6856 (PrimitiveKind::MeasureRange, |element| {
6857 matches!(element, TypeSpecification::Measure { .. })
6858 }),
6859 (PrimitiveKind::RatioRange, |element| {
6860 matches!(element, TypeSpecification::Ratio { .. })
6861 }),
6862 (PrimitiveKind::DateRange, |element| {
6863 matches!(element, TypeSpecification::Date { .. })
6864 }),
6865 (PrimitiveKind::TimeRange, |element| {
6866 matches!(element, TypeSpecification::Time { .. })
6867 }),
6868 ];
6869 for (kind, matches_element) in cases {
6870 let range_spec = type_spec_for_primitive(kind);
6871 let element = range_spec
6872 .element_from_range()
6873 .unwrap_or_else(|| panic!("{kind:?} must define element_from_range"));
6874 assert!(
6875 matches_element(&element),
6876 "{kind:?} element must match documented mapping, got {element:?}"
6877 );
6878 }
6879 }
6880
6881 #[test]
6882 fn element_from_range_returns_none_for_non_range_primitives() {
6883 let non_range = [
6884 type_spec_for_primitive(PrimitiveKind::Boolean),
6885 type_spec_for_primitive(PrimitiveKind::Measure),
6886 TypeSpecification::Undetermined,
6887 TypeSpecification::veto(),
6888 ];
6889 for spec in non_range {
6890 assert!(
6891 spec.element_from_range().is_none(),
6892 "{spec:?} must not define element_from_range"
6893 );
6894 }
6895 }
6896}