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