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, 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::{BaseQuantityVector, QuantityUnit, QuantityUnits, RatioUnit, RatioUnits};
53
54pub fn combine_decompositions(
57 left: &BaseQuantityVector,
58 right: &BaseQuantityVector,
59 is_multiply: bool,
60) -> BaseQuantityVector {
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, RationalInteger};
148 match name {
149 "month" | "months" => Some(rational_one()),
150 "year" | "years" => Some(RationalInteger::new(12, 1)),
151 _ => None,
152 }
153}
154
155fn owner_declares_quantity_unit(owner: &LemmaType, unit_name: &str) -> bool {
156 owner
157 .quantity_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) -> crate::computation::rational::RationalInteger {
174 use crate::computation::rational::{checked_div, checked_mul, rational_one};
175 let mut acc = rational_one();
176 for (name, exponent) in signature {
177 let factor =
178 if let Some(owner) = owner.filter(|owner| owner_declares_quantity_unit(owner, name)) {
179 *owner.quantity_unit_factor(name)
180 } else if let Some(lemma_type) = expression_units.get(name) {
181 *lemma_type.quantity_unit_factor(name)
182 } else {
183 panic!(
184 "BUG: signature_factor called with unresolved unit name '{}'",
185 name
186 );
187 };
188 let mut term = rational_one();
189 let abs_exp = exponent.unsigned_abs();
190 for _ in 0..abs_exp {
191 term = checked_mul(&term, &factor)
192 .expect("BUG: signature_factor overflow during exponent expansion");
193 }
194 if *exponent >= 0 {
195 acc = checked_mul(&acc, &term)
196 .expect("BUG: signature_factor overflow during multiplication");
197 } else {
198 acc = checked_div(&acc, &term).expect("BUG: signature_factor overflow during division");
199 }
200 }
201 acc
202}
203
204pub fn canonicalize_signature(signature: &[(String, i32)]) -> Vec<(String, i32)> {
205 use std::collections::BTreeMap;
206 let mut accumulator: BTreeMap<String, i32> = BTreeMap::new();
207 for (name, exponent) in signature {
208 *accumulator.entry(name.clone()).or_insert(0) += exponent;
209 }
210 accumulator
211 .into_iter()
212 .filter(|(_, exponent)| *exponent != 0)
213 .collect()
214}
215
216pub const DURATION_DIMENSION: &str = "duration";
217pub const CALENDAR_DIMENSION: &str = "calendar";
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
220#[serde(rename_all = "snake_case")]
221pub enum QuantityTrait {
222 Duration,
223 Calendar,
224}
225
226pub fn duration_decomposition() -> BaseQuantityVector {
227 [(DURATION_DIMENSION.to_string(), 1i32)]
228 .into_iter()
229 .collect()
230}
231
232pub fn calendar_decomposition() -> BaseQuantityVector {
233 [(CALENDAR_DIMENSION.to_string(), 1i32)]
234 .into_iter()
235 .collect()
236}
237
238pub fn anonymous_quantity_type() -> LemmaType {
242 LemmaType::anonymous_for_decomposition(BaseQuantityVector::new())
243}
244
245pub fn negate_signature(signature: &[(String, i32)]) -> Vec<(String, i32)> {
248 signature.iter().map(|(n, e)| (n.clone(), -*e)).collect()
249}
250
251mod stored_quantity_declared_bound_serde {
252 use super::RationalInteger;
253 use crate::computation::rational::commit_rational_to_decimal;
254 use rust_decimal::Decimal;
255 use serde::{Deserialize, Deserializer, Serialize, Serializer};
256
257 fn lift(decimal: Decimal) -> Result<RationalInteger, String> {
258 crate::computation::rational::decimal_to_rational(decimal)
259 .map_err(|failure| failure.to_string())
260 }
261
262 pub mod option {
263 use super::*;
264
265 pub fn serialize<S: Serializer>(
266 value: &Option<(RationalInteger, String)>,
267 serializer: S,
268 ) -> Result<S::Ok, S::Error> {
269 match value {
270 None => serializer.serialize_none(),
271 Some((magnitude, unit_name)) => {
272 let decimal =
273 commit_rational_to_decimal(magnitude).map_err(serde::ser::Error::custom)?;
274 (decimal, unit_name.as_str()).serialize(serializer)
275 }
276 }
277 }
278
279 pub fn deserialize<'de, D: Deserializer<'de>>(
280 deserializer: D,
281 ) -> Result<Option<(RationalInteger, String)>, D::Error> {
282 let parsed: Option<(Decimal, String)> = Option::deserialize(deserializer)?;
283 parsed
284 .map(|(decimal, unit_name)| lift(decimal).map(|magnitude| (magnitude, unit_name)))
285 .transpose()
286 .map_err(serde::de::Error::custom)
287 }
288 }
289}
290
291#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
292#[serde(tag = "kind", rename_all = "lowercase")]
293pub enum TypeSpecification {
294 Boolean {
295 help: String,
296 },
297 Quantity {
298 #[serde(with = "stored_quantity_declared_bound_serde::option", default)]
299 minimum: Option<(RationalInteger, String)>,
300 #[serde(with = "stored_quantity_declared_bound_serde::option", default)]
301 maximum: Option<(RationalInteger, String)>,
302 decimals: Option<u8>,
303 units: QuantityUnits,
304 #[serde(default)]
305 traits: Vec<QuantityTrait>,
306 #[serde(default)]
311 decomposition: Option<BaseQuantityVector>,
312 help: String,
313 },
314 Number {
315 #[serde(with = "crate::literals::stored_rational_serde::option", default)]
316 minimum: Option<RationalInteger>,
317 #[serde(with = "crate::literals::stored_rational_serde::option", default)]
318 maximum: Option<RationalInteger>,
319 decimals: Option<u8>,
320 help: String,
321 },
322 NumberRange {
323 help: String,
324 },
325 Ratio {
326 #[serde(with = "crate::literals::stored_rational_serde::option", default)]
327 minimum: Option<RationalInteger>,
328 #[serde(with = "crate::literals::stored_rational_serde::option", default)]
329 maximum: Option<RationalInteger>,
330 decimals: Option<u8>,
331 units: RatioUnits,
332 help: String,
333 },
334 RatioRange {
335 units: RatioUnits,
336 help: String,
337 },
338 Text {
339 length: Option<usize>,
340 options: Vec<String>,
341 help: String,
342 },
343 Date {
344 minimum: Option<DateTimeValue>,
345 maximum: Option<DateTimeValue>,
346 help: String,
347 },
348 DateRange {
349 help: String,
350 },
351 Time {
352 minimum: Option<TimeValue>,
353 maximum: Option<TimeValue>,
354 help: String,
355 },
356 TimeRange {
357 help: String,
358 },
359 QuantityRange {
360 units: QuantityUnits,
361 #[serde(default)]
362 decomposition: Option<BaseQuantityVector>,
363 help: String,
364 },
365 Veto {
366 message: Option<String>,
367 },
368 Undetermined,
372}
373
374impl std::fmt::Display for TypeSpecification {
375 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
376 let label = match self {
377 Self::Boolean { .. } => "boolean",
378 Self::Quantity { .. } => "quantity",
379 Self::QuantityRange { .. } => "quantity range",
380 Self::Number { .. } => "number",
381 Self::NumberRange { .. } => "number range",
382 Self::Text { .. } => "text",
383 Self::Date { .. } => "date",
384 Self::DateRange { .. } => "date range",
385 Self::Time { .. } => "time",
386 Self::TimeRange { .. } => "time range",
387 Self::Ratio { .. } => "ratio",
388 Self::RatioRange { .. } => "ratio range",
389 Self::Veto { .. } => "veto",
390 Self::Undetermined => "undetermined",
391 };
392 f.write_str(label)
393 }
394}
395
396impl TypeSpecification {
397 pub fn help(&self) -> &str {
399 match self {
400 Self::Boolean { help, .. }
401 | Self::Quantity { help, .. }
402 | Self::Number { help, .. }
403 | Self::NumberRange { help, .. }
404 | Self::Text { help, .. }
405 | Self::Date { help, .. }
406 | Self::DateRange { help, .. }
407 | Self::Time { help, .. }
408 | Self::TimeRange { help, .. }
409 | Self::Ratio { help, .. }
410 | Self::RatioRange { help, .. }
411 | Self::QuantityRange { help, .. } => help.as_str(),
412 Self::Veto { .. } | Self::Undetermined => "",
413 }
414 }
415}
416
417fn require_literal<'a>(
423 args: &'a [CommandArg],
424 cmd: &str,
425) -> Result<&'a crate::literals::Value, String> {
426 let arg = args
427 .first()
428 .ok_or_else(|| format!("{} requires an argument", cmd))?;
429 match arg {
430 CommandArg::Literal(v) => Ok(v),
431 CommandArg::Label(name) => Err(format!(
432 "{} requires a literal value, got identifier '{}'",
433 cmd, name
434 )),
435 CommandArg::UnitExpr(_) => Err(format!(
436 "{} requires a literal value, got a unit expression (only valid for 'unit' command)",
437 cmd
438 )),
439 }
440}
441
442fn apply_type_help_command(help: &mut String, args: &[CommandArg]) -> Result<(), String> {
443 match require_literal(args, "help")? {
444 crate::literals::Value::Text(s) => {
445 *help = s.clone();
446 Ok(())
447 }
448 other => Err(format!(
449 "help requires a text literal (quoted string), got {}",
450 value_kind_name(other)
451 )),
452 }
453}
454
455fn format_quantity_units_list(units: &QuantityUnits) -> String {
456 units
457 .iter()
458 .map(|u| u.name.as_str())
459 .collect::<Vec<_>>()
460 .join(", ")
461}
462
463#[derive(Debug, Clone, Copy, PartialEq, Eq)]
465pub(crate) enum DefaultExpectation {
466 QuantityUnits,
467 Text,
468 Number,
469 Boolean,
470 Date,
471 Time,
472 Ratio,
473 NumberRange,
474 DateRange,
475 TimeRange,
476 QuantityRange,
477 RatioRange,
478}
479
480pub(crate) fn default_value_mismatch_error(
481 calendar_unit: &str,
482 type_name: &str,
483 expectation: DefaultExpectation,
484 quantity_units: Option<&QuantityUnits>,
485) -> String {
486 let unit_label = calendar_unit;
487 let first = format!("Unit '{unit_label}' is for calendar data.");
488 match expectation {
489 DefaultExpectation::QuantityUnits => {
490 let list = quantity_units
491 .map(format_quantity_units_list)
492 .unwrap_or_default();
493 format!("{first} Valid '{type_name}' units are: {list}.")
494 }
495 DefaultExpectation::Text => format!(
496 "{first} Please provide a text value in double quotes, for example `-> default \"my default value\"`."
497 ),
498 DefaultExpectation::Number => format!(
499 "{first} Please provide a number, for example `-> default 42`."
500 ),
501 DefaultExpectation::Boolean => format!(
502 "{first} Please provide true or false, for example `-> default true`."
503 ),
504 DefaultExpectation::Date => format!(
505 "{first} Please provide a date, for example `-> default 2024-06-15`."
506 ),
507 DefaultExpectation::Time => format!(
508 "{first} Please provide a time, for example `-> default 09:00:00`."
509 ),
510 DefaultExpectation::Ratio | DefaultExpectation::RatioRange => format!(
511 "{first} Please provide a ratio, for example `-> default 25%`."
512 ),
513 DefaultExpectation::NumberRange => format!(
514 "{first} Please provide a number range, for example `-> default 10...100`."
515 ),
516 DefaultExpectation::DateRange => format!(
517 "{first} Please provide a date range, for example `-> default 2024-01-01...2024-12-31`."
518 ),
519 DefaultExpectation::TimeRange => format!(
520 "{first} Please provide a time range, for example `-> default 09:00...17:00`."
521 ),
522 DefaultExpectation::QuantityRange => format!(
523 "{first} Please provide a range with units valid for '{type_name}', for example `-> default 30 kilogram...35 kilogram`."
524 ),
525 }
526}
527
528fn quantity_default_unit_error(unit: &str, type_name: &str, units: &QuantityUnits) -> String {
529 format!(
530 "Unit '{unit}' is not defined on '{type_name}'. Valid '{type_name}' units are: {}.",
531 format_quantity_units_list(units)
532 )
533}
534
535fn quantity_default_wrong_shape_error(type_name: &str, traits: &[QuantityTrait]) -> String {
536 let example = if traits.contains(&QuantityTrait::Duration) {
537 "4 weeks"
538 } else if traits.contains(&QuantityTrait::Calendar) {
539 "3 month"
540 } else {
541 "30 kilogram"
542 };
543 format!(
544 "Please provide a value with a unit valid for '{type_name}', for example `-> default {example}`."
545 )
546}
547
548fn validate_calendar_range_default_endpoint(
549 value: &crate::literals::Value,
550 type_name: &str,
551 units: &QuantityUnits,
552) -> Result<(), String> {
553 let unit_name = match value {
554 crate::literals::Value::NumberWithUnit(_, u) => u.as_str(),
555 _ => {
556 return Err(
557 "Please provide a range with calendar units, for example `-> default 18 year...67 year`."
558 .to_string(),
559 );
560 }
561 };
562 if calendar_unit_factor(unit_name).is_none() {
563 return Err(
564 "Please provide a range with calendar units, for example `-> default 18 year...67 year`."
565 .to_string(),
566 );
567 }
568 if units.get(unit_name).is_err() {
569 return Err(quantity_default_unit_error(unit_name, type_name, units));
570 }
571 Ok(())
572}
573
574fn reject_calendar_for_default(
575 value: &crate::literals::Value,
576 type_name: &str,
577 expectation: DefaultExpectation,
578 quantity_units: Option<&QuantityUnits>,
579) -> Result<(), String> {
580 if let crate::literals::Value::NumberWithUnit(_, unit) = value {
581 if calendar_unit_factor(unit).is_some() {
582 return Err(default_value_mismatch_error(
583 unit,
584 type_name,
585 expectation,
586 quantity_units,
587 ));
588 }
589 }
590 Ok(())
591}
592
593fn value_kind_name(v: &crate::literals::Value) -> &'static str {
595 use crate::literals::Value;
596 match v {
597 Value::Number(_) => "number",
598 Value::NumberWithUnit(_, _) => "number_with_unit",
599 Value::Text(_) => "text",
600 Value::Date(_) => "date",
601 Value::Time(_) => "time",
602 Value::Boolean(_) => "boolean",
603 Value::Range(_, _) => "range",
604 }
605}
606
607fn require_default_range_endpoints<'a>(
608 args: &'a [CommandArg],
609 type_name: &str,
610 expectation: DefaultExpectation,
611 quantity_units: Option<&QuantityUnits>,
612) -> Result<(&'a crate::literals::Value, &'a crate::literals::Value), String> {
613 match require_literal(args, "default")? {
614 crate::literals::Value::NumberWithUnit(_, unit)
615 if calendar_unit_factor(unit).is_some() =>
616 {
617 Err(default_value_mismatch_error(
618 unit,
619 type_name,
620 expectation,
621 quantity_units,
622 ))
623 }
624 crate::literals::Value::Range(left, right) => Ok((left.as_ref(), right.as_ref())),
625 _ => Err(match expectation {
626 DefaultExpectation::NumberRange => {
627 "Please provide a number range, for example `-> default 10...100`.".to_string()
628 }
629 DefaultExpectation::DateRange => {
630 "Please provide a date range, for example `-> default 2024-01-01...2024-12-31`."
631 .to_string()
632 }
633 DefaultExpectation::RatioRange => {
634 "Please provide a ratio range, for example `-> default 10%...50%`.".to_string()
635 }
636 DefaultExpectation::QuantityRange => format!(
637 "Please provide a range with units valid for '{type_name}', for example `-> default 30 kilogram...35 kilogram`."
638 ),
639 _ => unreachable!("BUG: require_default_range_endpoints called with non-range expectation"),
640 }),
641 }
642}
643
644fn lift_parser_decimal(decimal: rust_decimal::Decimal) -> Result<RationalInteger, String> {
645 crate::computation::rational::decimal_to_rational(decimal)
646 .map_err(|failure| format!("literal failed rational lift: {failure}"))
647}
648
649pub fn range_element_type_specification(
651 range_spec: &TypeSpecification,
652) -> Option<TypeSpecification> {
653 range_spec.element_from_range()
654}
655
656fn range_endpoints_compatible(left: &LemmaType, right: &LemmaType) -> bool {
657 match (&left.specifications, &right.specifications) {
658 (TypeSpecification::Date { .. }, TypeSpecification::Date { .. }) => true,
659 (TypeSpecification::Time { .. }, TypeSpecification::Time { .. }) => true,
660 (TypeSpecification::Number { .. }, TypeSpecification::Number { .. }) => true,
661 (TypeSpecification::Quantity { .. }, TypeSpecification::Quantity { .. }) => {
662 left.same_quantity_family(right)
663 || left.compatible_with_anonymous_quantity(right)
664 || right.compatible_with_anonymous_quantity(left)
665 }
666 (TypeSpecification::Ratio { .. }, TypeSpecification::Ratio { .. }) => true,
667 _ => false,
668 }
669}
670
671pub fn range_type_specification_from_endpoints(
673 left: &LemmaType,
674 right: &LemmaType,
675) -> Option<TypeSpecification> {
676 if !range_endpoints_compatible(left, right) {
677 return None;
678 }
679 left.specifications.range_from_element()
680}
681
682fn lift_range_endpoint(
686 value: &crate::parsing::ast::Value,
687 element_spec: &TypeSpecification,
688) -> Result<LiteralValue, String> {
689 use crate::parsing::ast::Value;
690 let kind = match value {
691 Value::NumberWithUnit(_, _) => parser_value_to_value_kind(value, element_spec)?,
692 _ => value_to_semantic(value)?,
693 };
694 Ok(LiteralValue {
695 value: kind,
696 lemma_type: Arc::new(LemmaType::primitive(element_spec.clone())),
697 })
698}
699
700fn literal_value_from_parser_value(
701 value: &crate::parsing::ast::Value,
702) -> Result<LiteralValue, String> {
703 use crate::parsing::ast::Value;
704
705 match value {
706 Value::Number(n) => Ok(LiteralValue::number(lift_parser_decimal(*n)?)),
707 Value::Text(s) => Ok(LiteralValue::text(s.clone())),
708 Value::Date(dt) => Ok(LiteralValue::date(date_time_to_semantic(dt))),
709 Value::Time(t) => Ok(LiteralValue::time(time_to_semantic(t))),
710 Value::Boolean(b) => Ok(LiteralValue::from_bool(bool::from(*b))),
711 Value::NumberWithUnit(n, unit) => Ok(LiteralValue::number_interpreted_as_quantity(
712 lift_parser_decimal(*n)?,
713 unit.clone(),
714 )),
715 Value::Range(left, right) => {
716 let left = literal_value_from_parser_value(left)?;
717 let right = literal_value_from_parser_value(right)?;
718 let compatible = match (
719 &left.lemma_type.specifications,
720 &right.lemma_type.specifications,
721 ) {
722 (TypeSpecification::Date { .. }, TypeSpecification::Date { .. }) => true,
723 (TypeSpecification::Time { .. }, TypeSpecification::Time { .. }) => true,
724 (TypeSpecification::Number { .. }, TypeSpecification::Number { .. }) => true,
725 (TypeSpecification::Quantity { .. }, TypeSpecification::Quantity { .. }) => {
726 left.lemma_type.same_quantity_family(&right.lemma_type)
727 || left
728 .lemma_type
729 .compatible_with_anonymous_quantity(&right.lemma_type)
730 || right
731 .lemma_type
732 .compatible_with_anonymous_quantity(&left.lemma_type)
733 }
734 (TypeSpecification::Ratio { .. }, TypeSpecification::Ratio { .. }) => true,
735 _ => false,
736 };
737 if !compatible {
738 return Err(format!(
739 "range endpoints must have the same supported base type, got {} and {}",
740 left.lemma_type.name(),
741 right.lemma_type.name()
742 ));
743 }
744 Ok(LiteralValue::range(left, right))
745 }
746 }
747}
748
749fn decimal_to_u8(d: RationalInteger, ctx: &str) -> Result<u8, String> {
751 if *d.denom() != 1 {
752 return Err(format!(
753 "{} requires a whole number, got fractional value",
754 ctx
755 ));
756 }
757 u8::try_from(*d.numer()).map_err(|_| format!("{} value out of range for u8", ctx))
758}
759
760fn decimal_to_usize(d: RationalInteger, ctx: &str) -> Result<usize, String> {
762 if *d.denom() != 1 {
763 return Err(format!(
764 "{} requires a whole number, got fractional value",
765 ctx
766 ));
767 }
768 usize::try_from(*d.numer()).map_err(|_| format!("{} value out of range for usize", ctx))
769}
770
771fn ratio_bound_to_canonical_rational(
777 args: &[CommandArg],
778 cmd: &str,
779 units: &RatioUnits,
780) -> Result<RationalInteger, String> {
781 use crate::computation::rational::{checked_div, decimal_to_rational};
782 let lit = require_literal(args, cmd)?;
783 match lit {
784 crate::literals::Value::NumberWithUnit(magnitude, unit_name) => {
785 let unit = units.get(unit_name.as_str())?;
786 let magnitude_rational = decimal_to_rational(*magnitude)
787 .map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
788 checked_div(&magnitude_rational, &unit.value)
789 .map_err(|failure| format!("{cmd}: unit conversion failed: {failure}"))
790 }
791 other => Err(format!(
792 "{cmd} requires a ratio literal with a unit, got {}",
793 value_kind_name(other)
794 )),
795 }
796}
797
798fn require_decimal_literal(args: &[CommandArg], cmd: &str) -> Result<RationalInteger, String> {
799 use crate::computation::rational::decimal_to_rational;
800 match require_literal(args, cmd)? {
801 crate::literals::Value::Number(d) => decimal_to_rational(*d)
802 .map_err(|failure| format!("{} literal failed rational lift: {}", cmd, failure)),
803 other => Err(format!(
804 "{} requires a number literal, got {}",
805 cmd,
806 value_kind_name(other)
807 )),
808 }
809}
810
811enum UnitConstraintField {
812 Minimum,
813 Maximum,
814 DefaultMagnitude,
815}
816
817fn quantity_declared_bound_to_canonical(
818 magnitude: &RationalInteger,
819 unit_name: &str,
820 units: &QuantityUnits,
821 type_name: &str,
822 command: &str,
823) -> Result<RationalInteger, String> {
824 use crate::computation::rational::checked_mul;
825 let unit = units.get(unit_name).map_err(|_| {
826 format!(
827 "Unit '{unit_name}' is not defined on '{type_name}'. Valid units are: {}.",
828 format_quantity_units_list(units)
829 )
830 })?;
831 checked_mul(magnitude, &unit.factor)
832 .map_err(|failure| format!("{command}: unit conversion overflow: {failure}"))
833}
834
835fn parse_quantity_declared_bound(
836 args: &[CommandArg],
837 cmd: &str,
838 units: &QuantityUnits,
839 type_name: &str,
840) -> Result<(RationalInteger, String), String> {
841 use crate::computation::rational::decimal_to_rational;
842 let lit = require_literal(args, cmd)?;
843 let (magnitude, unit_name) = match lit {
844 crate::literals::Value::NumberWithUnit(n, unit) => (*n, unit.clone()),
845 other => {
846 return Err(format!(
847 "{cmd} requires a quantity literal with a unit, got {}",
848 value_kind_name(other)
849 ));
850 }
851 };
852 units.get(unit_name.as_str()).map_err(|_| {
853 format!(
854 "Unit '{unit_name}' is not defined on '{type_name}'. Valid units are: {}.",
855 format_quantity_units_list(units)
856 )
857 })?;
858 let magnitude_rational = decimal_to_rational(magnitude)
859 .map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
860 Ok((magnitude_rational, unit_name))
861}
862
863fn sync_quantity_units_from_canonical(
864 units: &mut QuantityUnits,
865 canonical: &RationalInteger,
866 field: UnitConstraintField,
867) -> Result<(), String> {
868 use crate::computation::rational::checked_div;
869 for unit in &mut units.0 {
870 let magnitude = checked_div(canonical, &unit.factor).map_err(|failure| {
871 format!(
872 "cannot derive per-unit constraint for unit '{}': {failure}",
873 unit.name
874 )
875 })?;
876 match field {
877 UnitConstraintField::Minimum => unit.minimum = Some(magnitude),
878 UnitConstraintField::Maximum => unit.maximum = Some(magnitude),
879 UnitConstraintField::DefaultMagnitude => unit.default_magnitude = Some(magnitude),
880 }
881 }
882 Ok(())
883}
884
885fn sync_ratio_units_from_canonical(
886 units: &mut RatioUnits,
887 canonical: &RationalInteger,
888 field: UnitConstraintField,
889) -> Result<(), String> {
890 use crate::computation::rational::checked_mul;
891 for unit in &mut units.0 {
892 let magnitude = checked_mul(canonical, &unit.value).map_err(|failure| {
893 format!(
894 "cannot derive per-unit constraint for ratio unit '{}': {failure}",
895 unit.name
896 )
897 })?;
898 match field {
899 UnitConstraintField::Minimum => unit.minimum = Some(magnitude),
900 UnitConstraintField::Maximum => unit.maximum = Some(magnitude),
901 UnitConstraintField::DefaultMagnitude => unit.default_magnitude = Some(magnitude),
902 }
903 }
904 Ok(())
905}
906
907fn sync_quantity_default_units(
908 units: &mut QuantityUnits,
909 default: &ValueKind,
910 type_name: &str,
911) -> Result<(), String> {
912 let ValueKind::Quantity(magnitude, signature) = default else {
913 return Ok(());
914 };
915 let unit_name = signature.first().map(|(n, _)| n.as_str()).expect(
916 "BUG: Quantity default value has empty signature; literal lift must produce single-term",
917 );
918 units.get(unit_name).map_err(|_| {
919 format!("Default unit '{unit_name}' is not defined on quantity type '{type_name}'.")
920 })?;
921 sync_quantity_units_from_canonical(units, magnitude, UnitConstraintField::DefaultMagnitude)
922}
923
924pub(crate) fn finalize_quantity_unit_constraint_magnitudes(
925 specification: &mut TypeSpecification,
926 declared_default: Option<&ValueKind>,
927 type_name: &str,
928) -> Result<(), String> {
929 let TypeSpecification::Quantity {
930 minimum,
931 maximum,
932 units,
933 ..
934 } = specification
935 else {
936 return Ok(());
937 };
938
939 if let Some((magnitude, unit_name)) = minimum.clone() {
940 let canonical = quantity_declared_bound_to_canonical(
941 &magnitude, &unit_name, units, type_name, "minimum",
942 )?;
943 sync_quantity_units_from_canonical(units, &canonical, UnitConstraintField::Minimum)?;
944 }
945 if let Some((magnitude, unit_name)) = maximum.clone() {
946 let canonical = quantity_declared_bound_to_canonical(
947 &magnitude, &unit_name, units, type_name, "maximum",
948 )?;
949 sync_quantity_units_from_canonical(units, &canonical, UnitConstraintField::Maximum)?;
950 }
951 if let Some(default) = declared_default {
952 sync_quantity_default_units(units, default, type_name)?;
953 }
954 Ok(())
955}
956
957pub(crate) fn quantity_declared_bound_canonical(
958 bound: &(RationalInteger, String),
959 units: &QuantityUnits,
960 type_name: &str,
961 command: &str,
962) -> Result<RationalInteger, String> {
963 let (magnitude, unit_name) = bound;
964 quantity_declared_bound_to_canonical(magnitude, unit_name, units, type_name, command)
965}
966
967fn sync_ratio_default_units(units: &mut RatioUnits, default: &ValueKind) -> Result<(), String> {
968 let ValueKind::Ratio(canonical, _) = default else {
969 return Ok(());
970 };
971 sync_ratio_units_from_canonical(units, canonical, UnitConstraintField::DefaultMagnitude)
972}
973
974fn option_name(arg: &CommandArg, cmd: &str) -> Result<String, String> {
980 match arg {
981 CommandArg::Literal(crate::literals::Value::Text(s)) => Ok(s.clone()),
982 CommandArg::Label(name) => Ok(name.clone()),
983 CommandArg::Literal(other) => Err(format!(
984 "{} requires a text literal or identifier, got {}",
985 cmd,
986 value_kind_name(other)
987 )),
988 CommandArg::UnitExpr(_) => Err(format!(
989 "{} requires a text literal or identifier, got a unit expression",
990 cmd
991 )),
992 }
993}
994
995fn label_name(arg: &CommandArg, cmd: &str) -> Result<String, String> {
996 match arg {
997 CommandArg::Label(name) => Ok(name.clone()),
998 CommandArg::Literal(other) => Err(format!(
999 "{} requires an identifier, got {}",
1000 cmd,
1001 value_kind_name(other)
1002 )),
1003 CommandArg::UnitExpr(_) => Err(format!(
1004 "{} requires an identifier, got a unit expression",
1005 cmd
1006 )),
1007 }
1008}
1009
1010fn quantity_trait_name(quantity_trait: QuantityTrait) -> &'static str {
1011 match quantity_trait {
1012 QuantityTrait::Duration => "duration",
1013 QuantityTrait::Calendar => "calendar",
1014 }
1015}
1016
1017fn parse_quantity_trait(args: &[CommandArg]) -> Result<QuantityTrait, String> {
1018 if args.len() != 1 {
1019 return Err("trait requires exactly one identifier argument".to_string());
1020 }
1021 match label_name(&args[0], "trait")?
1022 .trim()
1023 .to_lowercase()
1024 .as_str()
1025 {
1026 "duration" => Ok(QuantityTrait::Duration),
1027 "calendar" => Ok(QuantityTrait::Calendar),
1028 other => Err(format!("Unknown quantity trait '{}'", other)),
1029 }
1030}
1031
1032fn validate_calendar_trait_requirements(units: &QuantityUnits) -> Result<(), String> {
1033 let month_unit = units
1034 .iter()
1035 .find(|unit| unit.name == "month")
1036 .ok_or_else(|| {
1037 "trait calendar requires a canonical 'month' unit declared before 'trait calendar'"
1038 .to_string()
1039 })?;
1040 if !month_unit.is_canonical_factor() {
1041 return Err("trait calendar requires unit month 1".to_string());
1042 }
1043 Ok(())
1044}
1045
1046fn validate_duration_trait_requirements(units: &QuantityUnits) -> Result<(), String> {
1047 let second_unit = units
1048 .iter()
1049 .find(|unit| unit.name == "second")
1050 .ok_or_else(|| {
1051 "trait duration requires a canonical 'second' unit declared before 'trait duration'"
1052 .to_string()
1053 })?;
1054 if !second_unit.is_canonical_factor() {
1055 return Err("trait duration requires unit second 1".to_string());
1056 }
1057 Ok(())
1058}
1059
1060fn require_date_literal(args: &[CommandArg], cmd: &str) -> Result<DateTimeValue, String> {
1062 match require_literal(args, cmd)? {
1063 crate::literals::Value::Date(dt) => Ok(dt.clone()),
1064 other => Err(format!(
1065 "{} requires a date literal (e.g. 2024-01-01), got {}",
1066 cmd,
1067 value_kind_name(other)
1068 )),
1069 }
1070}
1071
1072fn require_time_literal(args: &[CommandArg], cmd: &str) -> Result<TimeValue, String> {
1074 match require_literal(args, cmd)? {
1075 crate::literals::Value::Time(t) => Ok(t.clone()),
1076 other => Err(format!(
1077 "{} requires a time literal (e.g. 12:30:00), got {}",
1078 cmd,
1079 value_kind_name(other)
1080 )),
1081 }
1082}
1083
1084#[must_use]
1086pub fn default_help_for_primitive(kind: PrimitiveKind) -> &'static str {
1087 use PrimitiveKind::*;
1088 match kind {
1089 Boolean => "Whether this holds (true or false).",
1090 Number => "A dimensionless number.",
1091 NumberRange => "The lower and upper bound of the number range.",
1092 Text => "A text value.",
1093 Quantity => "A numeric amount in one of this type's units.",
1094 QuantityRange => "The lower and upper bound of the quantity range in the same unit.",
1095 Ratio | Percent => "A ratio in one of this type's units (e.g. percent).",
1096 RatioRange => "The lower and upper bound of the ratio range.",
1097 Date => "A date, or a date and time with optional timezone.",
1098 DateRange => "The start date and end date of the date range.",
1099 Time => "A time of day, with optional timezone.",
1100 TimeRange => "The start time and end time of the time range.",
1101 }
1102}
1103
1104impl TypeSpecification {
1105 pub fn boolean() -> Self {
1106 TypeSpecification::Boolean {
1107 help: default_help_for_primitive(PrimitiveKind::Boolean).to_string(),
1108 }
1109 }
1110 pub fn quantity() -> Self {
1111 TypeSpecification::Quantity {
1112 minimum: None,
1113 maximum: None,
1114 decimals: None,
1115 units: QuantityUnits::new(),
1116 traits: Vec::new(),
1117 decomposition: None,
1118 help: default_help_for_primitive(PrimitiveKind::Quantity).to_string(),
1119 }
1120 }
1121 pub fn number() -> Self {
1122 TypeSpecification::Number {
1123 minimum: None,
1124 maximum: None,
1125 decimals: None,
1126 help: default_help_for_primitive(PrimitiveKind::Number).to_string(),
1127 }
1128 }
1129 pub fn number_range() -> Self {
1130 TypeSpecification::NumberRange {
1131 help: default_help_for_primitive(PrimitiveKind::NumberRange).to_string(),
1132 }
1133 }
1134 pub fn ratio() -> Self {
1135 TypeSpecification::Ratio {
1136 minimum: None,
1137 maximum: None,
1138 decimals: None,
1139 units: RatioUnits(vec![
1140 RatioUnit {
1141 name: "percent".to_string(),
1142 value: crate::computation::rational::RationalInteger::new(100, 1),
1143 minimum: None,
1144 maximum: None,
1145 default_magnitude: None,
1146 },
1147 RatioUnit {
1148 name: "permille".to_string(),
1149 value: crate::computation::rational::RationalInteger::new(1000, 1),
1150 minimum: None,
1151 maximum: None,
1152 default_magnitude: None,
1153 },
1154 ]),
1155 help: default_help_for_primitive(PrimitiveKind::Ratio).to_string(),
1156 }
1157 }
1158 pub fn ratio_range() -> Self {
1159 TypeSpecification::RatioRange {
1160 units: match TypeSpecification::ratio() {
1161 TypeSpecification::Ratio { units, .. } => units,
1162 _ => unreachable!("BUG: ratio constructor must return a ratio type"),
1163 },
1164 help: default_help_for_primitive(PrimitiveKind::RatioRange).to_string(),
1165 }
1166 }
1167 pub fn text() -> Self {
1168 TypeSpecification::Text {
1169 length: None,
1170 options: vec![],
1171 help: default_help_for_primitive(PrimitiveKind::Text).to_string(),
1172 }
1173 }
1174 pub fn date() -> Self {
1175 TypeSpecification::Date {
1176 minimum: None,
1177 maximum: None,
1178 help: default_help_for_primitive(PrimitiveKind::Date).to_string(),
1179 }
1180 }
1181 pub fn date_range() -> Self {
1182 TypeSpecification::DateRange {
1183 help: default_help_for_primitive(PrimitiveKind::DateRange).to_string(),
1184 }
1185 }
1186 pub fn time() -> Self {
1187 TypeSpecification::Time {
1188 minimum: None,
1189 maximum: None,
1190 help: default_help_for_primitive(PrimitiveKind::Time).to_string(),
1191 }
1192 }
1193 pub fn time_range() -> Self {
1194 TypeSpecification::TimeRange {
1195 help: default_help_for_primitive(PrimitiveKind::TimeRange).to_string(),
1196 }
1197 }
1198 pub fn quantity_range() -> Self {
1199 TypeSpecification::QuantityRange {
1200 units: QuantityUnits::new(),
1201 decomposition: None,
1202 help: default_help_for_primitive(PrimitiveKind::QuantityRange).to_string(),
1203 }
1204 }
1205
1206 #[must_use]
1208 pub fn element_from_range(&self) -> Option<Self> {
1209 match self {
1210 TypeSpecification::NumberRange { .. } => Some(TypeSpecification::number()),
1211 TypeSpecification::QuantityRange {
1212 units,
1213 decomposition,
1214 ..
1215 } => Some(TypeSpecification::Quantity {
1216 minimum: None,
1217 maximum: None,
1218 decimals: None,
1219 units: units.clone(),
1220 traits: Vec::new(),
1221 decomposition: decomposition.clone(),
1222 help: String::new(),
1223 }),
1224 TypeSpecification::DateRange { .. } => Some(TypeSpecification::date()),
1225 TypeSpecification::TimeRange { .. } => Some(TypeSpecification::time()),
1226 TypeSpecification::RatioRange { units, .. } => Some(TypeSpecification::Ratio {
1227 minimum: None,
1228 maximum: None,
1229 decimals: None,
1230 units: units.clone(),
1231 help: String::new(),
1232 }),
1233 _ => None,
1234 }
1235 }
1236
1237 #[must_use]
1239 pub fn range_from_element(&self) -> Option<Self> {
1240 match self {
1241 TypeSpecification::Number { .. } => Some(TypeSpecification::number_range()),
1242 TypeSpecification::Quantity {
1243 units,
1244 decomposition,
1245 ..
1246 } => Some(TypeSpecification::QuantityRange {
1247 units: units.clone(),
1248 decomposition: decomposition.clone(),
1249 help: default_help_for_primitive(PrimitiveKind::QuantityRange).to_string(),
1250 }),
1251 TypeSpecification::Date { .. } => Some(TypeSpecification::date_range()),
1252 TypeSpecification::Time { .. } => Some(TypeSpecification::time_range()),
1253 TypeSpecification::Ratio { units, .. } => Some(TypeSpecification::RatioRange {
1254 units: units.clone(),
1255 help: default_help_for_primitive(PrimitiveKind::RatioRange).to_string(),
1256 }),
1257 _ => None,
1258 }
1259 }
1260
1261 #[must_use]
1263 pub fn minimum_decimal(&self) -> Option<Decimal> {
1264 use crate::computation::rational::commit_rational_to_decimal;
1265 match self {
1266 TypeSpecification::Number { minimum, .. }
1267 | TypeSpecification::Ratio { minimum, .. } => minimum
1268 .as_ref()
1269 .and_then(|bound| commit_rational_to_decimal(bound).ok()),
1270 TypeSpecification::Quantity { minimum, .. } => minimum
1271 .as_ref()
1272 .and_then(|(bound, _unit)| commit_rational_to_decimal(bound).ok()),
1273 _ => None,
1274 }
1275 }
1276
1277 #[must_use]
1279 pub fn maximum_decimal(&self) -> Option<Decimal> {
1280 use crate::computation::rational::commit_rational_to_decimal;
1281 match self {
1282 TypeSpecification::Number { maximum, .. }
1283 | TypeSpecification::Ratio { maximum, .. } => maximum
1284 .as_ref()
1285 .and_then(|bound| commit_rational_to_decimal(bound).ok()),
1286 TypeSpecification::Quantity { maximum, .. } => maximum
1287 .as_ref()
1288 .and_then(|(bound, _unit)| commit_rational_to_decimal(bound).ok()),
1289 _ => None,
1290 }
1291 }
1292
1293 pub fn veto() -> Self {
1294 TypeSpecification::Veto { message: None }
1295 }
1296
1297 pub fn apply_constraint(
1306 mut self,
1307 type_name: &str,
1308 command: TypeConstraintCommand,
1309 args: &[CommandArg],
1310 declared_default: &mut Option<RawDefault>,
1311 ) -> Result<Self, String> {
1312 if command == TypeConstraintCommand::Trait
1313 && !matches!(&self, TypeSpecification::Quantity { .. })
1314 {
1315 return Err("trait command is only valid on quantity types".to_string());
1316 }
1317 match &mut self {
1318 TypeSpecification::Boolean { help } => match command {
1319 TypeConstraintCommand::Help => {
1320 apply_type_help_command(help, args)?;
1321 }
1322 TypeConstraintCommand::Default => {
1323 let lit = require_literal(args, "default")?;
1324 reject_calendar_for_default(lit, type_name, DefaultExpectation::Boolean, None)?;
1325 match lit {
1326 crate::literals::Value::Boolean(bv) => {
1327 *declared_default =
1328 Some(RawDefault::Value(ValueKind::Boolean(bool::from(bv))));
1329 }
1330 _ => {
1331 return Err(
1332 "Please provide true or false, for example `-> default true`."
1333 .to_string(),
1334 );
1335 }
1336 }
1337 }
1338 other => {
1339 return Err(format!(
1340 "Invalid command '{}' for boolean type. Valid commands: help, default",
1341 other
1342 ));
1343 }
1344 },
1345 TypeSpecification::Quantity {
1346 decimals,
1347 minimum,
1348 maximum,
1349 units,
1350 traits,
1351 decomposition,
1352 help,
1353 ..
1354 } => match command {
1355 TypeConstraintCommand::Decimals => {
1356 let d = require_decimal_literal(args, "decimals")?;
1357 *decimals = Some(decimal_to_u8(d, "decimals")?);
1358 }
1359 TypeConstraintCommand::Unit => {
1360 let (unit_name, value, derived_quantity_factors) = match args {
1361 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
1362 (name.clone(), *v, Vec::new())
1363 }
1364 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Expr(
1365 prefix,
1366 factors,
1367 ))] => {
1368 let raw: Vec<(String, i32)> = factors
1369 .iter()
1370 .map(|f| (f.quantity_ref.clone(), f.exp))
1371 .collect();
1372 (name.clone(), *prefix, raw)
1373 }
1374 _ => {
1375 return Err(
1376 "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')"
1377 .to_string(),
1378 );
1379 }
1380 };
1381 if let Some(u) = units.0.iter_mut().find(|u| u.name == unit_name) {
1382 u.factor = crate::computation::rational::decimal_to_rational(value)
1383 .map_err(|failure| failure.to_string())?;
1384 u.derived_quantity_factors = derived_quantity_factors;
1385 } else {
1386 units.0.push(QuantityUnit::from_decimal_factor(
1387 unit_name,
1388 value,
1389 derived_quantity_factors,
1390 )?);
1391 }
1392 }
1393 TypeConstraintCommand::Trait => {
1394 let quantity_trait = parse_quantity_trait(args)?;
1395 if traits.contains(&quantity_trait) {
1396 return Err(format!(
1397 "Duplicate trait '{}' for quantity type.",
1398 quantity_trait_name(quantity_trait)
1399 ));
1400 }
1401 if quantity_trait == QuantityTrait::Duration {
1402 validate_duration_trait_requirements(units)?;
1403 }
1404 if quantity_trait == QuantityTrait::Calendar {
1405 validate_calendar_trait_requirements(units)?;
1406 }
1407 traits.push(quantity_trait);
1408 }
1409 TypeConstraintCommand::Minimum => {
1410 *minimum = Some(parse_quantity_declared_bound(
1411 args, "minimum", units, type_name,
1412 )?);
1413 }
1414 TypeConstraintCommand::Maximum => {
1415 *maximum = Some(parse_quantity_declared_bound(
1416 args, "maximum", units, type_name,
1417 )?);
1418 }
1419 TypeConstraintCommand::Help => {
1420 apply_type_help_command(help, args)?;
1421 }
1422 TypeConstraintCommand::Default => {
1423 let lit = require_literal(args, "default")?;
1424 if traits.contains(&QuantityTrait::Calendar) {
1425 match lit {
1426 crate::literals::Value::Range(left, right) => {
1427 validate_calendar_range_default_endpoint(left, type_name, units)?;
1428 validate_calendar_range_default_endpoint(right, type_name, units)?;
1429 let element_spec = TypeSpecification::Quantity {
1430 minimum: minimum.clone(),
1431 maximum: maximum.clone(),
1432 decimals: *decimals,
1433 units: units.clone(),
1434 traits: traits.clone(),
1435 decomposition: decomposition.clone(),
1436 help: String::new(),
1437 };
1438 let left = lift_range_endpoint(left, &element_spec)?;
1439 let right = lift_range_endpoint(right, &element_spec)?;
1440 *declared_default = Some(RawDefault::Value(ValueKind::Range(
1441 Box::new(left),
1442 Box::new(right),
1443 )));
1444 }
1445 crate::literals::Value::NumberWithUnit(_, _) => {
1446 let (magnitude, unit_name) = parse_quantity_declared_bound(
1447 args, "default", units, type_name,
1448 )?;
1449 *declared_default = Some(RawDefault::Quantity {
1450 magnitude,
1451 unit_name,
1452 });
1453 }
1454 _ => {
1455 return Err(quantity_default_wrong_shape_error(type_name, traits));
1456 }
1457 }
1458 } else {
1459 reject_calendar_for_default(
1460 lit,
1461 type_name,
1462 DefaultExpectation::QuantityUnits,
1463 Some(units),
1464 )?;
1465 let (magnitude, unit_name) =
1466 parse_quantity_declared_bound(args, "default", units, type_name)?;
1467 *declared_default = Some(RawDefault::Quantity {
1468 magnitude,
1469 unit_name,
1470 });
1471 }
1472 }
1473 _ => {
1474 return Err(format!(
1475 "Invalid command '{}' for quantity type. Valid commands: unit, trait, minimum, maximum, decimals, help, default",
1476 command
1477 ));
1478 }
1479 },
1480 TypeSpecification::Number {
1481 decimals,
1482 minimum,
1483 maximum,
1484 help,
1485 } => match command {
1486 TypeConstraintCommand::Decimals => {
1487 let d = require_decimal_literal(args, "decimals")?;
1488 *decimals = Some(decimal_to_u8(d, "decimals")?);
1489 }
1490 TypeConstraintCommand::Unit => {
1491 return Err(
1492 "Invalid command 'unit' for number type. Number types are dimensionless and cannot have units. Use 'quantity' type instead.".to_string()
1493 );
1494 }
1495 TypeConstraintCommand::Minimum => {
1496 *minimum = Some(require_decimal_literal(args, "minimum")?);
1497 }
1498 TypeConstraintCommand::Maximum => {
1499 *maximum = Some(require_decimal_literal(args, "maximum")?);
1500 }
1501 TypeConstraintCommand::Help => {
1502 apply_type_help_command(help, args)?;
1503 }
1504 TypeConstraintCommand::Default => {
1505 let lit = require_literal(args, "default")?;
1506 reject_calendar_for_default(lit, type_name, DefaultExpectation::Number, None)?;
1507 match lit {
1508 crate::literals::Value::Number(d) => {
1509 *declared_default = Some(RawDefault::Value(ValueKind::Number(
1510 lift_parser_decimal(*d)?,
1511 )));
1512 }
1513 _ => {
1514 return Err(
1515 "Please provide a number, for example `-> default 42`.".to_string()
1516 );
1517 }
1518 }
1519 }
1520 _ => {
1521 return Err(format!(
1522 "Invalid command '{}' for number type. Valid commands: minimum, maximum, decimals, help, default",
1523 command
1524 ));
1525 }
1526 },
1527 TypeSpecification::NumberRange { help } => match command {
1528 TypeConstraintCommand::Help => {
1529 apply_type_help_command(help, args)?;
1530 }
1531 TypeConstraintCommand::Default => {
1532 let (left, right) = require_default_range_endpoints(
1533 args,
1534 type_name,
1535 DefaultExpectation::NumberRange,
1536 None,
1537 )?;
1538 let left = literal_value_from_parser_value(left)?;
1539 let right = literal_value_from_parser_value(right)?;
1540 if !left.lemma_type.is_number() || !right.lemma_type.is_number() {
1541 return Err(
1542 "Please provide a number range, for example `-> default 10...100`."
1543 .to_string(),
1544 );
1545 }
1546 *declared_default = Some(RawDefault::Value(ValueKind::Range(
1547 Box::new(left),
1548 Box::new(right),
1549 )));
1550 }
1551 _ => {
1552 return Err(format!(
1553 "Invalid command '{}' for number range type. Valid commands: help, default",
1554 command
1555 ));
1556 }
1557 },
1558 TypeSpecification::Ratio {
1559 decimals,
1560 minimum,
1561 maximum,
1562 units,
1563 help,
1564 } => match command {
1565 TypeConstraintCommand::Decimals => {
1566 let d = require_decimal_literal(args, "decimals")?;
1567 *decimals = Some(decimal_to_u8(d, "decimals")?);
1568 }
1569 TypeConstraintCommand::Unit => {
1570 let (unit_name, value_dec) = match args {
1571 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
1572 (name.clone(), *v)
1573 }
1574 _ => {
1575 return Err(
1576 "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."
1577 .to_string(),
1578 );
1579 }
1580 };
1581 let value = crate::computation::rational::decimal_to_rational(value_dec)
1582 .map_err(|failure| {
1583 format!(
1584 "ratio unit value is not exactly representable as a rational: {}",
1585 failure
1586 )
1587 })?;
1588 if let Some(u) = units.0.iter_mut().find(|u| u.name == unit_name) {
1589 u.value = value;
1590 } else {
1591 units.0.push(RatioUnit {
1592 name: unit_name,
1593 value,
1594 minimum: None,
1595 maximum: None,
1596 default_magnitude: None,
1597 });
1598 }
1599 }
1600 TypeConstraintCommand::Minimum => {
1601 let canonical = ratio_bound_to_canonical_rational(args, "minimum", units)?;
1602 sync_ratio_units_from_canonical(
1603 units,
1604 &canonical,
1605 UnitConstraintField::Minimum,
1606 )?;
1607 *minimum = Some(canonical);
1608 }
1609 TypeConstraintCommand::Maximum => {
1610 let canonical = ratio_bound_to_canonical_rational(args, "maximum", units)?;
1611 sync_ratio_units_from_canonical(
1612 units,
1613 &canonical,
1614 UnitConstraintField::Maximum,
1615 )?;
1616 *maximum = Some(canonical);
1617 }
1618 TypeConstraintCommand::Help => {
1619 apply_type_help_command(help, args)?;
1620 }
1621 TypeConstraintCommand::Default => {
1622 let lit = require_literal(args, "default")?;
1623 reject_calendar_for_default(lit, type_name, DefaultExpectation::Ratio, None)?;
1624 let default = match lit {
1625 crate::literals::Value::NumberWithUnit(_, _) => {
1626 let element_spec = TypeSpecification::Ratio {
1627 decimals: *decimals,
1628 minimum: *minimum,
1629 maximum: *maximum,
1630 units: units.clone(),
1631 help: help.clone(),
1632 };
1633 parser_value_to_value_kind(lit, &element_spec)?
1634 }
1635 other => {
1636 return Err(format!(
1637 "default requires a ratio literal with a unit, got {}. Please provide a ratio value with a unit, for example `-> default 25%`.",
1638 value_kind_name(other)
1639 ));
1640 }
1641 };
1642 sync_ratio_default_units(units, &default)?;
1643 *declared_default = Some(RawDefault::Value(default));
1644 }
1645 _ => {
1646 return Err(format!(
1647 "Invalid command '{}' for ratio type. Valid commands: unit, minimum, maximum, decimals, help, default",
1648 command
1649 ));
1650 }
1651 },
1652 TypeSpecification::RatioRange { units, help } => {
1653 match command {
1654 TypeConstraintCommand::Unit => {
1655 let (unit_name, value_dec) = match args {
1656 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
1657 (name.clone(), *v)
1658 }
1659 _ => {
1660 return Err(
1661 "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."
1662 .to_string(),
1663 );
1664 }
1665 };
1666 let value = crate::computation::rational::decimal_to_rational(value_dec)
1667 .map_err(|e| format!("ratio unit value is not exactly representable as a rational: {e}"))?;
1668 if let Some(u) = units.0.iter_mut().find(|u| u.name == unit_name) {
1669 u.value = value;
1670 } else {
1671 units.0.push(RatioUnit {
1672 name: unit_name,
1673 value,
1674 minimum: None,
1675 maximum: None,
1676 default_magnitude: None,
1677 });
1678 }
1679 }
1680 TypeConstraintCommand::Help => {
1681 apply_type_help_command(help, args)?;
1682 }
1683 TypeConstraintCommand::Default => {
1684 let (left, right) = require_default_range_endpoints(
1685 args,
1686 type_name,
1687 DefaultExpectation::RatioRange,
1688 None,
1689 )?;
1690 let element_spec = TypeSpecification::Ratio {
1691 decimals: None,
1692 minimum: None,
1693 maximum: None,
1694 units: units.clone(),
1695 help: String::new(),
1696 };
1697 let left = lift_range_endpoint(left, &element_spec)?;
1698 let right = lift_range_endpoint(right, &element_spec)?;
1699 if !left.lemma_type.is_ratio() || !right.lemma_type.is_ratio() {
1700 return Err(
1701 "Please provide a ratio range, for example `-> default 10%...50%`."
1702 .to_string(),
1703 );
1704 }
1705 *declared_default = Some(RawDefault::Value(ValueKind::Range(
1706 Box::new(left),
1707 Box::new(right),
1708 )));
1709 }
1710 _ => {
1711 return Err(format!(
1712 "Invalid command '{}' for ratio range type. Valid commands: unit, help, default",
1713 command
1714 ));
1715 }
1716 }
1717 }
1718 TypeSpecification::Text {
1719 length,
1720 options,
1721 help,
1722 } => match command {
1723 TypeConstraintCommand::Option => {
1724 if args.len() != 1 {
1725 return Err("option takes exactly one argument".to_string());
1726 }
1727 options.push(option_name(&args[0], "option")?);
1728 }
1729 TypeConstraintCommand::Options => {
1730 let mut collected = Vec::with_capacity(args.len());
1731 for arg in args {
1732 collected.push(option_name(arg, "options")?);
1733 }
1734 *options = collected;
1735 }
1736 TypeConstraintCommand::Length => {
1737 let d = require_decimal_literal(args, "length")?;
1738 *length = Some(decimal_to_usize(d, "length")?);
1739 }
1740 TypeConstraintCommand::Help => {
1741 apply_type_help_command(help, args)?;
1742 }
1743 TypeConstraintCommand::Default => {
1744 let lit = require_literal(args, "default")?;
1745 reject_calendar_for_default(lit, type_name, DefaultExpectation::Text, None)?;
1746 match lit {
1747 crate::literals::Value::Text(s) => {
1748 *declared_default = Some(RawDefault::Value(ValueKind::Text(s.clone())));
1749 }
1750 _ => {
1751 return Err(
1752 "Please provide a text value in double quotes, for example `-> default \"my default value\"`."
1753 .to_string(),
1754 );
1755 }
1756 }
1757 }
1758 _ => {
1759 return Err(format!(
1760 "Invalid command '{}' for text type. Valid commands: options, length, help, default",
1761 command
1762 ));
1763 }
1764 },
1765 TypeSpecification::Date {
1766 minimum,
1767 maximum,
1768 help,
1769 } => match command {
1770 TypeConstraintCommand::Minimum => {
1771 let dt = require_date_literal(args, "minimum")?;
1772 *minimum = Some(dt);
1773 }
1774 TypeConstraintCommand::Maximum => {
1775 let dt = require_date_literal(args, "maximum")?;
1776 *maximum = Some(dt);
1777 }
1778 TypeConstraintCommand::Help => {
1779 apply_type_help_command(help, args)?;
1780 }
1781 TypeConstraintCommand::Default => {
1782 let lit = require_literal(args, "default")?;
1783 reject_calendar_for_default(lit, type_name, DefaultExpectation::Date, None)?;
1784 match lit {
1785 crate::literals::Value::Date(dt) => {
1786 *declared_default = Some(RawDefault::Value(ValueKind::Date(
1787 date_time_to_semantic(dt),
1788 )));
1789 }
1790 _ => {
1791 return Err(
1792 "Please provide a date, for example `-> default 2024-06-15`."
1793 .to_string(),
1794 );
1795 }
1796 }
1797 }
1798 _ => {
1799 return Err(format!(
1800 "Invalid command '{}' for date type. Valid commands: minimum, maximum, help, default",
1801 command
1802 ));
1803 }
1804 },
1805 TypeSpecification::DateRange { help } => match command {
1806 TypeConstraintCommand::Help => {
1807 apply_type_help_command(help, args)?;
1808 }
1809 TypeConstraintCommand::Default => {
1810 let (left, right) = require_default_range_endpoints(
1811 args,
1812 type_name,
1813 DefaultExpectation::DateRange,
1814 None,
1815 )?;
1816 let left = literal_value_from_parser_value(left)?;
1817 let right = literal_value_from_parser_value(right)?;
1818 if !left.lemma_type.is_date() || !right.lemma_type.is_date() {
1819 return Err(
1820 "Please provide a date range, for example `-> default 2024-01-01...2024-12-31`."
1821 .to_string(),
1822 );
1823 }
1824 *declared_default = Some(RawDefault::Value(ValueKind::Range(
1825 Box::new(left),
1826 Box::new(right),
1827 )));
1828 }
1829 _ => {
1830 return Err(format!(
1831 "Invalid command '{}' for date range type. Valid commands: help, default",
1832 command
1833 ));
1834 }
1835 },
1836 TypeSpecification::Time {
1837 minimum,
1838 maximum,
1839 help,
1840 } => match command {
1841 TypeConstraintCommand::Minimum => {
1842 let t = require_time_literal(args, "minimum")?;
1843 *minimum = Some(t);
1844 }
1845 TypeConstraintCommand::Maximum => {
1846 let t = require_time_literal(args, "maximum")?;
1847 *maximum = Some(t);
1848 }
1849 TypeConstraintCommand::Help => {
1850 apply_type_help_command(help, args)?;
1851 }
1852 TypeConstraintCommand::Default => {
1853 let lit = require_literal(args, "default")?;
1854 reject_calendar_for_default(lit, type_name, DefaultExpectation::Time, None)?;
1855 match lit {
1856 crate::literals::Value::Time(t) => {
1857 *declared_default =
1858 Some(RawDefault::Value(ValueKind::Time(time_to_semantic(t))));
1859 }
1860 _ => {
1861 return Err(
1862 "Please provide a time, for example `-> default 09:00:00`."
1863 .to_string(),
1864 );
1865 }
1866 }
1867 }
1868 _ => {
1869 return Err(format!(
1870 "Invalid command '{}' for time type. Valid commands: minimum, maximum, help, default",
1871 command
1872 ));
1873 }
1874 },
1875 TypeSpecification::TimeRange { help } => match command {
1876 TypeConstraintCommand::Help => {
1877 apply_type_help_command(help, args)?;
1878 }
1879 TypeConstraintCommand::Default => {
1880 let (left, right) = require_default_range_endpoints(
1881 args,
1882 type_name,
1883 DefaultExpectation::TimeRange,
1884 None,
1885 )?;
1886 let left = literal_value_from_parser_value(left)?;
1887 let right = literal_value_from_parser_value(right)?;
1888 if !left.lemma_type.is_time() || !right.lemma_type.is_time() {
1889 return Err(
1890 "Please provide a time range, for example `-> default 09:00...17:00`."
1891 .to_string(),
1892 );
1893 }
1894 *declared_default = Some(RawDefault::Value(ValueKind::Range(
1895 Box::new(left),
1896 Box::new(right),
1897 )));
1898 }
1899 _ => {
1900 return Err(format!(
1901 "Invalid command '{}' for time range type. Valid commands: help, default",
1902 command
1903 ));
1904 }
1905 },
1906 TypeSpecification::QuantityRange {
1907 units,
1908 decomposition,
1909 help,
1910 ..
1911 } => match command {
1912 TypeConstraintCommand::Unit => {
1913 let (unit_name, value, derived_quantity_factors) = match args {
1914 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
1915 (name.clone(), *v, Vec::new())
1916 }
1917 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Expr(
1918 prefix,
1919 factors,
1920 ))] => {
1921 let raw: Vec<(String, i32)> = factors
1922 .iter()
1923 .map(|f| (f.quantity_ref.clone(), f.exp))
1924 .collect();
1925 (name.clone(), *prefix, raw)
1926 }
1927 _ => {
1928 return Err(
1929 "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')"
1930 .to_string(),
1931 );
1932 }
1933 };
1934 if let Some(u) = units.0.iter_mut().find(|u| u.name == unit_name) {
1935 u.factor = crate::computation::rational::decimal_to_rational(value)
1936 .map_err(|failure| failure.to_string())?;
1937 u.derived_quantity_factors = derived_quantity_factors;
1938 } else {
1939 units.0.push(QuantityUnit::from_decimal_factor(
1940 unit_name,
1941 value,
1942 derived_quantity_factors,
1943 )?);
1944 }
1945 }
1946 TypeConstraintCommand::Help => {
1947 apply_type_help_command(help, args)?;
1948 }
1949 TypeConstraintCommand::Default => {
1950 let (left, right) = require_default_range_endpoints(
1951 args,
1952 type_name,
1953 DefaultExpectation::QuantityRange,
1954 Some(units),
1955 )?;
1956 let element_spec = TypeSpecification::Quantity {
1957 minimum: None,
1958 maximum: None,
1959 decimals: None,
1960 units: units.clone(),
1961 traits: vec![],
1962 decomposition: decomposition.clone(),
1963 help: String::new(),
1964 };
1965 let left = lift_range_endpoint(left, &element_spec)?;
1966 let right = lift_range_endpoint(right, &element_spec)?;
1967 if !left.lemma_type.is_quantity() || !right.lemma_type.is_quantity() {
1968 return Err(format!(
1969 "Please provide a range with units valid for '{type_name}', for example `-> default 30 kilogram...35 kilogram`."
1970 ));
1971 }
1972 *declared_default = Some(RawDefault::Value(ValueKind::Range(
1973 Box::new(left),
1974 Box::new(right),
1975 )));
1976 }
1977 _ => {
1978 return Err(format!(
1979 "Invalid command '{}' for quantity range type. Valid commands: unit, help, default",
1980 command
1981 ));
1982 }
1983 },
1984 TypeSpecification::Veto { .. } => {
1985 return Err(format!(
1986 "Invalid command '{}' for veto type. Veto is not a user-declarable type and cannot have constraints",
1987 command
1988 ));
1989 }
1990 TypeSpecification::Undetermined => {
1991 return Err(format!(
1992 "Invalid command '{}' for undetermined sentinel type. Undetermined is an internal type used during type inference and cannot have constraints",
1993 command
1994 ));
1995 }
1996 }
1997 Ok(self)
1998 }
1999}
2000
2001pub fn parse_number_unit(
2004 value_str: &str,
2005 type_spec: &TypeSpecification,
2006) -> Result<crate::parsing::ast::Value, String> {
2007 use crate::literals::{NumberWithUnit, RatioLiteral};
2008 use crate::parsing::ast::Value;
2009
2010 let trimmed = value_str.trim();
2011 match type_spec {
2012 TypeSpecification::Quantity { units, .. } => {
2013 if units.is_empty() {
2014 unreachable!(
2015 "BUG: Quantity type has no units; should have been validated during planning"
2016 );
2017 }
2018 match trimmed.parse::<NumberWithUnit>() {
2019 Ok(n) => {
2020 let unit = units.get(&n.1).map_err(|e| e.to_string())?;
2021 Ok(Value::NumberWithUnit(n.0, unit.name.clone()))
2022 }
2023 Err(e) => {
2024 if trimmed.split_whitespace().count() == 1 && !trimmed.is_empty() {
2025 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
2026 let example_unit = units
2027 .iter()
2028 .next()
2029 .expect("BUG: units non-empty after guard")
2030 .name
2031 .as_str();
2032 Err(format!(
2033 "Quantity value must include a unit, for example: '{} {}'. Valid units: {}.",
2034 trimmed,
2035 example_unit,
2036 valid.join(", ")
2037 ))
2038 } else {
2039 Err(e)
2040 }
2041 }
2042 }
2043 }
2044 TypeSpecification::Ratio { units, .. } => {
2045 if units.is_empty() {
2046 unreachable!(
2047 "BUG: Ratio type has no units; should have been validated during planning"
2048 );
2049 }
2050 match trimmed.parse::<RatioLiteral>()? {
2051 RatioLiteral::Bare(_) => {
2052 Err("Ratio value requires a unit (e.g. '50%', '500 basis_points').".to_string())
2053 }
2054 RatioLiteral::Percent(n) => {
2055 let unit = units.get("percent").map_err(|e| e.to_string())?;
2056 Ok(Value::NumberWithUnit(n, unit.name.clone()))
2057 }
2058 RatioLiteral::Permille(n) => {
2059 let unit = units.get("permille").map_err(|e| e.to_string())?;
2060 Ok(Value::NumberWithUnit(n, unit.name.clone()))
2061 }
2062 RatioLiteral::Named { value, unit } => {
2063 let resolved = units.get(&unit).map_err(|e| e.to_string())?;
2064 Ok(Value::NumberWithUnit(value, resolved.name.clone()))
2065 }
2066 }
2067 }
2068 _ => Err("parse_number_unit only accepts Quantity or Ratio type".to_string()),
2069 }
2070}
2071
2072pub fn parse_value_from_string(
2075 value_str: &str,
2076 type_spec: &TypeSpecification,
2077 source: &Source,
2078) -> Result<crate::parsing::ast::Value, Error> {
2079 use crate::parsing::ast::Value;
2080
2081 let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
2082
2083 let parse_range_value = |element_spec: TypeSpecification| -> Result<Value, Error> {
2084 let (left_str, right_str) = value_str.split_once("...").ok_or_else(|| {
2085 to_err("Range value must use '...' between the two endpoints".to_string())
2086 })?;
2087 if left_str.trim().is_empty() || right_str.trim().is_empty() {
2088 return Err(to_err(
2089 "Range value must contain a non-empty left and right endpoint".to_string(),
2090 ));
2091 }
2092 let left = parse_value_from_string(left_str.trim(), &element_spec, source)?;
2093 let right = parse_value_from_string(right_str.trim(), &element_spec, source)?;
2094 Ok(Value::Range(Box::new(left), Box::new(right)))
2095 };
2096
2097 match type_spec {
2098 TypeSpecification::Text { .. } => value_str
2099 .parse::<crate::literals::TextLiteral>()
2100 .map(|t| Value::Text(t.0))
2101 .map_err(to_err),
2102 TypeSpecification::Number { .. } => value_str
2103 .parse::<crate::literals::NumberLiteral>()
2104 .map(|n| Value::Number(n.0))
2105 .map_err(to_err),
2106 TypeSpecification::Quantity { .. } => {
2107 parse_number_unit(value_str, type_spec).map_err(to_err)
2108 }
2109 TypeSpecification::Boolean { .. } => value_str
2110 .parse::<BooleanValue>()
2111 .map(Value::Boolean)
2112 .map_err(to_err),
2113 TypeSpecification::Date { .. } => {
2114 let date = value_str.parse::<DateTimeValue>().map_err(to_err)?;
2115 Ok(Value::Date(date))
2116 }
2117 TypeSpecification::Time { .. } => {
2118 let time = value_str.parse::<TimeValue>().map_err(to_err)?;
2119 Ok(Value::Time(time))
2120 }
2121 TypeSpecification::Ratio { .. } => {
2122 parse_number_unit(value_str, type_spec).map_err(to_err)
2123 }
2124 TypeSpecification::NumberRange { .. }
2125 | TypeSpecification::QuantityRange { .. }
2126 | TypeSpecification::DateRange { .. }
2127 | TypeSpecification::TimeRange { .. }
2128 | TypeSpecification::RatioRange { .. } => {
2129 let element_spec = range_element_type_specification(type_spec).unwrap_or_else(|| {
2130 unreachable!("BUG: range_element_type_specification missing arm for known range type")
2131 });
2132 parse_range_value(element_spec)
2133 }
2134 TypeSpecification::Veto { .. } => Err(to_err(
2135 "Veto type cannot be parsed from string".to_string(),
2136 )),
2137 TypeSpecification::Undetermined => unreachable!(
2138 "BUG: parse_value_from_string called with Undetermined sentinel type; this type exists only during type inference"
2139 ),
2140 }
2141}
2142
2143#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2148#[serde(rename_all = "snake_case")]
2149pub enum SemanticCalendarUnit {
2150 Month,
2151 Year,
2152}
2153
2154impl fmt::Display for SemanticCalendarUnit {
2155 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2156 let s = match self {
2157 SemanticCalendarUnit::Month => "month",
2158 SemanticCalendarUnit::Year => "year",
2159 };
2160 write!(f, "{}", s)
2161 }
2162}
2163
2164pub fn semantic_calendar_unit_from_unit_name(unit_name: &str) -> SemanticCalendarUnit {
2165 match unit_name {
2166 "month" | "months" => SemanticCalendarUnit::Month,
2167 "year" | "years" => SemanticCalendarUnit::Year,
2168 other => unreachable!(
2169 "BUG: calendar quantity signature unit must be month or year, got '{other}'"
2170 ),
2171 }
2172}
2173
2174pub fn semantic_calendar_unit_from_quantity_signature(
2175 signature: &[(String, i32)],
2176) -> SemanticCalendarUnit {
2177 let unit_name = signature
2178 .first()
2179 .map(|(name, _)| name.as_str())
2180 .expect("BUG: calendar quantity must carry a unit signature");
2181 semantic_calendar_unit_from_unit_name(unit_name)
2182}
2183
2184#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2186#[serde(rename_all = "snake_case")]
2187pub enum SemanticConversionTarget {
2188 Type(PrimitiveKind),
2189 Unit {
2191 unit_name: String,
2192 },
2193}
2194
2195impl SemanticConversionTarget {
2196 #[must_use]
2197 pub fn primitive_kind(&self) -> Option<PrimitiveKind> {
2198 match self {
2199 SemanticConversionTarget::Type(kind) => Some(*kind),
2200 SemanticConversionTarget::Unit { .. } => None,
2201 }
2202 }
2203}
2204
2205impl fmt::Display for SemanticConversionTarget {
2206 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2207 match self {
2208 SemanticConversionTarget::Type(kind) => write!(f, "{:?}", kind),
2209 SemanticConversionTarget::Unit { unit_name } => write!(f, "{unit_name}"),
2210 }
2211 }
2212}
2213
2214#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2216pub struct SemanticTimezone {
2217 pub offset_hours: i8,
2218 pub offset_minutes: u8,
2219}
2220
2221impl fmt::Display for SemanticTimezone {
2222 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2223 if self.offset_hours == 0 && self.offset_minutes == 0 {
2224 write!(f, "Z")
2225 } else {
2226 let sign = if self.offset_hours >= 0 { "+" } else { "-" };
2227 let hours = self.offset_hours.abs();
2228 write!(f, "{}{:02}:{:02}", sign, hours, self.offset_minutes)
2229 }
2230 }
2231}
2232
2233#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2235pub struct SemanticTime {
2236 pub hour: u32,
2237 pub minute: u32,
2238 pub second: u32,
2239 pub microsecond: u32,
2240 pub timezone: Option<SemanticTimezone>,
2241}
2242
2243impl fmt::Display for SemanticTime {
2244 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2245 write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
2246 if self.microsecond != 0 {
2247 write!(f, ".{:06}", self.microsecond)?;
2248 }
2249 if let Some(timezone) = &self.timezone {
2250 write!(f, "{}", timezone)?;
2251 }
2252 Ok(())
2253 }
2254}
2255
2256#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2258pub struct SemanticDateTime {
2259 pub year: i32,
2260 pub month: u32,
2261 pub day: u32,
2262 pub hour: u32,
2263 pub minute: u32,
2264 pub second: u32,
2265 #[serde(default)]
2266 pub microsecond: u32,
2267 pub timezone: Option<SemanticTimezone>,
2268}
2269
2270impl fmt::Display for SemanticDateTime {
2271 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2272 let has_time = self.hour != 0
2273 || self.minute != 0
2274 || self.second != 0
2275 || self.microsecond != 0
2276 || self.timezone.is_some();
2277 if !has_time {
2278 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
2279 } else {
2280 write!(
2281 f,
2282 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
2283 self.year, self.month, self.day, self.hour, self.minute, self.second
2284 )?;
2285 if self.microsecond != 0 {
2286 write!(f, ".{:06}", self.microsecond)?;
2287 }
2288 if let Some(tz) = &self.timezone {
2289 write!(f, "{}", tz)?;
2290 }
2291 Ok(())
2292 }
2293 }
2294}
2295
2296#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2300pub enum RawDefault {
2301 Value(ValueKind),
2302 Quantity {
2303 magnitude: RationalInteger,
2304 unit_name: String,
2305 },
2306}
2307
2308pub fn materialize_raw_default(
2309 raw: RawDefault,
2310 specifications: &TypeSpecification,
2311 type_name: &str,
2312) -> Result<ValueKind, String> {
2313 match raw {
2314 RawDefault::Value(vk) => Ok(vk),
2315 RawDefault::Quantity {
2316 magnitude,
2317 unit_name,
2318 } => {
2319 let TypeSpecification::Quantity { units, .. } = specifications else {
2320 return Err(format!(
2321 "BUG: RawDefault::Quantity for non-quantity type '{type_name}'"
2322 ));
2323 };
2324 let canonical = quantity_declared_bound_to_canonical(
2325 &magnitude, &unit_name, units, type_name, "default",
2326 )?;
2327 Ok(ValueKind::Quantity(canonical, vec![(unit_name, 1)]))
2328 }
2329 }
2330}
2331
2332#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2335pub enum ValueKind {
2336 Number(RationalInteger),
2337 Quantity(RationalInteger, Vec<(String, i32)>),
2345 Text(String),
2346 Date(SemanticDateTime),
2347 Time(SemanticTime),
2348 Boolean(bool),
2349 Ratio(RationalInteger, Option<String>),
2351 Range(Box<LiteralValue>, Box<LiteralValue>),
2352}
2353
2354impl ValueKind {
2355 pub fn as_decimal_magnitude(&self) -> Result<Decimal, String> {
2357 use crate::computation::rational::commit_rational_to_decimal;
2358 match self {
2359 ValueKind::Number(n) | ValueKind::Quantity(n, _) | ValueKind::Ratio(n, _) => {
2360 commit_rational_to_decimal(n).map_err(|failure| failure.to_string())
2361 }
2362 other => Err(format!("expected numeric value kind, got {other}")),
2363 }
2364 }
2365}
2366
2367fn format_rational_magnitude_for_display(rational: &RationalInteger) -> String {
2368 crate::computation::rational::rational_to_display_str(rational)
2369}
2370
2371fn format_number_with_unit_for_display(rational: &RationalInteger, unit: &str) -> String {
2372 use crate::computation::rational::{commit_rational_to_decimal, rational_to_display_str};
2373 use crate::parsing::ast::Value;
2374 match commit_rational_to_decimal(rational) {
2375 Ok(decimal) => format!("{}", Value::NumberWithUnit(decimal, unit.to_string())),
2376 Err(_) => format!("{} {}", rational_to_display_str(rational), unit),
2377 }
2378}
2379
2380impl fmt::Display for ValueKind {
2381 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2382 use crate::computation::rational::{checked_mul, rational_to_display_str};
2383 match self {
2384 ValueKind::Number(rational) => {
2385 write!(f, "{}", format_rational_magnitude_for_display(rational))
2386 }
2387 ValueKind::Quantity(rational, signature) => {
2388 let unit = signature.first().map(|(n, _)| n.as_str()).unwrap_or("");
2389 write!(f, "{}", format_number_with_unit_for_display(rational, unit))
2390 }
2391 ValueKind::Text(s) => write!(f, "{}", crate::parsing::ast::Value::Text(s.clone())),
2392 ValueKind::Ratio(rational, unit) => match unit.as_deref() {
2393 Some("percent") => {
2394 let display = match checked_mul(rational, &RationalInteger::new(100, 1)) {
2395 Ok(scaled) => format_number_with_unit_for_display(&scaled, "percent"),
2396 Err(_) => format!("{} percent", rational_to_display_str(rational)),
2397 };
2398 write!(f, "{}", display)
2399 }
2400 Some("permille") => {
2401 let display = match checked_mul(rational, &RationalInteger::new(1000, 1)) {
2402 Ok(scaled) => format_number_with_unit_for_display(&scaled, "permille"),
2403 Err(_) => format!("{} permille", rational_to_display_str(rational)),
2404 };
2405 write!(f, "{}", display)
2406 }
2407 Some(unit_name) => {
2408 write!(
2409 f,
2410 "{}",
2411 format_number_with_unit_for_display(rational, unit_name)
2412 )
2413 }
2414 None => write!(f, "{}", format_rational_magnitude_for_display(rational)),
2415 },
2416 ValueKind::Date(dt) => write!(f, "{}", dt),
2417 ValueKind::Time(t) => write!(
2418 f,
2419 "{}",
2420 crate::parsing::ast::Value::Time(crate::parsing::ast::TimeValue {
2421 hour: t.hour as u8,
2422 minute: t.minute as u8,
2423 second: t.second as u8,
2424 microsecond: t.microsecond,
2425 timezone: t
2426 .timezone
2427 .as_ref()
2428 .map(|tz| crate::parsing::ast::TimezoneValue {
2429 offset_hours: tz.offset_hours,
2430 offset_minutes: tz.offset_minutes,
2431 }),
2432 })
2433 ),
2434 ValueKind::Boolean(b) => write!(f, "{}", b),
2435 ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
2436 }
2437 }
2438}
2439
2440fn decimal_from_serialized_str(s: &str) -> Result<Decimal, String> {
2441 Decimal::from_str(s.trim()).map_err(|e| format!("invalid decimal '{s}': {e}"))
2442}
2443
2444#[derive(Serialize, Deserialize)]
2445struct SerializedValueUnit {
2446 value: String,
2447 unit: String,
2448}
2449
2450#[derive(Serialize, Deserialize)]
2451struct SerializedQuantity {
2452 value: String,
2453 signature: Vec<(String, i32)>,
2454}
2455
2456#[derive(Serialize, Deserialize)]
2457struct SerializedRange {
2458 from: ValueKind,
2459 to: ValueKind,
2460}
2461
2462impl Serialize for ValueKind {
2463 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2464 use serde::ser::SerializeMap;
2465 let mut map = serializer.serialize_map(Some(1))?;
2466 match self {
2467 ValueKind::Number(rational) => {
2468 map.serialize_entry(
2469 "number",
2470 &crate::literals::rational_to_serialized_str(rational)
2471 .map_err(serde::ser::Error::custom)?,
2472 )?;
2473 }
2474 ValueKind::Quantity(rational, signature) => {
2475 map.serialize_entry(
2476 "quantity",
2477 &SerializedQuantity {
2478 value: crate::literals::rational_to_serialized_str(rational)
2479 .map_err(serde::ser::Error::custom)?,
2480 signature: signature.clone(),
2481 },
2482 )?;
2483 }
2484 ValueKind::Text(s) => {
2485 map.serialize_entry("text", s)?;
2486 }
2487 ValueKind::Date(dt) => {
2488 map.serialize_entry("date", dt)?;
2489 }
2490 ValueKind::Time(t) => {
2491 map.serialize_entry("time", t)?;
2492 }
2493 ValueKind::Boolean(b) => {
2494 map.serialize_entry("boolean", b)?;
2495 }
2496 ValueKind::Ratio(rational, unit) => {
2497 map.serialize_entry(
2498 "ratio",
2499 &SerializedValueUnit {
2500 value: crate::literals::rational_to_serialized_str(rational)
2501 .map_err(serde::ser::Error::custom)?,
2502 unit: unit.clone().unwrap_or_default(),
2503 },
2504 )?;
2505 }
2506 ValueKind::Range(left, right) => {
2507 map.serialize_entry(
2508 "range",
2509 &SerializedRange {
2510 from: left.value.clone(),
2511 to: right.value.clone(),
2512 },
2513 )?;
2514 }
2515 }
2516 map.end()
2517 }
2518}
2519
2520impl<'de> Deserialize<'de> for ValueKind {
2521 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2522 let map = <serde_json::Map<String, serde_json::Value>>::deserialize(deserializer)?;
2523 if map.len() != 1 {
2524 return Err(serde::de::Error::custom(format!(
2525 "ValueKind must have exactly one variant key, got {}",
2526 map.len()
2527 )));
2528 }
2529 let (tag, payload) = map.into_iter().next().expect("BUG: len checked");
2530 deserialize_value_kind_variant(&tag, payload).map_err(serde::de::Error::custom)
2531 }
2532}
2533
2534fn deserialize_value_kind_variant(
2535 tag: &str,
2536 payload: serde_json::Value,
2537) -> Result<ValueKind, String> {
2538 match tag {
2539 "number" => {
2540 let s = payload
2541 .as_str()
2542 .ok_or_else(|| "number must be a JSON string".to_string())?;
2543 let decimal = decimal_from_serialized_str(s)?;
2544 Ok(ValueKind::Number(
2545 crate::literals::rational_from_parsed_decimal(decimal)?,
2546 ))
2547 }
2548 "quantity" => {
2549 let pair: SerializedQuantity =
2550 serde_json::from_value(payload).map_err(|e| e.to_string())?;
2551 let decimal = decimal_from_serialized_str(&pair.value)?;
2552 Ok(ValueKind::Quantity(
2553 crate::literals::rational_from_parsed_decimal(decimal)?,
2554 pair.signature,
2555 ))
2556 }
2557 "ratio" => {
2558 let pair: SerializedValueUnit =
2559 serde_json::from_value(payload).map_err(|e| e.to_string())?;
2560 let unit = if pair.unit.is_empty() {
2561 None
2562 } else {
2563 Some(pair.unit)
2564 };
2565 let decimal = decimal_from_serialized_str(&pair.value)?;
2566 Ok(ValueKind::Ratio(
2567 crate::literals::rational_from_parsed_decimal(decimal)?,
2568 unit,
2569 ))
2570 }
2571 "calendar" => {
2572 let pair: SerializedValueUnit =
2573 serde_json::from_value(payload).map_err(|e| e.to_string())?;
2574 let unit = match pair.unit.as_str() {
2575 "month" | "months" => SemanticCalendarUnit::Month,
2576 "year" | "years" => SemanticCalendarUnit::Year,
2577 other => {
2578 return Err(format!(
2579 "unknown calendar unit '{other}' (expected 'month' or 'year')"
2580 ));
2581 }
2582 };
2583 let decimal = decimal_from_serialized_str(&pair.value)?;
2584 Ok(ValueKind::Quantity(
2585 crate::literals::rational_from_parsed_decimal(decimal)?,
2586 vec![(unit.to_string(), 1)],
2587 ))
2588 }
2589 "text" => {
2590 let s = payload
2591 .as_str()
2592 .ok_or_else(|| "text must be a JSON string".to_string())?;
2593 Ok(ValueKind::Text(s.to_string()))
2594 }
2595 "date" => {
2596 let dt: SemanticDateTime =
2597 serde_json::from_value(payload).map_err(|e| e.to_string())?;
2598 Ok(ValueKind::Date(dt))
2599 }
2600 "time" => {
2601 let t: SemanticTime = serde_json::from_value(payload).map_err(|e| e.to_string())?;
2602 Ok(ValueKind::Time(t))
2603 }
2604 "boolean" => {
2605 let b = payload
2606 .as_bool()
2607 .ok_or_else(|| "boolean must be a JSON bool".to_string())?;
2608 Ok(ValueKind::Boolean(b))
2609 }
2610 "range" => {
2611 let range: SerializedRange =
2612 serde_json::from_value(payload).map_err(|e| e.to_string())?;
2613 Ok(ValueKind::Range(
2614 Box::new(LiteralValue {
2615 value: range.from,
2616 lemma_type: primitive_number_arc().clone(),
2617 }),
2618 Box::new(LiteralValue {
2619 value: range.to,
2620 lemma_type: primitive_number_arc().clone(),
2621 }),
2622 ))
2623 }
2624 other => Err(format!("unknown ValueKind variant '{other}'")),
2625 }
2626}
2627
2628#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2637pub struct PathSegment {
2638 pub data: String,
2640 pub spec: String,
2642}
2643
2644#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2648pub struct DataPath {
2649 pub segments: Vec<PathSegment>,
2651 pub data: String,
2653}
2654
2655impl DataPath {
2656 pub fn new(segments: Vec<PathSegment>, data: String) -> Self {
2658 Self { segments, data }
2659 }
2660
2661 pub fn local(data: String) -> Self {
2663 Self {
2664 segments: vec![],
2665 data,
2666 }
2667 }
2668
2669 pub fn input_key(&self) -> String {
2672 let mut s = String::new();
2673 for segment in &self.segments {
2674 s.push_str(&segment.data);
2675 s.push('.');
2676 }
2677 s.push_str(&self.data);
2678 s
2679 }
2680}
2681
2682#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2686pub struct RulePath {
2687 pub segments: Vec<PathSegment>,
2689 pub rule: String,
2691}
2692
2693impl RulePath {
2694 pub fn new(segments: Vec<PathSegment>, rule: String) -> Self {
2696 Self { segments, rule }
2697 }
2698}
2699
2700#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2709pub struct Expression {
2710 pub kind: ExpressionKind,
2711 pub source_location: Option<Source>,
2712}
2713
2714impl Expression {
2715 pub fn new(kind: ExpressionKind, source_location: Source) -> Self {
2716 Self {
2717 kind,
2718 source_location: Some(source_location),
2719 }
2720 }
2721
2722 pub fn with_source(kind: ExpressionKind, source_location: Option<Source>) -> Self {
2724 Self {
2725 kind,
2726 source_location,
2727 }
2728 }
2729
2730 pub fn collect_data_paths(&self, data: &mut std::collections::HashSet<DataPath>) {
2732 self.kind.collect_data_paths(data);
2733 }
2734}
2735
2736#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2738#[serde(rename_all = "snake_case")]
2739pub enum ExpressionKind {
2740 Literal(Box<LiteralValue>),
2742 DataPath(DataPath),
2744 RulePath(RulePath),
2746 LogicalAnd(Arc<Expression>, Arc<Expression>),
2747 LogicalOr(Arc<Expression>, Arc<Expression>),
2748 Arithmetic(Arc<Expression>, ArithmeticComputation, Arc<Expression>),
2749 Comparison(Arc<Expression>, ComparisonComputation, Arc<Expression>),
2750 UnitConversion(Arc<Expression>, SemanticConversionTarget),
2751 LogicalNegation(Arc<Expression>, NegationType),
2752 MathematicalComputation(MathematicalComputation, Arc<Expression>),
2753 Veto(VetoExpression),
2754 Now,
2756 DateRelative(DateRelativeKind, Arc<Expression>),
2758 DateCalendar(DateCalendarKind, CalendarPeriodUnit, Arc<Expression>),
2760 RangeLiteral(Arc<Expression>, Arc<Expression>),
2761 PastFutureRange(DateRelativeKind, Arc<Expression>),
2762 RangeContainment(Arc<Expression>, Arc<Expression>),
2763 ResultIsVeto(Arc<Expression>),
2765}
2766
2767impl ExpressionKind {
2768 pub(crate) fn collect_data_paths(&self, data: &mut std::collections::HashSet<DataPath>) {
2770 match self {
2771 ExpressionKind::DataPath(fp) => {
2772 data.insert(fp.clone());
2773 }
2774 ExpressionKind::LogicalAnd(left, right) | ExpressionKind::LogicalOr(left, right) => {
2775 left.collect_data_paths(data);
2776 right.collect_data_paths(data);
2777 }
2778 ExpressionKind::Arithmetic(left, _, right)
2779 | ExpressionKind::Comparison(left, _, right)
2780 | ExpressionKind::RangeLiteral(left, right)
2781 | ExpressionKind::RangeContainment(left, right) => {
2782 left.collect_data_paths(data);
2783 right.collect_data_paths(data);
2784 }
2785 ExpressionKind::UnitConversion(inner, _)
2786 | ExpressionKind::LogicalNegation(inner, _)
2787 | ExpressionKind::MathematicalComputation(_, inner)
2788 | ExpressionKind::PastFutureRange(_, inner) => {
2789 inner.collect_data_paths(data);
2790 }
2791 ExpressionKind::DateRelative(_, date_expr) => {
2792 date_expr.collect_data_paths(data);
2793 }
2794 ExpressionKind::DateCalendar(_, _, date_expr) => {
2795 date_expr.collect_data_paths(data);
2796 }
2797 ExpressionKind::Literal(_)
2798 | ExpressionKind::RulePath(_)
2799 | ExpressionKind::Veto(_)
2800 | ExpressionKind::Now => {}
2801 ExpressionKind::ResultIsVeto(operand) => {
2802 operand.collect_data_paths(data);
2803 }
2804 }
2805 }
2806}
2807
2808#[derive(Clone, Debug, Serialize, Deserialize)]
2814#[serde(tag = "kind", rename_all = "snake_case")]
2815pub enum TypeDefiningSpec {
2816 Local,
2818 Import { spec: Arc<LemmaSpec> },
2820}
2821
2822#[derive(Clone, Debug, Serialize, Deserialize)]
2824#[serde(rename_all = "snake_case")]
2825pub enum TypeExtends {
2826 Primitive,
2828 Custom {
2831 parent: String,
2832 family: String,
2833 defining_spec: TypeDefiningSpec,
2834 },
2835}
2836
2837impl PartialEq for TypeExtends {
2838 fn eq(&self, other: &Self) -> bool {
2839 match (self, other) {
2840 (TypeExtends::Primitive, TypeExtends::Primitive) => true,
2841 (
2842 TypeExtends::Custom {
2843 parent: lp,
2844 family: lf,
2845 defining_spec: ld,
2846 },
2847 TypeExtends::Custom {
2848 parent: rp,
2849 family: rf,
2850 defining_spec: rd,
2851 },
2852 ) => {
2853 lp == rp
2854 && lf == rf
2855 && match (ld, rd) {
2856 (TypeDefiningSpec::Local, TypeDefiningSpec::Local) => true,
2857 (
2858 TypeDefiningSpec::Import { spec: left },
2859 TypeDefiningSpec::Import { spec: right },
2860 ) => Arc::ptr_eq(left, right),
2861 _ => false,
2862 }
2863 }
2864 _ => false,
2865 }
2866 }
2867}
2868
2869impl Eq for TypeExtends {}
2870
2871impl std::hash::Hash for TypeDefiningSpec {
2872 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
2873 match self {
2874 TypeDefiningSpec::Local => {
2875 0u8.hash(state);
2876 }
2877 TypeDefiningSpec::Import { spec } => {
2878 1u8.hash(state);
2879 Arc::as_ptr(spec).hash(state);
2880 }
2881 }
2882 }
2883}
2884
2885impl std::hash::Hash for TypeExtends {
2886 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
2887 match self {
2888 TypeExtends::Primitive => {
2889 0u8.hash(state);
2890 }
2891 TypeExtends::Custom {
2892 parent,
2893 family,
2894 defining_spec,
2895 } => {
2896 1u8.hash(state);
2897 parent.hash(state);
2898 family.hash(state);
2899 defining_spec.hash(state);
2900 }
2901 }
2902 }
2903}
2904
2905impl TypeExtends {
2906 #[must_use]
2908 pub fn custom_local(parent: String, family: String) -> Self {
2909 TypeExtends::Custom {
2910 parent,
2911 family,
2912 defining_spec: TypeDefiningSpec::Local,
2913 }
2914 }
2915
2916 #[must_use]
2918 pub fn parent_name(&self) -> Option<&str> {
2919 match self {
2920 TypeExtends::Primitive => None,
2921 TypeExtends::Custom { parent, .. } => Some(parent.as_str()),
2922 }
2923 }
2924}
2925
2926#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
2931pub struct LemmaType {
2932 pub name: Option<String>,
2934 #[serde(flatten)]
2939 pub specifications: TypeSpecification,
2940 pub extends: TypeExtends,
2942}
2943
2944impl LemmaType {
2945 pub fn map_quantity<F>(self, f: F) -> Self
2949 where
2950 F: FnOnce(
2951 QuantityUnits,
2952 Option<BaseQuantityVector>,
2953 ) -> (QuantityUnits, Option<BaseQuantityVector>),
2954 {
2955 let LemmaType {
2956 name,
2957 specifications,
2958 extends,
2959 } = self;
2960 let specifications = match specifications {
2961 TypeSpecification::Quantity {
2962 minimum,
2963 maximum,
2964 decimals,
2965 units,
2966 traits,
2967 decomposition,
2968 help,
2969 } => {
2970 let (units, decomposition) = f(units, decomposition);
2971 TypeSpecification::Quantity {
2972 minimum,
2973 maximum,
2974 decimals,
2975 units,
2976 traits,
2977 decomposition,
2978 help,
2979 }
2980 }
2981 other => other,
2982 };
2983 LemmaType {
2984 name,
2985 specifications,
2986 extends,
2987 }
2988 }
2989
2990 pub fn new(name: String, specifications: TypeSpecification, extends: TypeExtends) -> Self {
2992 Self {
2993 name: Some(name),
2994 specifications,
2995 extends,
2996 }
2997 }
2998
2999 pub fn without_name(specifications: TypeSpecification, extends: TypeExtends) -> Self {
3001 Self {
3002 name: None,
3003 specifications,
3004 extends,
3005 }
3006 }
3007
3008 pub fn primitive(specifications: TypeSpecification) -> Self {
3010 Self {
3011 name: None,
3012 specifications,
3013 extends: TypeExtends::Primitive,
3014 }
3015 }
3016
3017 pub fn name(&self) -> String {
3019 self.name
3020 .clone()
3021 .unwrap_or_else(|| self.specifications.to_string())
3022 }
3023
3024 pub fn is_boolean(&self) -> bool {
3026 matches!(&self.specifications, TypeSpecification::Boolean { .. })
3027 }
3028
3029 pub fn matches_primitive_kind(&self, kind: PrimitiveKind) -> bool {
3030 matches!(
3031 (kind, &self.specifications),
3032 (PrimitiveKind::Number, TypeSpecification::Number { .. })
3033 | (PrimitiveKind::Text, TypeSpecification::Text { .. })
3034 | (PrimitiveKind::Boolean, TypeSpecification::Boolean { .. })
3035 | (PrimitiveKind::Date, TypeSpecification::Date { .. })
3036 | (PrimitiveKind::Time, TypeSpecification::Time { .. })
3037 | (PrimitiveKind::Ratio, TypeSpecification::Ratio { .. })
3038 | (PrimitiveKind::Quantity, TypeSpecification::Quantity { .. })
3039 )
3040 }
3041
3042 pub fn is_quantity(&self) -> bool {
3044 matches!(&self.specifications, TypeSpecification::Quantity { .. })
3045 }
3046
3047 pub fn is_quantity_range(&self) -> bool {
3048 matches!(
3049 &self.specifications,
3050 TypeSpecification::QuantityRange { .. }
3051 )
3052 }
3053
3054 pub fn is_number(&self) -> bool {
3056 matches!(&self.specifications, TypeSpecification::Number { .. })
3057 }
3058
3059 pub fn is_number_range(&self) -> bool {
3060 matches!(&self.specifications, TypeSpecification::NumberRange { .. })
3061 }
3062
3063 pub fn is_numeric(&self) -> bool {
3065 matches!(
3066 &self.specifications,
3067 TypeSpecification::Quantity { .. } | TypeSpecification::Number { .. }
3068 )
3069 }
3070
3071 pub fn is_text(&self) -> bool {
3073 matches!(&self.specifications, TypeSpecification::Text { .. })
3074 }
3075
3076 pub fn is_date(&self) -> bool {
3078 matches!(&self.specifications, TypeSpecification::Date { .. })
3079 }
3080
3081 pub fn is_date_range(&self) -> bool {
3082 matches!(&self.specifications, TypeSpecification::DateRange { .. })
3083 }
3084
3085 pub fn is_time_range(&self) -> bool {
3086 matches!(&self.specifications, TypeSpecification::TimeRange { .. })
3087 }
3088
3089 pub fn is_time(&self) -> bool {
3091 matches!(&self.specifications, TypeSpecification::Time { .. })
3092 }
3093
3094 pub fn has_trait_duration(&self) -> bool {
3095 match &self.specifications {
3096 TypeSpecification::Quantity { traits, .. } => traits.contains(&QuantityTrait::Duration),
3097 _ => false,
3098 }
3099 }
3100
3101 pub fn is_duration_like_quantity(&self) -> bool {
3102 if !self.is_quantity() {
3103 return false;
3104 }
3105 if self.has_trait_duration() {
3106 return true;
3107 }
3108 self.is_anonymous_quantity()
3109 && self
3110 .quantity_type_decomposition()
3111 .is_some_and(|d| *d == duration_decomposition())
3112 }
3113
3114 pub fn is_duration_like(&self) -> bool {
3115 self.is_duration_like_quantity()
3116 }
3117
3118 pub fn has_trait_calendar(&self) -> bool {
3119 match &self.specifications {
3120 TypeSpecification::Quantity { traits, .. } => traits.contains(&QuantityTrait::Calendar),
3121 _ => false,
3122 }
3123 }
3124
3125 pub fn is_calendar_like_quantity(&self) -> bool {
3126 if !self.is_quantity() {
3127 return false;
3128 }
3129 if self.has_trait_calendar() {
3130 return true;
3131 }
3132 self.is_anonymous_quantity()
3133 && self
3134 .quantity_type_decomposition()
3135 .is_some_and(|d| *d == calendar_decomposition())
3136 }
3137
3138 pub fn is_calendar_like(&self) -> bool {
3139 self.is_calendar_like_quantity()
3140 }
3141
3142 pub fn is_ratio(&self) -> bool {
3144 matches!(&self.specifications, TypeSpecification::Ratio { .. })
3145 }
3146
3147 pub fn is_ratio_range(&self) -> bool {
3148 matches!(&self.specifications, TypeSpecification::RatioRange { .. })
3149 }
3150
3151 pub fn is_calendar_quantity_range(&self) -> bool {
3152 matches!(
3153 &self.specifications,
3154 TypeSpecification::QuantityRange { decomposition: Some(decomposition), .. }
3155 if *decomposition == calendar_decomposition()
3156 )
3157 }
3158
3159 pub fn is_calendar_like_range(&self) -> bool {
3160 self.is_calendar_quantity_range()
3161 }
3162
3163 pub fn is_range(&self) -> bool {
3164 matches!(
3165 &self.specifications,
3166 TypeSpecification::DateRange { .. }
3167 | TypeSpecification::TimeRange { .. }
3168 | TypeSpecification::NumberRange { .. }
3169 | TypeSpecification::QuantityRange { .. }
3170 | TypeSpecification::RatioRange { .. }
3171 )
3172 }
3173
3174 pub fn vetoed(&self) -> bool {
3176 matches!(&self.specifications, TypeSpecification::Veto { .. })
3177 }
3178
3179 pub fn is_undetermined(&self) -> bool {
3181 matches!(&self.specifications, TypeSpecification::Undetermined)
3182 }
3183
3184 pub fn has_same_base_type(&self, other: &LemmaType) -> bool {
3186 use TypeSpecification::*;
3187 matches!(
3188 (&self.specifications, &other.specifications),
3189 (Boolean { .. }, Boolean { .. })
3190 | (Number { .. }, Number { .. })
3191 | (NumberRange { .. }, NumberRange { .. })
3192 | (Quantity { .. }, Quantity { .. })
3193 | (QuantityRange { .. }, QuantityRange { .. })
3194 | (Text { .. }, Text { .. })
3195 | (Date { .. }, Date { .. })
3196 | (DateRange { .. }, DateRange { .. })
3197 | (Time { .. }, Time { .. })
3198 | (TimeRange { .. }, TimeRange { .. })
3199 | (Ratio { .. }, Ratio { .. })
3200 | (RatioRange { .. }, RatioRange { .. })
3201 | (Veto { .. }, Veto { .. })
3202 | (Undetermined, Undetermined)
3203 )
3204 }
3205
3206 #[must_use]
3208 pub fn quantity_family_name(&self) -> Option<&str> {
3209 if !self.is_quantity() {
3210 return None;
3211 }
3212 match &self.extends {
3213 TypeExtends::Custom { family, .. } => Some(family.as_str()),
3214 TypeExtends::Primitive => self.name.as_deref(),
3215 }
3216 }
3217
3218 #[must_use]
3220 pub fn same_quantity_family(&self, other: &LemmaType) -> bool {
3221 if !self.is_quantity() || !other.is_quantity() {
3222 return false;
3223 }
3224 match (self.quantity_family_name(), other.quantity_family_name()) {
3225 (Some(self_family), Some(other_family)) => self_family == other_family,
3226 _ => false,
3227 }
3228 }
3229
3230 #[must_use]
3231 pub fn compatible_with_anonymous_quantity(&self, other: &LemmaType) -> bool {
3232 if !self.is_quantity() || !other.is_quantity() {
3233 return false;
3234 }
3235 if !self.is_anonymous_quantity() && !other.is_anonymous_quantity() {
3236 return false;
3237 }
3238 match (
3239 self.quantity_type_decomposition(),
3240 other.quantity_type_decomposition(),
3241 ) {
3242 (Some(a), Some(b)) => a == b,
3243 _ => false,
3244 }
3245 }
3246
3247 pub fn veto_type() -> Self {
3249 Self::primitive(TypeSpecification::veto())
3250 }
3251
3252 pub fn undetermined_type() -> Self {
3255 Self::primitive(TypeSpecification::Undetermined)
3256 }
3257
3258 pub fn decimal_places(&self) -> Option<u8> {
3261 match &self.specifications {
3262 TypeSpecification::Number { decimals, .. } => *decimals,
3263 TypeSpecification::Quantity { decimals, .. } => *decimals,
3264 TypeSpecification::Ratio { decimals, .. } => *decimals,
3265 _ => None,
3266 }
3267 }
3268
3269 pub fn example_value(&self) -> &'static str {
3271 match &self.specifications {
3272 TypeSpecification::Text { .. } => "\"hello world\"",
3273 TypeSpecification::Quantity { .. } => "12.50 eur",
3274 TypeSpecification::QuantityRange { .. } => "30 kilogram...35 kilogram",
3275 TypeSpecification::Number { .. } => "3.14",
3276 TypeSpecification::NumberRange { .. } => "0...100",
3277 TypeSpecification::Boolean { .. } => "true",
3278 TypeSpecification::Date { .. } => "2023-12-25T14:30:00Z",
3279 TypeSpecification::DateRange { .. } => "2024-01-01...2024-12-31",
3280 TypeSpecification::TimeRange { .. } => "09:00...17:00",
3281 TypeSpecification::Veto { .. } => "veto",
3282 TypeSpecification::Time { .. } => "14:30:00",
3283 TypeSpecification::Ratio { .. } => "50%",
3284 TypeSpecification::RatioRange { .. } => "10%...50%",
3285 TypeSpecification::Undetermined => unreachable!(
3286 "BUG: example_value called on Undetermined sentinel type; this type must never reach user-facing code"
3287 ),
3288 }
3289 }
3290
3291 #[must_use]
3295 pub fn quantity_type_decomposition(&self) -> Option<&BaseQuantityVector> {
3299 match &self.specifications {
3300 TypeSpecification::Quantity { decomposition, .. } => decomposition.as_ref(),
3301 _ => unreachable!(
3302 "BUG: quantity_type_decomposition called on non-quantity type {}",
3303 self.name()
3304 ),
3305 }
3306 }
3307
3308 pub fn is_anonymous_quantity(&self) -> bool {
3311 self.name.is_none() && matches!(&self.specifications, TypeSpecification::Quantity { .. })
3312 }
3313
3314 pub fn anonymous_for_decomposition(decomposition: BaseQuantityVector) -> Self {
3318 Self {
3319 name: None,
3320 specifications: TypeSpecification::Quantity {
3321 minimum: None,
3322 maximum: None,
3323 decimals: None,
3324 units: crate::literals::QuantityUnits::new(),
3325 traits: Vec::new(),
3326 decomposition: Some(decomposition),
3327 help: String::new(),
3328 },
3329 extends: TypeExtends::Primitive,
3330 }
3331 }
3332
3333 #[must_use]
3335 pub fn quantity_unit_names(&self) -> Option<Vec<&str>> {
3336 match &self.specifications {
3337 TypeSpecification::Quantity { units, .. } if !units.is_empty() => {
3338 Some(units.iter().map(|unit| unit.name.as_str()).collect())
3339 }
3340 TypeSpecification::QuantityRange { units, .. } if !units.is_empty() => {
3341 Some(units.iter().map(|unit| unit.name.as_str()).collect())
3342 }
3343 _ => None,
3344 }
3345 }
3346
3347 pub fn quantity_unit_factor(
3349 &self,
3350 unit_name: &str,
3351 ) -> &crate::computation::rational::RationalInteger {
3352 let units = match &self.specifications {
3353 TypeSpecification::Quantity { units, .. } => units,
3354 TypeSpecification::QuantityRange { units, .. } => units,
3355 _ => unreachable!(
3356 "BUG: quantity_unit_factor called with non-quantity type {}; only call during evaluation after planning validated quantity conversion",
3357 self.name()
3358 ),
3359 };
3360 match units.get(unit_name) {
3361 Ok(QuantityUnit { factor, .. }) => factor,
3362 Err(_) => {
3363 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
3364 unreachable!(
3365 "BUG: unknown unit '{}' for quantity type {} (valid: {}); planning must reject invalid conversions with Error",
3366 unit_name,
3367 self.name(),
3368 valid.join(", ")
3369 );
3370 }
3371 }
3372 }
3373
3374 pub fn ratio_unit_factor(
3375 &self,
3376 unit_name: &str,
3377 ) -> &crate::computation::rational::RationalInteger {
3378 let units = match &self.specifications {
3379 TypeSpecification::Ratio { units, .. } => units,
3380 _ => unreachable!(
3381 "BUG: ratio_unit_factor called with non-ratio type {}; only call during evaluation after planning validated ratio conversion",
3382 self.name()
3383 ),
3384 };
3385 match units.get(unit_name) {
3386 Ok(RatioUnit { value, .. }) => value,
3387 Err(_) => {
3388 let valid: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
3389 unreachable!(
3390 "BUG: unknown unit '{}' for ratio type {} (valid: {}); planning must reject invalid conversions with Error",
3391 unit_name,
3392 self.name(),
3393 valid.join(", ")
3394 );
3395 }
3396 }
3397 }
3398}
3399
3400#[derive(Clone, Debug, PartialEq, Eq, Hash)]
3402pub struct LiteralValue {
3403 pub value: ValueKind,
3404 pub lemma_type: Arc<LemmaType>,
3405}
3406
3407impl Serialize for LiteralValue {
3408 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
3409 where
3410 S: serde::Serializer,
3411 {
3412 use serde::ser::SerializeStruct;
3413 let mut state = serializer.serialize_struct("LiteralValue", 3)?;
3414 state.serialize_field("value", &self.value)?;
3415 state.serialize_field("lemma_type", self.lemma_type.as_ref())?;
3416 state.serialize_field("display_value", &self.display_value())?;
3417 state.end()
3418 }
3419}
3420
3421impl<'de> Deserialize<'de> for LiteralValue {
3422 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3423 where
3424 D: serde::Deserializer<'de>,
3425 {
3426 #[derive(Deserialize)]
3427 struct Raw {
3428 value: ValueKind,
3429 lemma_type: LemmaType,
3430 }
3431 let raw = Raw::deserialize(deserializer)?;
3432 Ok(Self {
3433 value: raw.value,
3434 lemma_type: Arc::new(raw.lemma_type),
3435 })
3436 }
3437}
3438
3439impl LiteralValue {
3440 pub fn text(s: String) -> Self {
3441 Self {
3442 value: ValueKind::Text(s),
3443 lemma_type: primitive_text_arc().clone(),
3444 }
3445 }
3446
3447 pub fn text_with_type(s: String, lemma_type: Arc<LemmaType>) -> Self {
3448 Self {
3449 value: ValueKind::Text(s),
3450 lemma_type,
3451 }
3452 }
3453
3454 pub fn number(n: RationalInteger) -> Self {
3455 Self {
3456 value: ValueKind::Number(n),
3457 lemma_type: primitive_number_arc().clone(),
3458 }
3459 }
3460
3461 pub fn number_from_decimal(decimal: Decimal) -> Self {
3462 Self::number(
3463 crate::literals::rational_from_parsed_decimal(decimal)
3464 .expect("BUG: literal number from decimal must lift at boundary"),
3465 )
3466 }
3467
3468 pub fn number_with_type(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
3469 Self {
3470 value: ValueKind::Number(n),
3471 lemma_type,
3472 }
3473 }
3474
3475 pub fn number_with_type_from_decimal(decimal: Decimal, lemma_type: Arc<LemmaType>) -> Self {
3476 Self::number_with_type(
3477 crate::literals::rational_from_parsed_decimal(decimal)
3478 .expect("BUG: literal number from decimal must lift at boundary"),
3479 lemma_type,
3480 )
3481 }
3482
3483 pub fn quantity_with_type(
3487 n: RationalInteger,
3488 unit: String,
3489 lemma_type: Arc<LemmaType>,
3490 ) -> Self {
3491 Self {
3492 value: ValueKind::Quantity(n, vec![(unit, 1)]),
3493 lemma_type,
3494 }
3495 }
3496
3497 pub fn quantity_with_signature(
3500 n: RationalInteger,
3501 signature: Vec<(String, i32)>,
3502 lemma_type: Arc<LemmaType>,
3503 ) -> Self {
3504 Self {
3505 value: ValueKind::Quantity(n, signature),
3506 lemma_type,
3507 }
3508 }
3509
3510 pub fn number_interpreted_as_quantity(value: RationalInteger, unit_name: String) -> Self {
3513 Self {
3514 value: ValueKind::Quantity(value, vec![(unit_name, 1)]),
3515 lemma_type: Arc::new(anonymous_quantity_type()),
3516 }
3517 }
3518
3519 pub fn from_bool(b: bool) -> Self {
3520 Self {
3521 value: ValueKind::Boolean(b),
3522 lemma_type: primitive_boolean_arc().clone(),
3523 }
3524 }
3525
3526 pub fn from_datetime(dt: &crate::parsing::ast::DateTimeValue) -> Self {
3527 Self::date(date_time_to_semantic(dt))
3528 }
3529
3530 #[must_use]
3532 pub fn magnitude_default_for_decimal_prompt(&self) -> Option<String> {
3533 use crate::computation::rational::{checked_mul, rational_to_display_str, RationalInteger};
3534 match &self.value {
3535 ValueKind::Number(n) => Some(rational_to_display_str(n)),
3536 ValueKind::Quantity(n, signature) if signature.len() == 1 && signature[0].1 == 1 => {
3537 Some(rational_to_display_str(n))
3538 }
3539 ValueKind::Ratio(n, Some(unit)) if unit == "percent" => {
3540 checked_mul(n, &RationalInteger::new(100, 1))
3541 .ok()
3542 .map(|scaled| rational_to_display_str(&scaled))
3543 }
3544 ValueKind::Ratio(n, Some(unit)) if unit == "permille" => {
3545 checked_mul(n, &RationalInteger::new(1000, 1))
3546 .ok()
3547 .map(|scaled| rational_to_display_str(&scaled))
3548 }
3549 ValueKind::Ratio(n, _) => Some(rational_to_display_str(n)),
3550 _ => None,
3551 }
3552 }
3553
3554 pub fn date(dt: SemanticDateTime) -> Self {
3555 Self {
3556 value: ValueKind::Date(dt),
3557 lemma_type: primitive_date_arc().clone(),
3558 }
3559 }
3560
3561 pub fn date_with_type(dt: SemanticDateTime, lemma_type: Arc<LemmaType>) -> Self {
3562 Self {
3563 value: ValueKind::Date(dt),
3564 lemma_type,
3565 }
3566 }
3567
3568 pub fn time(t: SemanticTime) -> Self {
3569 Self {
3570 value: ValueKind::Time(t),
3571 lemma_type: primitive_time_arc().clone(),
3572 }
3573 }
3574
3575 pub fn time_with_type(t: SemanticTime, lemma_type: Arc<LemmaType>) -> Self {
3576 Self {
3577 value: ValueKind::Time(t),
3578 lemma_type,
3579 }
3580 }
3581
3582 pub fn calendar(
3583 value: RationalInteger,
3584 unit: SemanticCalendarUnit,
3585 lemma_type: Arc<LemmaType>,
3586 ) -> Self {
3587 Self::quantity_with_type(value, unit.to_string(), lemma_type)
3588 }
3589
3590 pub fn calendar_from_decimal(
3591 value: Decimal,
3592 unit: SemanticCalendarUnit,
3593 lemma_type: Arc<LemmaType>,
3594 ) -> Self {
3595 Self::calendar(
3596 crate::literals::rational_from_parsed_decimal(value)
3597 .expect("BUG: calendar literal from decimal must lift at boundary"),
3598 unit,
3599 lemma_type,
3600 )
3601 }
3602
3603 pub fn calendar_with_type(
3604 value: RationalInteger,
3605 unit: SemanticCalendarUnit,
3606 lemma_type: Arc<LemmaType>,
3607 ) -> Self {
3608 Self::calendar(value, unit, lemma_type)
3609 }
3610
3611 pub fn duration_canonical_seconds(&self) -> RationalInteger {
3613 let ValueKind::Quantity(magnitude, _) = &self.value else {
3614 unreachable!(
3615 "BUG: duration_canonical_seconds called with {:?}",
3616 self.value
3617 );
3618 };
3619 if !self.lemma_type.is_duration_like_quantity() {
3620 unreachable!(
3621 "BUG: duration_canonical_seconds called with type {}",
3622 self.lemma_type.name()
3623 );
3624 }
3625 let factor = self.lemma_type.quantity_unit_factor("second");
3626 checked_div(magnitude, factor).expect("BUG: duration unit factor cannot be zero")
3627 }
3628
3629 pub fn calendar_canonical_months(&self) -> RationalInteger {
3631 let ValueKind::Quantity(magnitude, _) = &self.value else {
3632 unreachable!(
3633 "BUG: calendar_canonical_months called with {:?}",
3634 self.value
3635 );
3636 };
3637 if !self.lemma_type.is_calendar_like() {
3638 unreachable!(
3639 "BUG: calendar_canonical_months called with type {}",
3640 self.lemma_type.name()
3641 );
3642 }
3643 let factor = self.lemma_type.quantity_unit_factor("month");
3644 checked_div(magnitude, factor).expect("BUG: calendar unit factor cannot be zero")
3645 }
3646
3647 pub fn ratio(r: RationalInteger, unit: Option<String>) -> Self {
3648 Self {
3649 value: ValueKind::Ratio(r, unit),
3650 lemma_type: primitive_ratio_arc().clone(),
3651 }
3652 }
3653
3654 pub fn ratio_from_decimal(r: Decimal, unit: Option<String>) -> Self {
3655 Self::ratio(
3656 crate::literals::rational_from_parsed_decimal(r)
3657 .expect("BUG: ratio literal from decimal must lift at boundary"),
3658 unit,
3659 )
3660 }
3661
3662 pub fn ratio_with_type(
3663 r: RationalInteger,
3664 unit: Option<String>,
3665 lemma_type: Arc<LemmaType>,
3666 ) -> Self {
3667 Self {
3668 value: ValueKind::Ratio(r, unit),
3669 lemma_type,
3670 }
3671 }
3672
3673 pub fn range(left: LiteralValue, right: LiteralValue) -> Self {
3674 let specifications =
3675 range_type_specification_from_endpoints(&left.lemma_type, &right.lemma_type)
3676 .unwrap_or_else(|| {
3677 unreachable!(
3678 "BUG: attempted to construct a range literal from incompatible endpoint types"
3679 )
3680 });
3681
3682 Self {
3683 value: ValueKind::Range(Box::new(left), Box::new(right)),
3684 lemma_type: Arc::new(LemmaType::primitive(specifications)),
3685 }
3686 }
3687
3688 pub fn display_value(&self) -> String {
3690 format!("{}", self)
3691 }
3692
3693 pub fn byte_size(&self) -> usize {
3695 format!("{}", self).len()
3696 }
3697
3698 pub fn get_type(&self) -> &LemmaType {
3700 &self.lemma_type
3701 }
3702}
3703
3704#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3706#[serde(rename_all = "snake_case")]
3707pub enum DataValue {
3708 Definition {
3709 schema_type: LemmaType,
3710 #[serde(default, skip_serializing_if = "Option::is_none")]
3711 bound_value: Option<LiteralValue>,
3712 },
3713}
3714
3715impl DataValue {
3716 #[must_use]
3717 pub fn from_bound_literal(value: LiteralValue) -> Self {
3718 let schema_type = value.get_type().clone();
3719 Self::Definition {
3720 schema_type,
3721 bound_value: Some(value),
3722 }
3723 }
3724}
3725
3726#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3728pub struct Data {
3729 pub path: DataPath,
3730 pub value: DataValue,
3731 pub source: Option<Source>,
3732}
3733
3734#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3737#[serde(rename_all = "snake_case", tag = "kind")]
3738pub enum ReferenceTarget {
3739 Data(DataPath),
3740 Rule(RulePath),
3741}
3742
3743#[derive(Clone, Debug, Serialize, Deserialize)]
3745#[serde(rename_all = "snake_case")]
3746pub enum DataDefinition {
3747 Value { value: LiteralValue, source: Source },
3749 TypeDeclaration {
3754 resolved_type: Arc<LemmaType>,
3755 declared_default: Option<ValueKind>,
3756 source: Source,
3757 },
3758 Import {
3760 spec: Arc<crate::parsing::ast::LemmaSpec>,
3761 source: Source,
3762 },
3763 Reference {
3789 target: ReferenceTarget,
3790 resolved_type: Arc<LemmaType>,
3791 local_constraints: Option<Vec<Constraint>>,
3792 local_default: Option<ValueKind>,
3793 source: Source,
3794 },
3795 Violated { reason: String, source: Source },
3797}
3798
3799impl DataDefinition {
3800 pub fn schema_type(&self) -> Option<&LemmaType> {
3802 match self {
3803 DataDefinition::Value { value, .. } => Some(value.lemma_type.as_ref()),
3804 DataDefinition::TypeDeclaration { resolved_type, .. } => Some(resolved_type.as_ref()),
3805 DataDefinition::Reference { resolved_type, .. } => Some(resolved_type.as_ref()),
3806 DataDefinition::Import { .. } | DataDefinition::Violated { .. } => None,
3807 }
3808 }
3809
3810 pub fn value(&self) -> Option<&LiteralValue> {
3814 match self {
3815 DataDefinition::Value { value, .. } => Some(value),
3816 DataDefinition::TypeDeclaration { .. }
3817 | DataDefinition::Import { .. }
3818 | DataDefinition::Reference { .. }
3819 | DataDefinition::Violated { .. } => None,
3820 }
3821 }
3822
3823 #[inline]
3827 pub fn bound_value(&self) -> Option<&LiteralValue> {
3828 self.value()
3829 }
3830
3831 pub fn default_suggestion(&self) -> Option<LiteralValue> {
3836 match self {
3837 DataDefinition::TypeDeclaration {
3838 resolved_type,
3839 declared_default: Some(dv),
3840 ..
3841 } => Some(LiteralValue {
3842 value: dv.clone(),
3843 lemma_type: Arc::clone(resolved_type),
3844 }),
3845 DataDefinition::Reference {
3846 resolved_type,
3847 local_default: Some(dv),
3848 ..
3849 } => Some(LiteralValue {
3850 value: dv.clone(),
3851 lemma_type: Arc::clone(resolved_type),
3852 }),
3853 DataDefinition::Value { .. }
3854 | DataDefinition::TypeDeclaration { .. }
3855 | DataDefinition::Reference { .. }
3856 | DataDefinition::Import { .. }
3857 | DataDefinition::Violated { .. } => None,
3858 }
3859 }
3860
3861 pub fn source(&self) -> &Source {
3863 match self {
3864 DataDefinition::Value { source, .. } => source,
3865 DataDefinition::TypeDeclaration { source, .. } => source,
3866 DataDefinition::Import { source, .. } => source,
3867 DataDefinition::Reference { source, .. } => source,
3868 DataDefinition::Violated { source, .. } => source,
3869 }
3870 }
3871
3872 pub fn reference_target(&self) -> Option<&ReferenceTarget> {
3875 match self {
3876 DataDefinition::Reference { target, .. } => Some(target),
3877 _ => None,
3878 }
3879 }
3880}
3881
3882pub fn number_with_unit_to_value_kind(
3884 magnitude: rust_decimal::Decimal,
3885 unit_name: &str,
3886 lemma_type: &LemmaType,
3887) -> Result<ValueKind, String> {
3888 match &lemma_type.specifications {
3889 TypeSpecification::Ratio { units, .. } => {
3890 use crate::computation::rational::{checked_div, decimal_to_rational};
3891 let unit = units.get(unit_name)?;
3892 let magnitude_rational = decimal_to_rational(magnitude)
3893 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
3894 let canonical_rational = checked_div(&magnitude_rational, &unit.value)
3895 .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
3896 Ok(ValueKind::Ratio(
3897 canonical_rational,
3898 Some(unit.name.clone()),
3899 ))
3900 }
3901 TypeSpecification::Quantity { units, .. } => {
3902 use crate::computation::rational::checked_mul;
3903 let rational = lift_parser_decimal(magnitude)?;
3904 let unit = units.get(unit_name)?;
3905 let canonical = checked_mul(&rational, &unit.factor)
3906 .map_err(|failure| format!("quantity canonicalization overflow: {failure}"))?;
3907 Ok(ValueKind::Quantity(
3908 canonical,
3909 vec![(unit_name.to_string(), 1)],
3910 ))
3911 }
3912 _ => Err(format!(
3913 "Unit '{}' is defined on type '{}' which is not quantity or ratio",
3914 unit_name,
3915 lemma_type.name()
3916 )),
3917 }
3918}
3919
3920pub(crate) fn value_kind_matches_spec(value: &ValueKind, type_spec: &TypeSpecification) -> bool {
3923 matches!(
3924 (type_spec, value),
3925 (TypeSpecification::Number { .. }, ValueKind::Number(_))
3926 | (TypeSpecification::Text { .. }, ValueKind::Text(_))
3927 | (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
3928 | (TypeSpecification::Date { .. }, ValueKind::Date(_))
3929 | (TypeSpecification::Time { .. }, ValueKind::Time(_))
3930 | (
3931 TypeSpecification::Quantity { .. },
3932 ValueKind::Quantity(_, _)
3933 )
3934 | (TypeSpecification::Ratio { .. }, ValueKind::Ratio(_, _))
3935 | (TypeSpecification::Ratio { .. }, ValueKind::Number(_))
3936 | (
3937 TypeSpecification::NumberRange { .. },
3938 ValueKind::Range(_, _)
3939 )
3940 | (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
3941 | (TypeSpecification::TimeRange { .. }, ValueKind::Range(_, _))
3942 | (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
3943 | (
3944 TypeSpecification::QuantityRange { .. },
3945 ValueKind::Range(_, _)
3946 )
3947 | (TypeSpecification::Veto { .. }, _)
3948 | (TypeSpecification::Undetermined, _)
3949 )
3950}
3951
3952fn value_kind_tag_for_type(spec: &TypeSpecification) -> &'static str {
3953 match spec {
3954 TypeSpecification::Boolean { .. } => "boolean",
3955 TypeSpecification::Quantity { .. } => "quantity",
3956 TypeSpecification::Number { .. } => "number",
3957 TypeSpecification::NumberRange { .. }
3958 | TypeSpecification::QuantityRange { .. }
3959 | TypeSpecification::DateRange { .. }
3960 | TypeSpecification::TimeRange { .. }
3961 | TypeSpecification::RatioRange { .. } => "range",
3962 TypeSpecification::Ratio { .. } => "ratio",
3963 TypeSpecification::Text { .. } => "text",
3964 TypeSpecification::Date { .. } => "date",
3965 TypeSpecification::Time { .. } => "time",
3966 TypeSpecification::Veto { .. } => "veto",
3967 TypeSpecification::Undetermined => "undetermined",
3968 }
3969}
3970
3971fn parser_value_type_mismatch(
3972 value: &crate::literals::Value,
3973 type_spec: &TypeSpecification,
3974) -> String {
3975 use crate::parsing::ast::AsLemmaSource;
3976 let value_str = format!("{}", AsLemmaSource(value));
3977 let expected = value_kind_tag_for_type(type_spec);
3978 match type_spec {
3979 TypeSpecification::Quantity { units, .. } => {
3980 let unit_hint = units
3981 .iter()
3982 .find(|u| u.factor == crate::computation::rational::rational_one())
3983 .map(|u| u.name.as_str())
3984 .or_else(|| units.iter().next().map(|u| u.name.as_str()))
3985 .unwrap_or("unit");
3986 format!("cannot use {value_str} as {expected}: expected `<n> {unit_hint}`")
3987 }
3988 TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
3989 let unit_hint = units
3990 .iter()
3991 .next()
3992 .map(|u| u.name.as_str())
3993 .unwrap_or("unit");
3994 format!(
3995 "cannot use {value_str} as {expected}: expected `<n> {unit_hint}` or bare ratio"
3996 )
3997 }
3998 _ => format!("cannot use {value_str} as {expected}"),
3999 }
4000}
4001
4002pub fn refresh_quantity_literal_canonical_magnitude(
4007 lit: &mut LiteralValue,
4008 resolved_type: &LemmaType,
4009) {
4010 let ValueKind::Quantity(magnitude, signature) = &mut lit.value else {
4011 return;
4012 };
4013 let (unit_name, exponent) = signature
4014 .first()
4015 .expect("BUG: quantity literal has empty signature during canonical magnitude refresh");
4016 if *exponent != 1 || signature.len() != 1 {
4017 return;
4018 }
4019 let stored_factor = lit.lemma_type.quantity_unit_factor(unit_name);
4020 let resolved_factor = resolved_type.quantity_unit_factor(unit_name);
4021 if stored_factor == resolved_factor {
4022 lit.lemma_type = Arc::new(resolved_type.clone());
4023 return;
4024 }
4025 let scaled = checked_mul(magnitude, resolved_factor)
4026 .expect("BUG: quantity recanonicalization multiply overflow");
4027 *magnitude = checked_div(&scaled, stored_factor)
4028 .expect("BUG: quantity recanonicalization divide failed");
4029 lit.lemma_type = Arc::new(resolved_type.clone());
4030}
4031
4032pub fn parser_value_to_value_kind(
4034 value: &crate::literals::Value,
4035 type_spec: &TypeSpecification,
4036) -> Result<ValueKind, String> {
4037 use crate::computation::rational::decimal_to_rational;
4038 use crate::literals::Value;
4039 match (value, type_spec) {
4040 (Value::NumberWithUnit(magnitude, unit_name), TypeSpecification::Ratio { units, .. }) => {
4041 use crate::computation::rational::checked_div;
4042 let unit = units.get(unit_name.as_str())?;
4043 let magnitude_rational = decimal_to_rational(*magnitude)
4044 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
4045 let canonical_rational = checked_div(&magnitude_rational, &unit.value)
4046 .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
4047 Ok(ValueKind::Ratio(
4048 canonical_rational,
4049 Some(unit.name.clone()),
4050 ))
4051 }
4052 (
4053 Value::NumberWithUnit(magnitude, unit_name),
4054 TypeSpecification::Quantity { units, .. },
4055 ) => {
4056 use crate::computation::rational::checked_mul;
4057 let rational = lift_parser_decimal(*magnitude)?;
4058 let unit = units.get(unit_name.as_str())?;
4059 let canonical = checked_mul(&rational, &unit.factor)
4060 .map_err(|failure| format!("quantity canonicalization overflow: {failure}"))?;
4061 Ok(ValueKind::Quantity(canonical, vec![(unit_name.clone(), 1)]))
4062 }
4063 (Value::NumberWithUnit(_, _), _) => {
4064 Err("number_with_unit literal requires a quantity or ratio type".to_string())
4065 }
4066 (Value::Number(n), TypeSpecification::Number { .. }) => {
4067 Ok(ValueKind::Number(lift_parser_decimal(*n)?))
4068 }
4069 (Value::Number(n), TypeSpecification::Ratio { .. }) => {
4070 let r = decimal_to_rational(*n)
4071 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
4072 Ok(ValueKind::Ratio(r, None))
4073 }
4074 (Value::Text(s), TypeSpecification::Text { .. }) => Ok(ValueKind::Text(s.clone())),
4075 (Value::Boolean(b), TypeSpecification::Boolean { .. }) => Ok(ValueKind::Boolean(b.into())),
4076 (Value::Date(dt), TypeSpecification::Date { .. }) => {
4077 Ok(ValueKind::Date(date_time_to_semantic(dt)))
4078 }
4079 (Value::Time(t), TypeSpecification::Time { .. }) => {
4080 Ok(ValueKind::Time(time_to_semantic(t)))
4081 }
4082 (
4083 Value::Range(left, right),
4084 range_spec @ (TypeSpecification::NumberRange { .. }
4085 | TypeSpecification::DateRange { .. }
4086 | TypeSpecification::TimeRange { .. }
4087 | TypeSpecification::RatioRange { .. }
4088 | TypeSpecification::QuantityRange { .. }),
4089 ) => {
4090 let endpoint = range_element_type_specification(range_spec).ok_or_else(|| {
4091 "BUG: range_element_type_specification missing arm for range type".to_string()
4092 })?;
4093 let left_lit = lift_range_endpoint(left, &endpoint)?;
4094 let right_lit = lift_range_endpoint(right, &endpoint)?;
4095 Ok(ValueKind::Range(Box::new(left_lit), Box::new(right_lit)))
4096 }
4097 (value, type_spec) => Err(parser_value_type_mismatch(value, type_spec)),
4098 }
4099}
4100
4101pub fn value_to_semantic(value: &crate::parsing::ast::Value) -> Result<ValueKind, String> {
4105 use crate::parsing::ast::Value;
4106 Ok(match value {
4107 Value::Number(n) => ValueKind::Number(lift_parser_decimal(*n)?),
4108 Value::Text(s) => ValueKind::Text(s.clone()),
4109 Value::Boolean(b) => ValueKind::Boolean(bool::from(*b)),
4110 Value::Date(dt) => ValueKind::Date(date_time_to_semantic(dt)),
4111 Value::Time(t) => ValueKind::Time(time_to_semantic(t)),
4112 Value::NumberWithUnit(_, _) => {
4113 return Err(
4114 "number_with_unit literal requires type context (quantity or ratio)".to_string(),
4115 );
4116 }
4117 Value::Range(left, right) => ValueKind::Range(
4118 Box::new(literal_value_from_parser_value(left)?),
4119 Box::new(literal_value_from_parser_value(right)?),
4120 ),
4121 })
4122}
4123
4124pub(crate) fn date_time_to_semantic(dt: &crate::parsing::ast::DateTimeValue) -> SemanticDateTime {
4126 SemanticDateTime {
4127 year: dt.year,
4128 month: dt.month,
4129 day: dt.day,
4130 hour: dt.hour,
4131 minute: dt.minute,
4132 second: dt.second,
4133 microsecond: dt.microsecond,
4134 timezone: dt.timezone.as_ref().map(|tz| SemanticTimezone {
4135 offset_hours: tz.offset_hours,
4136 offset_minutes: tz.offset_minutes,
4137 }),
4138 }
4139}
4140
4141pub(crate) fn time_to_semantic(t: &crate::parsing::ast::TimeValue) -> SemanticTime {
4143 SemanticTime {
4144 hour: t.hour.into(),
4145 minute: t.minute.into(),
4146 second: t.second.into(),
4147 microsecond: t.microsecond,
4148 timezone: t.timezone.as_ref().map(|tz| SemanticTimezone {
4149 offset_hours: tz.offset_hours,
4150 offset_minutes: tz.offset_minutes,
4151 }),
4152 }
4153}
4154
4155pub(crate) fn compare_semantic_dates(
4159 left: &SemanticDateTime,
4160 right: &SemanticDateTime,
4161) -> std::cmp::Ordering {
4162 left.year
4163 .cmp(&right.year)
4164 .then_with(|| left.month.cmp(&right.month))
4165 .then_with(|| left.day.cmp(&right.day))
4166 .then_with(|| left.hour.cmp(&right.hour))
4167 .then_with(|| left.minute.cmp(&right.minute))
4168 .then_with(|| left.second.cmp(&right.second))
4169 .then_with(|| left.microsecond.cmp(&right.microsecond))
4170}
4171
4172pub(crate) fn compare_semantic_times(
4175 left: &SemanticTime,
4176 right: &SemanticTime,
4177) -> std::cmp::Ordering {
4178 left.hour
4179 .cmp(&right.hour)
4180 .then_with(|| left.minute.cmp(&right.minute))
4181 .then_with(|| left.second.cmp(&right.second))
4182 .then_with(|| left.microsecond.cmp(&right.microsecond))
4183}
4184
4185pub fn conversion_target_to_semantic(
4187 ct: &ConversionTarget,
4188 unit_index: Option<&HashMap<String, Arc<LemmaType>>>,
4189) -> Result<SemanticConversionTarget, String> {
4190 match ct {
4191 ConversionTarget::Type(kind) => Ok(SemanticConversionTarget::Type(*kind)),
4192 ConversionTarget::Unit { unit_name } => {
4193 let unit_name = crate::parsing::ast::ascii_lowercase_logical_name(unit_name.clone());
4194 if let Some(index) = unit_index {
4195 if index.get(&unit_name).is_none() {
4196 return Err(format!("Unknown unit '{unit_name}'."));
4197 }
4198 }
4199 Ok(SemanticConversionTarget::Unit { unit_name })
4200 }
4201 }
4202}
4203
4204static PRIMITIVE_BOOLEAN: OnceLock<Arc<LemmaType>> = OnceLock::new();
4210static PRIMITIVE_NUMBER: OnceLock<Arc<LemmaType>> = OnceLock::new();
4211static PRIMITIVE_TEXT: OnceLock<Arc<LemmaType>> = OnceLock::new();
4212static PRIMITIVE_DATE: OnceLock<Arc<LemmaType>> = OnceLock::new();
4213static PRIMITIVE_DATE_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
4214static PRIMITIVE_TIME: OnceLock<Arc<LemmaType>> = OnceLock::new();
4215static PRIMITIVE_RATIO: OnceLock<Arc<LemmaType>> = OnceLock::new();
4216
4217#[must_use]
4218pub fn primitive_boolean_arc() -> &'static Arc<LemmaType> {
4219 PRIMITIVE_BOOLEAN.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::boolean())))
4220}
4221
4222#[must_use]
4223pub fn primitive_number_arc() -> &'static Arc<LemmaType> {
4224 PRIMITIVE_NUMBER.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::number())))
4225}
4226
4227#[must_use]
4228pub fn primitive_text_arc() -> &'static Arc<LemmaType> {
4229 PRIMITIVE_TEXT.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::text())))
4230}
4231
4232#[must_use]
4233pub fn primitive_date_arc() -> &'static Arc<LemmaType> {
4234 PRIMITIVE_DATE.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::date())))
4235}
4236
4237#[must_use]
4238pub fn primitive_date_range_arc() -> &'static Arc<LemmaType> {
4239 PRIMITIVE_DATE_RANGE
4240 .get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::date_range())))
4241}
4242
4243#[must_use]
4244pub fn primitive_time_arc() -> &'static Arc<LemmaType> {
4245 PRIMITIVE_TIME.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::time())))
4246}
4247
4248#[must_use]
4249pub fn primitive_ratio_arc() -> &'static Arc<LemmaType> {
4250 PRIMITIVE_RATIO.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::ratio())))
4251}
4252
4253#[cfg(test)]
4255static PRIMITIVE_QUANTITY: OnceLock<Arc<LemmaType>> = OnceLock::new();
4256
4257#[cfg(test)]
4258#[must_use]
4259pub fn primitive_boolean() -> &'static LemmaType {
4260 primitive_boolean_arc().as_ref()
4261}
4262
4263#[cfg(test)]
4264#[must_use]
4265pub fn primitive_quantity() -> &'static LemmaType {
4266 primitive_quantity_arc().as_ref()
4267}
4268
4269#[cfg(test)]
4270#[must_use]
4271pub fn primitive_quantity_arc() -> &'static Arc<LemmaType> {
4272 PRIMITIVE_QUANTITY.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::quantity())))
4273}
4274
4275#[cfg(test)]
4276#[must_use]
4277pub fn primitive_number() -> &'static LemmaType {
4278 primitive_number_arc().as_ref()
4279}
4280
4281#[cfg(test)]
4282#[must_use]
4283pub fn primitive_text() -> &'static LemmaType {
4284 primitive_text_arc().as_ref()
4285}
4286
4287#[cfg(test)]
4288#[must_use]
4289pub fn primitive_date() -> &'static LemmaType {
4290 primitive_date_arc().as_ref()
4291}
4292
4293#[cfg(test)]
4294#[must_use]
4295pub fn primitive_time() -> &'static LemmaType {
4296 primitive_time_arc().as_ref()
4297}
4298
4299#[cfg(test)]
4300#[must_use]
4301pub fn primitive_ratio() -> &'static LemmaType {
4302 primitive_ratio_arc().as_ref()
4303}
4304
4305#[must_use]
4307pub fn type_spec_for_primitive(kind: PrimitiveKind) -> TypeSpecification {
4308 match kind {
4309 PrimitiveKind::Boolean => TypeSpecification::boolean(),
4310 PrimitiveKind::Quantity => TypeSpecification::quantity(),
4311 PrimitiveKind::QuantityRange => TypeSpecification::quantity_range(),
4312 PrimitiveKind::Number => TypeSpecification::number(),
4313 PrimitiveKind::NumberRange => TypeSpecification::number_range(),
4314 PrimitiveKind::Percent | PrimitiveKind::Ratio => TypeSpecification::ratio(),
4315 PrimitiveKind::RatioRange => TypeSpecification::ratio_range(),
4316 PrimitiveKind::Text => TypeSpecification::text(),
4317 PrimitiveKind::Date => TypeSpecification::date(),
4318 PrimitiveKind::DateRange => TypeSpecification::date_range(),
4319 PrimitiveKind::Time => TypeSpecification::time(),
4320 PrimitiveKind::TimeRange => TypeSpecification::time_range(),
4321 }
4322}
4323
4324impl fmt::Display for PathSegment {
4329 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4330 write!(f, "{} → {}", self.data, self.spec)
4331 }
4332}
4333
4334impl fmt::Display for DataPath {
4335 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4336 for segment in &self.segments {
4337 write!(f, "{}.", segment)?;
4338 }
4339 write!(f, "{}", self.data)
4340 }
4341}
4342
4343impl fmt::Display for RulePath {
4344 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4345 for segment in &self.segments {
4346 write!(f, "{}.", segment)?;
4347 }
4348 write!(f, "{}", self.rule)
4349 }
4350}
4351
4352impl fmt::Display for LemmaType {
4353 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4354 write!(f, "{}", self.name())
4355 }
4356}
4357
4358fn decimal_places_in_display_value(decimal: &rust_decimal::Decimal) -> u32 {
4359 if decimal.is_integer() {
4360 return 0;
4361 }
4362 decimal.fract().normalize().scale()
4363}
4364
4365fn format_decimal_for_quantity_display(
4366 decimal: rust_decimal::Decimal,
4367 decimals: Option<u8>,
4368) -> String {
4369 match decimals {
4370 Some(dp) => {
4371 let rounded = decimal.round_dp(u32::from(dp));
4372 format!("{:.prec$}", rounded, prec = dp as usize)
4373 }
4374 None => decimal.normalize().to_string(),
4375 }
4376}
4377
4378fn format_quantity_canonical_for_display(
4379 canonical: &crate::computation::rational::RationalInteger,
4380 lemma_type: &LemmaType,
4381 signature: &[(String, i32)],
4382) -> String {
4383 use crate::computation::rational::{
4384 checked_div, commit_rational_to_decimal, rational_to_display_str,
4385 };
4386 use rust_decimal::Decimal;
4387
4388 let decimals = lemma_type.decimal_places();
4389
4390 if let TypeSpecification::Quantity { units, .. } = &lemma_type.specifications {
4391 if !units.is_empty() {
4392 struct UnitDisplayCandidate {
4393 unit_name: String,
4394 decimal_places: u32,
4395 under_1000: bool,
4396 abs_magnitude: Decimal,
4397 formatted: String,
4398 }
4399
4400 let mut candidates: Vec<UnitDisplayCandidate> = Vec::with_capacity(units.len());
4401 for unit in units.iter() {
4402 let in_unit = checked_div(canonical, &unit.factor)
4403 .expect("BUG: de-canonicalization for quantity display must not fail");
4404 let formatted = match commit_rational_to_decimal(&in_unit) {
4405 Ok(decimal) => format_decimal_for_quantity_display(decimal, decimals),
4406 Err(_) => rational_to_display_str(&in_unit),
4407 };
4408 let abs_magnitude = match commit_rational_to_decimal(&in_unit) {
4409 Ok(decimal) => decimal.abs(),
4410 Err(_) => Decimal::MAX,
4411 };
4412 let decimal_places = match commit_rational_to_decimal(&in_unit) {
4413 Ok(decimal) => decimal_places_in_display_value(&decimal),
4414 Err(_) => u32::MAX,
4415 };
4416 let under_1000 = abs_magnitude < Decimal::from(1000);
4417 candidates.push(UnitDisplayCandidate {
4418 unit_name: unit.name.clone(),
4419 decimal_places,
4420 under_1000,
4421 abs_magnitude,
4422 formatted,
4423 });
4424 }
4425
4426 let pool: Vec<&UnitDisplayCandidate> = {
4427 let under: Vec<_> = candidates.iter().filter(|c| c.under_1000).collect();
4428 if under.is_empty() {
4429 candidates.iter().collect()
4430 } else {
4431 under
4432 }
4433 };
4434 let best = pool
4435 .iter()
4436 .min_by(|left, right| {
4437 left.decimal_places
4438 .cmp(&right.decimal_places)
4439 .then_with(|| left.abs_magnitude.cmp(&right.abs_magnitude))
4440 })
4441 .expect("BUG: quantity type must have at least one declared unit");
4442 return format!("{} {}", best.formatted, best.unit_name);
4443 }
4444 }
4445
4446 let unit_label = match signature {
4447 [] => String::new(),
4448 [(name, 1)] => name.clone(),
4449 _ => format_signature_operator_style(signature),
4450 };
4451 let formatted = match commit_rational_to_decimal(canonical) {
4452 Ok(decimal) => format_decimal_for_quantity_display(decimal, decimals),
4453 Err(_) => rational_to_display_str(canonical),
4454 };
4455 if unit_label.is_empty() {
4456 formatted
4457 } else {
4458 format!("{formatted} {unit_label}")
4459 }
4460}
4461
4462impl fmt::Display for LiteralValue {
4463 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4464 match &self.value {
4465 ValueKind::Quantity(n, signature) => {
4466 write!(
4467 f,
4468 "{}",
4469 format_quantity_canonical_for_display(n, &self.lemma_type, signature)
4470 )
4471 }
4472 ValueKind::Ratio(_, Some(_unit_name)) => write!(f, "{}", self.value),
4473 ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
4474 _ => write!(f, "{}", self.value),
4475 }
4476 }
4477}
4478
4479#[cfg(test)]
4484mod tests {
4485 use super::*;
4486 use crate::computation::rational::{decimal_to_rational, RationalInteger};
4487 use crate::literals::Value;
4488 use crate::parsing::ast::{BooleanValue, DateTimeValue, LemmaSpec, PrimitiveKind, TimeValue};
4489 use rust_decimal::Decimal;
4490 use std::str::FromStr;
4491 use std::sync::Arc;
4492
4493 #[test]
4494 fn default_primitive_help_is_goal_oriented() {
4495 let kinds = [
4496 PrimitiveKind::Boolean,
4497 PrimitiveKind::Quantity,
4498 PrimitiveKind::QuantityRange,
4499 PrimitiveKind::Number,
4500 PrimitiveKind::NumberRange,
4501 PrimitiveKind::Percent,
4502 PrimitiveKind::Ratio,
4503 PrimitiveKind::RatioRange,
4504 PrimitiveKind::Text,
4505 PrimitiveKind::Date,
4506 PrimitiveKind::DateRange,
4507 PrimitiveKind::Time,
4508 PrimitiveKind::TimeRange,
4509 ];
4510 for kind in kinds {
4511 let spec = type_spec_for_primitive(kind);
4512 let help = match &spec {
4513 TypeSpecification::Boolean { help, .. }
4514 | TypeSpecification::Number { help, .. }
4515 | TypeSpecification::NumberRange { help }
4516 | TypeSpecification::Text { help, .. }
4517 | TypeSpecification::Quantity { help, .. }
4518 | TypeSpecification::QuantityRange { help, .. }
4519 | TypeSpecification::Ratio { help, .. }
4520 | TypeSpecification::RatioRange { help, .. }
4521 | TypeSpecification::Date { help, .. }
4522 | TypeSpecification::DateRange { help }
4523 | TypeSpecification::TimeRange { help }
4524 | TypeSpecification::Time { help, .. } => help,
4525 TypeSpecification::Veto { .. } | TypeSpecification::Undetermined => {
4526 unreachable!(
4527 "BUG: primitive kind {:?} mapped to non-primitive spec",
4528 kind
4529 )
4530 }
4531 };
4532 assert!(!help.is_empty(), "help for {:?}", kind);
4533 assert!(
4534 !help.to_ascii_lowercase().contains("format:"),
4535 "help for {:?} must not describe syntax: {:?}",
4536 kind,
4537 help
4538 );
4539 assert_eq!(help, default_help_for_primitive(kind));
4540 }
4541 }
4542
4543 #[test]
4544 fn test_negated_comparison() {
4545 assert_eq!(
4546 negated_comparison(ComparisonComputation::LessThan),
4547 ComparisonComputation::GreaterThanOrEqual
4548 );
4549 assert_eq!(
4550 negated_comparison(ComparisonComputation::GreaterThanOrEqual),
4551 ComparisonComputation::LessThan
4552 );
4553 assert_eq!(
4554 negated_comparison(ComparisonComputation::Is),
4555 ComparisonComputation::IsNot
4556 );
4557 assert_eq!(
4558 negated_comparison(ComparisonComputation::IsNot),
4559 ComparisonComputation::Is
4560 );
4561 }
4562
4563 #[test]
4564 fn value_to_semantic_number_is_decimal() {
4565 let kind = value_to_semantic(&Value::Number(Decimal::from(42))).unwrap();
4566 assert!(matches!(kind, ValueKind::Number(d) if d == RationalInteger::new(42, 1)));
4567 }
4568
4569 #[test]
4570 fn value_kind_quantity_serializes_with_signature() {
4571 let kind = ValueKind::Quantity(
4572 decimal_to_rational(Decimal::from_str("99.50").unwrap()).unwrap(),
4573 vec![("eur".to_string(), 1)],
4574 );
4575 let json = serde_json::to_value(&kind).unwrap();
4576 assert_eq!(json["quantity"]["value"], "99.5");
4577 assert_eq!(json["quantity"]["signature"][0][0], "eur");
4578 assert_eq!(json["quantity"]["signature"][0][1], 1);
4579 }
4580
4581 #[test]
4582 fn value_kind_quantity_compound_signature_roundtrips() {
4583 let original = ValueKind::Quantity(
4584 decimal_to_rational(Decimal::from_str("4800").unwrap()).unwrap(),
4585 vec![
4586 ("eur".to_string(), 1),
4587 ("hour".to_string(), 1),
4588 ("minute".to_string(), -1),
4589 ],
4590 );
4591 let json = serde_json::to_string(&original).unwrap();
4592 let parsed: ValueKind = serde_json::from_str(&json).unwrap();
4593 assert_eq!(original, parsed);
4594 }
4595
4596 #[test]
4597 fn value_kind_quantity_empty_signature_roundtrips() {
4598 let original = ValueKind::Quantity(
4599 decimal_to_rational(Decimal::from_str("12.5").unwrap()).unwrap(),
4600 Vec::new(),
4601 );
4602 let json = serde_json::to_string(&original).unwrap();
4603 let parsed: ValueKind = serde_json::from_str(&json).unwrap();
4604 assert_eq!(original, parsed);
4605 }
4606
4607 #[test]
4608 fn literal_value_number_serde_not_rational_array() {
4609 let lit = LiteralValue::number_from_decimal(Decimal::from(20));
4610 let json = serde_json::to_value(&lit).unwrap();
4611 let number = json
4612 .get("value")
4613 .and_then(|v| v.get("number"))
4614 .expect("number field");
4615 assert!(number.is_string());
4616 assert_eq!(number.as_str(), Some("20"));
4617 assert!(
4618 !number.is_array(),
4619 "stored number must not serialize as [n,d]"
4620 );
4621 }
4622
4623 #[test]
4624 fn test_literal_value_to_primitive_type() {
4625 let one = RationalInteger::new(1, 1);
4626
4627 assert_eq!(LiteralValue::text("".to_string()).lemma_type.name(), "text");
4628 assert_eq!(LiteralValue::number(one).lemma_type.name(), "number");
4629 assert_eq!(
4630 LiteralValue::from_bool(bool::from(BooleanValue::True))
4631 .lemma_type
4632 .name(),
4633 "boolean"
4634 );
4635
4636 let dt = DateTimeValue {
4637 year: 2024,
4638 month: 1,
4639 day: 1,
4640 hour: 0,
4641 minute: 0,
4642 second: 0,
4643 microsecond: 0,
4644 timezone: None,
4645 };
4646 assert_eq!(
4647 LiteralValue::date(date_time_to_semantic(&dt))
4648 .lemma_type
4649 .name(),
4650 "date"
4651 );
4652 assert_eq!(
4653 LiteralValue::ratio_from_decimal(Decimal::new(1, 2), Some("percent".to_string()))
4654 .lemma_type
4655 .name(),
4656 "ratio"
4657 );
4658 let dur_type = LemmaType::new(
4659 "duration".to_string(),
4660 TypeSpecification::Quantity {
4661 minimum: None,
4662 maximum: None,
4663 decimals: None,
4664 units: QuantityUnits::from(vec![QuantityUnit {
4665 name: "second".to_string(),
4666 factor: crate::computation::rational::rational_one(),
4667 derived_quantity_factors: Vec::new(),
4668 decomposition: BaseQuantityVector::new(),
4669 minimum: None,
4670 maximum: None,
4671 default_magnitude: None,
4672 }]),
4673 traits: vec![QuantityTrait::Duration],
4674 decomposition: None,
4675 help: String::new(),
4676 },
4677 TypeExtends::Primitive,
4678 );
4679 assert_eq!(
4680 LiteralValue::quantity_with_type(one, "second".to_string(), Arc::new(dur_type))
4681 .lemma_type
4682 .name(),
4683 "duration"
4684 );
4685 }
4686
4687 #[test]
4688 fn test_type_display() {
4689 let specs = TypeSpecification::text();
4690 let lemma_type = LemmaType::new("name".to_string(), specs, TypeExtends::Primitive);
4691 assert_eq!(format!("{}", lemma_type), "name");
4692 }
4693
4694 #[test]
4695 fn test_type_serialization() {
4696 let specs = TypeSpecification::number();
4697 let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
4698 let serialized = serde_json::to_string(&lemma_type).unwrap();
4699 let deserialized: LemmaType = serde_json::from_str(&serialized).unwrap();
4700 assert_eq!(lemma_type, deserialized);
4701 }
4702
4703 #[test]
4704 fn test_literal_value_display_value() {
4705 let ten = RationalInteger::new(10, 1);
4706
4707 assert_eq!(
4708 LiteralValue::text("hello".to_string()).display_value(),
4709 "hello"
4710 );
4711 assert_eq!(LiteralValue::number(ten).display_value(), "10");
4712 assert_eq!(LiteralValue::from_bool(true).display_value(), "true");
4713 assert_eq!(LiteralValue::from_bool(false).display_value(), "false");
4714
4715 let ten_percent_ratio =
4717 LiteralValue::ratio_from_decimal(Decimal::new(1, 1), Some("percent".to_string()));
4718 assert_eq!(ten_percent_ratio.display_value(), "10%");
4719
4720 let time = TimeValue {
4721 hour: 14,
4722 minute: 30,
4723 second: 0,
4724 microsecond: 0,
4725 timezone: None,
4726 };
4727 let time_display = LiteralValue::time(time_to_semantic(&time)).display_value();
4728 assert!(time_display.contains("14"));
4729 assert!(time_display.contains("30"));
4730 }
4731
4732 #[test]
4733 fn test_quantity_display_respects_type_decimals() {
4734 let money_type = LemmaType {
4735 name: Some("money".to_string()),
4736 specifications: TypeSpecification::Quantity {
4737 minimum: None,
4738 maximum: None,
4739 decimals: Some(2),
4740 units: QuantityUnits::from(vec![QuantityUnit {
4741 name: "eur".to_string(),
4742 factor: crate::computation::rational::rational_one(),
4743 derived_quantity_factors: Vec::new(),
4744 decomposition: BaseQuantityVector::new(),
4745 minimum: None,
4746 maximum: None,
4747 default_magnitude: None,
4748 }]),
4749 traits: Vec::new(),
4750 decomposition: None,
4751 help: String::new(),
4752 },
4753 extends: TypeExtends::Primitive,
4754 };
4755 let money_type = Arc::new(money_type);
4756 let val = LiteralValue::quantity_with_type(
4757 decimal_to_rational(Decimal::from_str("1.8").unwrap()).unwrap(),
4758 "eur".to_string(),
4759 money_type.clone(),
4760 );
4761 assert_eq!(val.display_value(), "1.80 eur");
4762 let more_precision = LiteralValue::quantity_with_type(
4763 decimal_to_rational(Decimal::from_str("1.80000").unwrap()).unwrap(),
4764 "eur".to_string(),
4765 money_type,
4766 );
4767 assert_eq!(more_precision.display_value(), "1.80 eur");
4768 let quantity_no_decimals = LemmaType {
4769 name: Some("count".to_string()),
4770 specifications: TypeSpecification::Quantity {
4771 minimum: None,
4772 maximum: None,
4773 decimals: None,
4774 units: QuantityUnits::from(vec![QuantityUnit {
4775 name: "items".to_string(),
4776 factor: crate::computation::rational::rational_one(),
4777 derived_quantity_factors: Vec::new(),
4778 decomposition: BaseQuantityVector::new(),
4779 minimum: None,
4780 maximum: None,
4781 default_magnitude: None,
4782 }]),
4783 traits: Vec::new(),
4784 decomposition: None,
4785 help: String::new(),
4786 },
4787 extends: TypeExtends::Primitive,
4788 };
4789 let val_any = LiteralValue::quantity_with_type(
4790 decimal_to_rational(Decimal::from_str("42.50").unwrap()).unwrap(),
4791 "items".to_string(),
4792 Arc::new(quantity_no_decimals),
4793 );
4794 assert_eq!(val_any.display_value(), "42.5 items");
4795 }
4796
4797 #[test]
4798 fn test_literal_value_time_type() {
4799 let time = TimeValue {
4800 hour: 14,
4801 minute: 30,
4802 second: 0,
4803 microsecond: 0,
4804 timezone: None,
4805 };
4806 let lit = LiteralValue::time(time_to_semantic(&time));
4807 assert_eq!(lit.lemma_type.name(), "time");
4808 }
4809
4810 #[test]
4811 fn test_quantity_family_name_primitive_root() {
4812 let quantity_spec = TypeSpecification::quantity();
4813 let money_primitive = LemmaType::new(
4814 "money".to_string(),
4815 quantity_spec.clone(),
4816 TypeExtends::Primitive,
4817 );
4818 assert_eq!(money_primitive.quantity_family_name(), Some("money"));
4819 }
4820
4821 #[test]
4822 fn test_quantity_family_name_custom() {
4823 let quantity_spec = TypeSpecification::quantity();
4824 let money_custom = LemmaType::new(
4825 "money".to_string(),
4826 quantity_spec,
4827 TypeExtends::custom_local("money".to_string(), "money".to_string()),
4828 );
4829 assert_eq!(money_custom.quantity_family_name(), Some("money"));
4830 }
4831
4832 #[test]
4833 fn test_same_quantity_family_same_name_different_extends() {
4834 let quantity_spec = TypeSpecification::quantity();
4835 let money_primitive = LemmaType::new(
4836 "money".to_string(),
4837 quantity_spec.clone(),
4838 TypeExtends::Primitive,
4839 );
4840 let money_custom = LemmaType::new(
4841 "money".to_string(),
4842 quantity_spec,
4843 TypeExtends::custom_local("money".to_string(), "money".to_string()),
4844 );
4845 assert!(money_primitive.same_quantity_family(&money_custom));
4846 assert!(money_custom.same_quantity_family(&money_primitive));
4847 }
4848
4849 #[test]
4850 fn test_same_quantity_family_parent_and_child() {
4851 let quantity_spec = TypeSpecification::quantity();
4852 let type_x = LemmaType::new(
4853 "x".to_string(),
4854 quantity_spec.clone(),
4855 TypeExtends::Primitive,
4856 );
4857 let type_x2 = LemmaType::new(
4858 "x2".to_string(),
4859 quantity_spec,
4860 TypeExtends::custom_local("x".to_string(), "x".to_string()),
4861 );
4862 assert_eq!(type_x.quantity_family_name(), Some("x"));
4863 assert_eq!(type_x2.quantity_family_name(), Some("x"));
4864 assert!(type_x.same_quantity_family(&type_x2));
4865 assert!(type_x2.same_quantity_family(&type_x));
4866 }
4867
4868 #[test]
4869 fn test_same_quantity_family_siblings() {
4870 let quantity_spec = TypeSpecification::quantity();
4871 let type_x2_a = LemmaType::new(
4872 "x2a".to_string(),
4873 quantity_spec.clone(),
4874 TypeExtends::custom_local("x".to_string(), "x".to_string()),
4875 );
4876 let type_x2_b = LemmaType::new(
4877 "x2b".to_string(),
4878 quantity_spec,
4879 TypeExtends::custom_local("x".to_string(), "x".to_string()),
4880 );
4881 assert!(type_x2_a.same_quantity_family(&type_x2_b));
4882 }
4883
4884 #[test]
4885 fn test_same_quantity_family_different_families() {
4886 let quantity_spec = TypeSpecification::quantity();
4887 let money = LemmaType::new(
4888 "money".to_string(),
4889 quantity_spec.clone(),
4890 TypeExtends::Primitive,
4891 );
4892 let temperature = LemmaType::new(
4893 "temperature".to_string(),
4894 quantity_spec,
4895 TypeExtends::Primitive,
4896 );
4897 assert!(!money.same_quantity_family(&temperature));
4898 assert!(!temperature.same_quantity_family(&money));
4899 }
4900
4901 #[test]
4902 fn test_same_quantity_family_quantity_vs_non_quantity() {
4903 let quantity_spec = TypeSpecification::quantity();
4904 let number_spec = TypeSpecification::number();
4905 let quantity_type =
4906 LemmaType::new("money".to_string(), quantity_spec, TypeExtends::Primitive);
4907 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
4908 assert!(!quantity_type.same_quantity_family(&number_type));
4909 assert!(!number_type.same_quantity_family(&quantity_type));
4910 }
4911
4912 #[test]
4913 fn test_same_quantity_family_anonymous_quantitys_are_not_family_compatible() {
4914 let left = LemmaType::anonymous_for_decomposition(duration_decomposition());
4915 let right = LemmaType::anonymous_for_decomposition(duration_decomposition());
4916
4917 assert!(!left.same_quantity_family(&right));
4918 assert!(left.compatible_with_anonymous_quantity(&right));
4919 }
4920
4921 #[test]
4922 fn test_quantity_family_name_non_quantity_returns_none() {
4923 let number_spec = TypeSpecification::number();
4924 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
4925 assert_eq!(number_type.quantity_family_name(), None);
4926 }
4927
4928 #[test]
4929 fn test_lemma_type_inequality_local_vs_import_same_shape() {
4930 let dep = Arc::new(LemmaSpec::new("dep".to_string()));
4931 let quantity_spec = TypeSpecification::quantity();
4932 let local = LemmaType::new(
4933 "t".to_string(),
4934 quantity_spec.clone(),
4935 TypeExtends::custom_local("money".to_string(), "money".to_string()),
4936 );
4937 let imported = LemmaType::new(
4938 "t".to_string(),
4939 quantity_spec,
4940 TypeExtends::Custom {
4941 parent: "money".to_string(),
4942 family: "money".to_string(),
4943 defining_spec: TypeDefiningSpec::Import {
4944 spec: Arc::clone(&dep),
4945 },
4946 },
4947 );
4948 assert_ne!(local, imported);
4949 }
4950
4951 #[test]
4952 fn test_lemma_type_equality_import_same_arc_pointer_identity() {
4953 let shared_spec = Arc::new(LemmaSpec::new("dep".to_string()));
4957 let quantity_spec = TypeSpecification::quantity();
4958 let left = LemmaType::new(
4959 "t".to_string(),
4960 quantity_spec.clone(),
4961 TypeExtends::Custom {
4962 parent: "money".to_string(),
4963 family: "money".to_string(),
4964 defining_spec: TypeDefiningSpec::Import {
4965 spec: Arc::clone(&shared_spec),
4966 },
4967 },
4968 );
4969 let right = LemmaType::new(
4970 "t".to_string(),
4971 quantity_spec,
4972 TypeExtends::Custom {
4973 parent: "money".to_string(),
4974 family: "money".to_string(),
4975 defining_spec: TypeDefiningSpec::Import {
4976 spec: Arc::clone(&shared_spec),
4977 },
4978 },
4979 );
4980 assert_eq!(left, right);
4981 }
4982
4983 #[test]
4984 fn test_lemma_type_inequality_import_different_arc_pointer() {
4985 let spec_a = Arc::new(LemmaSpec::new("dep".to_string()));
4987 let spec_b = Arc::new(LemmaSpec::new("dep".to_string()));
4988 let quantity_spec = TypeSpecification::quantity();
4989 let left = LemmaType::new(
4990 "t".to_string(),
4991 quantity_spec.clone(),
4992 TypeExtends::Custom {
4993 parent: "money".to_string(),
4994 family: "money".to_string(),
4995 defining_spec: TypeDefiningSpec::Import {
4996 spec: Arc::clone(&spec_a),
4997 },
4998 },
4999 );
5000 let right = LemmaType::new(
5001 "t".to_string(),
5002 quantity_spec,
5003 TypeExtends::Custom {
5004 parent: "money".to_string(),
5005 family: "money".to_string(),
5006 defining_spec: TypeDefiningSpec::Import { spec: spec_b },
5007 },
5008 );
5009 assert_ne!(left, right);
5010 }
5011
5012 fn month_default_arg() -> CommandArg {
5013 CommandArg::Literal(crate::literals::Value::NumberWithUnit(
5014 Decimal::ONE,
5015 "month".to_string(),
5016 ))
5017 }
5018
5019 fn unit_factor_arg(name: &str, factor: i64) -> [CommandArg; 2] {
5020 [
5021 CommandArg::Label(name.to_string()),
5022 CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::from(factor))),
5023 ]
5024 }
5025
5026 #[test]
5027 fn default_calendar_on_text_reports_hint() {
5028 let specs = TypeSpecification::text();
5029 let mut default = None;
5030 let err = specs
5031 .apply_constraint(
5032 "notes",
5033 TypeConstraintCommand::Default,
5034 &[month_default_arg()],
5035 &mut default,
5036 )
5037 .unwrap_err();
5038 assert!(err.contains("Unit 'month' is for calendar data"));
5039 assert!(err.contains("double quotes"));
5040 }
5041
5042 #[test]
5043 fn default_calendar_on_duration_reports_valid_units() {
5044 let mut specs = TypeSpecification::quantity();
5045 specs = specs
5046 .apply_constraint(
5047 "duration",
5048 TypeConstraintCommand::Unit,
5049 &unit_factor_arg("second", 1),
5050 &mut None,
5051 )
5052 .unwrap();
5053 specs = specs
5054 .apply_constraint(
5055 "duration",
5056 TypeConstraintCommand::Unit,
5057 &unit_factor_arg("week", 604_800),
5058 &mut None,
5059 )
5060 .unwrap();
5061 specs = specs
5062 .apply_constraint(
5063 "duration",
5064 TypeConstraintCommand::Trait,
5065 &[CommandArg::Label("duration".to_string())],
5066 &mut None,
5067 )
5068 .unwrap();
5069 let mut default = None;
5070 let err = specs
5071 .apply_constraint(
5072 "duration",
5073 TypeConstraintCommand::Default,
5074 &[month_default_arg()],
5075 &mut default,
5076 )
5077 .unwrap_err();
5078 assert!(err.contains("Unit 'month' is for calendar data"));
5079 assert!(err.contains("Valid 'duration' units are"));
5080 assert!(err.contains("week"));
5081 }
5082
5083 #[test]
5084 fn default_valid_duration_weeks_accepted() {
5085 let mut specs = TypeSpecification::quantity();
5086 specs = specs
5087 .apply_constraint(
5088 "duration",
5089 TypeConstraintCommand::Unit,
5090 &unit_factor_arg("second", 1),
5091 &mut None,
5092 )
5093 .unwrap();
5094 specs = specs
5095 .apply_constraint(
5096 "duration",
5097 TypeConstraintCommand::Unit,
5098 &unit_factor_arg("week", 604_800),
5099 &mut None,
5100 )
5101 .unwrap();
5102 specs = specs
5103 .apply_constraint(
5104 "duration",
5105 TypeConstraintCommand::Trait,
5106 &[CommandArg::Label("duration".to_string())],
5107 &mut None,
5108 )
5109 .unwrap();
5110 let mut default = None;
5111 specs
5112 .apply_constraint(
5113 "duration",
5114 TypeConstraintCommand::Default,
5115 &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
5116 Decimal::from(4),
5117 "week".to_string(),
5118 ))],
5119 &mut default,
5120 )
5121 .unwrap();
5122 assert!(matches!(
5123 default,
5124 Some(RawDefault::Quantity {
5125 unit_name,
5126 ..
5127 }) if unit_name == "week"
5128 ));
5129 }
5130
5131 #[test]
5132 fn default_unknown_unit_on_duration_lists_valid_units() {
5133 let mut specs = TypeSpecification::quantity();
5134 specs = specs
5135 .apply_constraint(
5136 "duration",
5137 TypeConstraintCommand::Unit,
5138 &unit_factor_arg("second", 1),
5139 &mut None,
5140 )
5141 .unwrap();
5142 specs = specs
5143 .apply_constraint(
5144 "duration",
5145 TypeConstraintCommand::Trait,
5146 &[CommandArg::Label("duration".to_string())],
5147 &mut None,
5148 )
5149 .unwrap();
5150 let mut default = None;
5151 let err = specs
5152 .apply_constraint(
5153 "duration",
5154 TypeConstraintCommand::Default,
5155 &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
5156 Decimal::ONE,
5157 "fortnight".to_string(),
5158 ))],
5159 &mut default,
5160 )
5161 .unwrap_err();
5162 assert!(err.contains("fortnight"));
5163 assert!(err.contains("not defined on 'duration'"));
5164 assert!(err.contains("Valid units are"));
5165 }
5166
5167 fn money_quantity_type() -> LemmaType {
5168 LemmaType::new(
5169 "Money".to_string(),
5170 TypeSpecification::Quantity {
5171 minimum: None,
5172 maximum: None,
5173 decimals: None,
5174 units: QuantityUnits::from(vec![
5175 QuantityUnit {
5176 name: "eur".to_string(),
5177 factor: crate::computation::rational::rational_one(),
5178 derived_quantity_factors: Vec::new(),
5179 decomposition: BaseQuantityVector::new(),
5180 minimum: None,
5181 maximum: None,
5182 default_magnitude: None,
5183 },
5184 QuantityUnit {
5185 name: "usd".to_string(),
5186 factor: crate::computation::rational::decimal_to_rational(Decimal::new(
5187 91, 2,
5188 ))
5189 .expect("factor"),
5190 derived_quantity_factors: Vec::new(),
5191 decomposition: BaseQuantityVector::new(),
5192 minimum: None,
5193 maximum: None,
5194 default_magnitude: None,
5195 },
5196 ]),
5197 traits: Vec::new(),
5198 decomposition: None,
5199 help: String::new(),
5200 },
5201 TypeExtends::Primitive,
5202 )
5203 }
5204
5205 #[test]
5206 fn quantity_unit_names_for_named_quantity() {
5207 let money = money_quantity_type();
5208 assert_eq!(money.quantity_unit_names(), Some(vec!["eur", "usd"]));
5209 }
5210
5211 fn sig(pairs: &[(&str, i32)]) -> Vec<(String, i32)> {
5216 pairs.iter().map(|(s, e)| (s.to_string(), *e)).collect()
5217 }
5218
5219 #[test]
5220 fn combine_signatures_multiply_adds_exponents() {
5221 let left = sig(&[("eur", 1)]);
5222 let right = sig(&[("hour", -1)]);
5223 let result = combine_signatures(&left, &right, true);
5224 assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
5225 }
5226
5227 #[test]
5228 fn combine_signatures_divide_subtracts_exponents() {
5229 let left = sig(&[("eur", 1)]);
5230 let right = sig(&[("hour", 1)]);
5231 let result = combine_signatures(&left, &right, false);
5232 assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
5233 }
5234
5235 #[test]
5236 fn combine_signatures_cancels_to_empty() {
5237 let left = sig(&[("ce", 1), ("minute", -1)]);
5238 let right = sig(&[("minute", 1)]);
5239 let result = combine_signatures(&left, &right, true);
5240 assert_eq!(result, sig(&[("ce", 1)]));
5242 }
5243
5244 #[test]
5245 fn combine_signatures_output_is_canonical_form() {
5246 let left = sig(&[("eur", 1), ("hour", 1)]);
5247 let right = sig(&[("minute", 1)]);
5248 let result = combine_signatures(&left, &right, false); let expected = sig(&[("eur", 1), ("hour", 1), ("minute", -1)]);
5251 assert_eq!(result, expected);
5252 }
5253
5254 #[test]
5255 fn canonicalize_signature_drops_zero_exponents() {
5256 let sig_with_zero = sig(&[("eur", 1), ("hour", 0), ("minute", -1)]);
5257 let result = canonicalize_signature(&sig_with_zero);
5258 assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
5259 }
5260
5261 #[test]
5262 fn canonicalize_signature_sorts_by_name() {
5263 let unsorted = sig(&[("minute", -1), ("eur", 1)]);
5264 let result = canonicalize_signature(&unsorted);
5265 assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
5266 }
5267
5268 #[test]
5274 fn format_signature_operator_style_numerator_only() {
5275 let signature = sig(&[("eur", 1)]);
5276 let result = format_signature_operator_style(&signature);
5277 assert_eq!(result, "eur");
5278 }
5279
5280 #[test]
5281 fn format_signature_operator_style_with_denominator() {
5282 let signature = sig(&[("eur", 1), ("hour", -1)]);
5283 let result = format_signature_operator_style(&signature);
5284 assert_eq!(result, "eur/hour");
5285 }
5286
5287 #[test]
5288 fn format_signature_operator_style_denominator_only() {
5289 let signature = sig(&[("meter", -1)]);
5290 let result = format_signature_operator_style(&signature);
5291 assert_eq!(result, "1/meter");
5292 }
5293
5294 #[test]
5295 fn format_signature_operator_style_with_exponents() {
5296 let signature = sig(&[("meter", 2), ("second", -2)]);
5297 let result = format_signature_operator_style(&signature);
5298 assert_eq!(result, "meter^2/second^2");
5299 }
5300
5301 #[test]
5306 fn calendar_unit_factor_table_completeness() {
5307 for unit in &[SemanticCalendarUnit::Month, SemanticCalendarUnit::Year] {
5310 let name = unit.to_string();
5311 assert!(
5312 calendar_unit_factor(&name).is_some(),
5313 "calendar_unit_factor('{}') must return Some",
5314 name
5315 );
5316 }
5317 }
5318
5319 #[test]
5320 fn semantic_calendar_unit_display_returns_singular() {
5321 assert_eq!(SemanticCalendarUnit::Month.to_string(), "month");
5324 assert_eq!(SemanticCalendarUnit::Year.to_string(), "year");
5325 }
5326
5327 #[test]
5332 fn signature_factor_with_calendar_units() {
5333 use crate::computation::rational::RationalInteger;
5334 use std::collections::HashMap;
5335 let calendar = test_calendar_type_for_signature_factor();
5336 let unit_index: HashMap<String, Arc<LemmaType>> = HashMap::new();
5337 let sig_month_per_year = sig(&[("month", 1), ("year", -1)]);
5340 let factor = signature_factor(&sig_month_per_year, &unit_index, Some(&calendar));
5341 let expected = RationalInteger::new(1, 12);
5342 assert_eq!(factor, expected, "month/year factor must be 1/12");
5343 }
5344
5345 fn test_calendar_type_for_signature_factor() -> LemmaType {
5346 use crate::computation::rational::{decimal_to_rational, rational_one};
5347 use crate::literals::{QuantityUnit, QuantityUnits};
5348 use rust_decimal::Decimal;
5349 LemmaType::new(
5350 "calendar".to_string(),
5351 TypeSpecification::Quantity {
5352 minimum: None,
5353 maximum: None,
5354 decimals: None,
5355 units: QuantityUnits::from(vec![
5356 QuantityUnit {
5357 name: "month".to_string(),
5358 factor: rational_one(),
5359 minimum: None,
5360 maximum: None,
5361 default_magnitude: None,
5362 decomposition: calendar_decomposition(),
5363 derived_quantity_factors: Vec::new(),
5364 },
5365 QuantityUnit {
5366 name: "year".to_string(),
5367 factor: decimal_to_rational(Decimal::from(12)).expect("year factor"),
5368 minimum: None,
5369 maximum: None,
5370 default_magnitude: None,
5371 decomposition: calendar_decomposition(),
5372 derived_quantity_factors: Vec::new(),
5373 },
5374 ]),
5375 traits: vec![QuantityTrait::Calendar],
5376 decomposition: Some(calendar_decomposition()),
5377 help: String::new(),
5378 },
5379 TypeExtends::Primitive,
5380 )
5381 }
5382
5383 #[test]
5384 #[should_panic(expected = "BUG: signature_factor called with unresolved unit name")]
5385 fn signature_factor_panics_on_unresolved_name() {
5386 use std::collections::HashMap;
5387 let unit_index: HashMap<String, Arc<LemmaType>> = HashMap::new();
5388 let bad_sig = sig(&[("nonexistent_unit_xyz", 1)]);
5389 signature_factor(&bad_sig, &unit_index, None);
5390 }
5391
5392 #[test]
5393 fn signature_factor_uses_owner_when_expression_index_empty() {
5394 use crate::computation::rational::RationalInteger;
5395 use std::collections::HashMap;
5396 let money = test_money_type_for_signature_factor();
5397 let expression_units: HashMap<String, Arc<LemmaType>> = HashMap::new();
5398 let sig_usd = sig(&[("usd", 1)]);
5399 let factor = signature_factor(&sig_usd, &expression_units, Some(&money));
5400 assert_eq!(factor, RationalInteger::new(91, 100));
5401 }
5402
5403 fn test_money_type_for_signature_factor() -> LemmaType {
5404 use crate::computation::rational::decimal_to_rational;
5405 use crate::literals::{QuantityUnit, QuantityUnits};
5406 use rust_decimal::Decimal;
5407 LemmaType::new(
5408 "money".to_string(),
5409 TypeSpecification::Quantity {
5410 minimum: None,
5411 maximum: None,
5412 decimals: Some(2),
5413 units: QuantityUnits::from(vec![
5414 QuantityUnit {
5415 name: "eur".to_string(),
5416 factor: crate::computation::rational::rational_one(),
5417 minimum: None,
5418 maximum: None,
5419 default_magnitude: None,
5420 decomposition: BaseQuantityVector::new(),
5421 derived_quantity_factors: Vec::new(),
5422 },
5423 QuantityUnit {
5424 name: "usd".to_string(),
5425 factor: decimal_to_rational(Decimal::new(91, 2)).expect("usd factor"),
5426 minimum: None,
5427 maximum: None,
5428 default_magnitude: None,
5429 decomposition: BaseQuantityVector::new(),
5430 derived_quantity_factors: Vec::new(),
5431 },
5432 ]),
5433 traits: Vec::new(),
5434 decomposition: None,
5435 help: String::new(),
5436 },
5437 TypeExtends::Primitive,
5438 )
5439 }
5440
5441 fn quantity_type_with_kilogram() -> TypeSpecification {
5442 use crate::computation::rational::rational_one;
5443 use crate::literals::{QuantityUnit, QuantityUnits};
5444 let mut units = QuantityUnits::new();
5445 units.push(QuantityUnit {
5446 name: "kilogram".to_string(),
5447 factor: rational_one(),
5448 minimum: None,
5449 maximum: None,
5450 default_magnitude: None,
5451 decomposition: BaseQuantityVector::new(),
5452 derived_quantity_factors: Vec::new(),
5453 });
5454 TypeSpecification::Quantity {
5455 minimum: None,
5456 maximum: None,
5457 decimals: None,
5458 units,
5459 traits: Vec::new(),
5460 decomposition: None,
5461 help: String::new(),
5462 }
5463 }
5464
5465 #[test]
5466 fn parser_value_to_value_kind_rejects_bare_number_for_quantity() {
5467 let ten = Value::Number(Decimal::from(10));
5468 let err = parser_value_to_value_kind(&ten, &quantity_type_with_kilogram())
5469 .expect_err("bare number must not bind to quantity");
5470 assert!(
5471 err.contains("kilogram"),
5472 "error must hint expected unit, got: {err}"
5473 );
5474 }
5475
5476 #[test]
5477 fn parser_value_to_value_kind_accepts_number_with_unit_for_quantity() {
5478 let ten_kg = Value::NumberWithUnit(Decimal::from(10), "kilogram".to_string());
5479 let kind = parser_value_to_value_kind(&ten_kg, &quantity_type_with_kilogram())
5480 .expect("10 kilogram must bind to quantity");
5481 assert!(matches!(kind, ValueKind::Quantity(_, _)));
5482 }
5483
5484 #[test]
5485 fn parser_value_to_value_kind_accepts_bare_number_for_ratio() {
5486 let ten = Value::Number(Decimal::from(10));
5487 let kind =
5488 parser_value_to_value_kind(&ten, &TypeSpecification::ratio()).expect("number -> ratio");
5489 assert!(matches!(kind, ValueKind::Ratio(_, None)));
5490 }
5491
5492 #[test]
5493 fn value_kind_matches_spec_rejects_number_for_quantity() {
5494 use crate::computation::rational::RationalInteger;
5495 let n = ValueKind::Number(RationalInteger::new(10, 1));
5496 assert!(!value_kind_matches_spec(&n, &quantity_type_with_kilogram()));
5497 }
5498}