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;
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 pub fn measure_unit_factor(
3997 &self,
3998 unit_name: &str,
3999 ) -> &crate::computation::rational::RationalInteger {
4000 let units = match &self.specifications {
4001 TypeSpecification::Measure { units, .. } => units,
4002 TypeSpecification::MeasureRange { units, .. } => units,
4003 _ => unreachable!(
4004 "BUG: measure_unit_factor called with non-measure type {}; only call during evaluation after planning validated measure conversion",
4005 self.name()
4006 ),
4007 };
4008 match units.get(unit_name) {
4009 Ok(MeasureUnit { factor, .. }) => factor,
4010 Err(_) => {
4011 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
4012 unreachable!(
4013 "BUG: unknown unit '{}' for measure type {} (valid: {}); planning must reject invalid conversions with Error",
4014 unit_name,
4015 self.name(),
4016 valid.join(", ")
4017 );
4018 }
4019 }
4020 }
4021
4022 pub fn ratio_unit_factor(
4023 &self,
4024 unit_name: &str,
4025 ) -> &crate::computation::rational::RationalInteger {
4026 let units = match &self.specifications {
4027 TypeSpecification::Ratio { units, .. } => units,
4028 _ => unreachable!(
4029 "BUG: ratio_unit_factor called with non-ratio type {}; only call during evaluation after planning validated ratio conversion",
4030 self.name()
4031 ),
4032 };
4033 match units.get(unit_name) {
4034 Ok(RatioUnit { value, .. }) => value,
4035 Err(_) => {
4036 let valid: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
4037 unreachable!(
4038 "BUG: unknown unit '{}' for ratio type {} (valid: {}); planning must reject invalid conversions with Error",
4039 unit_name,
4040 self.name(),
4041 valid.join(", ")
4042 );
4043 }
4044 }
4045 }
4046
4047 pub(crate) fn measure_literal_in_all_units(
4049 &self,
4050 literal: &LiteralValue,
4051 ) -> Result<BTreeMap<String, String>, LiteralUnitMapFailure> {
4052 use crate::computation::rational::checked_div;
4053
4054 let unit_names = self
4055 .measure_unit_names()
4056 .expect("BUG: measure literal in all units requires declared units");
4057 let ValueKind::Measure(magnitude, _signature) = &literal.value else {
4058 panic!("BUG: measure_literal_in_all_units called with non-measure value");
4059 };
4060 let mut map = BTreeMap::new();
4061 for unit_name in unit_names {
4062 let unit_factor = self.measure_unit_factor(unit_name);
4063 let magnitude_in_unit = checked_div(magnitude, unit_factor)
4064 .map_err(LiteralUnitMapFailure::UnitConversion)?;
4065 let decimal_string = self
4066 .try_rational_as_decimal_string(&magnitude_in_unit)
4067 .map_err(LiteralUnitMapFailure::Commit)?;
4068 map.insert(unit_name.to_string(), decimal_string);
4069 }
4070 Ok(map)
4071 }
4072
4073 pub(crate) fn ratio_literal_in_all_units(
4075 &self,
4076 literal: &LiteralValue,
4077 ) -> Result<BTreeMap<String, String>, LiteralUnitMapFailure> {
4078 use crate::computation::rational::checked_mul;
4079
4080 let ratio_api_type = match &self.specifications {
4081 TypeSpecification::Ratio { .. } => self,
4082 TypeSpecification::RatioRange { .. } => {
4083 let element = range_element_type_specification(&self.specifications)
4084 .expect("BUG: ratio range type must have ratio element specification");
4085 let TypeSpecification::Ratio {
4086 units, decimals, ..
4087 } = element
4088 else {
4089 panic!("BUG: ratio range element spec must be Ratio");
4090 };
4091 return LemmaType::primitive(TypeSpecification::Ratio {
4092 minimum: None,
4093 maximum: None,
4094 decimals,
4095 units,
4096 help: String::new(),
4097 })
4098 .ratio_literal_in_all_units(literal);
4099 }
4100 _ => {
4101 panic!(
4102 "BUG: ratio_literal_in_all_units called with non-ratio type {}",
4103 self.name()
4104 );
4105 }
4106 };
4107 let units = match &ratio_api_type.specifications {
4108 TypeSpecification::Ratio { units, .. } => units,
4109 _ => unreachable!("BUG: ratio API type must be Ratio"),
4110 };
4111 let ValueKind::Ratio(canonical, _) = &literal.value else {
4112 panic!("BUG: ratio_literal_in_all_units called with non-ratio value");
4113 };
4114 if units.is_empty() {
4115 panic!(
4116 "BUG: ratio literal type '{}' must have declared units",
4117 self.name()
4118 );
4119 }
4120 let mut map = BTreeMap::new();
4121 for unit in units.iter() {
4122 let magnitude_in_unit = checked_mul(canonical, &unit.value)
4123 .map_err(LiteralUnitMapFailure::UnitConversion)?;
4124 let decimal_string = ratio_api_type
4125 .try_rational_as_decimal_string(&magnitude_in_unit)
4126 .map_err(LiteralUnitMapFailure::Commit)?;
4127 map.insert(unit.name.clone(), decimal_string);
4128 }
4129 Ok(map)
4130 }
4131}
4132
4133#[derive(Debug, Clone, PartialEq, Eq)]
4135pub(crate) enum LiteralUnitMapFailure {
4136 Commit(crate::computation::rational::NumericFailure),
4137 UnitConversion(crate::computation::rational::NumericFailure),
4138}
4139
4140#[derive(Clone, Debug, PartialEq, Eq, Hash)]
4142pub struct LiteralValue {
4143 pub value: ValueKind,
4144 pub lemma_type: Arc<LemmaType>,
4145}
4146
4147impl LiteralValue {
4148 fn single_measure_signature_unit_name(signature: &[(String, i32)]) -> Option<&str> {
4149 match signature {
4150 [(unit_name, 1)] => Some(unit_name.as_str()),
4151 _ => None,
4152 }
4153 }
4154}
4155
4156impl Serialize for LiteralValue {
4157 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
4158 where
4159 S: serde::Serializer,
4160 {
4161 use serde::ser::SerializeStruct;
4162 let mut state = serializer.serialize_struct("LiteralValue", 3)?;
4163 state.serialize_field("value", &self.value)?;
4164 state.serialize_field("lemma_type", self.lemma_type.as_ref())?;
4165 state.serialize_field("display_value", &self.display_value())?;
4166 state.end()
4167 }
4168}
4169
4170impl<'de> Deserialize<'de> for LiteralValue {
4171 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4172 where
4173 D: serde::Deserializer<'de>,
4174 {
4175 #[derive(Deserialize)]
4176 struct Raw {
4177 value: ValueKind,
4178 lemma_type: LemmaType,
4179 }
4180 let raw = Raw::deserialize(deserializer)?;
4181 Ok(Self {
4182 value: raw.value,
4183 lemma_type: Arc::new(raw.lemma_type),
4184 })
4185 }
4186}
4187
4188impl LiteralValue {
4189 pub fn text(s: String) -> Self {
4190 Self {
4191 value: ValueKind::Text(s),
4192 lemma_type: primitive_text_arc().clone(),
4193 }
4194 }
4195
4196 pub fn text_with_type(s: String, lemma_type: Arc<LemmaType>) -> Self {
4197 Self {
4198 value: ValueKind::Text(s),
4199 lemma_type,
4200 }
4201 }
4202
4203 pub fn number(n: RationalInteger) -> Self {
4204 Self {
4205 value: ValueKind::Number(n),
4206 lemma_type: primitive_number_arc().clone(),
4207 }
4208 }
4209
4210 pub fn number_from_decimal(decimal: Decimal) -> Self {
4211 Self::number(
4212 crate::literals::rational_from_parsed_decimal(decimal)
4213 .expect("BUG: literal number from decimal must lift at boundary"),
4214 )
4215 }
4216
4217 pub fn number_with_type(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
4218 Self {
4219 value: ValueKind::Number(n),
4220 lemma_type,
4221 }
4222 }
4223
4224 pub fn number_with_type_from_decimal(decimal: Decimal, lemma_type: Arc<LemmaType>) -> Self {
4225 Self::number_with_type(
4226 crate::literals::rational_from_parsed_decimal(decimal)
4227 .expect("BUG: literal number from decimal must lift at boundary"),
4228 lemma_type,
4229 )
4230 }
4231
4232 pub fn measure_with_type(n: RationalInteger, unit: String, lemma_type: Arc<LemmaType>) -> Self {
4236 Self {
4237 value: ValueKind::Measure(n, vec![(unit, 1)]),
4238 lemma_type,
4239 }
4240 }
4241
4242 pub fn measure_with_signature(
4245 n: RationalInteger,
4246 signature: Vec<(String, i32)>,
4247 lemma_type: Arc<LemmaType>,
4248 ) -> Self {
4249 Self {
4250 value: ValueKind::Measure(n, signature),
4251 lemma_type,
4252 }
4253 }
4254
4255 pub fn number_interpreted_as_measure(value: RationalInteger, unit_name: String) -> Self {
4258 Self {
4259 value: ValueKind::Measure(value, vec![(unit_name, 1)]),
4260 lemma_type: Arc::new(anonymous_measure_type()),
4261 }
4262 }
4263
4264 pub fn from_bool(b: bool) -> Self {
4265 Self {
4266 value: ValueKind::Boolean(b),
4267 lemma_type: primitive_boolean_arc().clone(),
4268 }
4269 }
4270
4271 pub fn from_datetime(dt: &crate::parsing::ast::DateTimeValue) -> Self {
4272 Self::date(date_time_to_semantic(dt))
4273 }
4274
4275 #[must_use]
4277 pub fn magnitude_suggestion_for_decimal_prompt(&self) -> Option<String> {
4278 match &self.value {
4279 ValueKind::Number(n) => Some(
4280 self.lemma_type
4281 .try_rational_as_decimal_string(n)
4282 .expect("BUG: stored number literal must convert to decimal for prompt"),
4283 ),
4284 ValueKind::Measure(n, signature) => {
4285 let unit_name = Self::single_measure_signature_unit_name(signature).expect(
4286 "BUG: measure prompt requires exactly one signature unit with exponent 1",
4287 );
4288 Some(
4289 self.lemma_type
4290 .try_measure_canonical_as_decimal_in_unit(n, unit_name)
4291 .expect("BUG: stored measure literal must convert to decimal for prompt"),
4292 )
4293 }
4294 ValueKind::Ratio(n, Some(unit_name)) => Some(
4295 self.lemma_type
4296 .try_ratio_canonical_as_decimal_in_unit(n, unit_name)
4297 .expect("BUG: stored ratio literal must convert to decimal for prompt"),
4298 ),
4299 ValueKind::Ratio(n, None) => Some(
4300 self.lemma_type
4301 .try_rational_as_decimal_string(n)
4302 .expect("BUG: stored bare ratio literal must convert to decimal for prompt"),
4303 ),
4304 _ => None,
4305 }
4306 }
4307
4308 #[must_use]
4310 pub fn measure_units(&self) -> Option<BTreeMap<String, String>> {
4311 if !matches!(self.value, ValueKind::Measure(_, _)) {
4312 return None;
4313 }
4314 self.lemma_type.measure_unit_names()?;
4315 self.lemma_type.measure_literal_in_all_units(self).ok()
4316 }
4317
4318 #[must_use]
4320 pub fn ratio_units(&self) -> Option<BTreeMap<String, String>> {
4321 if !matches!(self.value, ValueKind::Ratio(_, _)) {
4322 return None;
4323 }
4324 let has_declared_units = match &self.lemma_type.specifications {
4325 TypeSpecification::Ratio { units, .. } => !units.is_empty(),
4326 TypeSpecification::RatioRange { .. } => true,
4327 _ => return None,
4328 };
4329 if !has_declared_units {
4330 return None;
4331 }
4332 self.lemma_type.ratio_literal_in_all_units(self).ok()
4333 }
4334
4335 #[must_use]
4337 pub fn magnitude_in_unit(&self, unit: &str) -> Option<String> {
4338 self.measure_units()
4339 .and_then(|map| map.get(unit).cloned())
4340 .or_else(|| self.ratio_units().and_then(|map| map.get(unit).cloned()))
4341 }
4342
4343 pub fn date(dt: SemanticDateTime) -> Self {
4344 Self {
4345 value: ValueKind::Date(dt),
4346 lemma_type: primitive_date_arc().clone(),
4347 }
4348 }
4349
4350 pub fn date_with_type(dt: SemanticDateTime, lemma_type: Arc<LemmaType>) -> Self {
4351 Self {
4352 value: ValueKind::Date(dt),
4353 lemma_type,
4354 }
4355 }
4356
4357 pub fn time(t: SemanticTime) -> Self {
4358 Self {
4359 value: ValueKind::Time(t),
4360 lemma_type: primitive_time_arc().clone(),
4361 }
4362 }
4363
4364 pub fn time_with_type(t: SemanticTime, lemma_type: Arc<LemmaType>) -> Self {
4365 Self {
4366 value: ValueKind::Time(t),
4367 lemma_type,
4368 }
4369 }
4370
4371 pub fn calendar(
4372 value: RationalInteger,
4373 unit: SemanticCalendarUnit,
4374 lemma_type: Arc<LemmaType>,
4375 ) -> Self {
4376 Self::measure_with_type(value, unit.to_string(), lemma_type)
4377 }
4378
4379 pub fn calendar_from_decimal(
4380 value: Decimal,
4381 unit: SemanticCalendarUnit,
4382 lemma_type: Arc<LemmaType>,
4383 ) -> Self {
4384 Self::calendar(
4385 crate::literals::rational_from_parsed_decimal(value)
4386 .expect("BUG: calendar literal from decimal must lift at boundary"),
4387 unit,
4388 lemma_type,
4389 )
4390 }
4391
4392 pub fn calendar_with_type(
4393 value: RationalInteger,
4394 unit: SemanticCalendarUnit,
4395 lemma_type: Arc<LemmaType>,
4396 ) -> Self {
4397 Self::calendar(value, unit, lemma_type)
4398 }
4399
4400 pub fn duration_canonical_seconds(&self) -> RationalInteger {
4402 let ValueKind::Measure(magnitude, _) = &self.value else {
4403 unreachable!(
4404 "BUG: duration_canonical_seconds called with {:?}",
4405 self.value
4406 );
4407 };
4408 if !self.lemma_type.is_duration_like_measure() {
4409 unreachable!(
4410 "BUG: duration_canonical_seconds called with type {}",
4411 self.lemma_type.name()
4412 );
4413 }
4414 let factor = self.lemma_type.measure_unit_factor("second");
4415 checked_div(magnitude, factor).expect("BUG: duration unit factor cannot be zero")
4416 }
4417
4418 pub fn calendar_canonical_months(&self) -> RationalInteger {
4420 let ValueKind::Measure(magnitude, _) = &self.value else {
4421 unreachable!(
4422 "BUG: calendar_canonical_months called with {:?}",
4423 self.value
4424 );
4425 };
4426 if !self.lemma_type.is_calendar_like() {
4427 unreachable!(
4428 "BUG: calendar_canonical_months called with type {}",
4429 self.lemma_type.name()
4430 );
4431 }
4432 let factor = self.lemma_type.measure_unit_factor("month");
4433 checked_div(magnitude, factor).expect("BUG: calendar unit factor cannot be zero")
4434 }
4435
4436 pub fn ratio(r: RationalInteger, unit: Option<String>) -> Self {
4437 Self {
4438 value: ValueKind::Ratio(r, unit),
4439 lemma_type: primitive_ratio_arc().clone(),
4440 }
4441 }
4442
4443 pub fn ratio_from_decimal(r: Decimal, unit: Option<String>) -> Self {
4444 Self::ratio(
4445 crate::literals::rational_from_parsed_decimal(r)
4446 .expect("BUG: ratio literal from decimal must lift at boundary"),
4447 unit,
4448 )
4449 }
4450
4451 pub fn ratio_with_type(
4452 r: RationalInteger,
4453 unit: Option<String>,
4454 lemma_type: Arc<LemmaType>,
4455 ) -> Self {
4456 Self {
4457 value: ValueKind::Ratio(r, unit),
4458 lemma_type,
4459 }
4460 }
4461
4462 pub fn range(left: LiteralValue, right: LiteralValue) -> Self {
4463 let specifications =
4464 range_type_specification_from_endpoints(&left.lemma_type, &right.lemma_type)
4465 .unwrap_or_else(|| {
4466 unreachable!(
4467 "BUG: attempted to construct a range literal from incompatible endpoint types"
4468 )
4469 });
4470
4471 Self {
4472 value: ValueKind::Range(Box::new(left), Box::new(right)),
4473 lemma_type: Arc::new(LemmaType::primitive(specifications)),
4474 }
4475 }
4476
4477 pub fn display_value(&self) -> String {
4479 format!("{}", self)
4480 }
4481
4482 pub fn byte_size(&self) -> usize {
4484 format!("{}", self).len()
4485 }
4486
4487 pub fn get_type(&self) -> &LemmaType {
4489 &self.lemma_type
4490 }
4491}
4492
4493#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
4496#[serde(rename_all = "snake_case", tag = "kind")]
4497pub enum ReferenceTarget {
4498 Data(DataPath),
4499 Rule(RulePath),
4500}
4501
4502#[derive(Clone, Debug, Serialize, Deserialize)]
4504#[serde(rename_all = "snake_case")]
4505pub enum DataDefinition {
4506 Value { value: LiteralValue, source: Source },
4508 TypeDeclaration {
4514 resolved_type: Arc<LemmaType>,
4515 declared_suggestion: Option<ValueKind>,
4516 source: Source,
4517 },
4518 Import { target_name: String, source: Source },
4520 Reference {
4545 target: ReferenceTarget,
4546 resolved_type: Arc<LemmaType>,
4547 local_constraints: Option<Vec<Constraint>>,
4548 local_suggestion: Option<ValueKind>,
4549 source: Source,
4550 },
4551}
4552
4553impl DataDefinition {
4554 pub fn lemma_type(&self) -> Option<&LemmaType> {
4556 match self {
4557 DataDefinition::Value { value, .. } => Some(value.lemma_type.as_ref()),
4558 DataDefinition::TypeDeclaration { resolved_type, .. } => Some(resolved_type.as_ref()),
4559 DataDefinition::Reference { resolved_type, .. } => Some(resolved_type.as_ref()),
4560 DataDefinition::Import { .. } => None,
4561 }
4562 }
4563
4564 #[inline]
4566 pub fn schema_type(&self) -> Option<&LemmaType> {
4567 self.lemma_type()
4568 }
4569
4570 pub fn value(&self) -> Option<&LiteralValue> {
4574 match self {
4575 DataDefinition::Value { value, .. } => Some(value),
4576 DataDefinition::TypeDeclaration { .. }
4577 | DataDefinition::Import { .. }
4578 | DataDefinition::Reference { .. } => None,
4579 }
4580 }
4581
4582 #[inline]
4585 pub fn prefilled_value(&self) -> Option<&LiteralValue> {
4586 self.value()
4587 }
4588
4589 pub fn suggestion(&self) -> Option<LiteralValue> {
4593 match self {
4594 DataDefinition::TypeDeclaration {
4595 resolved_type,
4596 declared_suggestion: Some(dv),
4597 ..
4598 } => Some(LiteralValue {
4599 value: dv.clone(),
4600 lemma_type: Arc::clone(resolved_type),
4601 }),
4602 DataDefinition::Reference {
4603 resolved_type,
4604 local_suggestion: Some(dv),
4605 ..
4606 } => Some(LiteralValue {
4607 value: dv.clone(),
4608 lemma_type: Arc::clone(resolved_type),
4609 }),
4610 DataDefinition::Value { .. }
4611 | DataDefinition::TypeDeclaration {
4612 declared_suggestion: None,
4613 ..
4614 }
4615 | DataDefinition::Reference {
4616 local_suggestion: None,
4617 ..
4618 }
4619 | DataDefinition::Import { .. } => None,
4620 }
4621 }
4622
4623 pub fn source(&self) -> &Source {
4625 match self {
4626 DataDefinition::Value { source, .. } => source,
4627 DataDefinition::TypeDeclaration { source, .. } => source,
4628 DataDefinition::Import { source, .. } => source,
4629 DataDefinition::Reference { source, .. } => source,
4630 }
4631 }
4632}
4633
4634pub fn number_with_unit_to_value_kind(
4636 magnitude: rust_decimal::Decimal,
4637 unit_name: &str,
4638 lemma_type: &LemmaType,
4639) -> Result<ValueKind, String> {
4640 match &lemma_type.specifications {
4641 TypeSpecification::Ratio { units, .. } => {
4642 use crate::computation::rational::{checked_div, decimal_to_rational};
4643 let unit = units.get(unit_name)?;
4644 let magnitude_rational = decimal_to_rational(magnitude)
4645 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
4646 let canonical_rational = checked_div(&magnitude_rational, &unit.value)
4647 .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
4648 Ok(ValueKind::Ratio(
4649 canonical_rational,
4650 Some(unit.name.clone()),
4651 ))
4652 }
4653 TypeSpecification::Measure { units, .. } => {
4654 use crate::computation::rational::checked_mul;
4655 let rational = lift_parser_decimal(magnitude)?;
4656 let unit = units.get(unit_name)?;
4657 let canonical = checked_mul(&rational, &unit.factor)
4658 .map_err(|failure| format!("measure canonicalization overflow: {failure}"))?;
4659 Ok(ValueKind::Measure(
4660 canonical,
4661 vec![(unit_name.to_string(), 1)],
4662 ))
4663 }
4664 _ => Err(format!(
4665 "Unit '{}' is defined on type '{}' which is not measure or ratio",
4666 unit_name,
4667 lemma_type.name()
4668 )),
4669 }
4670}
4671
4672pub(crate) fn value_kind_matches_spec(value: &ValueKind, type_spec: &TypeSpecification) -> bool {
4675 matches!(
4676 (type_spec, value),
4677 (TypeSpecification::Number { .. }, ValueKind::Number(_))
4678 | (TypeSpecification::Text { .. }, ValueKind::Text(_))
4679 | (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
4680 | (TypeSpecification::Date { .. }, ValueKind::Date(_))
4681 | (TypeSpecification::Time { .. }, ValueKind::Time(_))
4682 | (TypeSpecification::Measure { .. }, ValueKind::Measure(_, _))
4683 | (TypeSpecification::Ratio { .. }, ValueKind::Ratio(_, _))
4684 | (TypeSpecification::Ratio { .. }, ValueKind::Number(_))
4685 | (
4686 TypeSpecification::NumberRange { .. },
4687 ValueKind::Range(_, _)
4688 )
4689 | (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
4690 | (TypeSpecification::TimeRange { .. }, ValueKind::Range(_, _))
4691 | (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
4692 | (
4693 TypeSpecification::MeasureRange { .. },
4694 ValueKind::Range(_, _)
4695 )
4696 | (TypeSpecification::Veto { .. }, _)
4697 | (TypeSpecification::Undetermined, _)
4698 )
4699}
4700
4701fn parser_value_type_mismatch(
4702 value: &crate::literals::Value,
4703 type_spec: &TypeSpecification,
4704) -> String {
4705 use crate::parsing::ast::AsLemmaSource;
4706 let value_str = format!("{}", AsLemmaSource(value));
4707 match type_spec {
4708 TypeSpecification::Measure { units, .. } => {
4709 let unit_hint = units
4710 .iter()
4711 .find(|u| u.factor == crate::computation::rational::rational_one())
4712 .map(|u| u.name.as_str())
4713 .or_else(|| units.iter().next().map(|u| u.name.as_str()))
4714 .unwrap_or("unit");
4715 format!("cannot use {value_str} as {type_spec}: expected `<n> {unit_hint}`")
4716 }
4717 TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
4718 let unit_hint = units
4719 .iter()
4720 .next()
4721 .map(|u| u.name.as_str())
4722 .unwrap_or("unit");
4723 format!(
4724 "cannot use {value_str} as {type_spec}: expected `<n> {unit_hint}` or bare ratio"
4725 )
4726 }
4727 _ => format!("cannot use {value_str} as {type_spec}"),
4728 }
4729}
4730
4731pub fn refresh_measure_literal_canonical_magnitude(
4736 lit: &mut LiteralValue,
4737 resolved_type: &LemmaType,
4738) {
4739 let ValueKind::Measure(magnitude, signature) = &mut lit.value else {
4740 return;
4741 };
4742 let (unit_name, exponent) = signature
4743 .first()
4744 .expect("BUG: measure literal has empty signature during canonical magnitude refresh");
4745 if *exponent != 1 || signature.len() != 1 {
4746 return;
4747 }
4748 let stored_factor = lit.lemma_type.measure_unit_factor(unit_name);
4749 let resolved_factor = resolved_type.measure_unit_factor(unit_name);
4750 if stored_factor == resolved_factor {
4751 lit.lemma_type = Arc::new(resolved_type.clone());
4752 return;
4753 }
4754 let scaled = checked_mul(magnitude, resolved_factor)
4755 .expect("BUG: measure recanonicalization multiply overflow");
4756 *magnitude =
4757 checked_div(&scaled, stored_factor).expect("BUG: measure recanonicalization divide failed");
4758 lit.lemma_type = Arc::new(resolved_type.clone());
4759}
4760
4761pub fn parser_value_to_value_kind(
4763 value: &crate::literals::Value,
4764 type_spec: &TypeSpecification,
4765) -> Result<ValueKind, String> {
4766 use crate::computation::rational::decimal_to_rational;
4767 use crate::literals::Value;
4768 match (value, type_spec) {
4769 (Value::NumberWithUnit(magnitude, unit_name), TypeSpecification::Ratio { units, .. }) => {
4770 use crate::computation::rational::checked_div;
4771 let unit = units.get(unit_name.as_str())?;
4772 let magnitude_rational = decimal_to_rational(*magnitude)
4773 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
4774 let canonical_rational = checked_div(&magnitude_rational, &unit.value)
4775 .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
4776 Ok(ValueKind::Ratio(
4777 canonical_rational,
4778 Some(unit.name.clone()),
4779 ))
4780 }
4781 (Value::NumberWithUnit(magnitude, unit_name), TypeSpecification::Measure { units, .. }) => {
4782 use crate::computation::rational::checked_mul;
4783 let rational = lift_parser_decimal(*magnitude)?;
4784 let unit = units.get(unit_name.as_str())?;
4785 let canonical = checked_mul(&rational, &unit.factor)
4786 .map_err(|failure| format!("measure canonicalization overflow: {failure}"))?;
4787 Ok(ValueKind::Measure(canonical, vec![(unit_name.clone(), 1)]))
4788 }
4789 (Value::NumberWithUnit(_, _), _) => {
4790 Err("number_with_unit literal requires a measure or ratio type".to_string())
4791 }
4792 (Value::Number(n), TypeSpecification::Number { .. }) => {
4793 Ok(ValueKind::Number(lift_parser_decimal(*n)?))
4794 }
4795 (Value::Number(n), TypeSpecification::Ratio { .. }) => {
4796 let r = decimal_to_rational(*n)
4797 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
4798 Ok(ValueKind::Ratio(r, None))
4799 }
4800 (Value::Text(s), TypeSpecification::Text { .. }) => Ok(ValueKind::Text(s.clone())),
4801 (Value::Boolean(b), TypeSpecification::Boolean { .. }) => Ok(ValueKind::Boolean(b.into())),
4802 (Value::Date(dt), TypeSpecification::Date { .. }) => {
4803 Ok(ValueKind::Date(date_time_to_semantic(dt)))
4804 }
4805 (Value::Time(t), TypeSpecification::Time { .. }) => {
4806 Ok(ValueKind::Time(time_to_semantic(t)))
4807 }
4808 (
4809 Value::Range(left, right),
4810 range_spec @ (TypeSpecification::NumberRange { .. }
4811 | TypeSpecification::DateRange { .. }
4812 | TypeSpecification::TimeRange { .. }
4813 | TypeSpecification::RatioRange { .. }
4814 | TypeSpecification::MeasureRange { .. }),
4815 ) => {
4816 let endpoint = range_element_type_specification(range_spec).ok_or_else(|| {
4817 "BUG: range_element_type_specification missing arm for range type".to_string()
4818 })?;
4819 let left_lit = lift_range_endpoint(left, &endpoint)?;
4820 let right_lit = lift_range_endpoint(right, &endpoint)?;
4821 Ok(ValueKind::Range(Box::new(left_lit), Box::new(right_lit)))
4822 }
4823 (value, type_spec) => Err(parser_value_type_mismatch(value, type_spec)),
4824 }
4825}
4826
4827pub fn value_to_semantic(value: &crate::parsing::ast::Value) -> Result<ValueKind, String> {
4831 use crate::parsing::ast::Value;
4832 Ok(match value {
4833 Value::Number(n) => ValueKind::Number(lift_parser_decimal(*n)?),
4834 Value::Text(s) => ValueKind::Text(s.clone()),
4835 Value::Boolean(b) => ValueKind::Boolean(bool::from(*b)),
4836 Value::Date(dt) => ValueKind::Date(date_time_to_semantic(dt)),
4837 Value::Time(t) => ValueKind::Time(time_to_semantic(t)),
4838 Value::NumberWithUnit(_, _) => {
4839 return Err(
4840 "number_with_unit literal requires type context (measure or ratio)".to_string(),
4841 );
4842 }
4843 Value::Range(_, _) => literal_value_from_parser_value(value)?.value,
4844 })
4845}
4846
4847pub(crate) fn date_time_to_semantic(dt: &crate::parsing::ast::DateTimeValue) -> SemanticDateTime {
4849 SemanticDateTime {
4850 year: dt.year,
4851 month: dt.month,
4852 day: dt.day,
4853 hour: dt.hour,
4854 minute: dt.minute,
4855 second: dt.second,
4856 microsecond: dt.microsecond,
4857 timezone: dt.timezone.as_ref().map(|tz| SemanticTimezone {
4858 offset_hours: tz.offset_hours,
4859 offset_minutes: tz.offset_minutes,
4860 }),
4861 }
4862}
4863
4864pub(crate) fn time_to_semantic(t: &crate::parsing::ast::TimeValue) -> SemanticTime {
4866 SemanticTime {
4867 hour: t.hour.into(),
4868 minute: t.minute.into(),
4869 second: t.second.into(),
4870 microsecond: t.microsecond,
4871 timezone: t.timezone.as_ref().map(|tz| SemanticTimezone {
4872 offset_hours: tz.offset_hours,
4873 offset_minutes: tz.offset_minutes,
4874 }),
4875 }
4876}
4877
4878pub(crate) fn compare_semantic_dates(
4882 left: &SemanticDateTime,
4883 right: &SemanticDateTime,
4884) -> std::cmp::Ordering {
4885 left.year
4886 .cmp(&right.year)
4887 .then_with(|| left.month.cmp(&right.month))
4888 .then_with(|| left.day.cmp(&right.day))
4889 .then_with(|| left.hour.cmp(&right.hour))
4890 .then_with(|| left.minute.cmp(&right.minute))
4891 .then_with(|| left.second.cmp(&right.second))
4892 .then_with(|| left.microsecond.cmp(&right.microsecond))
4893}
4894
4895pub(crate) fn compare_semantic_times(
4898 left: &SemanticTime,
4899 right: &SemanticTime,
4900) -> std::cmp::Ordering {
4901 left.hour
4902 .cmp(&right.hour)
4903 .then_with(|| left.minute.cmp(&right.minute))
4904 .then_with(|| left.second.cmp(&right.second))
4905 .then_with(|| left.microsecond.cmp(&right.microsecond))
4906}
4907
4908pub fn conversion_target_to_semantic(
4910 ct: &ConversionTarget,
4911 unit_index: Option<&crate::planning::unit_index::UnitIndex>,
4912) -> Result<SemanticConversionTarget, String> {
4913 match ct {
4914 ConversionTarget::Type(kind) => Ok(SemanticConversionTarget::Type(*kind)),
4915 ConversionTarget::Unit { unit_name } => {
4916 let index = unit_index.ok_or_else(|| format!("Unknown unit '{unit_name}'."))?;
4917 let (bare, owning_type) = index.resolve(unit_name)?;
4918 Ok(SemanticConversionTarget::Unit {
4919 unit_name: bare,
4920 owning_type,
4921 })
4922 }
4923 }
4924}
4925
4926static PRIMITIVE_BOOLEAN: OnceLock<Arc<LemmaType>> = OnceLock::new();
4932static PRIMITIVE_NUMBER: OnceLock<Arc<LemmaType>> = OnceLock::new();
4933static PRIMITIVE_TEXT: OnceLock<Arc<LemmaType>> = OnceLock::new();
4934static PRIMITIVE_DATE: OnceLock<Arc<LemmaType>> = OnceLock::new();
4935static PRIMITIVE_DATE_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
4936static PRIMITIVE_TIME: OnceLock<Arc<LemmaType>> = OnceLock::new();
4937static PRIMITIVE_RATIO: OnceLock<Arc<LemmaType>> = OnceLock::new();
4938
4939#[must_use]
4940pub fn primitive_boolean_arc() -> &'static Arc<LemmaType> {
4941 PRIMITIVE_BOOLEAN.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::boolean())))
4942}
4943
4944#[must_use]
4945pub fn primitive_number_arc() -> &'static Arc<LemmaType> {
4946 PRIMITIVE_NUMBER.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::number())))
4947}
4948
4949#[must_use]
4950pub fn primitive_text_arc() -> &'static Arc<LemmaType> {
4951 PRIMITIVE_TEXT.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::text())))
4952}
4953
4954#[must_use]
4955pub fn primitive_date_arc() -> &'static Arc<LemmaType> {
4956 PRIMITIVE_DATE.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::date())))
4957}
4958
4959#[must_use]
4960pub fn primitive_date_range_arc() -> &'static Arc<LemmaType> {
4961 PRIMITIVE_DATE_RANGE
4962 .get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::date_range())))
4963}
4964
4965#[must_use]
4966pub fn primitive_time_arc() -> &'static Arc<LemmaType> {
4967 PRIMITIVE_TIME.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::time())))
4968}
4969
4970#[must_use]
4971pub fn primitive_ratio_arc() -> &'static Arc<LemmaType> {
4972 PRIMITIVE_RATIO.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::ratio())))
4973}
4974
4975#[must_use]
4977pub fn type_spec_for_primitive(kind: PrimitiveKind) -> TypeSpecification {
4978 match kind {
4979 PrimitiveKind::Boolean => TypeSpecification::boolean(),
4980 PrimitiveKind::Measure => TypeSpecification::measure(),
4981 PrimitiveKind::MeasureRange => TypeSpecification::measure_range(),
4982 PrimitiveKind::Number => TypeSpecification::number(),
4983 PrimitiveKind::NumberRange => TypeSpecification::number_range(),
4984 PrimitiveKind::Ratio => TypeSpecification::ratio(),
4985 PrimitiveKind::RatioRange => TypeSpecification::ratio_range(),
4986 PrimitiveKind::Text => TypeSpecification::text(),
4987 PrimitiveKind::Date => TypeSpecification::date(),
4988 PrimitiveKind::DateRange => TypeSpecification::date_range(),
4989 PrimitiveKind::Time => TypeSpecification::time(),
4990 PrimitiveKind::TimeRange => TypeSpecification::time_range(),
4991 }
4992}
4993
4994impl fmt::Display for PathSegment {
4999 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5000 write!(f, "{} → {}", self.data, self.spec)
5001 }
5002}
5003
5004impl fmt::Display for DataPath {
5005 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5006 for segment in &self.segments {
5007 write!(f, "{}.", segment)?;
5008 }
5009 write!(f, "{}", self.data)
5010 }
5011}
5012
5013impl fmt::Display for RulePath {
5014 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5015 for segment in &self.segments {
5016 write!(f, "{}.", segment)?;
5017 }
5018 write!(f, "{}", self.rule)
5019 }
5020}
5021
5022impl fmt::Display for LemmaType {
5023 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5024 write!(f, "{}", self.name())
5025 }
5026}
5027
5028fn decimal_places_in_display_value(decimal: &rust_decimal::Decimal) -> u32 {
5029 if decimal.is_integer() {
5030 return 0;
5031 }
5032 decimal.fract().normalize().scale()
5033}
5034
5035fn format_decimal_for_api(decimal: rust_decimal::Decimal, decimal_places: Option<u8>) -> String {
5036 match decimal_places {
5037 Some(decimal_places) => {
5038 let rounded = decimal.round_dp(u32::from(decimal_places));
5039 format!("{:.prec$}", rounded, prec = decimal_places as usize)
5040 }
5041 None => {
5042 let normalized = decimal.normalize();
5043 if normalized.fract().is_zero() {
5044 normalized.trunc().to_string()
5045 } else {
5046 normalized.to_string()
5047 }
5048 }
5049 }
5050}
5051
5052fn format_decimal_for_human_display(
5053 decimal: rust_decimal::Decimal,
5054 decimal_places: Option<u8>,
5055) -> String {
5056 match decimal_places {
5057 Some(decimal_places) => {
5058 let rounded = decimal.round_dp(u32::from(decimal_places));
5059 format!("{:.prec$}", rounded, prec = decimal_places as usize)
5060 }
5061 None => decimal.normalize().to_string(),
5062 }
5063}
5064
5065fn format_rational_for_human_display(
5066 magnitude: &crate::computation::rational::RationalInteger,
5067 decimal_places: Option<u8>,
5068) -> String {
5069 match magnitude.try_to_decimal() {
5070 Ok(decimal) => format_decimal_for_human_display(decimal, decimal_places),
5071 Err(crate::computation::rational::NumericFailure::Overflow) => magnitude.display_str(),
5072 Err(_) => magnitude.display_str(),
5073 }
5074}
5075
5076fn format_measure_canonical_for_display(
5077 canonical: &crate::computation::rational::RationalInteger,
5078 lemma_type: &LemmaType,
5079 signature: &[(String, i32)],
5080) -> String {
5081 use crate::computation::rational::{checked_div, rational_new};
5082 use rust_decimal::Decimal;
5083
5084 let decimals = lemma_type.decimal_places();
5085
5086 if let TypeSpecification::Measure { units, .. } = &lemma_type.specifications {
5087 if !units.is_empty() {
5088 if let [(sig_unit, 1)] = signature {
5089 if let Some(unit) = units.iter().find(|u| u.name == *sig_unit) {
5090 let in_unit = checked_div(canonical, &unit.factor)
5091 .expect("BUG: de-canonicalization for measure display must not fail");
5092 let formatted = format_rational_for_human_display(&in_unit, decimals);
5093 return format!("{} {}", formatted, unit.name);
5094 }
5095 }
5096
5097 struct UnitDisplayCandidate {
5098 unit_name: String,
5099 decimal_places: u32,
5100 under_1000: bool,
5101 decimal_abs: Option<Decimal>,
5102 formatted: String,
5103 }
5104
5105 let thousand = rational_new(1000, 1);
5106 let mut candidates: Vec<UnitDisplayCandidate> = Vec::with_capacity(units.len());
5107 for unit in units.iter() {
5108 let in_unit = checked_div(canonical, &unit.factor)
5109 .expect("BUG: de-canonicalization for measure display must not fail");
5110 let formatted = format_rational_for_human_display(&in_unit, decimals);
5111 let decimal_abs = in_unit.try_to_decimal().ok().map(|decimal| decimal.abs());
5112 let decimal_places = decimal_abs
5113 .as_ref()
5114 .map(decimal_places_in_display_value)
5115 .unwrap_or(u32::MAX);
5116 let under_1000 = in_unit
5117 .try_cmp(&thousand)
5118 .ok()
5119 .is_some_and(|ordering| ordering == std::cmp::Ordering::Less);
5120 candidates.push(UnitDisplayCandidate {
5121 unit_name: unit.name.clone(),
5122 decimal_places,
5123 under_1000,
5124 decimal_abs,
5125 formatted,
5126 });
5127 }
5128
5129 let pool: Vec<&UnitDisplayCandidate> = {
5130 let under: Vec<_> = candidates.iter().filter(|c| c.under_1000).collect();
5131 if under.is_empty() {
5132 candidates.iter().collect()
5133 } else {
5134 under
5135 }
5136 };
5137 let best = pool
5138 .iter()
5139 .min_by(|left, right| {
5140 left.decimal_places
5141 .cmp(&right.decimal_places)
5142 .then_with(|| match (left.decimal_abs, right.decimal_abs) {
5143 (Some(left_abs), Some(right_abs)) => left_abs.cmp(&right_abs),
5144 (Some(_), None) => std::cmp::Ordering::Less,
5145 (None, Some(_)) => std::cmp::Ordering::Greater,
5146 (None, None) => std::cmp::Ordering::Equal,
5147 })
5148 })
5149 .expect("BUG: measure type must have at least one declared unit");
5150 return format!("{} {}", best.formatted, best.unit_name);
5151 }
5152 }
5153
5154 let unit_label = match signature {
5155 [] => String::new(),
5156 [(name, 1)] => name.clone(),
5157 _ => format_signature_operator_style(signature),
5158 };
5159 let formatted = format_rational_for_human_display(canonical, decimals);
5160 if unit_label.is_empty() {
5161 formatted
5162 } else {
5163 format!("{formatted} {unit_label}")
5164 }
5165}
5166
5167impl fmt::Display for LiteralValue {
5168 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5169 match &self.value {
5170 ValueKind::Measure(n, signature) => {
5171 write!(
5172 f,
5173 "{}",
5174 format_measure_canonical_for_display(n, &self.lemma_type, signature)
5175 )
5176 }
5177 ValueKind::Ratio(_, Some(_unit_name)) => write!(f, "{}", self.value),
5178 ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
5179 _ => write!(f, "{}", self.value),
5180 }
5181 }
5182}
5183
5184#[cfg(test)]
5189pub(crate) mod tests {
5190 use super::*;
5191 use crate::computation::rational::decimal_to_rational;
5192 use crate::literals::DateGranularity;
5193 use crate::literals::Value;
5194 use crate::parsing::ast::{BooleanValue, DateTimeValue, PrimitiveKind, TimeValue};
5195 use rust_decimal::Decimal;
5196 use std::str::FromStr;
5197 use std::sync::{Arc, OnceLock};
5198
5199 static PRIMITIVE_MEASURE: OnceLock<Arc<LemmaType>> = OnceLock::new();
5200
5201 #[must_use]
5202 pub(crate) fn primitive_measure_arc() -> &'static Arc<LemmaType> {
5203 PRIMITIVE_MEASURE
5204 .get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::measure())))
5205 }
5206
5207 #[must_use]
5208 pub(crate) fn primitive_measure() -> &'static LemmaType {
5209 primitive_measure_arc().as_ref()
5210 }
5211
5212 #[test]
5213 fn default_primitive_help_is_goal_oriented() {
5214 let kinds = [
5215 PrimitiveKind::Boolean,
5216 PrimitiveKind::Measure,
5217 PrimitiveKind::MeasureRange,
5218 PrimitiveKind::Number,
5219 PrimitiveKind::NumberRange,
5220 PrimitiveKind::Ratio,
5221 PrimitiveKind::RatioRange,
5222 PrimitiveKind::Text,
5223 PrimitiveKind::Date,
5224 PrimitiveKind::DateRange,
5225 PrimitiveKind::Time,
5226 PrimitiveKind::TimeRange,
5227 ];
5228 for kind in kinds {
5229 let spec = type_spec_for_primitive(kind);
5230 let help = match &spec {
5231 TypeSpecification::Boolean { help, .. }
5232 | TypeSpecification::Number { help, .. }
5233 | TypeSpecification::NumberRange { help, .. }
5234 | TypeSpecification::Text { help, .. }
5235 | TypeSpecification::Measure { help, .. }
5236 | TypeSpecification::MeasureRange { help, .. }
5237 | TypeSpecification::Ratio { help, .. }
5238 | TypeSpecification::RatioRange { help, .. }
5239 | TypeSpecification::Date { help, .. }
5240 | TypeSpecification::DateRange { help, .. }
5241 | TypeSpecification::TimeRange { help, .. }
5242 | TypeSpecification::Time { help, .. } => help,
5243 TypeSpecification::Veto { .. } | TypeSpecification::Undetermined => {
5244 unreachable!(
5245 "BUG: primitive kind {:?} mapped to non-primitive spec",
5246 kind
5247 )
5248 }
5249 };
5250 assert!(!help.is_empty(), "help for {:?}", kind);
5251 assert!(
5252 !help.to_ascii_lowercase().contains("format:"),
5253 "help for {:?} must not describe syntax: {:?}",
5254 kind,
5255 help
5256 );
5257 assert_eq!(help, default_help_for_primitive(kind));
5258 }
5259 }
5260
5261 #[test]
5262 fn test_negated_comparison() {
5263 assert_eq!(
5264 negated_comparison(ComparisonComputation::LessThan),
5265 ComparisonComputation::GreaterThanOrEqual
5266 );
5267 assert_eq!(
5268 negated_comparison(ComparisonComputation::GreaterThanOrEqual),
5269 ComparisonComputation::LessThan
5270 );
5271 assert_eq!(
5272 negated_comparison(ComparisonComputation::Is),
5273 ComparisonComputation::IsNot
5274 );
5275 assert_eq!(
5276 negated_comparison(ComparisonComputation::IsNot),
5277 ComparisonComputation::Is
5278 );
5279 }
5280
5281 #[test]
5282 fn value_to_semantic_number_is_decimal() {
5283 let kind = value_to_semantic(&Value::Number(Decimal::from(42))).unwrap();
5284 assert!(matches!(kind, ValueKind::Number(d) if d == rational_new(42, 1)));
5285 }
5286
5287 #[test]
5288 fn value_kind_measure_serializes_with_signature() {
5289 let kind = ValueKind::Measure(
5290 decimal_to_rational(Decimal::from_str("99.50").unwrap()).unwrap(),
5291 vec![("eur".to_string(), 1)],
5292 );
5293 let json = serde_json::to_value(&kind).unwrap();
5294 assert_eq!(json["measure"]["value"], "99.5");
5295 assert_eq!(json["measure"]["signature"][0][0], "eur");
5296 assert_eq!(json["measure"]["signature"][0][1], 1);
5297 }
5298
5299 #[test]
5300 fn value_kind_measure_compound_signature_roundtrips() {
5301 let original = ValueKind::Measure(
5302 decimal_to_rational(Decimal::from_str("4800").unwrap()).unwrap(),
5303 vec![
5304 ("eur".to_string(), 1),
5305 ("hour".to_string(), 1),
5306 ("minute".to_string(), -1),
5307 ],
5308 );
5309 let json = serde_json::to_string(&original).unwrap();
5310 let parsed: ValueKind = serde_json::from_str(&json).unwrap();
5311 assert_eq!(original, parsed);
5312 }
5313
5314 #[test]
5315 fn value_kind_measure_empty_signature_roundtrips() {
5316 let original = ValueKind::Measure(
5317 decimal_to_rational(Decimal::from_str("12.5").unwrap()).unwrap(),
5318 Vec::new(),
5319 );
5320 let json = serde_json::to_string(&original).unwrap();
5321 let parsed: ValueKind = serde_json::from_str(&json).unwrap();
5322 assert_eq!(original, parsed);
5323 }
5324
5325 #[test]
5326 fn literal_value_number_serde_not_rational_array() {
5327 let lit = LiteralValue::number_from_decimal(Decimal::from(20));
5328 let json = serde_json::to_value(&lit).unwrap();
5329 let number = json
5330 .get("value")
5331 .and_then(|v| v.get("number"))
5332 .expect("number field");
5333 assert!(number.is_string());
5334 assert_eq!(number.as_str(), Some("20"));
5335 assert!(
5336 !number.is_array(),
5337 "stored number must not serialize as [n,d]"
5338 );
5339 }
5340
5341 #[test]
5342 fn test_literal_value_to_primitive_type() {
5343 let one = rational_new(1, 1);
5344
5345 assert_eq!(LiteralValue::text("".to_string()).lemma_type.name(), "text");
5346 assert_eq!(
5347 LiteralValue::number(one.clone()).lemma_type.name(),
5348 "number"
5349 );
5350 assert_eq!(
5351 LiteralValue::from_bool(bool::from(BooleanValue::True))
5352 .lemma_type
5353 .name(),
5354 "boolean"
5355 );
5356
5357 let dt = DateTimeValue {
5358 year: 2024,
5359 month: 1,
5360 day: 1,
5361 hour: 0,
5362 minute: 0,
5363 second: 0,
5364 microsecond: 0,
5365 timezone: None,
5366
5367 granularity: DateGranularity::Full,
5368 };
5369 assert_eq!(
5370 LiteralValue::date(date_time_to_semantic(&dt))
5371 .lemma_type
5372 .name(),
5373 "date"
5374 );
5375 assert_eq!(
5376 LiteralValue::ratio_from_decimal(Decimal::new(1, 2), Some("percent".to_string()))
5377 .lemma_type
5378 .name(),
5379 "ratio"
5380 );
5381 let dur_type = LemmaType::new(
5382 "duration".to_string(),
5383 TypeSpecification::Measure {
5384 minimum: None,
5385 maximum: None,
5386 decimals: None,
5387 units: MeasureUnits::from(vec![MeasureUnit {
5388 name: "second".to_string(),
5389 factor: crate::computation::rational::rational_one(),
5390 derived_measure_factors: Vec::new(),
5391 decomposition: BaseMeasureVector::new(),
5392 minimum: None,
5393 maximum: None,
5394 suggestion_magnitude: None,
5395 }]),
5396 traits: vec![MeasureTrait::Duration],
5397 decomposition: None,
5398 help: String::new(),
5399 },
5400 TypeExtends::Primitive,
5401 );
5402 assert_eq!(
5403 LiteralValue::measure_with_type(one.clone(), "second".to_string(), Arc::new(dur_type))
5404 .lemma_type
5405 .name(),
5406 "duration"
5407 );
5408 }
5409
5410 #[test]
5411 fn test_type_display() {
5412 let specs = TypeSpecification::text();
5413 let lemma_type = LemmaType::new("name".to_string(), specs, TypeExtends::Primitive);
5414 assert_eq!(format!("{}", lemma_type), "name");
5415 }
5416
5417 #[test]
5418 fn test_type_serialization() {
5419 let specs = TypeSpecification::number();
5420 let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
5421 let serialized = serde_json::to_string(&lemma_type).unwrap();
5422 let deserialized: LemmaType = serde_json::from_str(&serialized).unwrap();
5423 assert_eq!(lemma_type, deserialized);
5424 }
5425
5426 #[test]
5427 fn test_literal_value_display_value() {
5428 let ten = rational_new(10, 1);
5429
5430 assert_eq!(
5431 LiteralValue::text("hello".to_string()).display_value(),
5432 "hello"
5433 );
5434 assert_eq!(LiteralValue::number(ten).display_value(), "10");
5435 assert_eq!(LiteralValue::from_bool(true).display_value(), "true");
5436 assert_eq!(LiteralValue::from_bool(false).display_value(), "false");
5437
5438 let ten_percent_ratio =
5440 LiteralValue::ratio_from_decimal(Decimal::new(1, 1), Some("percent".to_string()));
5441 assert_eq!(ten_percent_ratio.display_value(), "10%");
5442
5443 let time = TimeValue {
5444 hour: 14,
5445 minute: 30,
5446 second: 0,
5447 microsecond: 0,
5448 timezone: None,
5449 };
5450 let time_display = LiteralValue::time(time_to_semantic(&time)).display_value();
5451 assert!(time_display.contains("14"));
5452 assert!(time_display.contains("30"));
5453 }
5454
5455 #[test]
5456 fn test_measure_display_respects_type_decimals() {
5457 let money_type = LemmaType {
5458 name: Some("money".to_string()),
5459 specifications: TypeSpecification::Measure {
5460 minimum: None,
5461 maximum: None,
5462 decimals: Some(2),
5463 units: MeasureUnits::from(vec![MeasureUnit {
5464 name: "eur".to_string(),
5465 factor: crate::computation::rational::rational_one(),
5466 derived_measure_factors: Vec::new(),
5467 decomposition: BaseMeasureVector::new(),
5468 minimum: None,
5469 maximum: None,
5470 suggestion_magnitude: None,
5471 }]),
5472 traits: Vec::new(),
5473 decomposition: None,
5474 help: String::new(),
5475 },
5476 extends: TypeExtends::Primitive,
5477 };
5478 let money_type = Arc::new(money_type);
5479 let val = LiteralValue::measure_with_type(
5480 decimal_to_rational(Decimal::from_str("1.8").unwrap()).unwrap(),
5481 "eur".to_string(),
5482 money_type.clone(),
5483 );
5484 assert_eq!(val.display_value(), "1.80 eur");
5485 let more_precision = LiteralValue::measure_with_type(
5486 decimal_to_rational(Decimal::from_str("1.80000").unwrap()).unwrap(),
5487 "eur".to_string(),
5488 money_type,
5489 );
5490 assert_eq!(more_precision.display_value(), "1.80 eur");
5491 let measure_no_decimals = LemmaType {
5492 name: Some("count".to_string()),
5493 specifications: TypeSpecification::Measure {
5494 minimum: None,
5495 maximum: None,
5496 decimals: None,
5497 units: MeasureUnits::from(vec![MeasureUnit {
5498 name: "items".to_string(),
5499 factor: crate::computation::rational::rational_one(),
5500 derived_measure_factors: Vec::new(),
5501 decomposition: BaseMeasureVector::new(),
5502 minimum: None,
5503 maximum: None,
5504 suggestion_magnitude: None,
5505 }]),
5506 traits: Vec::new(),
5507 decomposition: None,
5508 help: String::new(),
5509 },
5510 extends: TypeExtends::Primitive,
5511 };
5512 let val_any = LiteralValue::measure_with_type(
5513 decimal_to_rational(Decimal::from_str("42.50").unwrap()).unwrap(),
5514 "items".to_string(),
5515 Arc::new(measure_no_decimals),
5516 );
5517 assert_eq!(val_any.display_value(), "42.5 items");
5518 }
5519
5520 #[test]
5521 fn test_literal_value_time_type() {
5522 let time = TimeValue {
5523 hour: 14,
5524 minute: 30,
5525 second: 0,
5526 microsecond: 0,
5527 timezone: None,
5528 };
5529 let lit = LiteralValue::time(time_to_semantic(&time));
5530 assert_eq!(lit.lemma_type.name(), "time");
5531 }
5532
5533 #[test]
5534 fn test_measure_family_name_primitive_root() {
5535 let measure_spec = TypeSpecification::measure();
5536 let money_primitive = LemmaType::new(
5537 "money".to_string(),
5538 measure_spec.clone(),
5539 TypeExtends::Primitive,
5540 );
5541 assert_eq!(money_primitive.measure_family_name(), Some("money"));
5542 }
5543
5544 #[test]
5545 fn test_measure_family_name_custom() {
5546 let measure_spec = TypeSpecification::measure();
5547 let money_custom = LemmaType::new(
5548 "money".to_string(),
5549 measure_spec,
5550 TypeExtends::custom_local("money".to_string(), "money".to_string()),
5551 );
5552 assert_eq!(money_custom.measure_family_name(), Some("money"));
5553 }
5554
5555 #[test]
5556 fn test_same_measure_family_same_name_different_extends() {
5557 let measure_spec = TypeSpecification::measure();
5558 let money_primitive = LemmaType::new(
5559 "money".to_string(),
5560 measure_spec.clone(),
5561 TypeExtends::Primitive,
5562 );
5563 let money_custom = LemmaType::new(
5564 "money".to_string(),
5565 measure_spec,
5566 TypeExtends::custom_local("money".to_string(), "money".to_string()),
5567 );
5568 assert!(money_primitive.same_measure_family(&money_custom));
5569 assert!(money_custom.same_measure_family(&money_primitive));
5570 }
5571
5572 #[test]
5573 fn test_same_measure_family_parent_and_child() {
5574 let measure_spec = TypeSpecification::measure();
5575 let type_x = LemmaType::new(
5576 "x".to_string(),
5577 measure_spec.clone(),
5578 TypeExtends::Primitive,
5579 );
5580 let type_x2 = LemmaType::new(
5581 "x2".to_string(),
5582 measure_spec,
5583 TypeExtends::custom_local("x".to_string(), "x".to_string()),
5584 );
5585 assert_eq!(type_x.measure_family_name(), Some("x"));
5586 assert_eq!(type_x2.measure_family_name(), Some("x"));
5587 assert!(type_x.same_measure_family(&type_x2));
5588 assert!(type_x2.same_measure_family(&type_x));
5589 }
5590
5591 #[test]
5592 fn test_same_measure_family_siblings() {
5593 let measure_spec = TypeSpecification::measure();
5594 let type_x2_a = LemmaType::new(
5595 "x2a".to_string(),
5596 measure_spec.clone(),
5597 TypeExtends::custom_local("x".to_string(), "x".to_string()),
5598 );
5599 let type_x2_b = LemmaType::new(
5600 "x2b".to_string(),
5601 measure_spec,
5602 TypeExtends::custom_local("x".to_string(), "x".to_string()),
5603 );
5604 assert!(type_x2_a.same_measure_family(&type_x2_b));
5605 }
5606
5607 #[test]
5608 fn test_same_measure_family_different_families() {
5609 let measure_spec = TypeSpecification::measure();
5610 let money = LemmaType::new(
5611 "money".to_string(),
5612 measure_spec.clone(),
5613 TypeExtends::Primitive,
5614 );
5615 let temperature = LemmaType::new(
5616 "temperature".to_string(),
5617 measure_spec,
5618 TypeExtends::Primitive,
5619 );
5620 assert!(!money.same_measure_family(&temperature));
5621 assert!(!temperature.same_measure_family(&money));
5622 }
5623
5624 #[test]
5625 fn test_same_measure_family_measure_vs_non_measure() {
5626 let measure_spec = TypeSpecification::measure();
5627 let number_spec = TypeSpecification::number();
5628 let measure_type =
5629 LemmaType::new("money".to_string(), measure_spec, TypeExtends::Primitive);
5630 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
5631 assert!(!measure_type.same_measure_family(&number_type));
5632 assert!(!number_type.same_measure_family(&measure_type));
5633 }
5634
5635 #[test]
5636 fn test_same_measure_family_anonymous_measures_are_not_family_compatible() {
5637 let left = LemmaType::anonymous_for_decomposition(duration_decomposition());
5638 let right = LemmaType::anonymous_for_decomposition(duration_decomposition());
5639
5640 assert!(!left.same_measure_family(&right));
5641 assert!(left.compatible_with_anonymous_measure(&right));
5642 }
5643
5644 #[test]
5645 fn test_measure_family_name_non_measure_returns_none() {
5646 let number_spec = TypeSpecification::number();
5647 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
5648 assert_eq!(number_type.measure_family_name(), None);
5649 }
5650
5651 #[test]
5652 fn test_lemma_type_inequality_local_vs_import_same_shape() {
5653 let measure_spec = TypeSpecification::measure();
5654 let local = LemmaType::new(
5655 "t".to_string(),
5656 measure_spec.clone(),
5657 TypeExtends::custom_local("money".to_string(), "money".to_string()),
5658 );
5659 let imported = LemmaType::new(
5660 "t".to_string(),
5661 measure_spec,
5662 TypeExtends::Custom {
5663 parent: "money".to_string(),
5664 family: "money".to_string(),
5665 defining_spec: TypeDefiningSpec::Import,
5666 },
5667 );
5668 assert_ne!(local, imported);
5669 }
5670
5671 #[test]
5672 fn test_lemma_type_equality_import_unit_variant() {
5673 let measure_spec = TypeSpecification::measure();
5674 let left = LemmaType::new(
5675 "t".to_string(),
5676 measure_spec.clone(),
5677 TypeExtends::Custom {
5678 parent: "money".to_string(),
5679 family: "money".to_string(),
5680 defining_spec: TypeDefiningSpec::Import,
5681 },
5682 );
5683 let right = LemmaType::new(
5684 "t".to_string(),
5685 measure_spec,
5686 TypeExtends::Custom {
5687 parent: "money".to_string(),
5688 family: "money".to_string(),
5689 defining_spec: TypeDefiningSpec::Import,
5690 },
5691 );
5692 assert_eq!(left, right);
5693 }
5694
5695 fn month_suggestion_arg() -> CommandArg {
5696 CommandArg::Literal(crate::literals::Value::NumberWithUnit(
5697 Decimal::ONE,
5698 "month".to_string(),
5699 ))
5700 }
5701
5702 fn unit_factor_arg(name: &str, factor: i64) -> [CommandArg; 2] {
5703 [
5704 CommandArg::Label(name.to_string()),
5705 CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::from(factor))),
5706 ]
5707 }
5708
5709 #[test]
5710 fn default_calendar_on_text_reports_hint() {
5711 let mut specs = TypeSpecification::text();
5712 let mut default = None;
5713 let err = specs
5714 .apply_constraint(
5715 "notes",
5716 TypeConstraintCommand::Suggest,
5717 &[month_suggestion_arg()],
5718 &mut default,
5719 )
5720 .unwrap_err();
5721 assert!(err.contains("Unit 'month' is for calendar data"));
5722 assert!(err.contains("double quotes"));
5723 }
5724
5725 #[test]
5726 fn default_calendar_on_duration_reports_valid_units() {
5727 let mut specs = TypeSpecification::measure();
5728 specs
5729 .apply_constraint(
5730 "duration",
5731 TypeConstraintCommand::Unit,
5732 &unit_factor_arg("second", 1),
5733 &mut None,
5734 )
5735 .unwrap();
5736 specs
5737 .apply_constraint(
5738 "duration",
5739 TypeConstraintCommand::Unit,
5740 &unit_factor_arg("week", 604_800),
5741 &mut None,
5742 )
5743 .unwrap();
5744 specs
5745 .apply_constraint(
5746 "duration",
5747 TypeConstraintCommand::Trait,
5748 &[CommandArg::Label("duration".to_string())],
5749 &mut None,
5750 )
5751 .unwrap();
5752 let mut default = None;
5753 let err = specs
5754 .apply_constraint(
5755 "duration",
5756 TypeConstraintCommand::Suggest,
5757 &[month_suggestion_arg()],
5758 &mut default,
5759 )
5760 .unwrap_err();
5761 assert!(err.contains("Unit 'month' is for calendar data"));
5762 assert!(err.contains("Valid 'duration' units are"));
5763 assert!(err.contains("week"));
5764 }
5765
5766 #[test]
5767 fn default_valid_duration_weeks_accepted() {
5768 let mut specs = TypeSpecification::measure();
5769 specs
5770 .apply_constraint(
5771 "duration",
5772 TypeConstraintCommand::Unit,
5773 &unit_factor_arg("second", 1),
5774 &mut None,
5775 )
5776 .unwrap();
5777 specs
5778 .apply_constraint(
5779 "duration",
5780 TypeConstraintCommand::Unit,
5781 &unit_factor_arg("week", 604_800),
5782 &mut None,
5783 )
5784 .unwrap();
5785 specs
5786 .apply_constraint(
5787 "duration",
5788 TypeConstraintCommand::Trait,
5789 &[CommandArg::Label("duration".to_string())],
5790 &mut None,
5791 )
5792 .unwrap();
5793 let mut default = None;
5794 specs
5795 .apply_constraint(
5796 "duration",
5797 TypeConstraintCommand::Suggest,
5798 &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
5799 Decimal::from(4),
5800 "week".to_string(),
5801 ))],
5802 &mut default,
5803 )
5804 .unwrap();
5805 assert!(matches!(
5806 default,
5807 Some(RawSuggestion::Measure {
5808 unit_name,
5809 ..
5810 }) if unit_name == "week"
5811 ));
5812 }
5813
5814 #[test]
5815 fn default_unknown_unit_on_duration_lists_valid_units() {
5816 let mut specs = TypeSpecification::measure();
5817 specs
5818 .apply_constraint(
5819 "duration",
5820 TypeConstraintCommand::Unit,
5821 &unit_factor_arg("second", 1),
5822 &mut None,
5823 )
5824 .unwrap();
5825 specs
5826 .apply_constraint(
5827 "duration",
5828 TypeConstraintCommand::Trait,
5829 &[CommandArg::Label("duration".to_string())],
5830 &mut None,
5831 )
5832 .unwrap();
5833 let mut default = None;
5834 let err = specs
5835 .apply_constraint(
5836 "duration",
5837 TypeConstraintCommand::Suggest,
5838 &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
5839 Decimal::ONE,
5840 "fortnight".to_string(),
5841 ))],
5842 &mut default,
5843 )
5844 .unwrap_err();
5845 assert!(err.contains("fortnight"));
5846 assert!(err.contains("not defined on 'duration'"));
5847 assert!(err.contains("Valid units are"));
5848 }
5849
5850 fn money_measure_type() -> LemmaType {
5851 LemmaType::new(
5852 "Money".to_string(),
5853 TypeSpecification::Measure {
5854 minimum: None,
5855 maximum: None,
5856 decimals: None,
5857 units: MeasureUnits::from(vec![
5858 MeasureUnit {
5859 name: "eur".to_string(),
5860 factor: crate::computation::rational::rational_one(),
5861 derived_measure_factors: Vec::new(),
5862 decomposition: BaseMeasureVector::new(),
5863 minimum: None,
5864 maximum: None,
5865 suggestion_magnitude: None,
5866 },
5867 MeasureUnit {
5868 name: "usd".to_string(),
5869 factor: crate::computation::rational::decimal_to_rational(Decimal::new(
5870 91, 2,
5871 ))
5872 .expect("factor"),
5873 derived_measure_factors: Vec::new(),
5874 decomposition: BaseMeasureVector::new(),
5875 minimum: None,
5876 maximum: None,
5877 suggestion_magnitude: None,
5878 },
5879 ]),
5880 traits: Vec::new(),
5881 decomposition: None,
5882 help: String::new(),
5883 },
5884 TypeExtends::Primitive,
5885 )
5886 }
5887
5888 #[test]
5889 fn measure_unit_names_for_named_measure() {
5890 let money = money_measure_type();
5891 assert_eq!(money.measure_unit_names(), Some(vec!["eur", "usd"]));
5892 }
5893
5894 fn sig(pairs: &[(&str, i32)]) -> Vec<(String, i32)> {
5899 pairs.iter().map(|(s, e)| (s.to_string(), *e)).collect()
5900 }
5901
5902 #[test]
5903 fn combine_signatures_multiply_adds_exponents() {
5904 let left = sig(&[("eur", 1)]);
5905 let right = sig(&[("hour", -1)]);
5906 let result = combine_signatures(&left, &right, true);
5907 assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
5908 }
5909
5910 #[test]
5911 fn combine_signatures_divide_subtracts_exponents() {
5912 let left = sig(&[("eur", 1)]);
5913 let right = sig(&[("hour", 1)]);
5914 let result = combine_signatures(&left, &right, false);
5915 assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
5916 }
5917
5918 #[test]
5919 fn combine_signatures_cancels_to_empty() {
5920 let left = sig(&[("ce", 1), ("minute", -1)]);
5921 let right = sig(&[("minute", 1)]);
5922 let result = combine_signatures(&left, &right, true);
5923 assert_eq!(result, sig(&[("ce", 1)]));
5925 }
5926
5927 #[test]
5928 fn combine_signatures_output_is_canonical_form() {
5929 let left = sig(&[("eur", 1), ("hour", 1)]);
5930 let right = sig(&[("minute", 1)]);
5931 let result = combine_signatures(&left, &right, false); let expected = sig(&[("eur", 1), ("hour", 1), ("minute", -1)]);
5934 assert_eq!(result, expected);
5935 }
5936
5937 #[test]
5938 fn canonicalize_signature_drops_zero_exponents() {
5939 let sig_with_zero = sig(&[("eur", 1), ("hour", 0), ("minute", -1)]);
5940 let result = canonicalize_signature(&sig_with_zero);
5941 assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
5942 }
5943
5944 #[test]
5945 fn canonicalize_signature_sorts_by_name() {
5946 let unsorted = sig(&[("minute", -1), ("eur", 1)]);
5947 let result = canonicalize_signature(&unsorted);
5948 assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
5949 }
5950
5951 #[test]
5957 fn format_signature_operator_style_numerator_only() {
5958 let signature = sig(&[("eur", 1)]);
5959 let result = format_signature_operator_style(&signature);
5960 assert_eq!(result, "eur");
5961 }
5962
5963 #[test]
5964 fn format_signature_operator_style_with_denominator() {
5965 let signature = sig(&[("eur", 1), ("hour", -1)]);
5966 let result = format_signature_operator_style(&signature);
5967 assert_eq!(result, "eur/hour");
5968 }
5969
5970 #[test]
5971 fn format_signature_operator_style_denominator_only() {
5972 let signature = sig(&[("meter", -1)]);
5973 let result = format_signature_operator_style(&signature);
5974 assert_eq!(result, "1/meter");
5975 }
5976
5977 #[test]
5978 fn format_signature_operator_style_with_exponents() {
5979 let signature = sig(&[("meter", 2), ("second", -2)]);
5980 let result = format_signature_operator_style(&signature);
5981 assert_eq!(result, "meter^2/second^2");
5982 }
5983
5984 #[test]
5989 fn calendar_unit_factor_table_completeness() {
5990 for unit in &[SemanticCalendarUnit::Month, SemanticCalendarUnit::Year] {
5993 let name = unit.to_string();
5994 assert!(
5995 calendar_unit_factor(&name).is_some(),
5996 "calendar_unit_factor('{}') must return Some",
5997 name
5998 );
5999 }
6000 }
6001
6002 #[test]
6003 fn semantic_calendar_unit_display_returns_singular() {
6004 assert_eq!(SemanticCalendarUnit::Month.to_string(), "month");
6007 assert_eq!(SemanticCalendarUnit::Year.to_string(), "year");
6008 }
6009
6010 #[test]
6015 fn signature_factor_with_calendar_units() {
6016 let calendar = test_calendar_type_for_signature_factor();
6017 let unit_index = crate::planning::unit_index::UnitIndex::new();
6018 let sig_month_per_year = sig(&[("month", 1), ("year", -1)]);
6021 let factor = signature_factor(&sig_month_per_year, &unit_index, Some(&calendar))
6022 .expect("must not overflow");
6023 let expected = rational_new(1, 12);
6024 assert_eq!(factor, expected, "month/year factor must be 1/12");
6025 }
6026
6027 fn test_calendar_type_for_signature_factor() -> LemmaType {
6028 use crate::computation::rational::{decimal_to_rational, rational_one};
6029 use crate::literals::{MeasureUnit, MeasureUnits};
6030 use rust_decimal::Decimal;
6031 LemmaType::new(
6032 "calendar".to_string(),
6033 TypeSpecification::Measure {
6034 minimum: None,
6035 maximum: None,
6036 decimals: None,
6037 units: MeasureUnits::from(vec![
6038 MeasureUnit {
6039 name: "month".to_string(),
6040 factor: rational_one(),
6041 minimum: None,
6042 maximum: None,
6043 suggestion_magnitude: None,
6044 decomposition: calendar_decomposition(),
6045 derived_measure_factors: Vec::new(),
6046 },
6047 MeasureUnit {
6048 name: "year".to_string(),
6049 factor: decimal_to_rational(Decimal::from(12)).expect("year factor"),
6050 minimum: None,
6051 maximum: None,
6052 suggestion_magnitude: None,
6053 decomposition: calendar_decomposition(),
6054 derived_measure_factors: Vec::new(),
6055 },
6056 ]),
6057 traits: vec![MeasureTrait::Calendar],
6058 decomposition: Some(calendar_decomposition()),
6059 help: String::new(),
6060 },
6061 TypeExtends::Primitive,
6062 )
6063 }
6064
6065 #[test]
6066 #[should_panic(expected = "BUG: signature_factor called with unresolved unit name")]
6067 fn signature_factor_panics_on_unresolved_name() {
6068 let unit_index = crate::planning::unit_index::UnitIndex::new();
6069 let bad_sig = sig(&[("nonexistent_unit_xyz", 1)]);
6070 let _ = signature_factor(&bad_sig, &unit_index, None);
6071 }
6072
6073 #[test]
6074 fn signature_factor_uses_owner_when_expression_index_empty() {
6075 let money = test_money_type_for_signature_factor();
6076 let expression_units = crate::planning::unit_index::UnitIndex::new();
6077 let sig_usd = sig(&[("usd", 1)]);
6078 let factor =
6079 signature_factor(&sig_usd, &expression_units, Some(&money)).expect("must not overflow");
6080 assert_eq!(factor, rational_new(91, 100));
6081 }
6082
6083 fn test_money_type_for_signature_factor() -> LemmaType {
6084 use crate::computation::rational::decimal_to_rational;
6085 use crate::literals::{MeasureUnit, MeasureUnits};
6086 use rust_decimal::Decimal;
6087 LemmaType::new(
6088 "money".to_string(),
6089 TypeSpecification::Measure {
6090 minimum: None,
6091 maximum: None,
6092 decimals: Some(2),
6093 units: MeasureUnits::from(vec![
6094 MeasureUnit {
6095 name: "eur".to_string(),
6096 factor: crate::computation::rational::rational_one(),
6097 minimum: None,
6098 maximum: None,
6099 suggestion_magnitude: None,
6100 decomposition: BaseMeasureVector::new(),
6101 derived_measure_factors: Vec::new(),
6102 },
6103 MeasureUnit {
6104 name: "usd".to_string(),
6105 factor: decimal_to_rational(Decimal::new(91, 2)).expect("usd factor"),
6106 minimum: None,
6107 maximum: None,
6108 suggestion_magnitude: None,
6109 decomposition: BaseMeasureVector::new(),
6110 derived_measure_factors: Vec::new(),
6111 },
6112 ]),
6113 traits: Vec::new(),
6114 decomposition: None,
6115 help: String::new(),
6116 },
6117 TypeExtends::Primitive,
6118 )
6119 }
6120
6121 fn measure_type_with_kilogram() -> TypeSpecification {
6122 use crate::computation::rational::rational_one;
6123 use crate::literals::{MeasureUnit, MeasureUnits};
6124 let mut units = MeasureUnits::new();
6125 units.push(MeasureUnit {
6126 name: "kilogram".to_string(),
6127 factor: rational_one(),
6128 minimum: None,
6129 maximum: None,
6130 suggestion_magnitude: None,
6131 decomposition: BaseMeasureVector::new(),
6132 derived_measure_factors: Vec::new(),
6133 });
6134 TypeSpecification::Measure {
6135 minimum: None,
6136 maximum: None,
6137 decimals: None,
6138 units,
6139 traits: Vec::new(),
6140 decomposition: None,
6141 help: String::new(),
6142 }
6143 }
6144
6145 #[test]
6146 fn parser_value_to_value_kind_rejects_bare_number_for_measure() {
6147 let ten = Value::Number(Decimal::from(10));
6148 let err = parser_value_to_value_kind(&ten, &measure_type_with_kilogram())
6149 .expect_err("bare number must not bind to measure");
6150 assert!(
6151 err.contains("kilogram"),
6152 "error must hint expected unit, got: {err}"
6153 );
6154 }
6155
6156 #[test]
6157 fn parser_value_to_value_kind_accepts_number_with_unit_for_measure() {
6158 let ten_kg = Value::NumberWithUnit(Decimal::from(10), "kilogram".to_string());
6159 let kind = parser_value_to_value_kind(&ten_kg, &measure_type_with_kilogram())
6160 .expect("10 kilogram must bind to measure");
6161 assert!(matches!(kind, ValueKind::Measure(_, _)));
6162 }
6163
6164 #[test]
6165 fn parser_value_to_value_kind_accepts_bare_number_for_ratio() {
6166 let ten = Value::Number(Decimal::from(10));
6167 let kind =
6168 parser_value_to_value_kind(&ten, &TypeSpecification::ratio()).expect("number -> ratio");
6169 assert!(matches!(kind, ValueKind::Ratio(_, None)));
6170 }
6171
6172 #[test]
6173 fn value_kind_matches_spec_rejects_number_for_measure() {
6174 let n = ValueKind::Number(rational_new(10, 1));
6175 assert!(!value_kind_matches_spec(&n, &measure_type_with_kilogram()));
6176 }
6177
6178 #[test]
6179 fn apply_constraint_rejects_inherited_unit_factor_change() {
6180 let mut specs = TypeSpecification::measure();
6181 specs
6182 .apply_constraint(
6183 "money",
6184 TypeConstraintCommand::Unit,
6185 &unit_factor_arg("eur", 1),
6186 &mut None,
6187 )
6188 .expect("seed eur");
6189 let err = specs
6190 .apply_constraint(
6191 "money",
6192 TypeConstraintCommand::Unit,
6193 &[
6194 CommandArg::Label("eur".to_string()),
6195 CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::new(11, 1))),
6196 ],
6197 &mut None,
6198 )
6199 .expect_err("must not change inherited unit factor");
6200 assert!(err.contains("eur"), "error must name unit, got: {err}");
6201 assert!(
6202 err.contains("inherited") || err.contains("cannot change"),
6203 "error must reject factor change, got: {err}"
6204 );
6205 }
6206
6207 #[test]
6208 fn apply_constraint_allows_additive_unit_on_inherited_spec() {
6209 let mut specs = TypeSpecification::measure();
6210 specs
6211 .apply_constraint(
6212 "money",
6213 TypeConstraintCommand::Unit,
6214 &unit_factor_arg("eur", 1),
6215 &mut None,
6216 )
6217 .expect("seed eur");
6218 specs
6219 .apply_constraint(
6220 "money",
6221 TypeConstraintCommand::Unit,
6222 &unit_factor_arg("usd", 1),
6223 &mut None,
6224 )
6225 .expect("add usd");
6226 match &specs {
6227 TypeSpecification::Measure { units, .. } => assert_eq!(units.len(), 2),
6228 other => panic!("expected Measure, got {other:?}"),
6229 }
6230 }
6231
6232 #[test]
6233 fn apply_constraint_idempotent_inherited_unit_redeclare() {
6234 let mut specs = TypeSpecification::measure();
6235 specs
6236 .apply_constraint(
6237 "money",
6238 TypeConstraintCommand::Unit,
6239 &unit_factor_arg("eur", 1),
6240 &mut None,
6241 )
6242 .expect("seed eur");
6243 specs
6244 .apply_constraint(
6245 "money",
6246 TypeConstraintCommand::Unit,
6247 &unit_factor_arg("eur", 1),
6248 &mut None,
6249 )
6250 .expect("idempotent eur");
6251 match &specs {
6252 TypeSpecification::Measure { units, .. } => {
6253 assert_eq!(units.len(), 1);
6254 assert_eq!(
6255 units.iter().find(|u| u.name == "eur").expect("eur").factor,
6256 crate::computation::rational::rational_one()
6257 );
6258 }
6259 other => panic!("expected Measure, got {other:?}"),
6260 }
6261 }
6262
6263 #[test]
6264 fn element_from_range_returns_element_for_every_range_primitive() {
6265 type RangeElementMatcher = fn(&TypeSpecification) -> bool;
6266 let cases: [(PrimitiveKind, RangeElementMatcher); 5] = [
6267 (PrimitiveKind::NumberRange, |element| {
6268 matches!(element, TypeSpecification::Number { .. })
6269 }),
6270 (PrimitiveKind::MeasureRange, |element| {
6271 matches!(element, TypeSpecification::Measure { .. })
6272 }),
6273 (PrimitiveKind::RatioRange, |element| {
6274 matches!(element, TypeSpecification::Ratio { .. })
6275 }),
6276 (PrimitiveKind::DateRange, |element| {
6277 matches!(element, TypeSpecification::Date { .. })
6278 }),
6279 (PrimitiveKind::TimeRange, |element| {
6280 matches!(element, TypeSpecification::Time { .. })
6281 }),
6282 ];
6283 for (kind, matches_element) in cases {
6284 let range_spec = type_spec_for_primitive(kind);
6285 let element = range_spec
6286 .element_from_range()
6287 .unwrap_or_else(|| panic!("{kind:?} must define element_from_range"));
6288 assert!(
6289 matches_element(&element),
6290 "{kind:?} element must match documented mapping, got {element:?}"
6291 );
6292 }
6293 }
6294
6295 #[test]
6296 fn element_from_range_returns_none_for_non_range_primitives() {
6297 let non_range = [
6298 type_spec_for_primitive(PrimitiveKind::Boolean),
6299 type_spec_for_primitive(PrimitiveKind::Measure),
6300 TypeSpecification::Undetermined,
6301 TypeSpecification::veto(),
6302 ];
6303 for spec in non_range {
6304 assert!(
6305 spec.element_from_range().is_none(),
6306 "{spec:?} must not define element_from_range"
6307 );
6308 }
6309 }
6310}