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(
1690 &mut self,
1691 type_name: &str,
1692 command: TypeConstraintCommand,
1693 args: &[CommandArg],
1694 declared_suggestion: &mut Option<RawSuggestion>,
1695 ) -> Result<(), String> {
1696 if command == TypeConstraintCommand::Trait
1697 && !matches!(&self, TypeSpecification::Measure { .. })
1698 {
1699 return Err("trait command is only valid on measure types".to_string());
1700 }
1701 match self {
1702 TypeSpecification::Boolean { help } => match command {
1703 TypeConstraintCommand::Help => {
1704 apply_type_help_command(help, args)?;
1705 }
1706 TypeConstraintCommand::Suggest => {
1707 let lit = require_literal(args, "suggest")?;
1708 reject_calendar_for_suggestion(
1709 lit,
1710 type_name,
1711 SuggestionExpectation::Boolean,
1712 None,
1713 )?;
1714 match lit {
1715 crate::literals::Value::Boolean(bv) => {
1716 *declared_suggestion =
1717 Some(RawSuggestion::Value(ValueKind::Boolean(bool::from(bv))));
1718 }
1719 _ => {
1720 return Err(
1721 "Please provide true or false, for example `-> suggest true`."
1722 .to_string(),
1723 );
1724 }
1725 }
1726 }
1727 other => {
1728 return Err(format!(
1729 "Invalid command '{}' for boolean type. Valid commands: help, suggest",
1730 other
1731 ));
1732 }
1733 },
1734 TypeSpecification::Measure {
1735 decimals,
1736 minimum,
1737 maximum,
1738 units,
1739 traits,
1740 help,
1741 ..
1742 } => match command {
1743 TypeConstraintCommand::Decimals => {
1744 let d = require_decimal_literal(args, "decimals")?;
1745 *decimals = Some(decimal_to_u8(d, "decimals")?);
1746 }
1747 TypeConstraintCommand::Unit => {
1748 let (unit_name, value, derived_measure_factors) = match args {
1749 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
1750 (name.clone(), *v, Vec::new())
1751 }
1752 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Expr(
1753 prefix,
1754 factors,
1755 ))] => {
1756 let raw: Vec<(String, i32)> = factors
1757 .iter()
1758 .map(|f| (f.measure_ref.clone(), f.exp))
1759 .collect();
1760 (name.clone(), *prefix, raw)
1761 }
1762 _ => {
1763 return Err(
1764 "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')"
1765 .to_string(),
1766 );
1767 }
1768 };
1769 if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
1770 let new_factor = crate::computation::rational::decimal_to_rational(value)
1771 .map_err(|failure| failure.to_string())?;
1772 if existing.factor != new_factor
1773 || existing.derived_measure_factors != derived_measure_factors
1774 {
1775 return Err(format!(
1776 "Unit '{unit_name}' is already defined in this type's inherited units; \
1777 cannot change factor or decomposition. Add a new unit name instead."
1778 ));
1779 }
1780 } else {
1781 units.0.push(MeasureUnit::from_decimal_factor(
1782 unit_name,
1783 value,
1784 derived_measure_factors,
1785 )?);
1786 }
1787 }
1788 TypeConstraintCommand::Trait => {
1789 let measure_trait = parse_measure_trait(args)?;
1790 if traits.contains(&measure_trait) {
1791 return Err(format!(
1792 "Duplicate trait '{}' for measure type.",
1793 measure_trait_name(measure_trait)
1794 ));
1795 }
1796 if measure_trait == MeasureTrait::Duration {
1797 validate_duration_trait_requirements(units)?;
1798 }
1799 if measure_trait == MeasureTrait::Calendar {
1800 validate_calendar_trait_requirements(units)?;
1801 }
1802 traits.push(measure_trait);
1803 }
1804 TypeConstraintCommand::Minimum => {
1805 *minimum = Some(parse_measure_declared_bound(
1806 args, "minimum", units, type_name,
1807 )?);
1808 }
1809 TypeConstraintCommand::Maximum => {
1810 *maximum = Some(parse_measure_declared_bound(
1811 args, "maximum", units, type_name,
1812 )?);
1813 }
1814 TypeConstraintCommand::Help => {
1815 apply_type_help_command(help, args)?;
1816 }
1817 TypeConstraintCommand::Suggest => {
1818 let lit = require_literal(args, "suggest")?;
1819 if !traits.contains(&MeasureTrait::Calendar) {
1820 reject_calendar_for_suggestion(
1821 lit,
1822 type_name,
1823 SuggestionExpectation::MeasureUnits,
1824 Some(units),
1825 )?;
1826 }
1827 match lit {
1828 crate::literals::Value::NumberWithUnit(_, _) => {
1829 let (magnitude, unit_name) =
1830 parse_measure_declared_bound(args, "suggest", units, type_name)?;
1831 *declared_suggestion = Some(RawSuggestion::Measure {
1832 magnitude,
1833 unit_name,
1834 });
1835 }
1836 _ => {
1837 return Err(measure_suggestion_wrong_shape_error(type_name, traits));
1838 }
1839 }
1840 }
1841 _ => {
1842 return Err(format!(
1843 "Invalid command '{}' for measure type. Valid commands: unit, trait, minimum, maximum, decimals, help, suggest",
1844 command
1845 ));
1846 }
1847 },
1848 TypeSpecification::Number {
1849 decimals,
1850 minimum,
1851 maximum,
1852 help,
1853 } => match command {
1854 TypeConstraintCommand::Decimals => {
1855 let d = require_decimal_literal(args, "decimals")?;
1856 *decimals = Some(decimal_to_u8(d, "decimals")?);
1857 }
1858 TypeConstraintCommand::Unit => {
1859 return Err(
1860 "Invalid command 'unit' for number type. Number types are dimensionless and cannot have units. Use 'measure' type instead.".to_string()
1861 );
1862 }
1863 TypeConstraintCommand::Minimum => {
1864 *minimum = Some(require_decimal_literal(args, "minimum")?);
1865 }
1866 TypeConstraintCommand::Maximum => {
1867 *maximum = Some(require_decimal_literal(args, "maximum")?);
1868 }
1869 TypeConstraintCommand::Help => {
1870 apply_type_help_command(help, args)?;
1871 }
1872 TypeConstraintCommand::Suggest => {
1873 let lit = require_literal(args, "suggest")?;
1874 reject_calendar_for_suggestion(
1875 lit,
1876 type_name,
1877 SuggestionExpectation::Number,
1878 None,
1879 )?;
1880 match lit {
1881 crate::literals::Value::Number(d) => {
1882 *declared_suggestion = Some(RawSuggestion::Value(ValueKind::Number(
1883 lift_parser_decimal(*d)?,
1884 )));
1885 }
1886 _ => {
1887 return Err(
1888 "Please provide a number, for example `-> suggest 42`.".to_string()
1889 );
1890 }
1891 }
1892 }
1893 _ => {
1894 return Err(format!(
1895 "Invalid command '{}' for number type. Valid commands: minimum, maximum, decimals, help, suggest",
1896 command
1897 ));
1898 }
1899 },
1900 TypeSpecification::NumberRange {
1901 lower,
1902 upper,
1903 minimum,
1904 maximum,
1905 help,
1906 } => match command {
1907 TypeConstraintCommand::Lower => {
1908 *lower = Some(require_decimal_literal(args, "lower")?);
1909 }
1910 TypeConstraintCommand::Upper => {
1911 *upper = Some(require_decimal_literal(args, "upper")?);
1912 }
1913 TypeConstraintCommand::Minimum => {
1914 let width = require_decimal_literal(args, "minimum")?;
1915 reject_negative_width_magnitude(&width, "minimum")?;
1916 *minimum = Some(width);
1917 }
1918 TypeConstraintCommand::Maximum => {
1919 let width = require_decimal_literal(args, "maximum")?;
1920 reject_negative_width_magnitude(&width, "maximum")?;
1921 *maximum = Some(width);
1922 }
1923 TypeConstraintCommand::Help => {
1924 apply_type_help_command(help, args)?;
1925 }
1926 TypeConstraintCommand::Suggest => {
1927 let (left, right) = require_suggestion_range_endpoints(
1928 args,
1929 type_name,
1930 SuggestionExpectation::NumberRange,
1931 None,
1932 )?;
1933 let left = literal_value_from_parser_value(left)?;
1934 let right = literal_value_from_parser_value(right)?;
1935 if !left.lemma_type.is_number() || !right.lemma_type.is_number() {
1936 return Err(
1937 "Please provide a number range, for example `-> suggest 10...100`."
1938 .to_string(),
1939 );
1940 }
1941 *declared_suggestion = Some(RawSuggestion::Value(ValueKind::Range(
1942 Box::new(left),
1943 Box::new(right),
1944 )));
1945 }
1946 _ => {
1947 return Err(format!(
1948 "Invalid command '{}' for number range type. Valid commands: lower, upper, minimum, maximum, help, suggest",
1949 command
1950 ));
1951 }
1952 },
1953 TypeSpecification::Ratio {
1954 decimals,
1955 minimum,
1956 maximum,
1957 units,
1958 help,
1959 } => match command {
1960 TypeConstraintCommand::Decimals => {
1961 let d = require_decimal_literal(args, "decimals")?;
1962 *decimals = Some(decimal_to_u8(d, "decimals")?);
1963 }
1964 TypeConstraintCommand::Unit => {
1965 let (unit_name, value_dec) = match args {
1966 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
1967 (name.clone(), *v)
1968 }
1969 _ => {
1970 return Err(
1971 "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."
1972 .to_string(),
1973 );
1974 }
1975 };
1976 let value = crate::computation::rational::decimal_to_rational(value_dec)
1977 .map_err(|failure| {
1978 format!(
1979 "ratio unit value is not exactly representable as a rational: {}",
1980 failure
1981 )
1982 })?;
1983 if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
1984 if existing.value != value {
1985 return Err(format!(
1986 "Unit '{unit_name}' is already defined in this type's inherited units; \
1987 cannot change factor. Add a new unit name instead."
1988 ));
1989 }
1990 } else {
1991 units.0.push(RatioUnit {
1992 name: unit_name,
1993 value,
1994 minimum: None,
1995 maximum: None,
1996 suggestion_magnitude: None,
1997 });
1998 }
1999 }
2000 TypeConstraintCommand::Minimum => {
2001 let canonical = ratio_bound_to_canonical_rational(args, "minimum", units)?;
2002 sync_ratio_units_from_canonical(
2003 units,
2004 &canonical,
2005 UnitConstraintField::Minimum,
2006 )?;
2007 *minimum = Some(canonical);
2008 }
2009 TypeConstraintCommand::Maximum => {
2010 let canonical = ratio_bound_to_canonical_rational(args, "maximum", units)?;
2011 sync_ratio_units_from_canonical(
2012 units,
2013 &canonical,
2014 UnitConstraintField::Maximum,
2015 )?;
2016 *maximum = Some(canonical);
2017 }
2018 TypeConstraintCommand::Help => {
2019 apply_type_help_command(help, args)?;
2020 }
2021 TypeConstraintCommand::Suggest => {
2022 let lit = require_literal(args, "suggest")?;
2023 reject_calendar_for_suggestion(
2024 lit,
2025 type_name,
2026 SuggestionExpectation::Ratio,
2027 None,
2028 )?;
2029 let default = match lit {
2030 crate::literals::Value::NumberWithUnit(_, _) => {
2031 let element_spec = TypeSpecification::Ratio {
2032 decimals: *decimals,
2033 minimum: minimum.clone(),
2034 maximum: maximum.clone(),
2035 units: units.clone(),
2036 help: help.clone(),
2037 };
2038 parser_value_to_value_kind(lit, &element_spec)?
2039 }
2040 other => {
2041 return Err(format!(
2042 "suggest requires a ratio literal with a unit, got {}. Please provide a ratio value with a unit, for example `-> suggest 25%`.",
2043 value_kind_name(other)
2044 ));
2045 }
2046 };
2047 sync_ratio_suggestion_units(units, &default)?;
2048 *declared_suggestion = Some(RawSuggestion::Value(default));
2049 }
2050 _ => {
2051 return Err(format!(
2052 "Invalid command '{}' for ratio type. Valid commands: unit, minimum, maximum, decimals, help, suggest",
2053 command
2054 ));
2055 }
2056 },
2057 TypeSpecification::RatioRange {
2058 lower,
2059 upper,
2060 minimum,
2061 maximum,
2062 units,
2063 help,
2064 } => match command {
2065 TypeConstraintCommand::Unit => {
2066 let (unit_name, value_dec) = match args {
2067 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
2068 (name.clone(), *v)
2069 }
2070 _ => {
2071 return Err(
2072 "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."
2073 .to_string(),
2074 );
2075 }
2076 };
2077 let value = crate::computation::rational::decimal_to_rational(value_dec)
2078 .map_err(|e| {
2079 format!(
2080 "ratio unit value is not exactly representable as a rational: {e}"
2081 )
2082 })?;
2083 if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
2084 if existing.value != value {
2085 return Err(format!(
2086 "Unit '{unit_name}' is already defined in this type's inherited units; \
2087 cannot change factor. Add a new unit name instead."
2088 ));
2089 }
2090 } else {
2091 units.0.push(RatioUnit {
2092 name: unit_name,
2093 value,
2094 minimum: None,
2095 maximum: None,
2096 suggestion_magnitude: None,
2097 });
2098 }
2099 }
2100 TypeConstraintCommand::Lower => {
2101 *lower = Some(ratio_bound_to_canonical_rational(args, "lower", units)?);
2102 }
2103 TypeConstraintCommand::Upper => {
2104 *upper = Some(ratio_bound_to_canonical_rational(args, "upper", units)?);
2105 }
2106 TypeConstraintCommand::Minimum => {
2107 let width = ratio_bound_to_canonical_rational(args, "minimum", units)?;
2108 reject_negative_width_magnitude(&width, "minimum")?;
2109 *minimum = Some(width);
2110 }
2111 TypeConstraintCommand::Maximum => {
2112 let width = ratio_bound_to_canonical_rational(args, "maximum", units)?;
2113 reject_negative_width_magnitude(&width, "maximum")?;
2114 *maximum = Some(width);
2115 }
2116 TypeConstraintCommand::Help => {
2117 apply_type_help_command(help, args)?;
2118 }
2119 TypeConstraintCommand::Suggest => {
2120 let (left, right) = require_suggestion_range_endpoints(
2121 args,
2122 type_name,
2123 SuggestionExpectation::RatioRange,
2124 None,
2125 )?;
2126 let element_spec = TypeSpecification::RatioRange {
2127 lower: lower.clone(),
2128 upper: upper.clone(),
2129 minimum: minimum.clone(),
2130 maximum: maximum.clone(),
2131 units: units.clone(),
2132 help: help.clone(),
2133 }
2134 .element_from_range()
2135 .expect("BUG: RatioRange must define element_from_range");
2136 let left = lift_range_endpoint(left, &element_spec)?;
2137 let right = lift_range_endpoint(right, &element_spec)?;
2138 if !left.lemma_type.is_ratio() || !right.lemma_type.is_ratio() {
2139 return Err(
2140 "Please provide a ratio range, for example `-> suggest 10%...50%`."
2141 .to_string(),
2142 );
2143 }
2144 *declared_suggestion = Some(RawSuggestion::Value(ValueKind::Range(
2145 Box::new(left),
2146 Box::new(right),
2147 )));
2148 }
2149 _ => {
2150 return Err(format!(
2151 "Invalid command '{}' for ratio range type. Valid commands: unit, lower, upper, minimum, maximum, help, suggest",
2152 command
2153 ));
2154 }
2155 },
2156 TypeSpecification::Text {
2157 length,
2158 options,
2159 help,
2160 } => match command {
2161 TypeConstraintCommand::Option => {
2162 if args.len() != 1 {
2163 return Err("option takes exactly one argument".to_string());
2164 }
2165 options.push(option_name(&args[0], "option")?);
2166 }
2167 TypeConstraintCommand::Options => {
2168 let mut collected = Vec::with_capacity(args.len());
2169 for arg in args {
2170 collected.push(option_name(arg, "options")?);
2171 }
2172 *options = collected;
2173 }
2174 TypeConstraintCommand::Length => {
2175 let d = require_decimal_literal(args, "length")?;
2176 *length = Some(decimal_to_usize(d, "length")?);
2177 }
2178 TypeConstraintCommand::Help => {
2179 apply_type_help_command(help, args)?;
2180 }
2181 TypeConstraintCommand::Suggest => {
2182 let lit = require_literal(args, "suggest")?;
2183 reject_calendar_for_suggestion(
2184 lit,
2185 type_name,
2186 SuggestionExpectation::Text,
2187 None,
2188 )?;
2189 match lit {
2190 crate::literals::Value::Text(s) => {
2191 *declared_suggestion =
2192 Some(RawSuggestion::Value(ValueKind::Text(s.clone())));
2193 }
2194 _ => {
2195 return Err(
2196 "Please provide a text value in double quotes, for example `-> suggest \"my default value\"`."
2197 .to_string(),
2198 );
2199 }
2200 }
2201 }
2202 _ => {
2203 return Err(format!(
2204 "Invalid command '{}' for text type. Valid commands: options, length, help, suggest",
2205 command
2206 ));
2207 }
2208 },
2209 TypeSpecification::Date {
2210 minimum,
2211 maximum,
2212 help,
2213 } => match command {
2214 TypeConstraintCommand::Minimum => {
2215 let dt = require_date_literal(args, "minimum")?;
2216 *minimum = Some(dt);
2217 }
2218 TypeConstraintCommand::Maximum => {
2219 let dt = require_date_literal(args, "maximum")?;
2220 *maximum = Some(dt);
2221 }
2222 TypeConstraintCommand::Help => {
2223 apply_type_help_command(help, args)?;
2224 }
2225 TypeConstraintCommand::Suggest => {
2226 let lit = require_literal(args, "suggest")?;
2227 reject_calendar_for_suggestion(
2228 lit,
2229 type_name,
2230 SuggestionExpectation::Date,
2231 None,
2232 )?;
2233 match lit {
2234 crate::literals::Value::Date(dt) => {
2235 *declared_suggestion = Some(RawSuggestion::Value(ValueKind::Date(
2236 date_time_to_semantic(dt),
2237 )));
2238 }
2239 _ => {
2240 return Err(
2241 "Please provide a date, for example `-> suggest 2024-06-15`."
2242 .to_string(),
2243 );
2244 }
2245 }
2246 }
2247 _ => {
2248 return Err(format!(
2249 "Invalid command '{}' for date type. Valid commands: minimum, maximum, help, suggest",
2250 command
2251 ));
2252 }
2253 },
2254 TypeSpecification::DateRange {
2255 lower,
2256 upper,
2257 minimum,
2258 maximum,
2259 help,
2260 } => match command {
2261 TypeConstraintCommand::Lower => {
2262 *lower = Some(require_date_literal(args, "lower")?);
2263 }
2264 TypeConstraintCommand::Upper => {
2265 *upper = Some(require_date_literal(args, "upper")?);
2266 }
2267 TypeConstraintCommand::Minimum => {
2268 *minimum = Some(parse_unresolved_width_bound(args, "minimum")?);
2269 }
2270 TypeConstraintCommand::Maximum => {
2271 *maximum = Some(parse_unresolved_width_bound(args, "maximum")?);
2272 }
2273 TypeConstraintCommand::Help => {
2274 apply_type_help_command(help, args)?;
2275 }
2276 TypeConstraintCommand::Suggest => {
2277 let (left, right) = require_suggestion_range_endpoints(
2278 args,
2279 type_name,
2280 SuggestionExpectation::DateRange,
2281 None,
2282 )?;
2283 let left = literal_value_from_parser_value(left)?;
2284 let right = literal_value_from_parser_value(right)?;
2285 if !left.lemma_type.is_date() || !right.lemma_type.is_date() {
2286 return Err(
2287 "Please provide a date range, for example `-> suggest 2024-01-01...2024-12-31`."
2288 .to_string(),
2289 );
2290 }
2291 *declared_suggestion = Some(RawSuggestion::Value(ValueKind::Range(
2292 Box::new(left),
2293 Box::new(right),
2294 )));
2295 }
2296 _ => {
2297 return Err(format!(
2298 "Invalid command '{}' for date range type. Valid commands: lower, upper, minimum, maximum, help, suggest",
2299 command
2300 ));
2301 }
2302 },
2303 TypeSpecification::Time {
2304 minimum,
2305 maximum,
2306 help,
2307 } => match command {
2308 TypeConstraintCommand::Minimum => {
2309 let t = require_time_literal(args, "minimum")?;
2310 *minimum = Some(t);
2311 }
2312 TypeConstraintCommand::Maximum => {
2313 let t = require_time_literal(args, "maximum")?;
2314 *maximum = Some(t);
2315 }
2316 TypeConstraintCommand::Help => {
2317 apply_type_help_command(help, args)?;
2318 }
2319 TypeConstraintCommand::Suggest => {
2320 let lit = require_literal(args, "suggest")?;
2321 reject_calendar_for_suggestion(
2322 lit,
2323 type_name,
2324 SuggestionExpectation::Time,
2325 None,
2326 )?;
2327 match lit {
2328 crate::literals::Value::Time(t) => {
2329 *declared_suggestion =
2330 Some(RawSuggestion::Value(ValueKind::Time(time_to_semantic(t))));
2331 }
2332 _ => {
2333 return Err(
2334 "Please provide a time, for example `-> suggest 09:00:00`."
2335 .to_string(),
2336 );
2337 }
2338 }
2339 }
2340 _ => {
2341 return Err(format!(
2342 "Invalid command '{}' for time type. Valid commands: minimum, maximum, help, suggest",
2343 command
2344 ));
2345 }
2346 },
2347 TypeSpecification::TimeRange {
2348 lower,
2349 upper,
2350 minimum,
2351 maximum,
2352 help,
2353 } => match command {
2354 TypeConstraintCommand::Lower => {
2355 *lower = Some(require_time_literal(args, "lower")?);
2356 }
2357 TypeConstraintCommand::Upper => {
2358 *upper = Some(require_time_literal(args, "upper")?);
2359 }
2360 TypeConstraintCommand::Minimum => {
2361 *minimum = Some(parse_unresolved_width_bound(args, "minimum")?);
2362 }
2363 TypeConstraintCommand::Maximum => {
2364 *maximum = Some(parse_unresolved_width_bound(args, "maximum")?);
2365 }
2366 TypeConstraintCommand::Help => {
2367 apply_type_help_command(help, args)?;
2368 }
2369 TypeConstraintCommand::Suggest => {
2370 let (left, right) = require_suggestion_range_endpoints(
2371 args,
2372 type_name,
2373 SuggestionExpectation::TimeRange,
2374 None,
2375 )?;
2376 let left = literal_value_from_parser_value(left)?;
2377 let right = literal_value_from_parser_value(right)?;
2378 if !left.lemma_type.is_time() || !right.lemma_type.is_time() {
2379 return Err(
2380 "Please provide a time range, for example `-> suggest 09:00...17:00`."
2381 .to_string(),
2382 );
2383 }
2384 *declared_suggestion = Some(RawSuggestion::Value(ValueKind::Range(
2385 Box::new(left),
2386 Box::new(right),
2387 )));
2388 }
2389 _ => {
2390 return Err(format!(
2391 "Invalid command '{}' for time range type. Valid commands: lower, upper, minimum, maximum, help, suggest",
2392 command
2393 ));
2394 }
2395 },
2396 TypeSpecification::MeasureRange {
2397 lower,
2398 upper,
2399 minimum,
2400 maximum,
2401 units,
2402 decomposition,
2403 help,
2404 } => match command {
2405 TypeConstraintCommand::Unit => {
2406 let (unit_name, value, derived_measure_factors) = match args {
2407 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
2408 (name.clone(), *v, Vec::new())
2409 }
2410 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Expr(
2411 prefix,
2412 factors,
2413 ))] => {
2414 let raw: Vec<(String, i32)> = factors
2415 .iter()
2416 .map(|f| (f.measure_ref.clone(), f.exp))
2417 .collect();
2418 (name.clone(), *prefix, raw)
2419 }
2420 _ => {
2421 return Err(
2422 "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')"
2423 .to_string(),
2424 );
2425 }
2426 };
2427 if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
2428 let new_factor = crate::computation::rational::decimal_to_rational(value)
2429 .map_err(|failure| failure.to_string())?;
2430 if existing.factor != new_factor
2431 || existing.derived_measure_factors != derived_measure_factors
2432 {
2433 return Err(format!(
2434 "Unit '{unit_name}' is already defined in this type's inherited units; \
2435 cannot change factor or decomposition. Add a new unit name instead."
2436 ));
2437 }
2438 } else {
2439 units.0.push(MeasureUnit::from_decimal_factor(
2440 unit_name,
2441 value,
2442 derived_measure_factors,
2443 )?);
2444 }
2445 }
2446 TypeConstraintCommand::Lower => {
2447 *lower = Some(parse_measure_declared_bound(
2448 args, "lower", units, type_name,
2449 )?);
2450 }
2451 TypeConstraintCommand::Upper => {
2452 *upper = Some(parse_measure_declared_bound(
2453 args, "upper", units, type_name,
2454 )?);
2455 }
2456 TypeConstraintCommand::Minimum => {
2457 let width = parse_measure_declared_bound(args, "minimum", units, type_name)?;
2458 reject_negative_width_magnitude(&width.0, "minimum")?;
2459 *minimum = Some(width);
2460 }
2461 TypeConstraintCommand::Maximum => {
2462 let width = parse_measure_declared_bound(args, "maximum", units, type_name)?;
2463 reject_negative_width_magnitude(&width.0, "maximum")?;
2464 *maximum = Some(width);
2465 }
2466 TypeConstraintCommand::Help => {
2467 apply_type_help_command(help, args)?;
2468 }
2469 TypeConstraintCommand::Suggest => {
2470 let (left, right) = require_suggestion_range_endpoints(
2471 args,
2472 type_name,
2473 SuggestionExpectation::MeasureRange,
2474 Some(units),
2475 )?;
2476 let element_spec = TypeSpecification::MeasureRange {
2477 lower: lower.clone(),
2478 upper: upper.clone(),
2479 minimum: minimum.clone(),
2480 maximum: maximum.clone(),
2481 units: units.clone(),
2482 decomposition: decomposition.clone(),
2483 help: help.clone(),
2484 }
2485 .element_from_range()
2486 .expect("BUG: MeasureRange must define element_from_range");
2487 let left = lift_range_endpoint(left, &element_spec)?;
2488 let right = lift_range_endpoint(right, &element_spec)?;
2489 if !left.lemma_type.is_measure() || !right.lemma_type.is_measure() {
2490 return Err(format!(
2491 "Please provide a range with units valid for '{type_name}', for example `-> suggest 30 kilogram...35 kilogram`."
2492 ));
2493 }
2494 *declared_suggestion = Some(RawSuggestion::Value(ValueKind::Range(
2495 Box::new(left),
2496 Box::new(right),
2497 )));
2498 }
2499 _ => {
2500 return Err(format!(
2501 "Invalid command '{}' for measure range type. Valid commands: unit, lower, upper, minimum, maximum, help, suggest",
2502 command
2503 ));
2504 }
2505 },
2506 TypeSpecification::Veto { .. } => {
2507 return Err(format!(
2508 "Invalid command '{}' for veto type. Veto is not a user-declarable type and cannot have constraints",
2509 command
2510 ));
2511 }
2512 TypeSpecification::Undetermined => {
2513 return Err(format!(
2514 "Invalid command '{}' for undetermined sentinel type. Undetermined is an internal type used during type inference and cannot have constraints",
2515 command
2516 ));
2517 }
2518 }
2519 Ok(())
2520 }
2521}
2522
2523pub fn parse_number_unit(
2526 value_str: &str,
2527 type_spec: &TypeSpecification,
2528) -> Result<crate::parsing::ast::Value, String> {
2529 use crate::literals::{NumberWithUnit, RatioLiteral};
2530 use crate::parsing::ast::Value;
2531
2532 let trimmed = value_str.trim();
2533 match type_spec {
2534 TypeSpecification::Measure { units, .. } => {
2535 if units.is_empty() {
2536 unreachable!(
2537 "BUG: Measure type has no units; should have been validated during planning"
2538 );
2539 }
2540 match trimmed.parse::<NumberWithUnit>() {
2541 Ok(n) => {
2542 let unit = units.get(&n.1).map_err(|e| e.to_string())?;
2543 Ok(Value::NumberWithUnit(n.0, unit.name.clone()))
2544 }
2545 Err(e) => {
2546 if trimmed.split_whitespace().count() == 1 && !trimmed.is_empty() {
2547 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
2548 let example_unit = units
2549 .iter()
2550 .next()
2551 .expect("BUG: units non-empty after guard")
2552 .name
2553 .as_str();
2554 Err(format!(
2555 "Measure value must include a unit, for example: '{} {}'. Valid units: {}.",
2556 trimmed,
2557 example_unit,
2558 valid.join(", ")
2559 ))
2560 } else {
2561 Err(e)
2562 }
2563 }
2564 }
2565 }
2566 TypeSpecification::Ratio { units, .. } => {
2567 if units.is_empty() {
2568 unreachable!(
2569 "BUG: Ratio type has no units; should have been validated during planning"
2570 );
2571 }
2572 match trimmed.parse::<RatioLiteral>()? {
2573 RatioLiteral::Bare(_) => {
2574 Err("Ratio value requires a unit (e.g. '50%', '500 basis_points').".to_string())
2575 }
2576 RatioLiteral::Percent(n) => {
2577 let unit = units.get("percent").map_err(|e| e.to_string())?;
2578 Ok(Value::NumberWithUnit(n, unit.name.clone()))
2579 }
2580 RatioLiteral::Permille(n) => {
2581 let unit = units.get("permille").map_err(|e| e.to_string())?;
2582 Ok(Value::NumberWithUnit(n, unit.name.clone()))
2583 }
2584 RatioLiteral::Named { value, unit } => {
2585 let resolved = units.get(&unit).map_err(|e| e.to_string())?;
2586 Ok(Value::NumberWithUnit(value, resolved.name.clone()))
2587 }
2588 }
2589 }
2590 _ => Err("parse_number_unit only accepts Measure or Ratio type".to_string()),
2591 }
2592}
2593
2594pub fn parse_value_from_string(
2597 value_str: &str,
2598 type_spec: &TypeSpecification,
2599 source: &Source,
2600) -> Result<crate::parsing::ast::Value, Error> {
2601 use crate::parsing::ast::Value;
2602
2603 let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
2604
2605 let parse_range_value = |element_spec: TypeSpecification| -> Result<Value, Error> {
2606 let (left_str, right_str) = value_str.split_once("...").ok_or_else(|| {
2607 to_err("Range value must use '...' between the two endpoints".to_string())
2608 })?;
2609 if left_str.trim().is_empty() || right_str.trim().is_empty() {
2610 return Err(to_err(
2611 "Range value must contain a non-empty left and right endpoint".to_string(),
2612 ));
2613 }
2614 let left = parse_value_from_string(left_str.trim(), &element_spec, source)?;
2615 let right = parse_value_from_string(right_str.trim(), &element_spec, source)?;
2616 Ok(Value::Range(Box::new(left), Box::new(right)))
2617 };
2618
2619 match type_spec {
2620 TypeSpecification::Text { .. } => value_str
2621 .parse::<crate::literals::TextLiteral>()
2622 .map(|t| Value::Text(t.0))
2623 .map_err(to_err),
2624 TypeSpecification::Number { .. } => value_str
2625 .parse::<crate::literals::NumberLiteral>()
2626 .map(|n| Value::Number(n.0))
2627 .map_err(to_err),
2628 TypeSpecification::Measure { .. } => {
2629 parse_number_unit(value_str, type_spec).map_err(to_err)
2630 }
2631 TypeSpecification::Boolean { .. } => value_str
2632 .parse::<BooleanValue>()
2633 .map(Value::Boolean)
2634 .map_err(to_err),
2635 TypeSpecification::Date { .. } => {
2636 let date = value_str.parse::<DateTimeValue>().map_err(to_err)?;
2637 Ok(Value::Date(date))
2638 }
2639 TypeSpecification::Time { .. } => {
2640 let time = value_str.parse::<TimeValue>().map_err(to_err)?;
2641 Ok(Value::Time(time))
2642 }
2643 TypeSpecification::Ratio { .. } => {
2644 parse_number_unit(value_str, type_spec).map_err(to_err)
2645 }
2646 TypeSpecification::NumberRange { .. }
2647 | TypeSpecification::MeasureRange { .. }
2648 | TypeSpecification::DateRange { .. }
2649 | TypeSpecification::TimeRange { .. }
2650 | TypeSpecification::RatioRange { .. } => {
2651 let element_spec = range_element_type_specification(type_spec).unwrap_or_else(|| {
2652 unreachable!("BUG: range_element_type_specification missing arm for known range type")
2653 });
2654 parse_range_value(element_spec)
2655 }
2656 TypeSpecification::Veto { .. } => Err(to_err(
2657 "Veto type cannot be parsed from string".to_string(),
2658 )),
2659 TypeSpecification::Undetermined => unreachable!(
2660 "BUG: parse_value_from_string called with Undetermined sentinel type; this type exists only during type inference"
2661 ),
2662 }
2663}
2664
2665#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2670#[serde(rename_all = "snake_case")]
2671pub enum SemanticCalendarUnit {
2672 Month,
2673 Year,
2674}
2675
2676impl fmt::Display for SemanticCalendarUnit {
2677 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2678 let s = match self {
2679 SemanticCalendarUnit::Month => "month",
2680 SemanticCalendarUnit::Year => "year",
2681 };
2682 write!(f, "{}", s)
2683 }
2684}
2685
2686pub fn semantic_calendar_unit_from_unit_name(unit_name: &str) -> SemanticCalendarUnit {
2687 match unit_name {
2688 "month" => SemanticCalendarUnit::Month,
2689 "year" => SemanticCalendarUnit::Year,
2690 other => unreachable!(
2691 "BUG: calendar measure signature unit must be month or year, got '{other}'"
2692 ),
2693 }
2694}
2695
2696pub fn semantic_calendar_unit_from_measure_signature(
2697 signature: &[(String, i32)],
2698) -> SemanticCalendarUnit {
2699 let unit_name = signature
2700 .first()
2701 .map(|(name, _)| name.as_str())
2702 .expect("BUG: calendar measure must carry a unit signature");
2703 semantic_calendar_unit_from_unit_name(unit_name)
2704}
2705
2706mod arc_lemma_type {
2707 use super::LemmaType;
2708 use serde::{Deserialize, Deserializer, Serialize, Serializer};
2709 use std::sync::Arc;
2710
2711 pub fn serialize<S>(value: &Arc<LemmaType>, serializer: S) -> Result<S::Ok, S::Error>
2712 where
2713 S: Serializer,
2714 {
2715 value.as_ref().serialize(serializer)
2716 }
2717
2718 pub fn deserialize<'de, D>(deserializer: D) -> Result<Arc<LemmaType>, D::Error>
2719 where
2720 D: Deserializer<'de>,
2721 {
2722 LemmaType::deserialize(deserializer).map(Arc::new)
2723 }
2724}
2725
2726#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2728#[serde(rename_all = "snake_case")]
2729pub enum SemanticConversionTarget {
2730 Type(PrimitiveKind),
2731 Unit {
2733 unit_name: String,
2734 #[serde(with = "arc_lemma_type")]
2736 owning_type: Arc<LemmaType>,
2737 },
2738}
2739
2740impl std::hash::Hash for SemanticConversionTarget {
2741 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
2742 match self {
2743 Self::Type(kind) => {
2744 0u8.hash(state);
2745 kind.hash(state);
2746 }
2747 Self::Unit {
2748 unit_name,
2749 owning_type,
2750 } => {
2751 1u8.hash(state);
2752 unit_name.hash(state);
2753 owning_type.hash(state);
2754 }
2755 }
2756 }
2757}
2758
2759impl SemanticConversionTarget {}
2760
2761impl fmt::Display for SemanticConversionTarget {
2762 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2763 match self {
2764 SemanticConversionTarget::Type(kind) => write!(f, "{kind}"),
2765 SemanticConversionTarget::Unit { unit_name, .. } => write!(f, "{unit_name}"),
2766 }
2767 }
2768}
2769
2770#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2772pub struct SemanticTimezone {
2773 pub offset_hours: i8,
2774 pub offset_minutes: u8,
2775}
2776
2777impl fmt::Display for SemanticTimezone {
2778 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2779 if self.offset_hours == 0 && self.offset_minutes == 0 {
2780 write!(f, "Z")
2781 } else {
2782 let sign = if self.offset_hours >= 0 { "+" } else { "-" };
2783 let hour = self.offset_hours.abs();
2784 write!(f, "{}{:02}:{:02}", sign, hour, self.offset_minutes)
2785 }
2786 }
2787}
2788
2789impl Serialize for SemanticTimezone {
2790 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2791 serializer.serialize_str(&self.to_string())
2792 }
2793}
2794
2795impl<'de> Deserialize<'de> for SemanticTimezone {
2796 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2797 let s = String::deserialize(deserializer)?;
2798 Self::from_str(&s).map_err(serde::de::Error::custom)
2799 }
2800}
2801
2802impl FromStr for SemanticTimezone {
2803 type Err = String;
2804
2805 fn from_str(s: &str) -> Result<Self, Self::Err> {
2806 let tz = TimezoneValue::from_str(s)?;
2807 Ok(Self {
2808 offset_hours: tz.offset_hours,
2809 offset_minutes: tz.offset_minutes,
2810 })
2811 }
2812}
2813
2814#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2816pub struct SemanticTime {
2817 pub hour: u32,
2818 pub minute: u32,
2819 pub second: u32,
2820 pub microsecond: u32,
2821 pub timezone: Option<SemanticTimezone>,
2822}
2823
2824impl fmt::Display for SemanticTime {
2825 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2826 write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
2827 if self.microsecond != 0 {
2828 write!(f, ".{:06}", self.microsecond)?;
2829 }
2830 if let Some(timezone) = &self.timezone {
2831 write!(f, "{}", timezone)?;
2832 }
2833 Ok(())
2834 }
2835}
2836
2837impl Serialize for SemanticTime {
2838 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2839 serializer.serialize_str(&self.to_string())
2840 }
2841}
2842
2843impl<'de> Deserialize<'de> for SemanticTime {
2844 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2845 let s = String::deserialize(deserializer)?;
2846 Self::from_str(&s).map_err(serde::de::Error::custom)
2847 }
2848}
2849
2850impl FromStr for SemanticTime {
2851 type Err = String;
2852
2853 fn from_str(s: &str) -> Result<Self, Self::Err> {
2854 Ok(time_to_semantic(&TimeValue::from_str(s)?))
2855 }
2856}
2857
2858#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2860pub struct SemanticDateTime {
2861 pub year: i32,
2862 pub month: u32,
2863 pub day: u32,
2864 pub hour: u32,
2865 pub minute: u32,
2866 pub second: u32,
2867 pub microsecond: u32,
2868 pub timezone: Option<SemanticTimezone>,
2869}
2870
2871impl fmt::Display for SemanticDateTime {
2872 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2873 let has_time = self.hour != 0
2874 || self.minute != 0
2875 || self.second != 0
2876 || self.microsecond != 0
2877 || self.timezone.is_some();
2878 if !has_time {
2879 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
2880 } else {
2881 write!(
2882 f,
2883 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
2884 self.year, self.month, self.day, self.hour, self.minute, self.second
2885 )?;
2886 if self.microsecond != 0 {
2887 write!(f, ".{:06}", self.microsecond)?;
2888 }
2889 if let Some(tz) = &self.timezone {
2890 write!(f, "{}", tz)?;
2891 }
2892 Ok(())
2893 }
2894 }
2895}
2896
2897impl Serialize for SemanticDateTime {
2898 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2899 serializer.serialize_str(&self.to_string())
2900 }
2901}
2902
2903impl<'de> Deserialize<'de> for SemanticDateTime {
2904 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2905 let s = String::deserialize(deserializer)?;
2906 Self::from_str(&s).map_err(serde::de::Error::custom)
2907 }
2908}
2909
2910impl FromStr for SemanticDateTime {
2911 type Err = String;
2912
2913 fn from_str(s: &str) -> Result<Self, Self::Err> {
2914 Ok(date_time_to_semantic(&DateTimeValue::from_str(s)?))
2915 }
2916}
2917
2918#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2922pub enum RawSuggestion {
2923 Value(ValueKind),
2924 Measure {
2925 magnitude: RationalInteger,
2926 unit_name: String,
2927 },
2928}
2929
2930pub fn value_kind_from_raw_suggestion(
2931 raw: RawSuggestion,
2932 specifications: &TypeSpecification,
2933 type_name: &str,
2934) -> Result<ValueKind, String> {
2935 match raw {
2936 RawSuggestion::Value(vk) => Ok(vk),
2937 RawSuggestion::Measure {
2938 magnitude,
2939 unit_name,
2940 } => {
2941 let TypeSpecification::Measure { units, .. } = specifications else {
2942 return Err(format!(
2943 "BUG: RawSuggestion::Measure for non-measure type '{type_name}'"
2944 ));
2945 };
2946 let canonical = measure_declared_bound_to_canonical(
2947 &magnitude, &unit_name, units, type_name, "suggest",
2948 )?;
2949 Ok(ValueKind::Measure(canonical, vec![(unit_name, 1)]))
2950 }
2951 }
2952}
2953
2954#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2957pub enum ValueKind {
2958 Number(RationalInteger),
2959 Measure(RationalInteger, Vec<(String, i32)>),
2965 Text(String),
2966 Date(SemanticDateTime),
2967 Time(SemanticTime),
2968 Boolean(bool),
2969 Ratio(RationalInteger, Option<String>),
2971 Range(Box<LiteralValue>, Box<LiteralValue>),
2972}
2973
2974impl ValueKind {
2975 pub fn as_decimal_magnitude(&self) -> Result<Decimal, String> {
2977 match self {
2978 ValueKind::Number(n) | ValueKind::Measure(n, _) | ValueKind::Ratio(n, _) => {
2979 n.try_to_decimal().map_err(|failure| failure.to_string())
2980 }
2981 other => Err(format!("expected numeric value kind, got {other}")),
2982 }
2983 }
2984}
2985
2986fn format_rational_magnitude_for_display(rational: &RationalInteger) -> String {
2987 rational.display_str()
2988}
2989
2990fn format_number_with_unit_for_display(rational: &RationalInteger, unit: &str) -> String {
2991 use crate::parsing::ast::Value;
2992 match rational.try_to_decimal() {
2993 Ok(decimal) => format!("{}", Value::NumberWithUnit(decimal, unit.to_string())),
2994 Err(_) => format!("{} {}", rational.display_str(), unit),
2995 }
2996}
2997
2998impl fmt::Display for ValueKind {
2999 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3000 use crate::computation::rational::checked_mul;
3001 match self {
3002 ValueKind::Number(rational) => {
3003 write!(f, "{}", format_rational_magnitude_for_display(rational))
3004 }
3005 ValueKind::Measure(rational, signature) => {
3006 let unit = signature.first().map(|(n, _)| n.as_str()).unwrap_or("");
3007 write!(f, "{}", format_number_with_unit_for_display(rational, unit))
3008 }
3009 ValueKind::Text(s) => write!(f, "{}", crate::parsing::ast::Value::Text(s.clone())),
3010 ValueKind::Ratio(rational, unit) => match unit.as_deref() {
3011 Some("percent") => {
3012 let display = match checked_mul(rational, &rational_new(100, 1)) {
3013 Ok(scaled) => format_number_with_unit_for_display(&scaled, "percent"),
3014 Err(_) => format!("{} percent", rational.display_str()),
3015 };
3016 write!(f, "{}", display)
3017 }
3018 Some("permille") => {
3019 let display = match checked_mul(rational, &rational_new(1000, 1)) {
3020 Ok(scaled) => format_number_with_unit_for_display(&scaled, "permille"),
3021 Err(_) => format!("{} permille", rational.display_str()),
3022 };
3023 write!(f, "{}", display)
3024 }
3025 Some(unit_name) => {
3026 write!(
3027 f,
3028 "{}",
3029 format_number_with_unit_for_display(rational, unit_name)
3030 )
3031 }
3032 None => write!(f, "{}", format_rational_magnitude_for_display(rational)),
3033 },
3034 ValueKind::Date(dt) => write!(f, "{}", dt),
3035 ValueKind::Time(t) => write!(
3036 f,
3037 "{}",
3038 crate::parsing::ast::Value::Time(crate::parsing::ast::TimeValue {
3039 hour: t.hour as u8,
3040 minute: t.minute as u8,
3041 second: t.second as u8,
3042 microsecond: t.microsecond,
3043 timezone: t
3044 .timezone
3045 .as_ref()
3046 .map(|tz| crate::parsing::ast::TimezoneValue {
3047 offset_hours: tz.offset_hours,
3048 offset_minutes: tz.offset_minutes,
3049 }),
3050 })
3051 ),
3052 ValueKind::Boolean(b) => write!(f, "{}", b),
3053 ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
3054 }
3055 }
3056}
3057
3058fn decimal_from_serialized_str(s: &str) -> Result<Decimal, String> {
3059 Decimal::from_str(s.trim()).map_err(|e| format!("invalid decimal '{s}': {e}"))
3060}
3061
3062#[derive(Serialize, Deserialize)]
3063struct SerializedValueUnit {
3064 value: String,
3065 unit: String,
3066}
3067
3068#[derive(Serialize, Deserialize)]
3069struct SerializedRatio {
3070 value: String,
3071 unit: Option<String>,
3072}
3073
3074#[derive(Serialize, Deserialize)]
3075struct SerializedMeasure {
3076 value: String,
3077 signature: Vec<(String, i32)>,
3078}
3079
3080#[derive(Serialize, Deserialize)]
3081struct SerializedRange {
3082 from: ValueKind,
3083 to: ValueKind,
3084}
3085
3086impl Serialize for ValueKind {
3087 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3088 use serde::ser::SerializeMap;
3089 let mut map = serializer.serialize_map(Some(1))?;
3090 match self {
3091 ValueKind::Number(rational) => {
3092 map.serialize_entry(
3093 "number",
3094 &crate::literals::rational_to_serialized_str(rational)
3095 .map_err(serde::ser::Error::custom)?,
3096 )?;
3097 }
3098 ValueKind::Measure(rational, signature) => {
3099 map.serialize_entry(
3100 "measure",
3101 &SerializedMeasure {
3102 value: crate::literals::rational_to_serialized_str(rational)
3103 .map_err(serde::ser::Error::custom)?,
3104 signature: signature.clone(),
3105 },
3106 )?;
3107 }
3108 ValueKind::Text(s) => {
3109 map.serialize_entry("text", s)?;
3110 }
3111 ValueKind::Date(dt) => {
3112 map.serialize_entry("date", dt)?;
3113 }
3114 ValueKind::Time(t) => {
3115 map.serialize_entry("time", t)?;
3116 }
3117 ValueKind::Boolean(b) => {
3118 map.serialize_entry("boolean", b)?;
3119 }
3120 ValueKind::Ratio(rational, unit) => {
3121 map.serialize_entry(
3122 "ratio",
3123 &SerializedRatio {
3124 value: crate::literals::rational_to_serialized_str(rational)
3125 .map_err(serde::ser::Error::custom)?,
3126 unit: unit.clone(),
3127 },
3128 )?;
3129 }
3130 ValueKind::Range(left, right) => {
3131 map.serialize_entry(
3132 "range",
3133 &SerializedRange {
3134 from: left.value.clone(),
3135 to: right.value.clone(),
3136 },
3137 )?;
3138 }
3139 }
3140 map.end()
3141 }
3142}
3143
3144impl<'de> Deserialize<'de> for ValueKind {
3145 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3146 let map = <serde_json::Map<String, serde_json::Value>>::deserialize(deserializer)?;
3147 if map.len() != 1 {
3148 return Err(serde::de::Error::custom(format!(
3149 "ValueKind must have exactly one variant key, got {}",
3150 map.len()
3151 )));
3152 }
3153 let (tag, payload) = map.into_iter().next().expect("BUG: len checked");
3154 deserialize_value_kind_variant(&tag, payload).map_err(serde::de::Error::custom)
3155 }
3156}
3157
3158fn deserialize_value_kind_variant(
3159 tag: &str,
3160 payload: serde_json::Value,
3161) -> Result<ValueKind, String> {
3162 match tag {
3163 "number" => {
3164 let s = payload
3165 .as_str()
3166 .ok_or_else(|| "number must be a JSON string".to_string())?;
3167 let decimal = decimal_from_serialized_str(s)?;
3168 Ok(ValueKind::Number(
3169 crate::literals::rational_from_parsed_decimal(decimal)?,
3170 ))
3171 }
3172 "measure" => {
3173 let pair: SerializedMeasure =
3174 serde_json::from_value(payload).map_err(|e| e.to_string())?;
3175 let decimal = decimal_from_serialized_str(&pair.value)?;
3176 Ok(ValueKind::Measure(
3177 crate::literals::rational_from_parsed_decimal(decimal)?,
3178 pair.signature,
3179 ))
3180 }
3181 "ratio" => {
3182 let pair: SerializedRatio =
3183 serde_json::from_value(payload).map_err(|e| e.to_string())?;
3184 let decimal = decimal_from_serialized_str(&pair.value)?;
3185 Ok(ValueKind::Ratio(
3186 crate::literals::rational_from_parsed_decimal(decimal)?,
3187 pair.unit,
3188 ))
3189 }
3190 "calendar" => {
3191 let pair: SerializedValueUnit =
3192 serde_json::from_value(payload).map_err(|e| e.to_string())?;
3193 let unit = match pair.unit.as_str() {
3194 "month" => SemanticCalendarUnit::Month,
3195 "year" => SemanticCalendarUnit::Year,
3196 other => {
3197 return Err(format!(
3198 "unknown calendar unit '{other}' (expected 'month' or 'year')"
3199 ));
3200 }
3201 };
3202 let decimal = decimal_from_serialized_str(&pair.value)?;
3203 Ok(ValueKind::Measure(
3204 crate::literals::rational_from_parsed_decimal(decimal)?,
3205 vec![(unit.to_string(), 1)],
3206 ))
3207 }
3208 "text" => {
3209 let s = payload
3210 .as_str()
3211 .ok_or_else(|| "text must be a JSON string".to_string())?;
3212 Ok(ValueKind::Text(s.to_string()))
3213 }
3214 "date" => {
3215 let dt: SemanticDateTime =
3216 serde_json::from_value(payload).map_err(|e| e.to_string())?;
3217 Ok(ValueKind::Date(dt))
3218 }
3219 "time" => {
3220 let t: SemanticTime = serde_json::from_value(payload).map_err(|e| e.to_string())?;
3221 Ok(ValueKind::Time(t))
3222 }
3223 "boolean" => {
3224 let b = payload
3225 .as_bool()
3226 .ok_or_else(|| "boolean must be a JSON bool".to_string())?;
3227 Ok(ValueKind::Boolean(b))
3228 }
3229 "range" => {
3230 let range: SerializedRange =
3231 serde_json::from_value(payload).map_err(|e| e.to_string())?;
3232 Ok(ValueKind::Range(
3233 Box::new(LiteralValue {
3234 value: range.from,
3235 lemma_type: primitive_number_arc().clone(),
3236 }),
3237 Box::new(LiteralValue {
3238 value: range.to,
3239 lemma_type: primitive_number_arc().clone(),
3240 }),
3241 ))
3242 }
3243 other => Err(format!("unknown ValueKind variant '{other}'")),
3244 }
3245}
3246
3247#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
3256pub struct PathSegment {
3257 pub data: String,
3259 pub spec: String,
3261}
3262
3263#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
3267pub struct DataPath {
3268 pub segments: Vec<PathSegment>,
3270 pub data: String,
3272}
3273
3274impl DataPath {
3275 pub fn new(segments: Vec<PathSegment>, data: String) -> Self {
3277 Self { segments, data }
3278 }
3279
3280 pub fn local(data: String) -> Self {
3282 Self {
3283 segments: vec![],
3284 data,
3285 }
3286 }
3287
3288 pub fn input_key(&self) -> String {
3291 let mut s = String::new();
3292 for segment in &self.segments {
3293 s.push_str(&segment.data);
3294 s.push('.');
3295 }
3296 s.push_str(&self.data);
3297 s
3298 }
3299}
3300
3301#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
3305pub struct RulePath {
3306 pub segments: Vec<PathSegment>,
3308 pub rule: String,
3310}
3311
3312impl RulePath {
3313 pub fn new(segments: Vec<PathSegment>, rule: String) -> Self {
3315 Self { segments, rule }
3316 }
3317}
3318
3319#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3328pub struct Expression {
3329 pub kind: ExpressionKind,
3330 pub source_location: Option<Source>,
3331}
3332
3333impl Expression {
3334 pub fn with_source(kind: ExpressionKind, source_location: Option<Source>) -> Self {
3336 Self {
3337 kind,
3338 source_location,
3339 }
3340 }
3341
3342 pub fn collect_data_paths(&self, data: &mut std::collections::HashSet<DataPath>) {
3344 self.kind.collect_data_paths(data);
3345 }
3346}
3347
3348#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3350#[serde(rename_all = "snake_case")]
3351pub enum ExpressionKind {
3352 Literal(Box<LiteralValue>),
3354 DataPath(DataPath),
3356 RulePath(RulePath),
3358 LogicalAnd(Arc<Expression>, Arc<Expression>),
3359 Arithmetic(Arc<Expression>, ArithmeticComputation, Arc<Expression>),
3360 Comparison(Arc<Expression>, ComparisonComputation, Arc<Expression>),
3361 UnitConversion(Arc<Expression>, SemanticConversionTarget),
3362 LogicalNegation(Arc<Expression>, NegationType),
3363 MathematicalComputation(MathematicalComputation, Arc<Expression>),
3364 Veto(VetoExpression),
3365 Now,
3367 DateRelative(DateRelativeKind, Arc<Expression>),
3369 DateCalendar(DateCalendarKind, CalendarPeriodUnit, Arc<Expression>),
3371 RangeLiteral(Arc<Expression>, Arc<Expression>),
3372 PastFutureRange(DateRelativeKind, Arc<Expression>),
3373 RangeContainment(Arc<Expression>, Arc<Expression>),
3374 ResultIsVeto(Arc<Expression>),
3376 Piecewise(Vec<(Arc<Expression>, Arc<Expression>)>),
3379}
3380
3381impl ExpressionKind {
3382 pub(crate) fn collect_data_paths(&self, data: &mut std::collections::HashSet<DataPath>) {
3384 match self {
3385 ExpressionKind::DataPath(fp) => {
3386 data.insert(fp.clone());
3387 }
3388 ExpressionKind::LogicalAnd(left, right) => {
3389 left.collect_data_paths(data);
3390 right.collect_data_paths(data);
3391 }
3392 ExpressionKind::Arithmetic(left, _, right)
3393 | ExpressionKind::Comparison(left, _, right)
3394 | ExpressionKind::RangeLiteral(left, right)
3395 | ExpressionKind::RangeContainment(left, right) => {
3396 left.collect_data_paths(data);
3397 right.collect_data_paths(data);
3398 }
3399 ExpressionKind::UnitConversion(inner, _)
3400 | ExpressionKind::LogicalNegation(inner, _)
3401 | ExpressionKind::MathematicalComputation(_, inner)
3402 | ExpressionKind::PastFutureRange(_, inner) => {
3403 inner.collect_data_paths(data);
3404 }
3405 ExpressionKind::DateRelative(_, date_expr) => {
3406 date_expr.collect_data_paths(data);
3407 }
3408 ExpressionKind::DateCalendar(_, _, date_expr) => {
3409 date_expr.collect_data_paths(data);
3410 }
3411 ExpressionKind::Literal(_)
3412 | ExpressionKind::RulePath(_)
3413 | ExpressionKind::Veto(_)
3414 | ExpressionKind::Now => {}
3415 ExpressionKind::ResultIsVeto(operand) => {
3416 operand.collect_data_paths(data);
3417 }
3418 ExpressionKind::Piecewise(arms) => {
3419 for (condition, result) in arms {
3420 condition.collect_data_paths(data);
3421 result.collect_data_paths(data);
3422 }
3423 }
3424 }
3425 }
3426}
3427
3428#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
3434#[serde(tag = "kind", rename_all = "snake_case")]
3435pub enum TypeDefiningSpec {
3436 Local,
3438 Import,
3440}
3441
3442#[derive(Clone, Debug, Serialize, Deserialize)]
3444#[serde(tag = "kind", rename_all = "snake_case")]
3445pub enum TypeExtends {
3446 Primitive,
3448 Custom {
3451 parent: String,
3452 family: String,
3453 defining_spec: TypeDefiningSpec,
3454 },
3455}
3456
3457impl PartialEq for TypeExtends {
3458 fn eq(&self, other: &Self) -> bool {
3459 match (self, other) {
3460 (TypeExtends::Primitive, TypeExtends::Primitive) => true,
3461 (
3462 TypeExtends::Custom {
3463 parent: lp,
3464 family: lf,
3465 defining_spec: ld,
3466 },
3467 TypeExtends::Custom {
3468 parent: rp,
3469 family: rf,
3470 defining_spec: rd,
3471 },
3472 ) => lp == rp && lf == rf && ld == rd,
3473 _ => false,
3474 }
3475 }
3476}
3477
3478impl Eq for TypeExtends {}
3479
3480impl std::hash::Hash for TypeExtends {
3481 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
3482 match self {
3483 TypeExtends::Primitive => {
3484 0u8.hash(state);
3485 }
3486 TypeExtends::Custom {
3487 parent,
3488 family,
3489 defining_spec,
3490 } => {
3491 1u8.hash(state);
3492 parent.hash(state);
3493 family.hash(state);
3494 defining_spec.hash(state);
3495 }
3496 }
3497 }
3498}
3499
3500impl TypeExtends {
3501 #[must_use]
3503 pub fn custom_local(parent: String, family: String) -> Self {
3504 TypeExtends::Custom {
3505 parent,
3506 family,
3507 defining_spec: TypeDefiningSpec::Local,
3508 }
3509 }
3510
3511 #[must_use]
3513 pub fn parent_name(&self) -> Option<&str> {
3514 match self {
3515 TypeExtends::Primitive => None,
3516 TypeExtends::Custom { parent, .. } => Some(parent.as_str()),
3517 }
3518 }
3519}
3520
3521#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
3526pub struct LemmaType {
3527 pub name: Option<String>,
3529 #[serde(flatten)]
3534 pub specifications: TypeSpecification,
3535 pub extends: TypeExtends,
3537}
3538
3539impl LemmaType {
3540 pub fn map_measure<F>(self, f: F) -> Self
3544 where
3545 F: FnOnce(
3546 MeasureUnits,
3547 Option<BaseMeasureVector>,
3548 ) -> (MeasureUnits, Option<BaseMeasureVector>),
3549 {
3550 let LemmaType {
3551 name,
3552 specifications,
3553 extends,
3554 } = self;
3555 let specifications = match specifications {
3556 TypeSpecification::Measure {
3557 minimum,
3558 maximum,
3559 decimals,
3560 units,
3561 traits,
3562 decomposition,
3563 help,
3564 } => {
3565 let (units, decomposition) = f(units, decomposition);
3566 TypeSpecification::Measure {
3567 minimum,
3568 maximum,
3569 decimals,
3570 units,
3571 traits,
3572 decomposition,
3573 help,
3574 }
3575 }
3576 other => other,
3577 };
3578 LemmaType {
3579 name,
3580 specifications,
3581 extends,
3582 }
3583 }
3584
3585 pub fn new(name: String, specifications: TypeSpecification, extends: TypeExtends) -> Self {
3587 Self {
3588 name: Some(name),
3589 specifications,
3590 extends,
3591 }
3592 }
3593
3594 pub fn without_name(specifications: TypeSpecification, extends: TypeExtends) -> Self {
3596 Self {
3597 name: None,
3598 specifications,
3599 extends,
3600 }
3601 }
3602
3603 pub fn primitive(specifications: TypeSpecification) -> Self {
3605 Self {
3606 name: None,
3607 specifications,
3608 extends: TypeExtends::Primitive,
3609 }
3610 }
3611
3612 pub fn name(&self) -> String {
3614 self.name
3615 .clone()
3616 .unwrap_or_else(|| self.specifications.to_string())
3617 }
3618
3619 pub fn is_boolean(&self) -> bool {
3621 matches!(&self.specifications, TypeSpecification::Boolean { .. })
3622 }
3623
3624 pub fn matches_primitive_kind(&self, kind: PrimitiveKind) -> bool {
3625 matches!(
3626 (kind, &self.specifications),
3627 (PrimitiveKind::Number, TypeSpecification::Number { .. })
3628 | (PrimitiveKind::Text, TypeSpecification::Text { .. })
3629 | (PrimitiveKind::Boolean, TypeSpecification::Boolean { .. })
3630 | (PrimitiveKind::Date, TypeSpecification::Date { .. })
3631 | (PrimitiveKind::Time, TypeSpecification::Time { .. })
3632 | (PrimitiveKind::Ratio, TypeSpecification::Ratio { .. })
3633 | (PrimitiveKind::Measure, TypeSpecification::Measure { .. })
3634 )
3635 }
3636
3637 pub fn is_measure(&self) -> bool {
3639 matches!(&self.specifications, TypeSpecification::Measure { .. })
3640 }
3641
3642 pub fn is_measure_range(&self) -> bool {
3643 matches!(&self.specifications, TypeSpecification::MeasureRange { .. })
3644 }
3645
3646 pub fn is_number(&self) -> bool {
3648 matches!(&self.specifications, TypeSpecification::Number { .. })
3649 }
3650
3651 pub fn is_number_range(&self) -> bool {
3652 matches!(&self.specifications, TypeSpecification::NumberRange { .. })
3653 }
3654
3655 pub fn is_numeric(&self) -> bool {
3657 matches!(
3658 &self.specifications,
3659 TypeSpecification::Measure { .. } | TypeSpecification::Number { .. }
3660 )
3661 }
3662
3663 pub fn is_text(&self) -> bool {
3665 matches!(&self.specifications, TypeSpecification::Text { .. })
3666 }
3667
3668 pub fn is_date(&self) -> bool {
3670 matches!(&self.specifications, TypeSpecification::Date { .. })
3671 }
3672
3673 pub fn is_date_range(&self) -> bool {
3674 matches!(&self.specifications, TypeSpecification::DateRange { .. })
3675 }
3676
3677 pub fn is_time_range(&self) -> bool {
3678 matches!(&self.specifications, TypeSpecification::TimeRange { .. })
3679 }
3680
3681 pub fn is_time(&self) -> bool {
3683 matches!(&self.specifications, TypeSpecification::Time { .. })
3684 }
3685
3686 pub fn has_trait_duration(&self) -> bool {
3687 match &self.specifications {
3688 TypeSpecification::Measure { traits, .. } => traits.contains(&MeasureTrait::Duration),
3689 _ => false,
3690 }
3691 }
3692
3693 pub fn is_duration_like_measure(&self) -> bool {
3694 if !self.is_measure() {
3695 return false;
3696 }
3697 if self.has_trait_duration() {
3698 return true;
3699 }
3700 self.is_anonymous_measure()
3701 && self
3702 .measure_type_decomposition()
3703 .is_some_and(|d| *d == duration_decomposition())
3704 }
3705
3706 pub fn is_duration_like(&self) -> bool {
3707 self.is_duration_like_measure()
3708 }
3709
3710 pub fn has_trait_calendar(&self) -> bool {
3711 match &self.specifications {
3712 TypeSpecification::Measure { traits, .. } => traits.contains(&MeasureTrait::Calendar),
3713 _ => false,
3714 }
3715 }
3716
3717 pub fn is_calendar_like_measure(&self) -> bool {
3718 if !self.is_measure() {
3719 return false;
3720 }
3721 if self.has_trait_calendar() {
3722 return true;
3723 }
3724 self.is_anonymous_measure()
3725 && self
3726 .measure_type_decomposition()
3727 .is_some_and(|d| *d == calendar_decomposition())
3728 }
3729
3730 pub fn is_calendar_like(&self) -> bool {
3731 self.is_calendar_like_measure()
3732 }
3733
3734 pub fn is_ratio(&self) -> bool {
3736 matches!(&self.specifications, TypeSpecification::Ratio { .. })
3737 }
3738
3739 pub fn is_ratio_range(&self) -> bool {
3740 matches!(&self.specifications, TypeSpecification::RatioRange { .. })
3741 }
3742
3743 pub fn is_calendar_measure_range(&self) -> bool {
3744 matches!(
3745 &self.specifications,
3746 TypeSpecification::MeasureRange { decomposition: Some(decomposition), .. }
3747 if *decomposition == calendar_decomposition()
3748 )
3749 }
3750
3751 pub fn is_calendar_like_range(&self) -> bool {
3752 self.is_calendar_measure_range()
3753 }
3754
3755 pub fn is_range(&self) -> bool {
3756 matches!(
3757 &self.specifications,
3758 TypeSpecification::DateRange { .. }
3759 | TypeSpecification::TimeRange { .. }
3760 | TypeSpecification::NumberRange { .. }
3761 | TypeSpecification::MeasureRange { .. }
3762 | TypeSpecification::RatioRange { .. }
3763 )
3764 }
3765
3766 pub fn vetoed(&self) -> bool {
3768 matches!(&self.specifications, TypeSpecification::Veto { .. })
3769 }
3770
3771 pub fn is_undetermined(&self) -> bool {
3773 matches!(&self.specifications, TypeSpecification::Undetermined)
3774 }
3775
3776 pub fn has_same_base_type(&self, other: &LemmaType) -> bool {
3778 use TypeSpecification::*;
3779 matches!(
3780 (&self.specifications, &other.specifications),
3781 (Boolean { .. }, Boolean { .. })
3782 | (Number { .. }, Number { .. })
3783 | (NumberRange { .. }, NumberRange { .. })
3784 | (Measure { .. }, Measure { .. })
3785 | (MeasureRange { .. }, MeasureRange { .. })
3786 | (Text { .. }, Text { .. })
3787 | (Date { .. }, Date { .. })
3788 | (DateRange { .. }, DateRange { .. })
3789 | (Time { .. }, Time { .. })
3790 | (TimeRange { .. }, TimeRange { .. })
3791 | (Ratio { .. }, Ratio { .. })
3792 | (RatioRange { .. }, RatioRange { .. })
3793 | (Veto { .. }, Veto { .. })
3794 | (Undetermined, Undetermined)
3795 )
3796 }
3797
3798 #[must_use]
3800 pub fn measure_family_name(&self) -> Option<&str> {
3801 if !self.is_measure() {
3802 return None;
3803 }
3804 match &self.extends {
3805 TypeExtends::Custom { family, .. } => Some(family.as_str()),
3806 TypeExtends::Primitive => self.name.as_deref(),
3807 }
3808 }
3809
3810 #[must_use]
3812 pub fn same_measure_family(&self, other: &LemmaType) -> bool {
3813 if !self.is_measure() || !other.is_measure() {
3814 return false;
3815 }
3816 match (self.measure_family_name(), other.measure_family_name()) {
3817 (Some(self_family), Some(other_family)) => self_family == other_family,
3818 _ => false,
3819 }
3820 }
3821
3822 #[must_use]
3823 pub fn compatible_with_anonymous_measure(&self, other: &LemmaType) -> bool {
3824 if !self.is_measure() || !other.is_measure() {
3825 return false;
3826 }
3827 if !self.is_anonymous_measure() && !other.is_anonymous_measure() {
3828 return false;
3829 }
3830 match (
3831 self.measure_type_decomposition(),
3832 other.measure_type_decomposition(),
3833 ) {
3834 (Some(a), Some(b)) => a == b,
3835 _ => false,
3836 }
3837 }
3838
3839 pub fn veto_type() -> Self {
3841 Self::primitive(TypeSpecification::veto())
3842 }
3843
3844 pub fn undetermined_type() -> Self {
3847 Self::primitive(TypeSpecification::Undetermined)
3848 }
3849
3850 pub fn decimal_places(&self) -> Option<u8> {
3853 match &self.specifications {
3854 TypeSpecification::Number { decimals, .. } => *decimals,
3855 TypeSpecification::Measure { decimals, .. } => *decimals,
3856 TypeSpecification::Ratio { decimals, .. } => *decimals,
3857 _ => None,
3858 }
3859 }
3860
3861 pub fn try_rational_as_decimal_string(
3866 &self,
3867 magnitude: &crate::computation::rational::RationalInteger,
3868 ) -> Result<String, crate::computation::rational::NumericFailure> {
3869 let decimal = magnitude.try_to_decimal()?;
3870 Ok(format_decimal_for_api(decimal, self.decimal_places()))
3871 }
3872
3873 pub fn try_measure_canonical_as_decimal_in_unit(
3875 &self,
3876 canonical_magnitude: &crate::computation::rational::RationalInteger,
3877 unit_name: &str,
3878 ) -> Result<String, crate::computation::rational::NumericFailure> {
3879 use crate::computation::rational::checked_div;
3880 let unit_factor = self.measure_unit_factor(unit_name);
3881 let magnitude_in_unit = checked_div(canonical_magnitude, unit_factor)?;
3882 self.try_rational_as_decimal_string(&magnitude_in_unit)
3883 }
3884
3885 pub fn try_ratio_canonical_as_decimal_in_unit(
3887 &self,
3888 canonical_magnitude: &crate::computation::rational::RationalInteger,
3889 unit_name: &str,
3890 ) -> Result<String, crate::computation::rational::NumericFailure> {
3891 use crate::computation::rational::checked_mul;
3892 let units = match &self.specifications {
3893 TypeSpecification::Ratio { units, .. } => units,
3894 _ => unreachable!(
3895 "BUG: try_ratio_canonical_as_decimal_in_unit called on non-ratio type {}",
3896 self.name()
3897 ),
3898 };
3899 let ratio_unit = units
3900 .iter()
3901 .find(|unit| unit.name == unit_name)
3902 .unwrap_or_else(|| {
3903 let valid: Vec<&str> = units.iter().map(|unit| unit.name.as_str()).collect();
3904 unreachable!(
3905 "BUG: unknown ratio unit '{}' for type {} (valid: {}); planning must reject invalid units",
3906 unit_name,
3907 self.name(),
3908 valid.join(", ")
3909 )
3910 });
3911 let magnitude_in_unit = checked_mul(canonical_magnitude, &ratio_unit.value)?;
3912 self.try_rational_as_decimal_string(&magnitude_in_unit)
3913 }
3914
3915 pub fn example_value(&self) -> &'static str {
3917 match &self.specifications {
3918 TypeSpecification::Text { .. } => "\"hello world\"",
3919 TypeSpecification::Measure { .. } => "12.50 eur",
3920 TypeSpecification::MeasureRange { .. } => "30 kilogram...35 kilogram",
3921 TypeSpecification::Number { .. } => "3.14",
3922 TypeSpecification::NumberRange { .. } => "0...100",
3923 TypeSpecification::Boolean { .. } => "true",
3924 TypeSpecification::Date { .. } => "2023-12-25T14:30:00Z",
3925 TypeSpecification::DateRange { .. } => "2024-01-01...2024-12-31",
3926 TypeSpecification::TimeRange { .. } => "09:00...17:00",
3927 TypeSpecification::Veto { .. } => "veto",
3928 TypeSpecification::Time { .. } => "14:30:00",
3929 TypeSpecification::Ratio { .. } => "50%",
3930 TypeSpecification::RatioRange { .. } => "10%...50%",
3931 TypeSpecification::Undetermined => unreachable!(
3932 "BUG: example_value called on Undetermined sentinel type; this type must never reach user-facing code"
3933 ),
3934 }
3935 }
3936
3937 #[must_use]
3941 pub fn measure_type_decomposition(&self) -> Option<&BaseMeasureVector> {
3945 match &self.specifications {
3946 TypeSpecification::Measure { decomposition, .. } => decomposition.as_ref(),
3947 _ => unreachable!(
3948 "BUG: measure_type_decomposition called on non-measure type {}",
3949 self.name()
3950 ),
3951 }
3952 }
3953
3954 pub fn is_anonymous_measure(&self) -> bool {
3957 self.name.is_none() && matches!(&self.specifications, TypeSpecification::Measure { .. })
3958 }
3959
3960 pub fn anonymous_for_decomposition(decomposition: BaseMeasureVector) -> Self {
3964 Self {
3965 name: None,
3966 specifications: TypeSpecification::Measure {
3967 minimum: None,
3968 maximum: None,
3969 decimals: None,
3970 units: crate::literals::MeasureUnits::new(),
3971 traits: Vec::new(),
3972 decomposition: Some(decomposition),
3973 help: String::new(),
3974 },
3975 extends: TypeExtends::Primitive,
3976 }
3977 }
3978
3979 #[must_use]
3981 pub fn measure_unit_names(&self) -> Option<Vec<&str>> {
3982 match &self.specifications {
3983 TypeSpecification::Measure { units, .. } if !units.is_empty() => {
3984 Some(units.iter().map(|unit| unit.name.as_str()).collect())
3985 }
3986 TypeSpecification::MeasureRange { units, .. } if !units.is_empty() => {
3987 Some(units.iter().map(|unit| unit.name.as_str()).collect())
3988 }
3989 _ => None,
3990 }
3991 }
3992
3993 pub fn measure_unit_factor(
3995 &self,
3996 unit_name: &str,
3997 ) -> &crate::computation::rational::RationalInteger {
3998 let units = match &self.specifications {
3999 TypeSpecification::Measure { units, .. } => units,
4000 TypeSpecification::MeasureRange { units, .. } => units,
4001 _ => unreachable!(
4002 "BUG: measure_unit_factor called with non-measure type {}; only call during evaluation after planning validated measure conversion",
4003 self.name()
4004 ),
4005 };
4006 match units.get(unit_name) {
4007 Ok(MeasureUnit { factor, .. }) => factor,
4008 Err(_) => {
4009 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
4010 unreachable!(
4011 "BUG: unknown unit '{}' for measure type {} (valid: {}); planning must reject invalid conversions with Error",
4012 unit_name,
4013 self.name(),
4014 valid.join(", ")
4015 );
4016 }
4017 }
4018 }
4019
4020 pub fn ratio_unit_factor(
4021 &self,
4022 unit_name: &str,
4023 ) -> &crate::computation::rational::RationalInteger {
4024 let units = match &self.specifications {
4025 TypeSpecification::Ratio { units, .. } => units,
4026 _ => unreachable!(
4027 "BUG: ratio_unit_factor called with non-ratio type {}; only call during evaluation after planning validated ratio conversion",
4028 self.name()
4029 ),
4030 };
4031 match units.get(unit_name) {
4032 Ok(RatioUnit { value, .. }) => value,
4033 Err(_) => {
4034 let valid: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
4035 unreachable!(
4036 "BUG: unknown unit '{}' for ratio type {} (valid: {}); planning must reject invalid conversions with Error",
4037 unit_name,
4038 self.name(),
4039 valid.join(", ")
4040 );
4041 }
4042 }
4043 }
4044
4045 pub(crate) fn measure_literal_in_all_units(
4047 &self,
4048 literal: &LiteralValue,
4049 ) -> Result<BTreeMap<String, String>, LiteralUnitMapFailure> {
4050 use crate::computation::rational::checked_div;
4051
4052 let unit_names = self
4053 .measure_unit_names()
4054 .expect("BUG: measure literal in all units requires declared units");
4055 let ValueKind::Measure(magnitude, _signature) = &literal.value else {
4056 panic!("BUG: measure_literal_in_all_units called with non-measure value");
4057 };
4058 let mut map = BTreeMap::new();
4059 for unit_name in unit_names {
4060 let unit_factor = self.measure_unit_factor(unit_name);
4061 let magnitude_in_unit = checked_div(magnitude, unit_factor)
4062 .map_err(LiteralUnitMapFailure::UnitConversion)?;
4063 let decimal_string = self
4064 .try_rational_as_decimal_string(&magnitude_in_unit)
4065 .map_err(LiteralUnitMapFailure::Commit)?;
4066 map.insert(unit_name.to_string(), decimal_string);
4067 }
4068 Ok(map)
4069 }
4070
4071 pub(crate) fn ratio_literal_in_all_units(
4073 &self,
4074 literal: &LiteralValue,
4075 ) -> Result<BTreeMap<String, String>, LiteralUnitMapFailure> {
4076 use crate::computation::rational::checked_mul;
4077
4078 let ratio_api_type = match &self.specifications {
4079 TypeSpecification::Ratio { .. } => self,
4080 TypeSpecification::RatioRange { .. } => {
4081 let element = range_element_type_specification(&self.specifications)
4082 .expect("BUG: ratio range type must have ratio element specification");
4083 let TypeSpecification::Ratio {
4084 units, decimals, ..
4085 } = element
4086 else {
4087 panic!("BUG: ratio range element spec must be Ratio");
4088 };
4089 return LemmaType::primitive(TypeSpecification::Ratio {
4090 minimum: None,
4091 maximum: None,
4092 decimals,
4093 units,
4094 help: String::new(),
4095 })
4096 .ratio_literal_in_all_units(literal);
4097 }
4098 _ => {
4099 panic!(
4100 "BUG: ratio_literal_in_all_units called with non-ratio type {}",
4101 self.name()
4102 );
4103 }
4104 };
4105 let units = match &ratio_api_type.specifications {
4106 TypeSpecification::Ratio { units, .. } => units,
4107 _ => unreachable!("BUG: ratio API type must be Ratio"),
4108 };
4109 let ValueKind::Ratio(canonical, _) = &literal.value else {
4110 panic!("BUG: ratio_literal_in_all_units called with non-ratio value");
4111 };
4112 if units.is_empty() {
4113 panic!(
4114 "BUG: ratio literal type '{}' must have declared units",
4115 self.name()
4116 );
4117 }
4118 let mut map = BTreeMap::new();
4119 for unit in units.iter() {
4120 let magnitude_in_unit = checked_mul(canonical, &unit.value)
4121 .map_err(LiteralUnitMapFailure::UnitConversion)?;
4122 let decimal_string = ratio_api_type
4123 .try_rational_as_decimal_string(&magnitude_in_unit)
4124 .map_err(LiteralUnitMapFailure::Commit)?;
4125 map.insert(unit.name.clone(), decimal_string);
4126 }
4127 Ok(map)
4128 }
4129}
4130
4131#[derive(Debug, Clone, PartialEq, Eq)]
4133pub(crate) enum LiteralUnitMapFailure {
4134 Commit(crate::computation::rational::NumericFailure),
4135 UnitConversion(crate::computation::rational::NumericFailure),
4136}
4137
4138#[derive(Clone, Debug, PartialEq, Eq, Hash)]
4140pub struct LiteralValue {
4141 pub value: ValueKind,
4142 pub lemma_type: Arc<LemmaType>,
4143}
4144
4145impl LiteralValue {
4146 fn single_measure_signature_unit_name(signature: &[(String, i32)]) -> Option<&str> {
4147 match signature {
4148 [(unit_name, 1)] => Some(unit_name.as_str()),
4149 _ => None,
4150 }
4151 }
4152}
4153
4154impl Serialize for LiteralValue {
4155 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
4156 where
4157 S: serde::Serializer,
4158 {
4159 use serde::ser::SerializeStruct;
4160 let mut state = serializer.serialize_struct("LiteralValue", 3)?;
4161 state.serialize_field("value", &self.value)?;
4162 state.serialize_field("lemma_type", self.lemma_type.as_ref())?;
4163 state.serialize_field("display_value", &self.display_value())?;
4164 state.end()
4165 }
4166}
4167
4168impl<'de> Deserialize<'de> for LiteralValue {
4169 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4170 where
4171 D: serde::Deserializer<'de>,
4172 {
4173 #[derive(Deserialize)]
4174 struct Raw {
4175 value: ValueKind,
4176 lemma_type: LemmaType,
4177 }
4178 let raw = Raw::deserialize(deserializer)?;
4179 Ok(Self {
4180 value: raw.value,
4181 lemma_type: Arc::new(raw.lemma_type),
4182 })
4183 }
4184}
4185
4186impl LiteralValue {
4187 pub fn text(s: String) -> Self {
4188 Self {
4189 value: ValueKind::Text(s),
4190 lemma_type: primitive_text_arc().clone(),
4191 }
4192 }
4193
4194 pub fn text_with_type(s: String, lemma_type: Arc<LemmaType>) -> Self {
4195 Self {
4196 value: ValueKind::Text(s),
4197 lemma_type,
4198 }
4199 }
4200
4201 pub fn number(n: RationalInteger) -> Self {
4202 Self {
4203 value: ValueKind::Number(n),
4204 lemma_type: primitive_number_arc().clone(),
4205 }
4206 }
4207
4208 pub fn number_from_decimal(decimal: Decimal) -> Self {
4209 Self::number(
4210 crate::literals::rational_from_parsed_decimal(decimal)
4211 .expect("BUG: literal number from decimal must lift at boundary"),
4212 )
4213 }
4214
4215 pub fn number_with_type(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
4216 Self {
4217 value: ValueKind::Number(n),
4218 lemma_type,
4219 }
4220 }
4221
4222 pub fn number_with_type_from_decimal(decimal: Decimal, lemma_type: Arc<LemmaType>) -> Self {
4223 Self::number_with_type(
4224 crate::literals::rational_from_parsed_decimal(decimal)
4225 .expect("BUG: literal number from decimal must lift at boundary"),
4226 lemma_type,
4227 )
4228 }
4229
4230 pub fn measure_with_type(n: RationalInteger, unit: String, lemma_type: Arc<LemmaType>) -> Self {
4234 Self {
4235 value: ValueKind::Measure(n, vec![(unit, 1)]),
4236 lemma_type,
4237 }
4238 }
4239
4240 pub fn measure_with_signature(
4243 n: RationalInteger,
4244 signature: Vec<(String, i32)>,
4245 lemma_type: Arc<LemmaType>,
4246 ) -> Self {
4247 Self {
4248 value: ValueKind::Measure(n, signature),
4249 lemma_type,
4250 }
4251 }
4252
4253 pub fn number_interpreted_as_measure(value: RationalInteger, unit_name: String) -> Self {
4256 Self {
4257 value: ValueKind::Measure(value, vec![(unit_name, 1)]),
4258 lemma_type: Arc::new(anonymous_measure_type()),
4259 }
4260 }
4261
4262 pub fn from_bool(b: bool) -> Self {
4263 Self {
4264 value: ValueKind::Boolean(b),
4265 lemma_type: primitive_boolean_arc().clone(),
4266 }
4267 }
4268
4269 pub fn from_datetime(dt: &crate::parsing::ast::DateTimeValue) -> Self {
4270 Self::date(date_time_to_semantic(dt))
4271 }
4272
4273 #[must_use]
4275 pub fn magnitude_suggestion_for_decimal_prompt(&self) -> Option<String> {
4276 match &self.value {
4277 ValueKind::Number(n) => Some(
4278 self.lemma_type
4279 .try_rational_as_decimal_string(n)
4280 .expect("BUG: stored number literal must convert to decimal for prompt"),
4281 ),
4282 ValueKind::Measure(n, signature) => {
4283 let unit_name = Self::single_measure_signature_unit_name(signature).expect(
4284 "BUG: measure prompt requires exactly one signature unit with exponent 1",
4285 );
4286 Some(
4287 self.lemma_type
4288 .try_measure_canonical_as_decimal_in_unit(n, unit_name)
4289 .expect("BUG: stored measure literal must convert to decimal for prompt"),
4290 )
4291 }
4292 ValueKind::Ratio(n, Some(unit_name)) => Some(
4293 self.lemma_type
4294 .try_ratio_canonical_as_decimal_in_unit(n, unit_name)
4295 .expect("BUG: stored ratio literal must convert to decimal for prompt"),
4296 ),
4297 ValueKind::Ratio(n, None) => Some(
4298 self.lemma_type
4299 .try_rational_as_decimal_string(n)
4300 .expect("BUG: stored bare ratio literal must convert to decimal for prompt"),
4301 ),
4302 _ => None,
4303 }
4304 }
4305
4306 #[must_use]
4308 pub fn measure_units(&self) -> Option<BTreeMap<String, String>> {
4309 if !matches!(self.value, ValueKind::Measure(_, _)) {
4310 return None;
4311 }
4312 self.lemma_type.measure_unit_names()?;
4313 self.lemma_type.measure_literal_in_all_units(self).ok()
4314 }
4315
4316 #[must_use]
4318 pub fn ratio_units(&self) -> Option<BTreeMap<String, String>> {
4319 if !matches!(self.value, ValueKind::Ratio(_, _)) {
4320 return None;
4321 }
4322 let has_declared_units = match &self.lemma_type.specifications {
4323 TypeSpecification::Ratio { units, .. } => !units.is_empty(),
4324 TypeSpecification::RatioRange { .. } => true,
4325 _ => return None,
4326 };
4327 if !has_declared_units {
4328 return None;
4329 }
4330 self.lemma_type.ratio_literal_in_all_units(self).ok()
4331 }
4332
4333 #[must_use]
4335 pub fn magnitude_in_unit(&self, unit: &str) -> Option<String> {
4336 self.measure_units()
4337 .and_then(|map| map.get(unit).cloned())
4338 .or_else(|| self.ratio_units().and_then(|map| map.get(unit).cloned()))
4339 }
4340
4341 pub fn date(dt: SemanticDateTime) -> Self {
4342 Self {
4343 value: ValueKind::Date(dt),
4344 lemma_type: primitive_date_arc().clone(),
4345 }
4346 }
4347
4348 pub fn date_with_type(dt: SemanticDateTime, lemma_type: Arc<LemmaType>) -> Self {
4349 Self {
4350 value: ValueKind::Date(dt),
4351 lemma_type,
4352 }
4353 }
4354
4355 pub fn time(t: SemanticTime) -> Self {
4356 Self {
4357 value: ValueKind::Time(t),
4358 lemma_type: primitive_time_arc().clone(),
4359 }
4360 }
4361
4362 pub fn time_with_type(t: SemanticTime, lemma_type: Arc<LemmaType>) -> Self {
4363 Self {
4364 value: ValueKind::Time(t),
4365 lemma_type,
4366 }
4367 }
4368
4369 pub fn calendar(
4370 value: RationalInteger,
4371 unit: SemanticCalendarUnit,
4372 lemma_type: Arc<LemmaType>,
4373 ) -> Self {
4374 Self::measure_with_type(value, unit.to_string(), lemma_type)
4375 }
4376
4377 pub fn calendar_from_decimal(
4378 value: Decimal,
4379 unit: SemanticCalendarUnit,
4380 lemma_type: Arc<LemmaType>,
4381 ) -> Self {
4382 Self::calendar(
4383 crate::literals::rational_from_parsed_decimal(value)
4384 .expect("BUG: calendar literal from decimal must lift at boundary"),
4385 unit,
4386 lemma_type,
4387 )
4388 }
4389
4390 pub fn calendar_with_type(
4391 value: RationalInteger,
4392 unit: SemanticCalendarUnit,
4393 lemma_type: Arc<LemmaType>,
4394 ) -> Self {
4395 Self::calendar(value, unit, lemma_type)
4396 }
4397
4398 pub fn duration_canonical_seconds(&self) -> RationalInteger {
4400 let ValueKind::Measure(magnitude, _) = &self.value else {
4401 unreachable!(
4402 "BUG: duration_canonical_seconds called with {:?}",
4403 self.value
4404 );
4405 };
4406 if !self.lemma_type.is_duration_like_measure() {
4407 unreachable!(
4408 "BUG: duration_canonical_seconds called with type {}",
4409 self.lemma_type.name()
4410 );
4411 }
4412 let factor = self.lemma_type.measure_unit_factor("second");
4413 checked_div(magnitude, factor).expect("BUG: duration unit factor cannot be zero")
4414 }
4415
4416 pub fn calendar_canonical_months(&self) -> RationalInteger {
4418 let ValueKind::Measure(magnitude, _) = &self.value else {
4419 unreachable!(
4420 "BUG: calendar_canonical_months called with {:?}",
4421 self.value
4422 );
4423 };
4424 if !self.lemma_type.is_calendar_like() {
4425 unreachable!(
4426 "BUG: calendar_canonical_months called with type {}",
4427 self.lemma_type.name()
4428 );
4429 }
4430 let factor = self.lemma_type.measure_unit_factor("month");
4431 checked_div(magnitude, factor).expect("BUG: calendar unit factor cannot be zero")
4432 }
4433
4434 pub fn ratio(r: RationalInteger, unit: Option<String>) -> Self {
4435 Self {
4436 value: ValueKind::Ratio(r, unit),
4437 lemma_type: primitive_ratio_arc().clone(),
4438 }
4439 }
4440
4441 pub fn ratio_from_decimal(r: Decimal, unit: Option<String>) -> Self {
4442 Self::ratio(
4443 crate::literals::rational_from_parsed_decimal(r)
4444 .expect("BUG: ratio literal from decimal must lift at boundary"),
4445 unit,
4446 )
4447 }
4448
4449 pub fn ratio_with_type(
4450 r: RationalInteger,
4451 unit: Option<String>,
4452 lemma_type: Arc<LemmaType>,
4453 ) -> Self {
4454 Self {
4455 value: ValueKind::Ratio(r, unit),
4456 lemma_type,
4457 }
4458 }
4459
4460 pub fn range(left: LiteralValue, right: LiteralValue) -> Self {
4461 let specifications =
4462 range_type_specification_from_endpoints(&left.lemma_type, &right.lemma_type)
4463 .unwrap_or_else(|| {
4464 unreachable!(
4465 "BUG: attempted to construct a range literal from incompatible endpoint types"
4466 )
4467 });
4468
4469 Self {
4470 value: ValueKind::Range(Box::new(left), Box::new(right)),
4471 lemma_type: Arc::new(LemmaType::primitive(specifications)),
4472 }
4473 }
4474
4475 pub fn display_value(&self) -> String {
4477 format!("{}", self)
4478 }
4479
4480 pub fn byte_size(&self) -> usize {
4482 format!("{}", self).len()
4483 }
4484
4485 pub fn get_type(&self) -> &LemmaType {
4487 &self.lemma_type
4488 }
4489}
4490
4491#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
4494#[serde(rename_all = "snake_case", tag = "kind")]
4495pub enum ReferenceTarget {
4496 Data(DataPath),
4497 Rule(RulePath),
4498}
4499
4500#[derive(Clone, Debug, Serialize, Deserialize)]
4502#[serde(rename_all = "snake_case")]
4503pub enum DataDefinition {
4504 Value { value: LiteralValue, source: Source },
4506 TypeDeclaration {
4512 resolved_type: Arc<LemmaType>,
4513 declared_suggestion: Option<ValueKind>,
4514 source: Source,
4515 },
4516 Import { target_name: String, source: Source },
4518 Reference {
4543 target: ReferenceTarget,
4544 resolved_type: Arc<LemmaType>,
4545 local_constraints: Option<Vec<Constraint>>,
4546 local_suggestion: Option<ValueKind>,
4547 source: Source,
4548 },
4549}
4550
4551impl DataDefinition {
4552 pub fn lemma_type(&self) -> Option<&LemmaType> {
4554 match self {
4555 DataDefinition::Value { value, .. } => Some(value.lemma_type.as_ref()),
4556 DataDefinition::TypeDeclaration { resolved_type, .. } => Some(resolved_type.as_ref()),
4557 DataDefinition::Reference { resolved_type, .. } => Some(resolved_type.as_ref()),
4558 DataDefinition::Import { .. } => None,
4559 }
4560 }
4561
4562 #[inline]
4564 pub fn schema_type(&self) -> Option<&LemmaType> {
4565 self.lemma_type()
4566 }
4567
4568 pub fn value(&self) -> Option<&LiteralValue> {
4572 match self {
4573 DataDefinition::Value { value, .. } => Some(value),
4574 DataDefinition::TypeDeclaration { .. }
4575 | DataDefinition::Import { .. }
4576 | DataDefinition::Reference { .. } => None,
4577 }
4578 }
4579
4580 #[inline]
4583 pub fn prefilled_value(&self) -> Option<&LiteralValue> {
4584 self.value()
4585 }
4586
4587 pub fn suggestion(&self) -> Option<LiteralValue> {
4591 match self {
4592 DataDefinition::TypeDeclaration {
4593 resolved_type,
4594 declared_suggestion: Some(dv),
4595 ..
4596 } => Some(LiteralValue {
4597 value: dv.clone(),
4598 lemma_type: Arc::clone(resolved_type),
4599 }),
4600 DataDefinition::Reference {
4601 resolved_type,
4602 local_suggestion: Some(dv),
4603 ..
4604 } => Some(LiteralValue {
4605 value: dv.clone(),
4606 lemma_type: Arc::clone(resolved_type),
4607 }),
4608 DataDefinition::Value { .. }
4609 | DataDefinition::TypeDeclaration {
4610 declared_suggestion: None,
4611 ..
4612 }
4613 | DataDefinition::Reference {
4614 local_suggestion: None,
4615 ..
4616 }
4617 | DataDefinition::Import { .. } => None,
4618 }
4619 }
4620
4621 pub fn source(&self) -> &Source {
4623 match self {
4624 DataDefinition::Value { source, .. } => source,
4625 DataDefinition::TypeDeclaration { source, .. } => source,
4626 DataDefinition::Import { source, .. } => source,
4627 DataDefinition::Reference { source, .. } => source,
4628 }
4629 }
4630}
4631
4632pub fn number_with_unit_to_value_kind(
4634 magnitude: rust_decimal::Decimal,
4635 unit_name: &str,
4636 lemma_type: &LemmaType,
4637) -> Result<ValueKind, String> {
4638 match &lemma_type.specifications {
4639 TypeSpecification::Ratio { units, .. } => {
4640 use crate::computation::rational::{checked_div, decimal_to_rational};
4641 let unit = units.get(unit_name)?;
4642 let magnitude_rational = decimal_to_rational(magnitude)
4643 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
4644 let canonical_rational = checked_div(&magnitude_rational, &unit.value)
4645 .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
4646 Ok(ValueKind::Ratio(
4647 canonical_rational,
4648 Some(unit.name.clone()),
4649 ))
4650 }
4651 TypeSpecification::Measure { units, .. } => {
4652 use crate::computation::rational::checked_mul;
4653 let rational = lift_parser_decimal(magnitude)?;
4654 let unit = units.get(unit_name)?;
4655 let canonical = checked_mul(&rational, &unit.factor)
4656 .map_err(|failure| format!("measure canonicalization overflow: {failure}"))?;
4657 Ok(ValueKind::Measure(
4658 canonical,
4659 vec![(unit_name.to_string(), 1)],
4660 ))
4661 }
4662 _ => Err(format!(
4663 "Unit '{}' is defined on type '{}' which is not measure or ratio",
4664 unit_name,
4665 lemma_type.name()
4666 )),
4667 }
4668}
4669
4670pub(crate) fn value_kind_matches_spec(value: &ValueKind, type_spec: &TypeSpecification) -> bool {
4673 matches!(
4674 (type_spec, value),
4675 (TypeSpecification::Number { .. }, ValueKind::Number(_))
4676 | (TypeSpecification::Text { .. }, ValueKind::Text(_))
4677 | (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
4678 | (TypeSpecification::Date { .. }, ValueKind::Date(_))
4679 | (TypeSpecification::Time { .. }, ValueKind::Time(_))
4680 | (TypeSpecification::Measure { .. }, ValueKind::Measure(_, _))
4681 | (TypeSpecification::Ratio { .. }, ValueKind::Ratio(_, _))
4682 | (TypeSpecification::Ratio { .. }, ValueKind::Number(_))
4683 | (
4684 TypeSpecification::NumberRange { .. },
4685 ValueKind::Range(_, _)
4686 )
4687 | (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
4688 | (TypeSpecification::TimeRange { .. }, ValueKind::Range(_, _))
4689 | (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
4690 | (
4691 TypeSpecification::MeasureRange { .. },
4692 ValueKind::Range(_, _)
4693 )
4694 | (TypeSpecification::Veto { .. }, _)
4695 | (TypeSpecification::Undetermined, _)
4696 )
4697}
4698
4699fn parser_value_type_mismatch(
4700 value: &crate::literals::Value,
4701 type_spec: &TypeSpecification,
4702) -> String {
4703 use crate::parsing::ast::AsLemmaSource;
4704 let value_str = format!("{}", AsLemmaSource(value));
4705 match type_spec {
4706 TypeSpecification::Measure { units, .. } => {
4707 let unit_hint = units
4708 .iter()
4709 .find(|u| u.factor == crate::computation::rational::rational_one())
4710 .map(|u| u.name.as_str())
4711 .or_else(|| units.iter().next().map(|u| u.name.as_str()))
4712 .unwrap_or("unit");
4713 format!("cannot use {value_str} as {type_spec}: expected `<n> {unit_hint}`")
4714 }
4715 TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
4716 let unit_hint = units
4717 .iter()
4718 .next()
4719 .map(|u| u.name.as_str())
4720 .unwrap_or("unit");
4721 format!(
4722 "cannot use {value_str} as {type_spec}: expected `<n> {unit_hint}` or bare ratio"
4723 )
4724 }
4725 _ => format!("cannot use {value_str} as {type_spec}"),
4726 }
4727}
4728
4729pub fn refresh_measure_literal_canonical_magnitude(
4734 lit: &mut LiteralValue,
4735 resolved_type: &LemmaType,
4736) {
4737 let ValueKind::Measure(magnitude, signature) = &mut lit.value else {
4738 return;
4739 };
4740 let (unit_name, exponent) = signature
4741 .first()
4742 .expect("BUG: measure literal has empty signature during canonical magnitude refresh");
4743 if *exponent != 1 || signature.len() != 1 {
4744 return;
4745 }
4746 let stored_factor = lit.lemma_type.measure_unit_factor(unit_name);
4747 let resolved_factor = resolved_type.measure_unit_factor(unit_name);
4748 if stored_factor == resolved_factor {
4749 lit.lemma_type = Arc::new(resolved_type.clone());
4750 return;
4751 }
4752 let scaled = checked_mul(magnitude, resolved_factor)
4753 .expect("BUG: measure recanonicalization multiply overflow");
4754 *magnitude =
4755 checked_div(&scaled, stored_factor).expect("BUG: measure recanonicalization divide failed");
4756 lit.lemma_type = Arc::new(resolved_type.clone());
4757}
4758
4759pub fn parser_value_to_value_kind(
4761 value: &crate::literals::Value,
4762 type_spec: &TypeSpecification,
4763) -> Result<ValueKind, String> {
4764 use crate::computation::rational::decimal_to_rational;
4765 use crate::literals::Value;
4766 match (value, type_spec) {
4767 (Value::NumberWithUnit(magnitude, unit_name), TypeSpecification::Ratio { units, .. }) => {
4768 use crate::computation::rational::checked_div;
4769 let unit = units.get(unit_name.as_str())?;
4770 let magnitude_rational = decimal_to_rational(*magnitude)
4771 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
4772 let canonical_rational = checked_div(&magnitude_rational, &unit.value)
4773 .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
4774 Ok(ValueKind::Ratio(
4775 canonical_rational,
4776 Some(unit.name.clone()),
4777 ))
4778 }
4779 (Value::NumberWithUnit(magnitude, unit_name), TypeSpecification::Measure { units, .. }) => {
4780 use crate::computation::rational::checked_mul;
4781 let rational = lift_parser_decimal(*magnitude)?;
4782 let unit = units.get(unit_name.as_str())?;
4783 let canonical = checked_mul(&rational, &unit.factor)
4784 .map_err(|failure| format!("measure canonicalization overflow: {failure}"))?;
4785 Ok(ValueKind::Measure(canonical, vec![(unit_name.clone(), 1)]))
4786 }
4787 (Value::NumberWithUnit(_, _), _) => {
4788 Err("number_with_unit literal requires a measure or ratio type".to_string())
4789 }
4790 (Value::Number(n), TypeSpecification::Number { .. }) => {
4791 Ok(ValueKind::Number(lift_parser_decimal(*n)?))
4792 }
4793 (Value::Number(n), TypeSpecification::Ratio { .. }) => {
4794 let r = decimal_to_rational(*n)
4795 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
4796 Ok(ValueKind::Ratio(r, None))
4797 }
4798 (Value::Text(s), TypeSpecification::Text { .. }) => Ok(ValueKind::Text(s.clone())),
4799 (Value::Boolean(b), TypeSpecification::Boolean { .. }) => Ok(ValueKind::Boolean(b.into())),
4800 (Value::Date(dt), TypeSpecification::Date { .. }) => {
4801 Ok(ValueKind::Date(date_time_to_semantic(dt)))
4802 }
4803 (Value::Time(t), TypeSpecification::Time { .. }) => {
4804 Ok(ValueKind::Time(time_to_semantic(t)))
4805 }
4806 (
4807 Value::Range(left, right),
4808 range_spec @ (TypeSpecification::NumberRange { .. }
4809 | TypeSpecification::DateRange { .. }
4810 | TypeSpecification::TimeRange { .. }
4811 | TypeSpecification::RatioRange { .. }
4812 | TypeSpecification::MeasureRange { .. }),
4813 ) => {
4814 let endpoint = range_element_type_specification(range_spec).ok_or_else(|| {
4815 "BUG: range_element_type_specification missing arm for range type".to_string()
4816 })?;
4817 let left_lit = lift_range_endpoint(left, &endpoint)?;
4818 let right_lit = lift_range_endpoint(right, &endpoint)?;
4819 Ok(ValueKind::Range(Box::new(left_lit), Box::new(right_lit)))
4820 }
4821 (value, type_spec) => Err(parser_value_type_mismatch(value, type_spec)),
4822 }
4823}
4824
4825pub fn value_to_semantic(value: &crate::parsing::ast::Value) -> Result<ValueKind, String> {
4829 use crate::parsing::ast::Value;
4830 Ok(match value {
4831 Value::Number(n) => ValueKind::Number(lift_parser_decimal(*n)?),
4832 Value::Text(s) => ValueKind::Text(s.clone()),
4833 Value::Boolean(b) => ValueKind::Boolean(bool::from(*b)),
4834 Value::Date(dt) => ValueKind::Date(date_time_to_semantic(dt)),
4835 Value::Time(t) => ValueKind::Time(time_to_semantic(t)),
4836 Value::NumberWithUnit(_, _) => {
4837 return Err(
4838 "number_with_unit literal requires type context (measure or ratio)".to_string(),
4839 );
4840 }
4841 Value::Range(_, _) => literal_value_from_parser_value(value)?.value,
4842 })
4843}
4844
4845pub(crate) fn date_time_to_semantic(dt: &crate::parsing::ast::DateTimeValue) -> SemanticDateTime {
4847 SemanticDateTime {
4848 year: dt.year,
4849 month: dt.month,
4850 day: dt.day,
4851 hour: dt.hour,
4852 minute: dt.minute,
4853 second: dt.second,
4854 microsecond: dt.microsecond,
4855 timezone: dt.timezone.as_ref().map(|tz| SemanticTimezone {
4856 offset_hours: tz.offset_hours,
4857 offset_minutes: tz.offset_minutes,
4858 }),
4859 }
4860}
4861
4862pub(crate) fn time_to_semantic(t: &crate::parsing::ast::TimeValue) -> SemanticTime {
4864 SemanticTime {
4865 hour: t.hour.into(),
4866 minute: t.minute.into(),
4867 second: t.second.into(),
4868 microsecond: t.microsecond,
4869 timezone: t.timezone.as_ref().map(|tz| SemanticTimezone {
4870 offset_hours: tz.offset_hours,
4871 offset_minutes: tz.offset_minutes,
4872 }),
4873 }
4874}
4875
4876pub(crate) fn compare_semantic_dates(
4880 left: &SemanticDateTime,
4881 right: &SemanticDateTime,
4882) -> std::cmp::Ordering {
4883 left.year
4884 .cmp(&right.year)
4885 .then_with(|| left.month.cmp(&right.month))
4886 .then_with(|| left.day.cmp(&right.day))
4887 .then_with(|| left.hour.cmp(&right.hour))
4888 .then_with(|| left.minute.cmp(&right.minute))
4889 .then_with(|| left.second.cmp(&right.second))
4890 .then_with(|| left.microsecond.cmp(&right.microsecond))
4891}
4892
4893pub(crate) fn compare_semantic_times(
4896 left: &SemanticTime,
4897 right: &SemanticTime,
4898) -> std::cmp::Ordering {
4899 left.hour
4900 .cmp(&right.hour)
4901 .then_with(|| left.minute.cmp(&right.minute))
4902 .then_with(|| left.second.cmp(&right.second))
4903 .then_with(|| left.microsecond.cmp(&right.microsecond))
4904}
4905
4906pub fn conversion_target_to_semantic(
4908 ct: &ConversionTarget,
4909 unit_index: Option<&crate::planning::unit_index::UnitIndex>,
4910) -> Result<SemanticConversionTarget, String> {
4911 match ct {
4912 ConversionTarget::Type(kind) => Ok(SemanticConversionTarget::Type(*kind)),
4913 ConversionTarget::Unit { unit_name } => {
4914 let index = unit_index.ok_or_else(|| format!("Unknown unit '{unit_name}'."))?;
4915 let (bare, owning_type) = index.resolve(unit_name)?;
4916 Ok(SemanticConversionTarget::Unit {
4917 unit_name: bare,
4918 owning_type,
4919 })
4920 }
4921 }
4922}
4923
4924static PRIMITIVE_BOOLEAN: OnceLock<Arc<LemmaType>> = OnceLock::new();
4930static PRIMITIVE_NUMBER: OnceLock<Arc<LemmaType>> = OnceLock::new();
4931static PRIMITIVE_TEXT: OnceLock<Arc<LemmaType>> = OnceLock::new();
4932static PRIMITIVE_DATE: OnceLock<Arc<LemmaType>> = OnceLock::new();
4933static PRIMITIVE_DATE_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
4934static PRIMITIVE_TIME: OnceLock<Arc<LemmaType>> = OnceLock::new();
4935static PRIMITIVE_RATIO: OnceLock<Arc<LemmaType>> = OnceLock::new();
4936
4937#[must_use]
4938pub fn primitive_boolean_arc() -> &'static Arc<LemmaType> {
4939 PRIMITIVE_BOOLEAN.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::boolean())))
4940}
4941
4942#[must_use]
4943pub fn primitive_number_arc() -> &'static Arc<LemmaType> {
4944 PRIMITIVE_NUMBER.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::number())))
4945}
4946
4947#[must_use]
4948pub fn primitive_text_arc() -> &'static Arc<LemmaType> {
4949 PRIMITIVE_TEXT.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::text())))
4950}
4951
4952#[must_use]
4953pub fn primitive_date_arc() -> &'static Arc<LemmaType> {
4954 PRIMITIVE_DATE.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::date())))
4955}
4956
4957#[must_use]
4958pub fn primitive_date_range_arc() -> &'static Arc<LemmaType> {
4959 PRIMITIVE_DATE_RANGE
4960 .get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::date_range())))
4961}
4962
4963#[must_use]
4964pub fn primitive_time_arc() -> &'static Arc<LemmaType> {
4965 PRIMITIVE_TIME.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::time())))
4966}
4967
4968#[must_use]
4969pub fn primitive_ratio_arc() -> &'static Arc<LemmaType> {
4970 PRIMITIVE_RATIO.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::ratio())))
4971}
4972
4973#[must_use]
4975pub fn type_spec_for_primitive(kind: PrimitiveKind) -> TypeSpecification {
4976 match kind {
4977 PrimitiveKind::Boolean => TypeSpecification::boolean(),
4978 PrimitiveKind::Measure => TypeSpecification::measure(),
4979 PrimitiveKind::MeasureRange => TypeSpecification::measure_range(),
4980 PrimitiveKind::Number => TypeSpecification::number(),
4981 PrimitiveKind::NumberRange => TypeSpecification::number_range(),
4982 PrimitiveKind::Ratio => TypeSpecification::ratio(),
4983 PrimitiveKind::RatioRange => TypeSpecification::ratio_range(),
4984 PrimitiveKind::Text => TypeSpecification::text(),
4985 PrimitiveKind::Date => TypeSpecification::date(),
4986 PrimitiveKind::DateRange => TypeSpecification::date_range(),
4987 PrimitiveKind::Time => TypeSpecification::time(),
4988 PrimitiveKind::TimeRange => TypeSpecification::time_range(),
4989 }
4990}
4991
4992impl fmt::Display for PathSegment {
4997 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4998 write!(f, "{} → {}", self.data, self.spec)
4999 }
5000}
5001
5002impl fmt::Display for DataPath {
5003 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5004 for segment in &self.segments {
5005 write!(f, "{}.", segment)?;
5006 }
5007 write!(f, "{}", self.data)
5008 }
5009}
5010
5011impl fmt::Display for RulePath {
5012 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5013 for segment in &self.segments {
5014 write!(f, "{}.", segment)?;
5015 }
5016 write!(f, "{}", self.rule)
5017 }
5018}
5019
5020impl fmt::Display for LemmaType {
5021 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5022 write!(f, "{}", self.name())
5023 }
5024}
5025
5026fn decimal_places_in_display_value(decimal: &rust_decimal::Decimal) -> u32 {
5027 if decimal.is_integer() {
5028 return 0;
5029 }
5030 decimal.fract().normalize().scale()
5031}
5032
5033fn format_decimal_for_api(decimal: rust_decimal::Decimal, decimal_places: Option<u8>) -> String {
5034 match decimal_places {
5035 Some(decimal_places) => {
5036 let rounded = decimal.round_dp(u32::from(decimal_places));
5037 format!("{:.prec$}", rounded, prec = decimal_places as usize)
5038 }
5039 None => {
5040 let normalized = decimal.normalize();
5041 if normalized.fract().is_zero() {
5042 normalized.trunc().to_string()
5043 } else {
5044 normalized.to_string()
5045 }
5046 }
5047 }
5048}
5049
5050fn format_decimal_for_human_display(
5051 decimal: rust_decimal::Decimal,
5052 decimal_places: Option<u8>,
5053) -> String {
5054 match decimal_places {
5055 Some(decimal_places) => {
5056 let rounded = decimal.round_dp(u32::from(decimal_places));
5057 format!("{:.prec$}", rounded, prec = decimal_places as usize)
5058 }
5059 None => decimal.normalize().to_string(),
5060 }
5061}
5062
5063fn format_rational_for_human_display(
5064 magnitude: &crate::computation::rational::RationalInteger,
5065 decimal_places: Option<u8>,
5066) -> String {
5067 match magnitude.try_to_decimal() {
5068 Ok(decimal) => format_decimal_for_human_display(decimal, decimal_places),
5069 Err(crate::computation::rational::NumericFailure::Overflow) => magnitude.display_str(),
5070 Err(_) => magnitude.display_str(),
5071 }
5072}
5073
5074fn format_measure_canonical_for_display(
5075 canonical: &crate::computation::rational::RationalInteger,
5076 lemma_type: &LemmaType,
5077 signature: &[(String, i32)],
5078) -> String {
5079 use crate::computation::rational::{checked_div, rational_new};
5080 use rust_decimal::Decimal;
5081
5082 let decimals = lemma_type.decimal_places();
5083
5084 if let TypeSpecification::Measure { units, .. } = &lemma_type.specifications {
5085 if !units.is_empty() {
5086 if let [(sig_unit, 1)] = signature {
5087 if let Some(unit) = units.iter().find(|u| u.name == *sig_unit) {
5088 let in_unit = checked_div(canonical, &unit.factor)
5089 .expect("BUG: de-canonicalization for measure display must not fail");
5090 let formatted = format_rational_for_human_display(&in_unit, decimals);
5091 return format!("{} {}", formatted, unit.name);
5092 }
5093 }
5094
5095 struct UnitDisplayCandidate {
5096 unit_name: String,
5097 decimal_places: u32,
5098 under_1000: bool,
5099 decimal_abs: Option<Decimal>,
5100 formatted: String,
5101 }
5102
5103 let thousand = rational_new(1000, 1);
5104 let mut candidates: Vec<UnitDisplayCandidate> = Vec::with_capacity(units.len());
5105 for unit in units.iter() {
5106 let in_unit = checked_div(canonical, &unit.factor)
5107 .expect("BUG: de-canonicalization for measure display must not fail");
5108 let formatted = format_rational_for_human_display(&in_unit, decimals);
5109 let decimal_abs = in_unit.try_to_decimal().ok().map(|decimal| decimal.abs());
5110 let decimal_places = decimal_abs
5111 .as_ref()
5112 .map(decimal_places_in_display_value)
5113 .unwrap_or(u32::MAX);
5114 let under_1000 = in_unit
5115 .try_cmp(&thousand)
5116 .ok()
5117 .is_some_and(|ordering| ordering == std::cmp::Ordering::Less);
5118 candidates.push(UnitDisplayCandidate {
5119 unit_name: unit.name.clone(),
5120 decimal_places,
5121 under_1000,
5122 decimal_abs,
5123 formatted,
5124 });
5125 }
5126
5127 let pool: Vec<&UnitDisplayCandidate> = {
5128 let under: Vec<_> = candidates.iter().filter(|c| c.under_1000).collect();
5129 if under.is_empty() {
5130 candidates.iter().collect()
5131 } else {
5132 under
5133 }
5134 };
5135 let best = pool
5136 .iter()
5137 .min_by(|left, right| {
5138 left.decimal_places
5139 .cmp(&right.decimal_places)
5140 .then_with(|| match (left.decimal_abs, right.decimal_abs) {
5141 (Some(left_abs), Some(right_abs)) => left_abs.cmp(&right_abs),
5142 (Some(_), None) => std::cmp::Ordering::Less,
5143 (None, Some(_)) => std::cmp::Ordering::Greater,
5144 (None, None) => std::cmp::Ordering::Equal,
5145 })
5146 })
5147 .expect("BUG: measure type must have at least one declared unit");
5148 return format!("{} {}", best.formatted, best.unit_name);
5149 }
5150 }
5151
5152 let unit_label = match signature {
5153 [] => String::new(),
5154 [(name, 1)] => name.clone(),
5155 _ => format_signature_operator_style(signature),
5156 };
5157 let formatted = format_rational_for_human_display(canonical, decimals);
5158 if unit_label.is_empty() {
5159 formatted
5160 } else {
5161 format!("{formatted} {unit_label}")
5162 }
5163}
5164
5165impl fmt::Display for LiteralValue {
5166 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5167 match &self.value {
5168 ValueKind::Measure(n, signature) => {
5169 write!(
5170 f,
5171 "{}",
5172 format_measure_canonical_for_display(n, &self.lemma_type, signature)
5173 )
5174 }
5175 ValueKind::Ratio(_, Some(_unit_name)) => write!(f, "{}", self.value),
5176 ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
5177 _ => write!(f, "{}", self.value),
5178 }
5179 }
5180}
5181
5182#[cfg(test)]
5187pub(crate) mod tests {
5188 use super::*;
5189 use crate::computation::rational::decimal_to_rational;
5190 use crate::literals::DateGranularity;
5191 use crate::literals::Value;
5192 use crate::parsing::ast::{BooleanValue, DateTimeValue, PrimitiveKind, TimeValue};
5193 use rust_decimal::Decimal;
5194 use std::str::FromStr;
5195 use std::sync::{Arc, OnceLock};
5196
5197 static PRIMITIVE_MEASURE: OnceLock<Arc<LemmaType>> = OnceLock::new();
5198
5199 #[must_use]
5200 pub(crate) fn primitive_measure_arc() -> &'static Arc<LemmaType> {
5201 PRIMITIVE_MEASURE
5202 .get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::measure())))
5203 }
5204
5205 #[must_use]
5206 pub(crate) fn primitive_measure() -> &'static LemmaType {
5207 primitive_measure_arc().as_ref()
5208 }
5209
5210 #[test]
5211 fn default_primitive_help_is_goal_oriented() {
5212 let kinds = [
5213 PrimitiveKind::Boolean,
5214 PrimitiveKind::Measure,
5215 PrimitiveKind::MeasureRange,
5216 PrimitiveKind::Number,
5217 PrimitiveKind::NumberRange,
5218 PrimitiveKind::Ratio,
5219 PrimitiveKind::RatioRange,
5220 PrimitiveKind::Text,
5221 PrimitiveKind::Date,
5222 PrimitiveKind::DateRange,
5223 PrimitiveKind::Time,
5224 PrimitiveKind::TimeRange,
5225 ];
5226 for kind in kinds {
5227 let spec = type_spec_for_primitive(kind);
5228 let help = match &spec {
5229 TypeSpecification::Boolean { help, .. }
5230 | TypeSpecification::Number { help, .. }
5231 | TypeSpecification::NumberRange { help, .. }
5232 | TypeSpecification::Text { help, .. }
5233 | TypeSpecification::Measure { help, .. }
5234 | TypeSpecification::MeasureRange { help, .. }
5235 | TypeSpecification::Ratio { help, .. }
5236 | TypeSpecification::RatioRange { help, .. }
5237 | TypeSpecification::Date { help, .. }
5238 | TypeSpecification::DateRange { help, .. }
5239 | TypeSpecification::TimeRange { help, .. }
5240 | TypeSpecification::Time { help, .. } => help,
5241 TypeSpecification::Veto { .. } | TypeSpecification::Undetermined => {
5242 unreachable!(
5243 "BUG: primitive kind {:?} mapped to non-primitive spec",
5244 kind
5245 )
5246 }
5247 };
5248 assert!(!help.is_empty(), "help for {:?}", kind);
5249 assert!(
5250 !help.to_ascii_lowercase().contains("format:"),
5251 "help for {:?} must not describe syntax: {:?}",
5252 kind,
5253 help
5254 );
5255 assert_eq!(help, default_help_for_primitive(kind));
5256 }
5257 }
5258
5259 #[test]
5260 fn test_negated_comparison() {
5261 assert_eq!(
5262 negated_comparison(ComparisonComputation::LessThan),
5263 ComparisonComputation::GreaterThanOrEqual
5264 );
5265 assert_eq!(
5266 negated_comparison(ComparisonComputation::GreaterThanOrEqual),
5267 ComparisonComputation::LessThan
5268 );
5269 assert_eq!(
5270 negated_comparison(ComparisonComputation::Is),
5271 ComparisonComputation::IsNot
5272 );
5273 assert_eq!(
5274 negated_comparison(ComparisonComputation::IsNot),
5275 ComparisonComputation::Is
5276 );
5277 }
5278
5279 #[test]
5280 fn value_to_semantic_number_is_decimal() {
5281 let kind = value_to_semantic(&Value::Number(Decimal::from(42))).unwrap();
5282 assert!(matches!(kind, ValueKind::Number(d) if d == rational_new(42, 1)));
5283 }
5284
5285 #[test]
5286 fn value_kind_measure_serializes_with_signature() {
5287 let kind = ValueKind::Measure(
5288 decimal_to_rational(Decimal::from_str("99.50").unwrap()).unwrap(),
5289 vec![("eur".to_string(), 1)],
5290 );
5291 let json = serde_json::to_value(&kind).unwrap();
5292 assert_eq!(json["measure"]["value"], "99.5");
5293 assert_eq!(json["measure"]["signature"][0][0], "eur");
5294 assert_eq!(json["measure"]["signature"][0][1], 1);
5295 }
5296
5297 #[test]
5298 fn value_kind_measure_compound_signature_roundtrips() {
5299 let original = ValueKind::Measure(
5300 decimal_to_rational(Decimal::from_str("4800").unwrap()).unwrap(),
5301 vec![
5302 ("eur".to_string(), 1),
5303 ("hour".to_string(), 1),
5304 ("minute".to_string(), -1),
5305 ],
5306 );
5307 let json = serde_json::to_string(&original).unwrap();
5308 let parsed: ValueKind = serde_json::from_str(&json).unwrap();
5309 assert_eq!(original, parsed);
5310 }
5311
5312 #[test]
5313 fn value_kind_measure_empty_signature_roundtrips() {
5314 let original = ValueKind::Measure(
5315 decimal_to_rational(Decimal::from_str("12.5").unwrap()).unwrap(),
5316 Vec::new(),
5317 );
5318 let json = serde_json::to_string(&original).unwrap();
5319 let parsed: ValueKind = serde_json::from_str(&json).unwrap();
5320 assert_eq!(original, parsed);
5321 }
5322
5323 #[test]
5324 fn literal_value_number_serde_not_rational_array() {
5325 let lit = LiteralValue::number_from_decimal(Decimal::from(20));
5326 let json = serde_json::to_value(&lit).unwrap();
5327 let number = json
5328 .get("value")
5329 .and_then(|v| v.get("number"))
5330 .expect("number field");
5331 assert!(number.is_string());
5332 assert_eq!(number.as_str(), Some("20"));
5333 assert!(
5334 !number.is_array(),
5335 "stored number must not serialize as [n,d]"
5336 );
5337 }
5338
5339 #[test]
5340 fn test_literal_value_to_primitive_type() {
5341 let one = rational_new(1, 1);
5342
5343 assert_eq!(LiteralValue::text("".to_string()).lemma_type.name(), "text");
5344 assert_eq!(
5345 LiteralValue::number(one.clone()).lemma_type.name(),
5346 "number"
5347 );
5348 assert_eq!(
5349 LiteralValue::from_bool(bool::from(BooleanValue::True))
5350 .lemma_type
5351 .name(),
5352 "boolean"
5353 );
5354
5355 let dt = DateTimeValue {
5356 year: 2024,
5357 month: 1,
5358 day: 1,
5359 hour: 0,
5360 minute: 0,
5361 second: 0,
5362 microsecond: 0,
5363 timezone: None,
5364
5365 granularity: DateGranularity::Full,
5366 };
5367 assert_eq!(
5368 LiteralValue::date(date_time_to_semantic(&dt))
5369 .lemma_type
5370 .name(),
5371 "date"
5372 );
5373 assert_eq!(
5374 LiteralValue::ratio_from_decimal(Decimal::new(1, 2), Some("percent".to_string()))
5375 .lemma_type
5376 .name(),
5377 "ratio"
5378 );
5379 let dur_type = LemmaType::new(
5380 "duration".to_string(),
5381 TypeSpecification::Measure {
5382 minimum: None,
5383 maximum: None,
5384 decimals: None,
5385 units: MeasureUnits::from(vec![MeasureUnit {
5386 name: "second".to_string(),
5387 factor: crate::computation::rational::rational_one(),
5388 derived_measure_factors: Vec::new(),
5389 decomposition: BaseMeasureVector::new(),
5390 minimum: None,
5391 maximum: None,
5392 suggestion_magnitude: None,
5393 }]),
5394 traits: vec![MeasureTrait::Duration],
5395 decomposition: None,
5396 help: String::new(),
5397 },
5398 TypeExtends::Primitive,
5399 );
5400 assert_eq!(
5401 LiteralValue::measure_with_type(one.clone(), "second".to_string(), Arc::new(dur_type))
5402 .lemma_type
5403 .name(),
5404 "duration"
5405 );
5406 }
5407
5408 #[test]
5409 fn test_type_display() {
5410 let specs = TypeSpecification::text();
5411 let lemma_type = LemmaType::new("name".to_string(), specs, TypeExtends::Primitive);
5412 assert_eq!(format!("{}", lemma_type), "name");
5413 }
5414
5415 #[test]
5416 fn test_type_serialization() {
5417 let specs = TypeSpecification::number();
5418 let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
5419 let serialized = serde_json::to_string(&lemma_type).unwrap();
5420 let deserialized: LemmaType = serde_json::from_str(&serialized).unwrap();
5421 assert_eq!(lemma_type, deserialized);
5422 }
5423
5424 #[test]
5425 fn test_literal_value_display_value() {
5426 let ten = rational_new(10, 1);
5427
5428 assert_eq!(
5429 LiteralValue::text("hello".to_string()).display_value(),
5430 "hello"
5431 );
5432 assert_eq!(LiteralValue::number(ten).display_value(), "10");
5433 assert_eq!(LiteralValue::from_bool(true).display_value(), "true");
5434 assert_eq!(LiteralValue::from_bool(false).display_value(), "false");
5435
5436 let ten_percent_ratio =
5438 LiteralValue::ratio_from_decimal(Decimal::new(1, 1), Some("percent".to_string()));
5439 assert_eq!(ten_percent_ratio.display_value(), "10%");
5440
5441 let time = TimeValue {
5442 hour: 14,
5443 minute: 30,
5444 second: 0,
5445 microsecond: 0,
5446 timezone: None,
5447 };
5448 let time_display = LiteralValue::time(time_to_semantic(&time)).display_value();
5449 assert!(time_display.contains("14"));
5450 assert!(time_display.contains("30"));
5451 }
5452
5453 #[test]
5454 fn test_measure_display_respects_type_decimals() {
5455 let money_type = LemmaType {
5456 name: Some("money".to_string()),
5457 specifications: TypeSpecification::Measure {
5458 minimum: None,
5459 maximum: None,
5460 decimals: Some(2),
5461 units: MeasureUnits::from(vec![MeasureUnit {
5462 name: "eur".to_string(),
5463 factor: crate::computation::rational::rational_one(),
5464 derived_measure_factors: Vec::new(),
5465 decomposition: BaseMeasureVector::new(),
5466 minimum: None,
5467 maximum: None,
5468 suggestion_magnitude: None,
5469 }]),
5470 traits: Vec::new(),
5471 decomposition: None,
5472 help: String::new(),
5473 },
5474 extends: TypeExtends::Primitive,
5475 };
5476 let money_type = Arc::new(money_type);
5477 let val = LiteralValue::measure_with_type(
5478 decimal_to_rational(Decimal::from_str("1.8").unwrap()).unwrap(),
5479 "eur".to_string(),
5480 money_type.clone(),
5481 );
5482 assert_eq!(val.display_value(), "1.80 eur");
5483 let more_precision = LiteralValue::measure_with_type(
5484 decimal_to_rational(Decimal::from_str("1.80000").unwrap()).unwrap(),
5485 "eur".to_string(),
5486 money_type,
5487 );
5488 assert_eq!(more_precision.display_value(), "1.80 eur");
5489 let measure_no_decimals = LemmaType {
5490 name: Some("count".to_string()),
5491 specifications: TypeSpecification::Measure {
5492 minimum: None,
5493 maximum: None,
5494 decimals: None,
5495 units: MeasureUnits::from(vec![MeasureUnit {
5496 name: "items".to_string(),
5497 factor: crate::computation::rational::rational_one(),
5498 derived_measure_factors: Vec::new(),
5499 decomposition: BaseMeasureVector::new(),
5500 minimum: None,
5501 maximum: None,
5502 suggestion_magnitude: None,
5503 }]),
5504 traits: Vec::new(),
5505 decomposition: None,
5506 help: String::new(),
5507 },
5508 extends: TypeExtends::Primitive,
5509 };
5510 let val_any = LiteralValue::measure_with_type(
5511 decimal_to_rational(Decimal::from_str("42.50").unwrap()).unwrap(),
5512 "items".to_string(),
5513 Arc::new(measure_no_decimals),
5514 );
5515 assert_eq!(val_any.display_value(), "42.5 items");
5516 }
5517
5518 #[test]
5519 fn test_literal_value_time_type() {
5520 let time = TimeValue {
5521 hour: 14,
5522 minute: 30,
5523 second: 0,
5524 microsecond: 0,
5525 timezone: None,
5526 };
5527 let lit = LiteralValue::time(time_to_semantic(&time));
5528 assert_eq!(lit.lemma_type.name(), "time");
5529 }
5530
5531 #[test]
5532 fn test_measure_family_name_primitive_root() {
5533 let measure_spec = TypeSpecification::measure();
5534 let money_primitive = LemmaType::new(
5535 "money".to_string(),
5536 measure_spec.clone(),
5537 TypeExtends::Primitive,
5538 );
5539 assert_eq!(money_primitive.measure_family_name(), Some("money"));
5540 }
5541
5542 #[test]
5543 fn test_measure_family_name_custom() {
5544 let measure_spec = TypeSpecification::measure();
5545 let money_custom = LemmaType::new(
5546 "money".to_string(),
5547 measure_spec,
5548 TypeExtends::custom_local("money".to_string(), "money".to_string()),
5549 );
5550 assert_eq!(money_custom.measure_family_name(), Some("money"));
5551 }
5552
5553 #[test]
5554 fn test_same_measure_family_same_name_different_extends() {
5555 let measure_spec = TypeSpecification::measure();
5556 let money_primitive = LemmaType::new(
5557 "money".to_string(),
5558 measure_spec.clone(),
5559 TypeExtends::Primitive,
5560 );
5561 let money_custom = LemmaType::new(
5562 "money".to_string(),
5563 measure_spec,
5564 TypeExtends::custom_local("money".to_string(), "money".to_string()),
5565 );
5566 assert!(money_primitive.same_measure_family(&money_custom));
5567 assert!(money_custom.same_measure_family(&money_primitive));
5568 }
5569
5570 #[test]
5571 fn test_same_measure_family_parent_and_child() {
5572 let measure_spec = TypeSpecification::measure();
5573 let type_x = LemmaType::new(
5574 "x".to_string(),
5575 measure_spec.clone(),
5576 TypeExtends::Primitive,
5577 );
5578 let type_x2 = LemmaType::new(
5579 "x2".to_string(),
5580 measure_spec,
5581 TypeExtends::custom_local("x".to_string(), "x".to_string()),
5582 );
5583 assert_eq!(type_x.measure_family_name(), Some("x"));
5584 assert_eq!(type_x2.measure_family_name(), Some("x"));
5585 assert!(type_x.same_measure_family(&type_x2));
5586 assert!(type_x2.same_measure_family(&type_x));
5587 }
5588
5589 #[test]
5590 fn test_same_measure_family_siblings() {
5591 let measure_spec = TypeSpecification::measure();
5592 let type_x2_a = LemmaType::new(
5593 "x2a".to_string(),
5594 measure_spec.clone(),
5595 TypeExtends::custom_local("x".to_string(), "x".to_string()),
5596 );
5597 let type_x2_b = LemmaType::new(
5598 "x2b".to_string(),
5599 measure_spec,
5600 TypeExtends::custom_local("x".to_string(), "x".to_string()),
5601 );
5602 assert!(type_x2_a.same_measure_family(&type_x2_b));
5603 }
5604
5605 #[test]
5606 fn test_same_measure_family_different_families() {
5607 let measure_spec = TypeSpecification::measure();
5608 let money = LemmaType::new(
5609 "money".to_string(),
5610 measure_spec.clone(),
5611 TypeExtends::Primitive,
5612 );
5613 let temperature = LemmaType::new(
5614 "temperature".to_string(),
5615 measure_spec,
5616 TypeExtends::Primitive,
5617 );
5618 assert!(!money.same_measure_family(&temperature));
5619 assert!(!temperature.same_measure_family(&money));
5620 }
5621
5622 #[test]
5623 fn test_same_measure_family_measure_vs_non_measure() {
5624 let measure_spec = TypeSpecification::measure();
5625 let number_spec = TypeSpecification::number();
5626 let measure_type =
5627 LemmaType::new("money".to_string(), measure_spec, TypeExtends::Primitive);
5628 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
5629 assert!(!measure_type.same_measure_family(&number_type));
5630 assert!(!number_type.same_measure_family(&measure_type));
5631 }
5632
5633 #[test]
5634 fn test_same_measure_family_anonymous_measures_are_not_family_compatible() {
5635 let left = LemmaType::anonymous_for_decomposition(duration_decomposition());
5636 let right = LemmaType::anonymous_for_decomposition(duration_decomposition());
5637
5638 assert!(!left.same_measure_family(&right));
5639 assert!(left.compatible_with_anonymous_measure(&right));
5640 }
5641
5642 #[test]
5643 fn test_measure_family_name_non_measure_returns_none() {
5644 let number_spec = TypeSpecification::number();
5645 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
5646 assert_eq!(number_type.measure_family_name(), None);
5647 }
5648
5649 #[test]
5650 fn test_lemma_type_inequality_local_vs_import_same_shape() {
5651 let measure_spec = TypeSpecification::measure();
5652 let local = LemmaType::new(
5653 "t".to_string(),
5654 measure_spec.clone(),
5655 TypeExtends::custom_local("money".to_string(), "money".to_string()),
5656 );
5657 let imported = LemmaType::new(
5658 "t".to_string(),
5659 measure_spec,
5660 TypeExtends::Custom {
5661 parent: "money".to_string(),
5662 family: "money".to_string(),
5663 defining_spec: TypeDefiningSpec::Import,
5664 },
5665 );
5666 assert_ne!(local, imported);
5667 }
5668
5669 #[test]
5670 fn test_lemma_type_equality_import_unit_variant() {
5671 let measure_spec = TypeSpecification::measure();
5672 let left = LemmaType::new(
5673 "t".to_string(),
5674 measure_spec.clone(),
5675 TypeExtends::Custom {
5676 parent: "money".to_string(),
5677 family: "money".to_string(),
5678 defining_spec: TypeDefiningSpec::Import,
5679 },
5680 );
5681 let right = LemmaType::new(
5682 "t".to_string(),
5683 measure_spec,
5684 TypeExtends::Custom {
5685 parent: "money".to_string(),
5686 family: "money".to_string(),
5687 defining_spec: TypeDefiningSpec::Import,
5688 },
5689 );
5690 assert_eq!(left, right);
5691 }
5692
5693 fn month_suggestion_arg() -> CommandArg {
5694 CommandArg::Literal(crate::literals::Value::NumberWithUnit(
5695 Decimal::ONE,
5696 "month".to_string(),
5697 ))
5698 }
5699
5700 fn unit_factor_arg(name: &str, factor: i64) -> [CommandArg; 2] {
5701 [
5702 CommandArg::Label(name.to_string()),
5703 CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::from(factor))),
5704 ]
5705 }
5706
5707 #[test]
5708 fn default_calendar_on_text_reports_hint() {
5709 let mut specs = TypeSpecification::text();
5710 let mut default = None;
5711 let err = specs
5712 .apply_constraint(
5713 "notes",
5714 TypeConstraintCommand::Suggest,
5715 &[month_suggestion_arg()],
5716 &mut default,
5717 )
5718 .unwrap_err();
5719 assert!(err.contains("Unit 'month' is for calendar data"));
5720 assert!(err.contains("double quotes"));
5721 }
5722
5723 #[test]
5724 fn default_calendar_on_duration_reports_valid_units() {
5725 let mut specs = TypeSpecification::measure();
5726 specs
5727 .apply_constraint(
5728 "duration",
5729 TypeConstraintCommand::Unit,
5730 &unit_factor_arg("second", 1),
5731 &mut None,
5732 )
5733 .unwrap();
5734 specs
5735 .apply_constraint(
5736 "duration",
5737 TypeConstraintCommand::Unit,
5738 &unit_factor_arg("week", 604_800),
5739 &mut None,
5740 )
5741 .unwrap();
5742 specs
5743 .apply_constraint(
5744 "duration",
5745 TypeConstraintCommand::Trait,
5746 &[CommandArg::Label("duration".to_string())],
5747 &mut None,
5748 )
5749 .unwrap();
5750 let mut default = None;
5751 let err = specs
5752 .apply_constraint(
5753 "duration",
5754 TypeConstraintCommand::Suggest,
5755 &[month_suggestion_arg()],
5756 &mut default,
5757 )
5758 .unwrap_err();
5759 assert!(err.contains("Unit 'month' is for calendar data"));
5760 assert!(err.contains("Valid 'duration' units are"));
5761 assert!(err.contains("week"));
5762 }
5763
5764 #[test]
5765 fn default_valid_duration_weeks_accepted() {
5766 let mut specs = TypeSpecification::measure();
5767 specs
5768 .apply_constraint(
5769 "duration",
5770 TypeConstraintCommand::Unit,
5771 &unit_factor_arg("second", 1),
5772 &mut None,
5773 )
5774 .unwrap();
5775 specs
5776 .apply_constraint(
5777 "duration",
5778 TypeConstraintCommand::Unit,
5779 &unit_factor_arg("week", 604_800),
5780 &mut None,
5781 )
5782 .unwrap();
5783 specs
5784 .apply_constraint(
5785 "duration",
5786 TypeConstraintCommand::Trait,
5787 &[CommandArg::Label("duration".to_string())],
5788 &mut None,
5789 )
5790 .unwrap();
5791 let mut default = None;
5792 specs
5793 .apply_constraint(
5794 "duration",
5795 TypeConstraintCommand::Suggest,
5796 &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
5797 Decimal::from(4),
5798 "week".to_string(),
5799 ))],
5800 &mut default,
5801 )
5802 .unwrap();
5803 assert!(matches!(
5804 default,
5805 Some(RawSuggestion::Measure {
5806 unit_name,
5807 ..
5808 }) if unit_name == "week"
5809 ));
5810 }
5811
5812 #[test]
5813 fn default_unknown_unit_on_duration_lists_valid_units() {
5814 let mut specs = TypeSpecification::measure();
5815 specs
5816 .apply_constraint(
5817 "duration",
5818 TypeConstraintCommand::Unit,
5819 &unit_factor_arg("second", 1),
5820 &mut None,
5821 )
5822 .unwrap();
5823 specs
5824 .apply_constraint(
5825 "duration",
5826 TypeConstraintCommand::Trait,
5827 &[CommandArg::Label("duration".to_string())],
5828 &mut None,
5829 )
5830 .unwrap();
5831 let mut default = None;
5832 let err = specs
5833 .apply_constraint(
5834 "duration",
5835 TypeConstraintCommand::Suggest,
5836 &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
5837 Decimal::ONE,
5838 "fortnight".to_string(),
5839 ))],
5840 &mut default,
5841 )
5842 .unwrap_err();
5843 assert!(err.contains("fortnight"));
5844 assert!(err.contains("not defined on 'duration'"));
5845 assert!(err.contains("Valid units are"));
5846 }
5847
5848 fn money_measure_type() -> LemmaType {
5849 LemmaType::new(
5850 "Money".to_string(),
5851 TypeSpecification::Measure {
5852 minimum: None,
5853 maximum: None,
5854 decimals: None,
5855 units: MeasureUnits::from(vec![
5856 MeasureUnit {
5857 name: "eur".to_string(),
5858 factor: crate::computation::rational::rational_one(),
5859 derived_measure_factors: Vec::new(),
5860 decomposition: BaseMeasureVector::new(),
5861 minimum: None,
5862 maximum: None,
5863 suggestion_magnitude: None,
5864 },
5865 MeasureUnit {
5866 name: "usd".to_string(),
5867 factor: crate::computation::rational::decimal_to_rational(Decimal::new(
5868 91, 2,
5869 ))
5870 .expect("factor"),
5871 derived_measure_factors: Vec::new(),
5872 decomposition: BaseMeasureVector::new(),
5873 minimum: None,
5874 maximum: None,
5875 suggestion_magnitude: None,
5876 },
5877 ]),
5878 traits: Vec::new(),
5879 decomposition: None,
5880 help: String::new(),
5881 },
5882 TypeExtends::Primitive,
5883 )
5884 }
5885
5886 #[test]
5887 fn measure_unit_names_for_named_measure() {
5888 let money = money_measure_type();
5889 assert_eq!(money.measure_unit_names(), Some(vec!["eur", "usd"]));
5890 }
5891
5892 fn sig(pairs: &[(&str, i32)]) -> Vec<(String, i32)> {
5897 pairs.iter().map(|(s, e)| (s.to_string(), *e)).collect()
5898 }
5899
5900 #[test]
5901 fn combine_signatures_multiply_adds_exponents() {
5902 let left = sig(&[("eur", 1)]);
5903 let right = sig(&[("hour", -1)]);
5904 let result = combine_signatures(&left, &right, true);
5905 assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
5906 }
5907
5908 #[test]
5909 fn combine_signatures_divide_subtracts_exponents() {
5910 let left = sig(&[("eur", 1)]);
5911 let right = sig(&[("hour", 1)]);
5912 let result = combine_signatures(&left, &right, false);
5913 assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
5914 }
5915
5916 #[test]
5917 fn combine_signatures_cancels_to_empty() {
5918 let left = sig(&[("ce", 1), ("minute", -1)]);
5919 let right = sig(&[("minute", 1)]);
5920 let result = combine_signatures(&left, &right, true);
5921 assert_eq!(result, sig(&[("ce", 1)]));
5923 }
5924
5925 #[test]
5926 fn combine_signatures_output_is_canonical_form() {
5927 let left = sig(&[("eur", 1), ("hour", 1)]);
5928 let right = sig(&[("minute", 1)]);
5929 let result = combine_signatures(&left, &right, false); let expected = sig(&[("eur", 1), ("hour", 1), ("minute", -1)]);
5932 assert_eq!(result, expected);
5933 }
5934
5935 #[test]
5936 fn canonicalize_signature_drops_zero_exponents() {
5937 let sig_with_zero = sig(&[("eur", 1), ("hour", 0), ("minute", -1)]);
5938 let result = canonicalize_signature(&sig_with_zero);
5939 assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
5940 }
5941
5942 #[test]
5943 fn canonicalize_signature_sorts_by_name() {
5944 let unsorted = sig(&[("minute", -1), ("eur", 1)]);
5945 let result = canonicalize_signature(&unsorted);
5946 assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
5947 }
5948
5949 #[test]
5955 fn format_signature_operator_style_numerator_only() {
5956 let signature = sig(&[("eur", 1)]);
5957 let result = format_signature_operator_style(&signature);
5958 assert_eq!(result, "eur");
5959 }
5960
5961 #[test]
5962 fn format_signature_operator_style_with_denominator() {
5963 let signature = sig(&[("eur", 1), ("hour", -1)]);
5964 let result = format_signature_operator_style(&signature);
5965 assert_eq!(result, "eur/hour");
5966 }
5967
5968 #[test]
5969 fn format_signature_operator_style_denominator_only() {
5970 let signature = sig(&[("meter", -1)]);
5971 let result = format_signature_operator_style(&signature);
5972 assert_eq!(result, "1/meter");
5973 }
5974
5975 #[test]
5976 fn format_signature_operator_style_with_exponents() {
5977 let signature = sig(&[("meter", 2), ("second", -2)]);
5978 let result = format_signature_operator_style(&signature);
5979 assert_eq!(result, "meter^2/second^2");
5980 }
5981
5982 #[test]
5987 fn calendar_unit_factor_table_completeness() {
5988 for unit in &[SemanticCalendarUnit::Month, SemanticCalendarUnit::Year] {
5991 let name = unit.to_string();
5992 assert!(
5993 calendar_unit_factor(&name).is_some(),
5994 "calendar_unit_factor('{}') must return Some",
5995 name
5996 );
5997 }
5998 }
5999
6000 #[test]
6001 fn semantic_calendar_unit_display_returns_singular() {
6002 assert_eq!(SemanticCalendarUnit::Month.to_string(), "month");
6005 assert_eq!(SemanticCalendarUnit::Year.to_string(), "year");
6006 }
6007
6008 #[test]
6013 fn signature_factor_with_calendar_units() {
6014 let calendar = test_calendar_type_for_signature_factor();
6015 let unit_index = crate::planning::unit_index::UnitIndex::new();
6016 let sig_month_per_year = sig(&[("month", 1), ("year", -1)]);
6019 let factor = signature_factor(&sig_month_per_year, &unit_index, Some(&calendar))
6020 .expect("must not overflow");
6021 let expected = rational_new(1, 12);
6022 assert_eq!(factor, expected, "month/year factor must be 1/12");
6023 }
6024
6025 fn test_calendar_type_for_signature_factor() -> LemmaType {
6026 use crate::computation::rational::{decimal_to_rational, rational_one};
6027 use crate::literals::{MeasureUnit, MeasureUnits};
6028 use rust_decimal::Decimal;
6029 LemmaType::new(
6030 "calendar".to_string(),
6031 TypeSpecification::Measure {
6032 minimum: None,
6033 maximum: None,
6034 decimals: None,
6035 units: MeasureUnits::from(vec![
6036 MeasureUnit {
6037 name: "month".to_string(),
6038 factor: rational_one(),
6039 minimum: None,
6040 maximum: None,
6041 suggestion_magnitude: None,
6042 decomposition: calendar_decomposition(),
6043 derived_measure_factors: Vec::new(),
6044 },
6045 MeasureUnit {
6046 name: "year".to_string(),
6047 factor: decimal_to_rational(Decimal::from(12)).expect("year factor"),
6048 minimum: None,
6049 maximum: None,
6050 suggestion_magnitude: None,
6051 decomposition: calendar_decomposition(),
6052 derived_measure_factors: Vec::new(),
6053 },
6054 ]),
6055 traits: vec![MeasureTrait::Calendar],
6056 decomposition: Some(calendar_decomposition()),
6057 help: String::new(),
6058 },
6059 TypeExtends::Primitive,
6060 )
6061 }
6062
6063 #[test]
6064 #[should_panic(expected = "BUG: signature_factor called with unresolved unit name")]
6065 fn signature_factor_panics_on_unresolved_name() {
6066 let unit_index = crate::planning::unit_index::UnitIndex::new();
6067 let bad_sig = sig(&[("nonexistent_unit_xyz", 1)]);
6068 let _ = signature_factor(&bad_sig, &unit_index, None);
6069 }
6070
6071 #[test]
6072 fn signature_factor_uses_owner_when_expression_index_empty() {
6073 let money = test_money_type_for_signature_factor();
6074 let expression_units = crate::planning::unit_index::UnitIndex::new();
6075 let sig_usd = sig(&[("usd", 1)]);
6076 let factor =
6077 signature_factor(&sig_usd, &expression_units, Some(&money)).expect("must not overflow");
6078 assert_eq!(factor, rational_new(91, 100));
6079 }
6080
6081 fn test_money_type_for_signature_factor() -> LemmaType {
6082 use crate::computation::rational::decimal_to_rational;
6083 use crate::literals::{MeasureUnit, MeasureUnits};
6084 use rust_decimal::Decimal;
6085 LemmaType::new(
6086 "money".to_string(),
6087 TypeSpecification::Measure {
6088 minimum: None,
6089 maximum: None,
6090 decimals: Some(2),
6091 units: MeasureUnits::from(vec![
6092 MeasureUnit {
6093 name: "eur".to_string(),
6094 factor: crate::computation::rational::rational_one(),
6095 minimum: None,
6096 maximum: None,
6097 suggestion_magnitude: None,
6098 decomposition: BaseMeasureVector::new(),
6099 derived_measure_factors: Vec::new(),
6100 },
6101 MeasureUnit {
6102 name: "usd".to_string(),
6103 factor: decimal_to_rational(Decimal::new(91, 2)).expect("usd factor"),
6104 minimum: None,
6105 maximum: None,
6106 suggestion_magnitude: None,
6107 decomposition: BaseMeasureVector::new(),
6108 derived_measure_factors: Vec::new(),
6109 },
6110 ]),
6111 traits: Vec::new(),
6112 decomposition: None,
6113 help: String::new(),
6114 },
6115 TypeExtends::Primitive,
6116 )
6117 }
6118
6119 fn measure_type_with_kilogram() -> TypeSpecification {
6120 use crate::computation::rational::rational_one;
6121 use crate::literals::{MeasureUnit, MeasureUnits};
6122 let mut units = MeasureUnits::new();
6123 units.push(MeasureUnit {
6124 name: "kilogram".to_string(),
6125 factor: rational_one(),
6126 minimum: None,
6127 maximum: None,
6128 suggestion_magnitude: None,
6129 decomposition: BaseMeasureVector::new(),
6130 derived_measure_factors: Vec::new(),
6131 });
6132 TypeSpecification::Measure {
6133 minimum: None,
6134 maximum: None,
6135 decimals: None,
6136 units,
6137 traits: Vec::new(),
6138 decomposition: None,
6139 help: String::new(),
6140 }
6141 }
6142
6143 #[test]
6144 fn parser_value_to_value_kind_rejects_bare_number_for_measure() {
6145 let ten = Value::Number(Decimal::from(10));
6146 let err = parser_value_to_value_kind(&ten, &measure_type_with_kilogram())
6147 .expect_err("bare number must not bind to measure");
6148 assert!(
6149 err.contains("kilogram"),
6150 "error must hint expected unit, got: {err}"
6151 );
6152 }
6153
6154 #[test]
6155 fn parser_value_to_value_kind_accepts_number_with_unit_for_measure() {
6156 let ten_kg = Value::NumberWithUnit(Decimal::from(10), "kilogram".to_string());
6157 let kind = parser_value_to_value_kind(&ten_kg, &measure_type_with_kilogram())
6158 .expect("10 kilogram must bind to measure");
6159 assert!(matches!(kind, ValueKind::Measure(_, _)));
6160 }
6161
6162 #[test]
6163 fn parser_value_to_value_kind_accepts_bare_number_for_ratio() {
6164 let ten = Value::Number(Decimal::from(10));
6165 let kind =
6166 parser_value_to_value_kind(&ten, &TypeSpecification::ratio()).expect("number -> ratio");
6167 assert!(matches!(kind, ValueKind::Ratio(_, None)));
6168 }
6169
6170 #[test]
6171 fn value_kind_matches_spec_rejects_number_for_measure() {
6172 let n = ValueKind::Number(rational_new(10, 1));
6173 assert!(!value_kind_matches_spec(&n, &measure_type_with_kilogram()));
6174 }
6175
6176 #[test]
6177 fn apply_constraint_rejects_inherited_unit_factor_change() {
6178 let mut specs = TypeSpecification::measure();
6179 specs
6180 .apply_constraint(
6181 "money",
6182 TypeConstraintCommand::Unit,
6183 &unit_factor_arg("eur", 1),
6184 &mut None,
6185 )
6186 .expect("seed eur");
6187 let err = specs
6188 .apply_constraint(
6189 "money",
6190 TypeConstraintCommand::Unit,
6191 &[
6192 CommandArg::Label("eur".to_string()),
6193 CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::new(11, 1))),
6194 ],
6195 &mut None,
6196 )
6197 .expect_err("must not change inherited unit factor");
6198 assert!(err.contains("eur"), "error must name unit, got: {err}");
6199 assert!(
6200 err.contains("inherited") || err.contains("cannot change"),
6201 "error must reject factor change, got: {err}"
6202 );
6203 }
6204
6205 #[test]
6206 fn apply_constraint_allows_additive_unit_on_inherited_spec() {
6207 let mut specs = TypeSpecification::measure();
6208 specs
6209 .apply_constraint(
6210 "money",
6211 TypeConstraintCommand::Unit,
6212 &unit_factor_arg("eur", 1),
6213 &mut None,
6214 )
6215 .expect("seed eur");
6216 specs
6217 .apply_constraint(
6218 "money",
6219 TypeConstraintCommand::Unit,
6220 &unit_factor_arg("usd", 1),
6221 &mut None,
6222 )
6223 .expect("add usd");
6224 match &specs {
6225 TypeSpecification::Measure { units, .. } => assert_eq!(units.len(), 2),
6226 other => panic!("expected Measure, got {other:?}"),
6227 }
6228 }
6229
6230 #[test]
6231 fn apply_constraint_idempotent_inherited_unit_redeclare() {
6232 let mut specs = TypeSpecification::measure();
6233 specs
6234 .apply_constraint(
6235 "money",
6236 TypeConstraintCommand::Unit,
6237 &unit_factor_arg("eur", 1),
6238 &mut None,
6239 )
6240 .expect("seed eur");
6241 specs
6242 .apply_constraint(
6243 "money",
6244 TypeConstraintCommand::Unit,
6245 &unit_factor_arg("eur", 1),
6246 &mut None,
6247 )
6248 .expect("idempotent eur");
6249 match &specs {
6250 TypeSpecification::Measure { units, .. } => {
6251 assert_eq!(units.len(), 1);
6252 assert_eq!(
6253 units.iter().find(|u| u.name == "eur").expect("eur").factor,
6254 crate::computation::rational::rational_one()
6255 );
6256 }
6257 other => panic!("expected Measure, got {other:?}"),
6258 }
6259 }
6260
6261 #[test]
6262 fn element_from_range_returns_element_for_every_range_primitive() {
6263 type RangeElementMatcher = fn(&TypeSpecification) -> bool;
6264 let cases: [(PrimitiveKind, RangeElementMatcher); 5] = [
6265 (PrimitiveKind::NumberRange, |element| {
6266 matches!(element, TypeSpecification::Number { .. })
6267 }),
6268 (PrimitiveKind::MeasureRange, |element| {
6269 matches!(element, TypeSpecification::Measure { .. })
6270 }),
6271 (PrimitiveKind::RatioRange, |element| {
6272 matches!(element, TypeSpecification::Ratio { .. })
6273 }),
6274 (PrimitiveKind::DateRange, |element| {
6275 matches!(element, TypeSpecification::Date { .. })
6276 }),
6277 (PrimitiveKind::TimeRange, |element| {
6278 matches!(element, TypeSpecification::Time { .. })
6279 }),
6280 ];
6281 for (kind, matches_element) in cases {
6282 let range_spec = type_spec_for_primitive(kind);
6283 let element = range_spec
6284 .element_from_range()
6285 .unwrap_or_else(|| panic!("{kind:?} must define element_from_range"));
6286 assert!(
6287 matches_element(&element),
6288 "{kind:?} element must match documented mapping, got {element:?}"
6289 );
6290 }
6291 }
6292
6293 #[test]
6294 fn element_from_range_returns_none_for_non_range_primitives() {
6295 let non_range = [
6296 type_spec_for_primitive(PrimitiveKind::Boolean),
6297 type_spec_for_primitive(PrimitiveKind::Measure),
6298 TypeSpecification::Undetermined,
6299 TypeSpecification::veto(),
6300 ];
6301 for spec in non_range {
6302 assert!(
6303 spec.element_from_range().is_none(),
6304 "{spec:?} must not define element_from_range"
6305 );
6306 }
6307 }
6308}