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