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