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