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