1#[cfg(test)]
9pub use crate::parsing::ast::Span;
10pub use crate::parsing::ast::{
11 ArithmeticComputation, ComparisonComputation, LogicalComputation, MathematicalComputation,
12 NegationType, VetoExpression,
13};
14pub use crate::parsing::source::Source;
15
16#[must_use]
19pub fn negated_comparison(op: ComparisonComputation) -> ComparisonComputation {
20 match op {
21 ComparisonComputation::LessThan => ComparisonComputation::GreaterThanOrEqual,
22 ComparisonComputation::LessThanOrEqual => ComparisonComputation::GreaterThan,
23 ComparisonComputation::GreaterThan => ComparisonComputation::LessThanOrEqual,
24 ComparisonComputation::GreaterThanOrEqual => ComparisonComputation::LessThan,
25 ComparisonComputation::Is => ComparisonComputation::IsNot,
26 ComparisonComputation::IsNot => ComparisonComputation::Is,
27 }
28}
29
30use crate::computation::rational::{checked_div, checked_mul, rational_new, RationalInteger};
32use crate::parsing::ast::Constraint;
33use crate::parsing::ast::{
34 BooleanValue, CalendarPeriodUnit, CommandArg, ConversionTarget, DateCalendarKind,
35 DateRelativeKind, DateTimeValue, LemmaSpec, PrimitiveKind, TimeValue, TypeConstraintCommand,
36};
37use crate::Error;
38use rust_decimal::Decimal;
39use serde::{Deserialize, Deserializer, Serialize, Serializer};
40use std::collections::HashMap;
41use std::fmt;
42use std::hash::Hash;
43use std::str::FromStr;
44use std::sync::{Arc, OnceLock};
45
46pub use crate::literals::{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;
148 match name {
149 "month" | "months" => Some(rational_one()),
150 "year" | "years" => Some(rational_new(12, 1)),
151 _ => None,
152 }
153}
154
155fn owner_declares_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).clone()
180 } else if let Some(lemma_type) = expression_units.get(name) {
181 lemma_type.quantity_unit_factor(name).clone()
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 = commit_rational_to_decimal(magnitude)
273 .expect("BUG: planned quantity declared bound must commit to decimal");
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 use crate::computation::bigint::BigInt;
752 if d.denom() != &BigInt::one() {
753 return Err(format!(
754 "{} requires a whole number, got fractional value",
755 ctx
756 ));
757 }
758 d.numer()
759 .to_u8()
760 .ok_or_else(|| format!("{} value out of range for u8", ctx))
761}
762
763fn decimal_to_usize(d: RationalInteger, ctx: &str) -> Result<usize, String> {
765 use crate::computation::bigint::BigInt;
766 if d.denom() != &BigInt::one() {
767 return Err(format!(
768 "{} requires a whole number, got fractional value",
769 ctx
770 ));
771 }
772 d.numer()
773 .to_usize()
774 .ok_or_else(|| format!("{} value out of range for usize", ctx))
775}
776
777fn ratio_bound_to_canonical_rational(
783 args: &[CommandArg],
784 cmd: &str,
785 units: &RatioUnits,
786) -> Result<RationalInteger, String> {
787 use crate::computation::rational::{checked_div, decimal_to_rational};
788 let lit = require_literal(args, cmd)?;
789 match lit {
790 crate::literals::Value::NumberWithUnit(magnitude, unit_name) => {
791 let unit = units.get(unit_name.as_str())?;
792 let magnitude_rational = decimal_to_rational(*magnitude)
793 .map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
794 checked_div(&magnitude_rational, &unit.value)
795 .map_err(|failure| format!("{cmd}: unit conversion failed: {failure}"))
796 }
797 other => Err(format!(
798 "{cmd} requires a ratio literal with a unit, got {}",
799 value_kind_name(other)
800 )),
801 }
802}
803
804fn require_decimal_literal(args: &[CommandArg], cmd: &str) -> Result<RationalInteger, String> {
805 use crate::computation::rational::decimal_to_rational;
806 match require_literal(args, cmd)? {
807 crate::literals::Value::Number(d) => decimal_to_rational(*d)
808 .map_err(|failure| format!("{} literal failed rational lift: {}", cmd, failure)),
809 other => Err(format!(
810 "{} requires a number literal, got {}",
811 cmd,
812 value_kind_name(other)
813 )),
814 }
815}
816
817enum UnitConstraintField {
818 Minimum,
819 Maximum,
820 DefaultMagnitude,
821}
822
823fn quantity_declared_bound_to_canonical(
824 magnitude: &RationalInteger,
825 unit_name: &str,
826 units: &QuantityUnits,
827 type_name: &str,
828 command: &str,
829) -> Result<RationalInteger, String> {
830 use crate::computation::rational::checked_mul;
831 let unit = units.get(unit_name).map_err(|_| {
832 format!(
833 "Unit '{unit_name}' is not defined on '{type_name}'. Valid units are: {}.",
834 format_quantity_units_list(units)
835 )
836 })?;
837 checked_mul(magnitude, &unit.factor)
838 .map_err(|failure| format!("{command}: unit conversion overflow: {failure}"))
839}
840
841fn parse_quantity_declared_bound(
842 args: &[CommandArg],
843 cmd: &str,
844 units: &QuantityUnits,
845 type_name: &str,
846) -> Result<(RationalInteger, String), String> {
847 use crate::computation::rational::decimal_to_rational;
848 let lit = require_literal(args, cmd)?;
849 let (magnitude, unit_name) = match lit {
850 crate::literals::Value::NumberWithUnit(n, unit) => (*n, unit.clone()),
851 other => {
852 return Err(format!(
853 "{cmd} requires a quantity literal with a unit, got {}",
854 value_kind_name(other)
855 ));
856 }
857 };
858 units.get(unit_name.as_str()).map_err(|_| {
859 format!(
860 "Unit '{unit_name}' is not defined on '{type_name}'. Valid units are: {}.",
861 format_quantity_units_list(units)
862 )
863 })?;
864 let magnitude_rational = decimal_to_rational(magnitude)
865 .map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
866 Ok((magnitude_rational, unit_name))
867}
868
869fn sync_quantity_units_from_canonical(
870 units: &mut QuantityUnits,
871 canonical: &RationalInteger,
872 field: UnitConstraintField,
873) -> Result<(), String> {
874 use crate::computation::rational::checked_div;
875 for unit in &mut units.0 {
876 let magnitude = checked_div(canonical, &unit.factor).map_err(|failure| {
877 format!(
878 "cannot derive per-unit constraint for unit '{}': {failure}",
879 unit.name
880 )
881 })?;
882 match field {
883 UnitConstraintField::Minimum => unit.minimum = Some(magnitude),
884 UnitConstraintField::Maximum => unit.maximum = Some(magnitude),
885 UnitConstraintField::DefaultMagnitude => unit.default_magnitude = Some(magnitude),
886 }
887 }
888 Ok(())
889}
890
891fn sync_ratio_units_from_canonical(
892 units: &mut RatioUnits,
893 canonical: &RationalInteger,
894 field: UnitConstraintField,
895) -> Result<(), String> {
896 use crate::computation::rational::checked_mul;
897 for unit in &mut units.0 {
898 let magnitude = checked_mul(canonical, &unit.value).map_err(|failure| {
899 format!(
900 "cannot derive per-unit constraint for ratio unit '{}': {failure}",
901 unit.name
902 )
903 })?;
904 match field {
905 UnitConstraintField::Minimum => unit.minimum = Some(magnitude),
906 UnitConstraintField::Maximum => unit.maximum = Some(magnitude),
907 UnitConstraintField::DefaultMagnitude => unit.default_magnitude = Some(magnitude),
908 }
909 }
910 Ok(())
911}
912
913fn sync_quantity_default_units(
914 units: &mut QuantityUnits,
915 default: &ValueKind,
916 type_name: &str,
917) -> Result<(), String> {
918 let ValueKind::Quantity(magnitude, signature) = default else {
919 return Ok(());
920 };
921 let unit_name = signature.first().map(|(n, _)| n.as_str()).expect(
922 "BUG: Quantity default value has empty signature; literal lift must produce single-term",
923 );
924 units.get(unit_name).map_err(|_| {
925 format!("Default unit '{unit_name}' is not defined on quantity type '{type_name}'.")
926 })?;
927 sync_quantity_units_from_canonical(units, magnitude, UnitConstraintField::DefaultMagnitude)
928}
929
930pub(crate) fn finalize_quantity_unit_constraint_magnitudes(
931 specification: &mut TypeSpecification,
932 declared_default: Option<&ValueKind>,
933 type_name: &str,
934) -> Result<(), String> {
935 let TypeSpecification::Quantity {
936 minimum,
937 maximum,
938 units,
939 traits,
940 ..
941 } = specification
942 else {
943 return Ok(());
944 };
945
946 if let Some((magnitude, unit_name)) = minimum.clone() {
947 let canonical = quantity_declared_bound_to_canonical(
948 &magnitude, &unit_name, units, type_name, "minimum",
949 )?;
950 sync_quantity_units_from_canonical(units, &canonical, UnitConstraintField::Minimum)?;
951 }
952 if let Some((magnitude, unit_name)) = maximum.clone() {
953 let canonical = quantity_declared_bound_to_canonical(
954 &magnitude, &unit_name, units, type_name, "maximum",
955 )?;
956 sync_quantity_units_from_canonical(units, &canonical, UnitConstraintField::Maximum)?;
957 }
958 if let Some(default) = declared_default {
959 sync_quantity_default_units(units, default, type_name)?;
960 }
961
962 if minimum.is_some() {
963 for unit in units.iter() {
964 assert!(
965 unit.minimum.is_some(),
966 "BUG: type '{type_name}' has minimum but unit '{}' missing per-unit minimum after finalize",
967 unit.name
968 );
969 }
970 }
971 if maximum.is_some() {
972 for unit in units.iter() {
973 assert!(
974 unit.maximum.is_some(),
975 "BUG: type '{type_name}' has maximum but unit '{}' missing per-unit maximum after finalize",
976 unit.name
977 );
978 }
979 }
980 let calendar_range_default = traits.contains(&QuantityTrait::Calendar)
981 && matches!(declared_default, Some(ValueKind::Range(_, _)));
982 if declared_default.is_some() && !calendar_range_default {
983 for unit in units.iter() {
984 assert!(
985 unit.default_magnitude.is_some(),
986 "BUG: type '{type_name}' has default but unit '{}' missing per-unit default after finalize",
987 unit.name
988 );
989 }
990 }
991
992 Ok(())
993}
994
995pub(crate) fn quantity_declared_bound_canonical(
996 bound: &(RationalInteger, String),
997 units: &QuantityUnits,
998 type_name: &str,
999 command: &str,
1000) -> Result<RationalInteger, String> {
1001 let (magnitude, unit_name) = bound;
1002 quantity_declared_bound_to_canonical(magnitude, unit_name, units, type_name, command)
1003}
1004
1005fn sync_ratio_default_units(units: &mut RatioUnits, default: &ValueKind) -> Result<(), String> {
1006 let ValueKind::Ratio(canonical, _) = default else {
1007 return Ok(());
1008 };
1009 sync_ratio_units_from_canonical(units, canonical, UnitConstraintField::DefaultMagnitude)
1010}
1011
1012fn option_name(arg: &CommandArg, cmd: &str) -> Result<String, String> {
1018 match arg {
1019 CommandArg::Literal(crate::literals::Value::Text(s)) => Ok(s.clone()),
1020 CommandArg::Label(name) => Ok(name.clone()),
1021 CommandArg::Literal(other) => Err(format!(
1022 "{} requires a text literal or identifier, got {}",
1023 cmd,
1024 value_kind_name(other)
1025 )),
1026 CommandArg::UnitExpr(_) => Err(format!(
1027 "{} requires a text literal or identifier, got a unit expression",
1028 cmd
1029 )),
1030 }
1031}
1032
1033fn label_name(arg: &CommandArg, cmd: &str) -> Result<String, String> {
1034 match arg {
1035 CommandArg::Label(name) => Ok(name.clone()),
1036 CommandArg::Literal(other) => Err(format!(
1037 "{} requires an identifier, got {}",
1038 cmd,
1039 value_kind_name(other)
1040 )),
1041 CommandArg::UnitExpr(_) => Err(format!(
1042 "{} requires an identifier, got a unit expression",
1043 cmd
1044 )),
1045 }
1046}
1047
1048fn quantity_trait_name(quantity_trait: QuantityTrait) -> &'static str {
1049 match quantity_trait {
1050 QuantityTrait::Duration => "duration",
1051 QuantityTrait::Calendar => "calendar",
1052 }
1053}
1054
1055fn parse_quantity_trait(args: &[CommandArg]) -> Result<QuantityTrait, String> {
1056 if args.len() != 1 {
1057 return Err("trait requires exactly one identifier argument".to_string());
1058 }
1059 match label_name(&args[0], "trait")?
1060 .trim()
1061 .to_lowercase()
1062 .as_str()
1063 {
1064 "duration" => Ok(QuantityTrait::Duration),
1065 "calendar" => Ok(QuantityTrait::Calendar),
1066 other => Err(format!("Unknown quantity trait '{}'", other)),
1067 }
1068}
1069
1070fn validate_calendar_trait_requirements(units: &QuantityUnits) -> Result<(), String> {
1071 let month_unit = units
1072 .iter()
1073 .find(|unit| unit.name == "month")
1074 .ok_or_else(|| {
1075 "trait calendar requires a canonical 'month' unit declared before 'trait calendar'"
1076 .to_string()
1077 })?;
1078 if !month_unit.is_canonical_factor() {
1079 return Err("trait calendar requires unit month 1".to_string());
1080 }
1081 Ok(())
1082}
1083
1084fn validate_duration_trait_requirements(units: &QuantityUnits) -> Result<(), String> {
1085 let second_unit = units
1086 .iter()
1087 .find(|unit| unit.name == "second")
1088 .ok_or_else(|| {
1089 "trait duration requires a canonical 'second' unit declared before 'trait duration'"
1090 .to_string()
1091 })?;
1092 if !second_unit.is_canonical_factor() {
1093 return Err("trait duration requires unit second 1".to_string());
1094 }
1095 Ok(())
1096}
1097
1098fn require_date_literal(args: &[CommandArg], cmd: &str) -> Result<DateTimeValue, String> {
1100 match require_literal(args, cmd)? {
1101 crate::literals::Value::Date(dt) => Ok(dt.clone()),
1102 other => Err(format!(
1103 "{} requires a date literal (e.g. 2024-01-01), got {}",
1104 cmd,
1105 value_kind_name(other)
1106 )),
1107 }
1108}
1109
1110fn require_time_literal(args: &[CommandArg], cmd: &str) -> Result<TimeValue, String> {
1112 match require_literal(args, cmd)? {
1113 crate::literals::Value::Time(t) => Ok(t.clone()),
1114 other => Err(format!(
1115 "{} requires a time literal (e.g. 12:30:00), got {}",
1116 cmd,
1117 value_kind_name(other)
1118 )),
1119 }
1120}
1121
1122#[must_use]
1124pub fn default_help_for_primitive(kind: PrimitiveKind) -> &'static str {
1125 use PrimitiveKind::*;
1126 match kind {
1127 Boolean => "Whether this holds (true or false).",
1128 Number => "A dimensionless number.",
1129 NumberRange => "The lower and upper bound of the number range.",
1130 Text => "A text value.",
1131 Quantity => "A numeric amount in one of this type's units.",
1132 QuantityRange => "The lower and upper bound of the quantity range in the same unit.",
1133 Ratio | Percent => "A ratio in one of this type's units (e.g. percent).",
1134 RatioRange => "The lower and upper bound of the ratio range.",
1135 Date => "A date, or a date and time with optional timezone.",
1136 DateRange => "The start date and end date of the date range.",
1137 Time => "A time of day, with optional timezone.",
1138 TimeRange => "The start time and end time of the time range.",
1139 }
1140}
1141
1142impl TypeSpecification {
1143 pub fn boolean() -> Self {
1144 TypeSpecification::Boolean {
1145 help: default_help_for_primitive(PrimitiveKind::Boolean).to_string(),
1146 }
1147 }
1148 pub fn quantity() -> Self {
1149 TypeSpecification::Quantity {
1150 minimum: None,
1151 maximum: None,
1152 decimals: None,
1153 units: QuantityUnits::new(),
1154 traits: Vec::new(),
1155 decomposition: None,
1156 help: default_help_for_primitive(PrimitiveKind::Quantity).to_string(),
1157 }
1158 }
1159 pub fn number() -> Self {
1160 TypeSpecification::Number {
1161 minimum: None,
1162 maximum: None,
1163 decimals: None,
1164 help: default_help_for_primitive(PrimitiveKind::Number).to_string(),
1165 }
1166 }
1167 pub fn number_range() -> Self {
1168 TypeSpecification::NumberRange {
1169 help: default_help_for_primitive(PrimitiveKind::NumberRange).to_string(),
1170 }
1171 }
1172 pub fn ratio() -> Self {
1173 TypeSpecification::Ratio {
1174 minimum: None,
1175 maximum: None,
1176 decimals: None,
1177 units: RatioUnits(vec![
1178 RatioUnit {
1179 name: "percent".to_string(),
1180 value: crate::computation::rational::rational_new(100, 1),
1181 minimum: None,
1182 maximum: None,
1183 default_magnitude: None,
1184 },
1185 RatioUnit {
1186 name: "permille".to_string(),
1187 value: crate::computation::rational::rational_new(1000, 1),
1188 minimum: None,
1189 maximum: None,
1190 default_magnitude: None,
1191 },
1192 ]),
1193 help: default_help_for_primitive(PrimitiveKind::Ratio).to_string(),
1194 }
1195 }
1196 pub fn ratio_range() -> Self {
1197 TypeSpecification::RatioRange {
1198 units: match TypeSpecification::ratio() {
1199 TypeSpecification::Ratio { units, .. } => units,
1200 _ => unreachable!("BUG: ratio constructor must return a ratio type"),
1201 },
1202 help: default_help_for_primitive(PrimitiveKind::RatioRange).to_string(),
1203 }
1204 }
1205 pub fn text() -> Self {
1206 TypeSpecification::Text {
1207 length: None,
1208 options: vec![],
1209 help: default_help_for_primitive(PrimitiveKind::Text).to_string(),
1210 }
1211 }
1212 pub fn date() -> Self {
1213 TypeSpecification::Date {
1214 minimum: None,
1215 maximum: None,
1216 help: default_help_for_primitive(PrimitiveKind::Date).to_string(),
1217 }
1218 }
1219 pub fn date_range() -> Self {
1220 TypeSpecification::DateRange {
1221 help: default_help_for_primitive(PrimitiveKind::DateRange).to_string(),
1222 }
1223 }
1224 pub fn time() -> Self {
1225 TypeSpecification::Time {
1226 minimum: None,
1227 maximum: None,
1228 help: default_help_for_primitive(PrimitiveKind::Time).to_string(),
1229 }
1230 }
1231 pub fn time_range() -> Self {
1232 TypeSpecification::TimeRange {
1233 help: default_help_for_primitive(PrimitiveKind::TimeRange).to_string(),
1234 }
1235 }
1236 pub fn quantity_range() -> Self {
1237 TypeSpecification::QuantityRange {
1238 units: QuantityUnits::new(),
1239 decomposition: None,
1240 help: default_help_for_primitive(PrimitiveKind::QuantityRange).to_string(),
1241 }
1242 }
1243
1244 #[must_use]
1246 pub fn element_from_range(&self) -> Option<Self> {
1247 match self {
1248 TypeSpecification::NumberRange { .. } => Some(TypeSpecification::number()),
1249 TypeSpecification::QuantityRange {
1250 units,
1251 decomposition,
1252 ..
1253 } => Some(TypeSpecification::Quantity {
1254 minimum: None,
1255 maximum: None,
1256 decimals: None,
1257 units: units.clone(),
1258 traits: Vec::new(),
1259 decomposition: decomposition.clone(),
1260 help: String::new(),
1261 }),
1262 TypeSpecification::DateRange { .. } => Some(TypeSpecification::date()),
1263 TypeSpecification::TimeRange { .. } => Some(TypeSpecification::time()),
1264 TypeSpecification::RatioRange { units, .. } => Some(TypeSpecification::Ratio {
1265 minimum: None,
1266 maximum: None,
1267 decimals: None,
1268 units: units.clone(),
1269 help: String::new(),
1270 }),
1271 _ => None,
1272 }
1273 }
1274
1275 #[must_use]
1277 pub fn range_from_element(&self) -> Option<Self> {
1278 match self {
1279 TypeSpecification::Number { .. } => Some(TypeSpecification::number_range()),
1280 TypeSpecification::Quantity {
1281 units,
1282 decomposition,
1283 ..
1284 } => Some(TypeSpecification::QuantityRange {
1285 units: units.clone(),
1286 decomposition: decomposition.clone(),
1287 help: default_help_for_primitive(PrimitiveKind::QuantityRange).to_string(),
1288 }),
1289 TypeSpecification::Date { .. } => Some(TypeSpecification::date_range()),
1290 TypeSpecification::Time { .. } => Some(TypeSpecification::time_range()),
1291 TypeSpecification::Ratio { units, .. } => Some(TypeSpecification::RatioRange {
1292 units: units.clone(),
1293 help: default_help_for_primitive(PrimitiveKind::RatioRange).to_string(),
1294 }),
1295 _ => None,
1296 }
1297 }
1298
1299 #[must_use]
1301 pub fn minimum_decimal(&self) -> Option<Decimal> {
1302 use crate::computation::rational::commit_rational_to_decimal;
1303 match self {
1304 TypeSpecification::Number { minimum, .. }
1305 | TypeSpecification::Ratio { minimum, .. } => minimum.as_ref().map(|bound| {
1306 commit_rational_to_decimal(bound)
1307 .expect("BUG: planned minimum must commit to decimal")
1308 }),
1309 TypeSpecification::Quantity { minimum, .. } => {
1310 minimum.as_ref().map(|(bound, _unit)| {
1311 commit_rational_to_decimal(bound)
1312 .expect("BUG: planned minimum must commit to decimal")
1313 })
1314 }
1315 _ => None,
1316 }
1317 }
1318
1319 #[must_use]
1321 pub fn maximum_decimal(&self) -> Option<Decimal> {
1322 use crate::computation::rational::commit_rational_to_decimal;
1323 match self {
1324 TypeSpecification::Number { maximum, .. }
1325 | TypeSpecification::Ratio { maximum, .. } => maximum.as_ref().map(|bound| {
1326 commit_rational_to_decimal(bound)
1327 .expect("BUG: planned maximum must commit to decimal")
1328 }),
1329 TypeSpecification::Quantity { maximum, .. } => {
1330 maximum.as_ref().map(|(bound, _unit)| {
1331 commit_rational_to_decimal(bound)
1332 .expect("BUG: planned maximum must commit to decimal")
1333 })
1334 }
1335 _ => None,
1336 }
1337 }
1338
1339 pub fn veto() -> Self {
1340 TypeSpecification::Veto { message: None }
1341 }
1342
1343 pub fn apply_constraint(
1352 mut self,
1353 type_name: &str,
1354 command: TypeConstraintCommand,
1355 args: &[CommandArg],
1356 declared_default: &mut Option<RawDefault>,
1357 ) -> Result<Self, String> {
1358 if command == TypeConstraintCommand::Trait
1359 && !matches!(&self, TypeSpecification::Quantity { .. })
1360 {
1361 return Err("trait command is only valid on quantity types".to_string());
1362 }
1363 match &mut self {
1364 TypeSpecification::Boolean { help } => match command {
1365 TypeConstraintCommand::Help => {
1366 apply_type_help_command(help, args)?;
1367 }
1368 TypeConstraintCommand::Default => {
1369 let lit = require_literal(args, "default")?;
1370 reject_calendar_for_default(lit, type_name, DefaultExpectation::Boolean, None)?;
1371 match lit {
1372 crate::literals::Value::Boolean(bv) => {
1373 *declared_default =
1374 Some(RawDefault::Value(ValueKind::Boolean(bool::from(bv))));
1375 }
1376 _ => {
1377 return Err(
1378 "Please provide true or false, for example `-> default true`."
1379 .to_string(),
1380 );
1381 }
1382 }
1383 }
1384 other => {
1385 return Err(format!(
1386 "Invalid command '{}' for boolean type. Valid commands: help, default",
1387 other
1388 ));
1389 }
1390 },
1391 TypeSpecification::Quantity {
1392 decimals,
1393 minimum,
1394 maximum,
1395 units,
1396 traits,
1397 decomposition,
1398 help,
1399 ..
1400 } => match command {
1401 TypeConstraintCommand::Decimals => {
1402 let d = require_decimal_literal(args, "decimals")?;
1403 *decimals = Some(decimal_to_u8(d, "decimals")?);
1404 }
1405 TypeConstraintCommand::Unit => {
1406 let (unit_name, value, derived_quantity_factors) = match args {
1407 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
1408 (name.clone(), *v, Vec::new())
1409 }
1410 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Expr(
1411 prefix,
1412 factors,
1413 ))] => {
1414 let raw: Vec<(String, i32)> = factors
1415 .iter()
1416 .map(|f| (f.quantity_ref.clone(), f.exp))
1417 .collect();
1418 (name.clone(), *prefix, raw)
1419 }
1420 _ => {
1421 return Err(
1422 "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')"
1423 .to_string(),
1424 );
1425 }
1426 };
1427 if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
1428 let new_factor = crate::computation::rational::decimal_to_rational(value)
1429 .map_err(|failure| failure.to_string())?;
1430 if existing.factor != new_factor
1431 || existing.derived_quantity_factors != derived_quantity_factors
1432 {
1433 return Err(format!(
1434 "Unit '{unit_name}' is already defined in this type's inherited units; \
1435 cannot change factor or decomposition. Add a new unit name instead."
1436 ));
1437 }
1438 } else {
1439 units.0.push(QuantityUnit::from_decimal_factor(
1440 unit_name,
1441 value,
1442 derived_quantity_factors,
1443 )?);
1444 }
1445 }
1446 TypeConstraintCommand::Trait => {
1447 let quantity_trait = parse_quantity_trait(args)?;
1448 if traits.contains(&quantity_trait) {
1449 return Err(format!(
1450 "Duplicate trait '{}' for quantity type.",
1451 quantity_trait_name(quantity_trait)
1452 ));
1453 }
1454 if quantity_trait == QuantityTrait::Duration {
1455 validate_duration_trait_requirements(units)?;
1456 }
1457 if quantity_trait == QuantityTrait::Calendar {
1458 validate_calendar_trait_requirements(units)?;
1459 }
1460 traits.push(quantity_trait);
1461 }
1462 TypeConstraintCommand::Minimum => {
1463 *minimum = Some(parse_quantity_declared_bound(
1464 args, "minimum", units, type_name,
1465 )?);
1466 }
1467 TypeConstraintCommand::Maximum => {
1468 *maximum = Some(parse_quantity_declared_bound(
1469 args, "maximum", units, type_name,
1470 )?);
1471 }
1472 TypeConstraintCommand::Help => {
1473 apply_type_help_command(help, args)?;
1474 }
1475 TypeConstraintCommand::Default => {
1476 let lit = require_literal(args, "default")?;
1477 if traits.contains(&QuantityTrait::Calendar) {
1478 match lit {
1479 crate::literals::Value::Range(left, right) => {
1480 validate_calendar_range_default_endpoint(left, type_name, units)?;
1481 validate_calendar_range_default_endpoint(right, type_name, units)?;
1482 let element_spec = TypeSpecification::Quantity {
1483 minimum: minimum.clone(),
1484 maximum: maximum.clone(),
1485 decimals: *decimals,
1486 units: units.clone(),
1487 traits: traits.clone(),
1488 decomposition: decomposition.clone(),
1489 help: String::new(),
1490 };
1491 let left = lift_range_endpoint(left, &element_spec)?;
1492 let right = lift_range_endpoint(right, &element_spec)?;
1493 *declared_default = Some(RawDefault::Value(ValueKind::Range(
1494 Box::new(left),
1495 Box::new(right),
1496 )));
1497 }
1498 crate::literals::Value::NumberWithUnit(_, _) => {
1499 let (magnitude, unit_name) = parse_quantity_declared_bound(
1500 args, "default", units, type_name,
1501 )?;
1502 *declared_default = Some(RawDefault::Quantity {
1503 magnitude,
1504 unit_name,
1505 });
1506 }
1507 _ => {
1508 return Err(quantity_default_wrong_shape_error(type_name, traits));
1509 }
1510 }
1511 } else {
1512 reject_calendar_for_default(
1513 lit,
1514 type_name,
1515 DefaultExpectation::QuantityUnits,
1516 Some(units),
1517 )?;
1518 let (magnitude, unit_name) =
1519 parse_quantity_declared_bound(args, "default", units, type_name)?;
1520 *declared_default = Some(RawDefault::Quantity {
1521 magnitude,
1522 unit_name,
1523 });
1524 }
1525 }
1526 _ => {
1527 return Err(format!(
1528 "Invalid command '{}' for quantity type. Valid commands: unit, trait, minimum, maximum, decimals, help, default",
1529 command
1530 ));
1531 }
1532 },
1533 TypeSpecification::Number {
1534 decimals,
1535 minimum,
1536 maximum,
1537 help,
1538 } => match command {
1539 TypeConstraintCommand::Decimals => {
1540 let d = require_decimal_literal(args, "decimals")?;
1541 *decimals = Some(decimal_to_u8(d, "decimals")?);
1542 }
1543 TypeConstraintCommand::Unit => {
1544 return Err(
1545 "Invalid command 'unit' for number type. Number types are dimensionless and cannot have units. Use 'quantity' type instead.".to_string()
1546 );
1547 }
1548 TypeConstraintCommand::Minimum => {
1549 *minimum = Some(require_decimal_literal(args, "minimum")?);
1550 }
1551 TypeConstraintCommand::Maximum => {
1552 *maximum = Some(require_decimal_literal(args, "maximum")?);
1553 }
1554 TypeConstraintCommand::Help => {
1555 apply_type_help_command(help, args)?;
1556 }
1557 TypeConstraintCommand::Default => {
1558 let lit = require_literal(args, "default")?;
1559 reject_calendar_for_default(lit, type_name, DefaultExpectation::Number, None)?;
1560 match lit {
1561 crate::literals::Value::Number(d) => {
1562 *declared_default = Some(RawDefault::Value(ValueKind::Number(
1563 lift_parser_decimal(*d)?,
1564 )));
1565 }
1566 _ => {
1567 return Err(
1568 "Please provide a number, for example `-> default 42`.".to_string()
1569 );
1570 }
1571 }
1572 }
1573 _ => {
1574 return Err(format!(
1575 "Invalid command '{}' for number type. Valid commands: minimum, maximum, decimals, help, default",
1576 command
1577 ));
1578 }
1579 },
1580 TypeSpecification::NumberRange { help } => match command {
1581 TypeConstraintCommand::Help => {
1582 apply_type_help_command(help, args)?;
1583 }
1584 TypeConstraintCommand::Default => {
1585 let (left, right) = require_default_range_endpoints(
1586 args,
1587 type_name,
1588 DefaultExpectation::NumberRange,
1589 None,
1590 )?;
1591 let left = literal_value_from_parser_value(left)?;
1592 let right = literal_value_from_parser_value(right)?;
1593 if !left.lemma_type.is_number() || !right.lemma_type.is_number() {
1594 return Err(
1595 "Please provide a number range, for example `-> default 10...100`."
1596 .to_string(),
1597 );
1598 }
1599 *declared_default = Some(RawDefault::Value(ValueKind::Range(
1600 Box::new(left),
1601 Box::new(right),
1602 )));
1603 }
1604 _ => {
1605 return Err(format!(
1606 "Invalid command '{}' for number range type. Valid commands: help, default",
1607 command
1608 ));
1609 }
1610 },
1611 TypeSpecification::Ratio {
1612 decimals,
1613 minimum,
1614 maximum,
1615 units,
1616 help,
1617 } => match command {
1618 TypeConstraintCommand::Decimals => {
1619 let d = require_decimal_literal(args, "decimals")?;
1620 *decimals = Some(decimal_to_u8(d, "decimals")?);
1621 }
1622 TypeConstraintCommand::Unit => {
1623 let (unit_name, value_dec) = match args {
1624 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
1625 (name.clone(), *v)
1626 }
1627 _ => {
1628 return Err(
1629 "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."
1630 .to_string(),
1631 );
1632 }
1633 };
1634 let value = crate::computation::rational::decimal_to_rational(value_dec)
1635 .map_err(|failure| {
1636 format!(
1637 "ratio unit value is not exactly representable as a rational: {}",
1638 failure
1639 )
1640 })?;
1641 if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
1642 if existing.value != value {
1643 return Err(format!(
1644 "Unit '{unit_name}' is already defined in this type's inherited units; \
1645 cannot change factor. Add a new unit name instead."
1646 ));
1647 }
1648 } else {
1649 units.0.push(RatioUnit {
1650 name: unit_name,
1651 value,
1652 minimum: None,
1653 maximum: None,
1654 default_magnitude: None,
1655 });
1656 }
1657 }
1658 TypeConstraintCommand::Minimum => {
1659 let canonical = ratio_bound_to_canonical_rational(args, "minimum", units)?;
1660 sync_ratio_units_from_canonical(
1661 units,
1662 &canonical,
1663 UnitConstraintField::Minimum,
1664 )?;
1665 *minimum = Some(canonical);
1666 }
1667 TypeConstraintCommand::Maximum => {
1668 let canonical = ratio_bound_to_canonical_rational(args, "maximum", units)?;
1669 sync_ratio_units_from_canonical(
1670 units,
1671 &canonical,
1672 UnitConstraintField::Maximum,
1673 )?;
1674 *maximum = Some(canonical);
1675 }
1676 TypeConstraintCommand::Help => {
1677 apply_type_help_command(help, args)?;
1678 }
1679 TypeConstraintCommand::Default => {
1680 let lit = require_literal(args, "default")?;
1681 reject_calendar_for_default(lit, type_name, DefaultExpectation::Ratio, None)?;
1682 let default = match lit {
1683 crate::literals::Value::NumberWithUnit(_, _) => {
1684 let element_spec = TypeSpecification::Ratio {
1685 decimals: *decimals,
1686 minimum: minimum.clone(),
1687 maximum: maximum.clone(),
1688 units: units.clone(),
1689 help: help.clone(),
1690 };
1691 parser_value_to_value_kind(lit, &element_spec)?
1692 }
1693 other => {
1694 return Err(format!(
1695 "default requires a ratio literal with a unit, got {}. Please provide a ratio value with a unit, for example `-> default 25%`.",
1696 value_kind_name(other)
1697 ));
1698 }
1699 };
1700 sync_ratio_default_units(units, &default)?;
1701 *declared_default = Some(RawDefault::Value(default));
1702 }
1703 _ => {
1704 return Err(format!(
1705 "Invalid command '{}' for ratio type. Valid commands: unit, minimum, maximum, decimals, help, default",
1706 command
1707 ));
1708 }
1709 },
1710 TypeSpecification::RatioRange { units, help } => match command {
1711 TypeConstraintCommand::Unit => {
1712 let (unit_name, value_dec) = match args {
1713 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
1714 (name.clone(), *v)
1715 }
1716 _ => {
1717 return Err(
1718 "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."
1719 .to_string(),
1720 );
1721 }
1722 };
1723 let value = crate::computation::rational::decimal_to_rational(value_dec)
1724 .map_err(|e| {
1725 format!(
1726 "ratio unit value is not exactly representable as a rational: {e}"
1727 )
1728 })?;
1729 if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
1730 if existing.value != value {
1731 return Err(format!(
1732 "Unit '{unit_name}' is already defined in this type's inherited units; \
1733 cannot change factor. Add a new unit name instead."
1734 ));
1735 }
1736 } else {
1737 units.0.push(RatioUnit {
1738 name: unit_name,
1739 value,
1740 minimum: None,
1741 maximum: None,
1742 default_magnitude: None,
1743 });
1744 }
1745 }
1746 TypeConstraintCommand::Help => {
1747 apply_type_help_command(help, args)?;
1748 }
1749 TypeConstraintCommand::Default => {
1750 let (left, right) = require_default_range_endpoints(
1751 args,
1752 type_name,
1753 DefaultExpectation::RatioRange,
1754 None,
1755 )?;
1756 let element_spec = TypeSpecification::Ratio {
1757 decimals: None,
1758 minimum: None,
1759 maximum: None,
1760 units: units.clone(),
1761 help: String::new(),
1762 };
1763 let left = lift_range_endpoint(left, &element_spec)?;
1764 let right = lift_range_endpoint(right, &element_spec)?;
1765 if !left.lemma_type.is_ratio() || !right.lemma_type.is_ratio() {
1766 return Err(
1767 "Please provide a ratio range, for example `-> default 10%...50%`."
1768 .to_string(),
1769 );
1770 }
1771 *declared_default = Some(RawDefault::Value(ValueKind::Range(
1772 Box::new(left),
1773 Box::new(right),
1774 )));
1775 }
1776 _ => {
1777 return Err(format!(
1778 "Invalid command '{}' for ratio range type. Valid commands: unit, help, default",
1779 command
1780 ));
1781 }
1782 },
1783 TypeSpecification::Text {
1784 length,
1785 options,
1786 help,
1787 } => match command {
1788 TypeConstraintCommand::Option => {
1789 if args.len() != 1 {
1790 return Err("option takes exactly one argument".to_string());
1791 }
1792 options.push(option_name(&args[0], "option")?);
1793 }
1794 TypeConstraintCommand::Options => {
1795 let mut collected = Vec::with_capacity(args.len());
1796 for arg in args {
1797 collected.push(option_name(arg, "options")?);
1798 }
1799 *options = collected;
1800 }
1801 TypeConstraintCommand::Length => {
1802 let d = require_decimal_literal(args, "length")?;
1803 *length = Some(decimal_to_usize(d, "length")?);
1804 }
1805 TypeConstraintCommand::Help => {
1806 apply_type_help_command(help, args)?;
1807 }
1808 TypeConstraintCommand::Default => {
1809 let lit = require_literal(args, "default")?;
1810 reject_calendar_for_default(lit, type_name, DefaultExpectation::Text, None)?;
1811 match lit {
1812 crate::literals::Value::Text(s) => {
1813 *declared_default = Some(RawDefault::Value(ValueKind::Text(s.clone())));
1814 }
1815 _ => {
1816 return Err(
1817 "Please provide a text value in double quotes, for example `-> default \"my default value\"`."
1818 .to_string(),
1819 );
1820 }
1821 }
1822 }
1823 _ => {
1824 return Err(format!(
1825 "Invalid command '{}' for text type. Valid commands: options, length, help, default",
1826 command
1827 ));
1828 }
1829 },
1830 TypeSpecification::Date {
1831 minimum,
1832 maximum,
1833 help,
1834 } => match command {
1835 TypeConstraintCommand::Minimum => {
1836 let dt = require_date_literal(args, "minimum")?;
1837 *minimum = Some(dt);
1838 }
1839 TypeConstraintCommand::Maximum => {
1840 let dt = require_date_literal(args, "maximum")?;
1841 *maximum = Some(dt);
1842 }
1843 TypeConstraintCommand::Help => {
1844 apply_type_help_command(help, args)?;
1845 }
1846 TypeConstraintCommand::Default => {
1847 let lit = require_literal(args, "default")?;
1848 reject_calendar_for_default(lit, type_name, DefaultExpectation::Date, None)?;
1849 match lit {
1850 crate::literals::Value::Date(dt) => {
1851 *declared_default = Some(RawDefault::Value(ValueKind::Date(
1852 date_time_to_semantic(dt),
1853 )));
1854 }
1855 _ => {
1856 return Err(
1857 "Please provide a date, for example `-> default 2024-06-15`."
1858 .to_string(),
1859 );
1860 }
1861 }
1862 }
1863 _ => {
1864 return Err(format!(
1865 "Invalid command '{}' for date type. Valid commands: minimum, maximum, help, default",
1866 command
1867 ));
1868 }
1869 },
1870 TypeSpecification::DateRange { help } => match command {
1871 TypeConstraintCommand::Help => {
1872 apply_type_help_command(help, args)?;
1873 }
1874 TypeConstraintCommand::Default => {
1875 let (left, right) = require_default_range_endpoints(
1876 args,
1877 type_name,
1878 DefaultExpectation::DateRange,
1879 None,
1880 )?;
1881 let left = literal_value_from_parser_value(left)?;
1882 let right = literal_value_from_parser_value(right)?;
1883 if !left.lemma_type.is_date() || !right.lemma_type.is_date() {
1884 return Err(
1885 "Please provide a date range, for example `-> default 2024-01-01...2024-12-31`."
1886 .to_string(),
1887 );
1888 }
1889 *declared_default = Some(RawDefault::Value(ValueKind::Range(
1890 Box::new(left),
1891 Box::new(right),
1892 )));
1893 }
1894 _ => {
1895 return Err(format!(
1896 "Invalid command '{}' for date range type. Valid commands: help, default",
1897 command
1898 ));
1899 }
1900 },
1901 TypeSpecification::Time {
1902 minimum,
1903 maximum,
1904 help,
1905 } => match command {
1906 TypeConstraintCommand::Minimum => {
1907 let t = require_time_literal(args, "minimum")?;
1908 *minimum = Some(t);
1909 }
1910 TypeConstraintCommand::Maximum => {
1911 let t = require_time_literal(args, "maximum")?;
1912 *maximum = Some(t);
1913 }
1914 TypeConstraintCommand::Help => {
1915 apply_type_help_command(help, args)?;
1916 }
1917 TypeConstraintCommand::Default => {
1918 let lit = require_literal(args, "default")?;
1919 reject_calendar_for_default(lit, type_name, DefaultExpectation::Time, None)?;
1920 match lit {
1921 crate::literals::Value::Time(t) => {
1922 *declared_default =
1923 Some(RawDefault::Value(ValueKind::Time(time_to_semantic(t))));
1924 }
1925 _ => {
1926 return Err(
1927 "Please provide a time, for example `-> default 09:00:00`."
1928 .to_string(),
1929 );
1930 }
1931 }
1932 }
1933 _ => {
1934 return Err(format!(
1935 "Invalid command '{}' for time type. Valid commands: minimum, maximum, help, default",
1936 command
1937 ));
1938 }
1939 },
1940 TypeSpecification::TimeRange { help } => match command {
1941 TypeConstraintCommand::Help => {
1942 apply_type_help_command(help, args)?;
1943 }
1944 TypeConstraintCommand::Default => {
1945 let (left, right) = require_default_range_endpoints(
1946 args,
1947 type_name,
1948 DefaultExpectation::TimeRange,
1949 None,
1950 )?;
1951 let left = literal_value_from_parser_value(left)?;
1952 let right = literal_value_from_parser_value(right)?;
1953 if !left.lemma_type.is_time() || !right.lemma_type.is_time() {
1954 return Err(
1955 "Please provide a time range, for example `-> default 09:00...17:00`."
1956 .to_string(),
1957 );
1958 }
1959 *declared_default = Some(RawDefault::Value(ValueKind::Range(
1960 Box::new(left),
1961 Box::new(right),
1962 )));
1963 }
1964 _ => {
1965 return Err(format!(
1966 "Invalid command '{}' for time range type. Valid commands: help, default",
1967 command
1968 ));
1969 }
1970 },
1971 TypeSpecification::QuantityRange {
1972 units,
1973 decomposition,
1974 help,
1975 ..
1976 } => match command {
1977 TypeConstraintCommand::Unit => {
1978 let (unit_name, value, derived_quantity_factors) = match args {
1979 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
1980 (name.clone(), *v, Vec::new())
1981 }
1982 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Expr(
1983 prefix,
1984 factors,
1985 ))] => {
1986 let raw: Vec<(String, i32)> = factors
1987 .iter()
1988 .map(|f| (f.quantity_ref.clone(), f.exp))
1989 .collect();
1990 (name.clone(), *prefix, raw)
1991 }
1992 _ => {
1993 return Err(
1994 "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')"
1995 .to_string(),
1996 );
1997 }
1998 };
1999 if let Some(existing) = units.0.iter().find(|u| u.name == unit_name) {
2000 let new_factor = crate::computation::rational::decimal_to_rational(value)
2001 .map_err(|failure| failure.to_string())?;
2002 if existing.factor != new_factor
2003 || existing.derived_quantity_factors != derived_quantity_factors
2004 {
2005 return Err(format!(
2006 "Unit '{unit_name}' is already defined in this type's inherited units; \
2007 cannot change factor or decomposition. Add a new unit name instead."
2008 ));
2009 }
2010 } else {
2011 units.0.push(QuantityUnit::from_decimal_factor(
2012 unit_name,
2013 value,
2014 derived_quantity_factors,
2015 )?);
2016 }
2017 }
2018 TypeConstraintCommand::Help => {
2019 apply_type_help_command(help, args)?;
2020 }
2021 TypeConstraintCommand::Default => {
2022 let (left, right) = require_default_range_endpoints(
2023 args,
2024 type_name,
2025 DefaultExpectation::QuantityRange,
2026 Some(units),
2027 )?;
2028 let element_spec = TypeSpecification::Quantity {
2029 minimum: None,
2030 maximum: None,
2031 decimals: None,
2032 units: units.clone(),
2033 traits: vec![],
2034 decomposition: decomposition.clone(),
2035 help: String::new(),
2036 };
2037 let left = lift_range_endpoint(left, &element_spec)?;
2038 let right = lift_range_endpoint(right, &element_spec)?;
2039 if !left.lemma_type.is_quantity() || !right.lemma_type.is_quantity() {
2040 return Err(format!(
2041 "Please provide a range with units valid for '{type_name}', for example `-> default 30 kilogram...35 kilogram`."
2042 ));
2043 }
2044 *declared_default = Some(RawDefault::Value(ValueKind::Range(
2045 Box::new(left),
2046 Box::new(right),
2047 )));
2048 }
2049 _ => {
2050 return Err(format!(
2051 "Invalid command '{}' for quantity range type. Valid commands: unit, help, default",
2052 command
2053 ));
2054 }
2055 },
2056 TypeSpecification::Veto { .. } => {
2057 return Err(format!(
2058 "Invalid command '{}' for veto type. Veto is not a user-declarable type and cannot have constraints",
2059 command
2060 ));
2061 }
2062 TypeSpecification::Undetermined => {
2063 return Err(format!(
2064 "Invalid command '{}' for undetermined sentinel type. Undetermined is an internal type used during type inference and cannot have constraints",
2065 command
2066 ));
2067 }
2068 }
2069 Ok(self)
2070 }
2071}
2072
2073pub fn parse_number_unit(
2076 value_str: &str,
2077 type_spec: &TypeSpecification,
2078) -> Result<crate::parsing::ast::Value, String> {
2079 use crate::literals::{NumberWithUnit, RatioLiteral};
2080 use crate::parsing::ast::Value;
2081
2082 let trimmed = value_str.trim();
2083 match type_spec {
2084 TypeSpecification::Quantity { units, .. } => {
2085 if units.is_empty() {
2086 unreachable!(
2087 "BUG: Quantity type has no units; should have been validated during planning"
2088 );
2089 }
2090 match trimmed.parse::<NumberWithUnit>() {
2091 Ok(n) => {
2092 let unit = units.get(&n.1).map_err(|e| e.to_string())?;
2093 Ok(Value::NumberWithUnit(n.0, unit.name.clone()))
2094 }
2095 Err(e) => {
2096 if trimmed.split_whitespace().count() == 1 && !trimmed.is_empty() {
2097 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
2098 let example_unit = units
2099 .iter()
2100 .next()
2101 .expect("BUG: units non-empty after guard")
2102 .name
2103 .as_str();
2104 Err(format!(
2105 "Quantity value must include a unit, for example: '{} {}'. Valid units: {}.",
2106 trimmed,
2107 example_unit,
2108 valid.join(", ")
2109 ))
2110 } else {
2111 Err(e)
2112 }
2113 }
2114 }
2115 }
2116 TypeSpecification::Ratio { units, .. } => {
2117 if units.is_empty() {
2118 unreachable!(
2119 "BUG: Ratio type has no units; should have been validated during planning"
2120 );
2121 }
2122 match trimmed.parse::<RatioLiteral>()? {
2123 RatioLiteral::Bare(_) => {
2124 Err("Ratio value requires a unit (e.g. '50%', '500 basis_points').".to_string())
2125 }
2126 RatioLiteral::Percent(n) => {
2127 let unit = units.get("percent").map_err(|e| e.to_string())?;
2128 Ok(Value::NumberWithUnit(n, unit.name.clone()))
2129 }
2130 RatioLiteral::Permille(n) => {
2131 let unit = units.get("permille").map_err(|e| e.to_string())?;
2132 Ok(Value::NumberWithUnit(n, unit.name.clone()))
2133 }
2134 RatioLiteral::Named { value, unit } => {
2135 let resolved = units.get(&unit).map_err(|e| e.to_string())?;
2136 Ok(Value::NumberWithUnit(value, resolved.name.clone()))
2137 }
2138 }
2139 }
2140 _ => Err("parse_number_unit only accepts Quantity or Ratio type".to_string()),
2141 }
2142}
2143
2144pub fn parse_value_from_string(
2147 value_str: &str,
2148 type_spec: &TypeSpecification,
2149 source: &Source,
2150) -> Result<crate::parsing::ast::Value, Error> {
2151 use crate::parsing::ast::Value;
2152
2153 let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
2154
2155 let parse_range_value = |element_spec: TypeSpecification| -> Result<Value, Error> {
2156 let (left_str, right_str) = value_str.split_once("...").ok_or_else(|| {
2157 to_err("Range value must use '...' between the two endpoints".to_string())
2158 })?;
2159 if left_str.trim().is_empty() || right_str.trim().is_empty() {
2160 return Err(to_err(
2161 "Range value must contain a non-empty left and right endpoint".to_string(),
2162 ));
2163 }
2164 let left = parse_value_from_string(left_str.trim(), &element_spec, source)?;
2165 let right = parse_value_from_string(right_str.trim(), &element_spec, source)?;
2166 Ok(Value::Range(Box::new(left), Box::new(right)))
2167 };
2168
2169 match type_spec {
2170 TypeSpecification::Text { .. } => value_str
2171 .parse::<crate::literals::TextLiteral>()
2172 .map(|t| Value::Text(t.0))
2173 .map_err(to_err),
2174 TypeSpecification::Number { .. } => value_str
2175 .parse::<crate::literals::NumberLiteral>()
2176 .map(|n| Value::Number(n.0))
2177 .map_err(to_err),
2178 TypeSpecification::Quantity { .. } => {
2179 parse_number_unit(value_str, type_spec).map_err(to_err)
2180 }
2181 TypeSpecification::Boolean { .. } => value_str
2182 .parse::<BooleanValue>()
2183 .map(Value::Boolean)
2184 .map_err(to_err),
2185 TypeSpecification::Date { .. } => {
2186 let date = value_str.parse::<DateTimeValue>().map_err(to_err)?;
2187 Ok(Value::Date(date))
2188 }
2189 TypeSpecification::Time { .. } => {
2190 let time = value_str.parse::<TimeValue>().map_err(to_err)?;
2191 Ok(Value::Time(time))
2192 }
2193 TypeSpecification::Ratio { .. } => {
2194 parse_number_unit(value_str, type_spec).map_err(to_err)
2195 }
2196 TypeSpecification::NumberRange { .. }
2197 | TypeSpecification::QuantityRange { .. }
2198 | TypeSpecification::DateRange { .. }
2199 | TypeSpecification::TimeRange { .. }
2200 | TypeSpecification::RatioRange { .. } => {
2201 let element_spec = range_element_type_specification(type_spec).unwrap_or_else(|| {
2202 unreachable!("BUG: range_element_type_specification missing arm for known range type")
2203 });
2204 parse_range_value(element_spec)
2205 }
2206 TypeSpecification::Veto { .. } => Err(to_err(
2207 "Veto type cannot be parsed from string".to_string(),
2208 )),
2209 TypeSpecification::Undetermined => unreachable!(
2210 "BUG: parse_value_from_string called with Undetermined sentinel type; this type exists only during type inference"
2211 ),
2212 }
2213}
2214
2215#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2220#[serde(rename_all = "snake_case")]
2221pub enum SemanticCalendarUnit {
2222 Month,
2223 Year,
2224}
2225
2226impl fmt::Display for SemanticCalendarUnit {
2227 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2228 let s = match self {
2229 SemanticCalendarUnit::Month => "month",
2230 SemanticCalendarUnit::Year => "year",
2231 };
2232 write!(f, "{}", s)
2233 }
2234}
2235
2236pub fn semantic_calendar_unit_from_unit_name(unit_name: &str) -> SemanticCalendarUnit {
2237 match unit_name {
2238 "month" | "months" => SemanticCalendarUnit::Month,
2239 "year" | "years" => SemanticCalendarUnit::Year,
2240 other => unreachable!(
2241 "BUG: calendar quantity signature unit must be month or year, got '{other}'"
2242 ),
2243 }
2244}
2245
2246pub fn semantic_calendar_unit_from_quantity_signature(
2247 signature: &[(String, i32)],
2248) -> SemanticCalendarUnit {
2249 let unit_name = signature
2250 .first()
2251 .map(|(name, _)| name.as_str())
2252 .expect("BUG: calendar quantity must carry a unit signature");
2253 semantic_calendar_unit_from_unit_name(unit_name)
2254}
2255
2256mod arc_lemma_type {
2257 use super::LemmaType;
2258 use serde::{Deserialize, Deserializer, Serialize, Serializer};
2259 use std::sync::Arc;
2260
2261 pub fn serialize<S>(value: &Arc<LemmaType>, serializer: S) -> Result<S::Ok, S::Error>
2262 where
2263 S: Serializer,
2264 {
2265 value.as_ref().serialize(serializer)
2266 }
2267
2268 pub fn deserialize<'de, D>(deserializer: D) -> Result<Arc<LemmaType>, D::Error>
2269 where
2270 D: Deserializer<'de>,
2271 {
2272 LemmaType::deserialize(deserializer).map(Arc::new)
2273 }
2274}
2275
2276#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2278#[serde(rename_all = "snake_case")]
2279pub enum SemanticConversionTarget {
2280 Type(PrimitiveKind),
2281 Unit {
2283 unit_name: String,
2284 #[serde(with = "arc_lemma_type")]
2286 owning_type: Arc<LemmaType>,
2287 },
2288}
2289
2290impl std::hash::Hash for SemanticConversionTarget {
2291 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
2292 match self {
2293 Self::Type(kind) => {
2294 0u8.hash(state);
2295 kind.hash(state);
2296 }
2297 Self::Unit {
2298 unit_name,
2299 owning_type,
2300 } => {
2301 1u8.hash(state);
2302 unit_name.hash(state);
2303 owning_type.hash(state);
2304 }
2305 }
2306 }
2307}
2308
2309impl SemanticConversionTarget {
2310 #[must_use]
2311 pub fn primitive_kind(&self) -> Option<PrimitiveKind> {
2312 match self {
2313 SemanticConversionTarget::Type(kind) => Some(*kind),
2314 SemanticConversionTarget::Unit { .. } => None,
2315 }
2316 }
2317}
2318
2319impl fmt::Display for SemanticConversionTarget {
2320 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2321 match self {
2322 SemanticConversionTarget::Type(kind) => write!(f, "{:?}", kind),
2323 SemanticConversionTarget::Unit { unit_name, .. } => write!(f, "{unit_name}"),
2324 }
2325 }
2326}
2327
2328#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2330pub struct SemanticTimezone {
2331 pub offset_hours: i8,
2332 pub offset_minutes: u8,
2333}
2334
2335impl fmt::Display for SemanticTimezone {
2336 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2337 if self.offset_hours == 0 && self.offset_minutes == 0 {
2338 write!(f, "Z")
2339 } else {
2340 let sign = if self.offset_hours >= 0 { "+" } else { "-" };
2341 let hours = self.offset_hours.abs();
2342 write!(f, "{}{:02}:{:02}", sign, hours, self.offset_minutes)
2343 }
2344 }
2345}
2346
2347#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2349pub struct SemanticTime {
2350 pub hour: u32,
2351 pub minute: u32,
2352 pub second: u32,
2353 pub microsecond: u32,
2354 pub timezone: Option<SemanticTimezone>,
2355}
2356
2357impl fmt::Display for SemanticTime {
2358 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2359 write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
2360 if self.microsecond != 0 {
2361 write!(f, ".{:06}", self.microsecond)?;
2362 }
2363 if let Some(timezone) = &self.timezone {
2364 write!(f, "{}", timezone)?;
2365 }
2366 Ok(())
2367 }
2368}
2369
2370#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2372pub struct SemanticDateTime {
2373 pub year: i32,
2374 pub month: u32,
2375 pub day: u32,
2376 pub hour: u32,
2377 pub minute: u32,
2378 pub second: u32,
2379 #[serde(default)]
2380 pub microsecond: u32,
2381 pub timezone: Option<SemanticTimezone>,
2382}
2383
2384impl fmt::Display for SemanticDateTime {
2385 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2386 let has_time = self.hour != 0
2387 || self.minute != 0
2388 || self.second != 0
2389 || self.microsecond != 0
2390 || self.timezone.is_some();
2391 if !has_time {
2392 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
2393 } else {
2394 write!(
2395 f,
2396 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
2397 self.year, self.month, self.day, self.hour, self.minute, self.second
2398 )?;
2399 if self.microsecond != 0 {
2400 write!(f, ".{:06}", self.microsecond)?;
2401 }
2402 if let Some(tz) = &self.timezone {
2403 write!(f, "{}", tz)?;
2404 }
2405 Ok(())
2406 }
2407 }
2408}
2409
2410#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2414pub enum RawDefault {
2415 Value(ValueKind),
2416 Quantity {
2417 magnitude: RationalInteger,
2418 unit_name: String,
2419 },
2420}
2421
2422pub fn materialize_raw_default(
2423 raw: RawDefault,
2424 specifications: &TypeSpecification,
2425 type_name: &str,
2426) -> Result<ValueKind, String> {
2427 match raw {
2428 RawDefault::Value(vk) => Ok(vk),
2429 RawDefault::Quantity {
2430 magnitude,
2431 unit_name,
2432 } => {
2433 let TypeSpecification::Quantity { units, .. } = specifications else {
2434 return Err(format!(
2435 "BUG: RawDefault::Quantity for non-quantity type '{type_name}'"
2436 ));
2437 };
2438 let canonical = quantity_declared_bound_to_canonical(
2439 &magnitude, &unit_name, units, type_name, "default",
2440 )?;
2441 Ok(ValueKind::Quantity(canonical, vec![(unit_name, 1)]))
2442 }
2443 }
2444}
2445
2446#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2449pub enum ValueKind {
2450 Number(RationalInteger),
2451 Quantity(RationalInteger, Vec<(String, i32)>),
2459 Text(String),
2460 Date(SemanticDateTime),
2461 Time(SemanticTime),
2462 Boolean(bool),
2463 Ratio(RationalInteger, Option<String>),
2465 Range(Box<LiteralValue>, Box<LiteralValue>),
2466}
2467
2468impl ValueKind {
2469 pub fn as_decimal_magnitude(&self) -> Result<Decimal, String> {
2471 use crate::computation::rational::commit_rational_to_decimal;
2472 match self {
2473 ValueKind::Number(n) | ValueKind::Quantity(n, _) | ValueKind::Ratio(n, _) => {
2474 commit_rational_to_decimal(n).map_err(|failure| failure.to_string())
2475 }
2476 other => Err(format!("expected numeric value kind, got {other}")),
2477 }
2478 }
2479}
2480
2481fn format_rational_magnitude_for_display(rational: &RationalInteger) -> String {
2482 crate::computation::rational::rational_to_display_str(rational)
2483}
2484
2485fn format_number_with_unit_for_display(rational: &RationalInteger, unit: &str) -> String {
2486 use crate::computation::rational::{commit_rational_to_decimal, rational_to_display_str};
2487 use crate::parsing::ast::Value;
2488 match commit_rational_to_decimal(rational) {
2489 Ok(decimal) => format!("{}", Value::NumberWithUnit(decimal, unit.to_string())),
2490 Err(_) => format!("{} {}", rational_to_display_str(rational), unit),
2491 }
2492}
2493
2494impl fmt::Display for ValueKind {
2495 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2496 use crate::computation::rational::{checked_mul, rational_to_display_str};
2497 match self {
2498 ValueKind::Number(rational) => {
2499 write!(f, "{}", format_rational_magnitude_for_display(rational))
2500 }
2501 ValueKind::Quantity(rational, signature) => {
2502 let unit = signature.first().map(|(n, _)| n.as_str()).unwrap_or("");
2503 write!(f, "{}", format_number_with_unit_for_display(rational, unit))
2504 }
2505 ValueKind::Text(s) => write!(f, "{}", crate::parsing::ast::Value::Text(s.clone())),
2506 ValueKind::Ratio(rational, unit) => match unit.as_deref() {
2507 Some("percent") => {
2508 let display = match checked_mul(rational, &rational_new(100, 1)) {
2509 Ok(scaled) => format_number_with_unit_for_display(&scaled, "percent"),
2510 Err(_) => format!("{} percent", rational_to_display_str(rational)),
2511 };
2512 write!(f, "{}", display)
2513 }
2514 Some("permille") => {
2515 let display = match checked_mul(rational, &rational_new(1000, 1)) {
2516 Ok(scaled) => format_number_with_unit_for_display(&scaled, "permille"),
2517 Err(_) => format!("{} permille", rational_to_display_str(rational)),
2518 };
2519 write!(f, "{}", display)
2520 }
2521 Some(unit_name) => {
2522 write!(
2523 f,
2524 "{}",
2525 format_number_with_unit_for_display(rational, unit_name)
2526 )
2527 }
2528 None => write!(f, "{}", format_rational_magnitude_for_display(rational)),
2529 },
2530 ValueKind::Date(dt) => write!(f, "{}", dt),
2531 ValueKind::Time(t) => write!(
2532 f,
2533 "{}",
2534 crate::parsing::ast::Value::Time(crate::parsing::ast::TimeValue {
2535 hour: t.hour as u8,
2536 minute: t.minute as u8,
2537 second: t.second as u8,
2538 microsecond: t.microsecond,
2539 timezone: t
2540 .timezone
2541 .as_ref()
2542 .map(|tz| crate::parsing::ast::TimezoneValue {
2543 offset_hours: tz.offset_hours,
2544 offset_minutes: tz.offset_minutes,
2545 }),
2546 })
2547 ),
2548 ValueKind::Boolean(b) => write!(f, "{}", b),
2549 ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
2550 }
2551 }
2552}
2553
2554fn decimal_from_serialized_str(s: &str) -> Result<Decimal, String> {
2555 Decimal::from_str(s.trim()).map_err(|e| format!("invalid decimal '{s}': {e}"))
2556}
2557
2558#[derive(Serialize, Deserialize)]
2559struct SerializedValueUnit {
2560 value: String,
2561 unit: String,
2562}
2563
2564#[derive(Serialize, Deserialize)]
2565struct SerializedQuantity {
2566 value: String,
2567 signature: Vec<(String, i32)>,
2568}
2569
2570#[derive(Serialize, Deserialize)]
2571struct SerializedRange {
2572 from: ValueKind,
2573 to: ValueKind,
2574}
2575
2576impl Serialize for ValueKind {
2577 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2578 use serde::ser::SerializeMap;
2579 let mut map = serializer.serialize_map(Some(1))?;
2580 match self {
2581 ValueKind::Number(rational) => {
2582 map.serialize_entry(
2583 "number",
2584 &crate::literals::rational_to_serialized_str(rational)
2585 .map_err(serde::ser::Error::custom)?,
2586 )?;
2587 }
2588 ValueKind::Quantity(rational, signature) => {
2589 map.serialize_entry(
2590 "quantity",
2591 &SerializedQuantity {
2592 value: crate::literals::rational_to_serialized_str(rational)
2593 .map_err(serde::ser::Error::custom)?,
2594 signature: signature.clone(),
2595 },
2596 )?;
2597 }
2598 ValueKind::Text(s) => {
2599 map.serialize_entry("text", s)?;
2600 }
2601 ValueKind::Date(dt) => {
2602 map.serialize_entry("date", dt)?;
2603 }
2604 ValueKind::Time(t) => {
2605 map.serialize_entry("time", t)?;
2606 }
2607 ValueKind::Boolean(b) => {
2608 map.serialize_entry("boolean", b)?;
2609 }
2610 ValueKind::Ratio(rational, unit) => {
2611 map.serialize_entry(
2612 "ratio",
2613 &SerializedValueUnit {
2614 value: crate::literals::rational_to_serialized_str(rational)
2615 .map_err(serde::ser::Error::custom)?,
2616 unit: unit.clone().unwrap_or_default(),
2617 },
2618 )?;
2619 }
2620 ValueKind::Range(left, right) => {
2621 map.serialize_entry(
2622 "range",
2623 &SerializedRange {
2624 from: left.value.clone(),
2625 to: right.value.clone(),
2626 },
2627 )?;
2628 }
2629 }
2630 map.end()
2631 }
2632}
2633
2634impl<'de> Deserialize<'de> for ValueKind {
2635 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2636 let map = <serde_json::Map<String, serde_json::Value>>::deserialize(deserializer)?;
2637 if map.len() != 1 {
2638 return Err(serde::de::Error::custom(format!(
2639 "ValueKind must have exactly one variant key, got {}",
2640 map.len()
2641 )));
2642 }
2643 let (tag, payload) = map.into_iter().next().expect("BUG: len checked");
2644 deserialize_value_kind_variant(&tag, payload).map_err(serde::de::Error::custom)
2645 }
2646}
2647
2648fn deserialize_value_kind_variant(
2649 tag: &str,
2650 payload: serde_json::Value,
2651) -> Result<ValueKind, String> {
2652 match tag {
2653 "number" => {
2654 let s = payload
2655 .as_str()
2656 .ok_or_else(|| "number must be a JSON string".to_string())?;
2657 let decimal = decimal_from_serialized_str(s)?;
2658 Ok(ValueKind::Number(
2659 crate::literals::rational_from_parsed_decimal(decimal)?,
2660 ))
2661 }
2662 "quantity" => {
2663 let pair: SerializedQuantity =
2664 serde_json::from_value(payload).map_err(|e| e.to_string())?;
2665 let decimal = decimal_from_serialized_str(&pair.value)?;
2666 Ok(ValueKind::Quantity(
2667 crate::literals::rational_from_parsed_decimal(decimal)?,
2668 pair.signature,
2669 ))
2670 }
2671 "ratio" => {
2672 let pair: SerializedValueUnit =
2673 serde_json::from_value(payload).map_err(|e| e.to_string())?;
2674 let unit = if pair.unit.is_empty() {
2675 None
2676 } else {
2677 Some(pair.unit)
2678 };
2679 let decimal = decimal_from_serialized_str(&pair.value)?;
2680 Ok(ValueKind::Ratio(
2681 crate::literals::rational_from_parsed_decimal(decimal)?,
2682 unit,
2683 ))
2684 }
2685 "calendar" => {
2686 let pair: SerializedValueUnit =
2687 serde_json::from_value(payload).map_err(|e| e.to_string())?;
2688 let unit = match pair.unit.as_str() {
2689 "month" | "months" => SemanticCalendarUnit::Month,
2690 "year" | "years" => SemanticCalendarUnit::Year,
2691 other => {
2692 return Err(format!(
2693 "unknown calendar unit '{other}' (expected 'month' or 'year')"
2694 ));
2695 }
2696 };
2697 let decimal = decimal_from_serialized_str(&pair.value)?;
2698 Ok(ValueKind::Quantity(
2699 crate::literals::rational_from_parsed_decimal(decimal)?,
2700 vec![(unit.to_string(), 1)],
2701 ))
2702 }
2703 "text" => {
2704 let s = payload
2705 .as_str()
2706 .ok_or_else(|| "text must be a JSON string".to_string())?;
2707 Ok(ValueKind::Text(s.to_string()))
2708 }
2709 "date" => {
2710 let dt: SemanticDateTime =
2711 serde_json::from_value(payload).map_err(|e| e.to_string())?;
2712 Ok(ValueKind::Date(dt))
2713 }
2714 "time" => {
2715 let t: SemanticTime = serde_json::from_value(payload).map_err(|e| e.to_string())?;
2716 Ok(ValueKind::Time(t))
2717 }
2718 "boolean" => {
2719 let b = payload
2720 .as_bool()
2721 .ok_or_else(|| "boolean must be a JSON bool".to_string())?;
2722 Ok(ValueKind::Boolean(b))
2723 }
2724 "range" => {
2725 let range: SerializedRange =
2726 serde_json::from_value(payload).map_err(|e| e.to_string())?;
2727 Ok(ValueKind::Range(
2728 Box::new(LiteralValue {
2729 value: range.from,
2730 lemma_type: primitive_number_arc().clone(),
2731 }),
2732 Box::new(LiteralValue {
2733 value: range.to,
2734 lemma_type: primitive_number_arc().clone(),
2735 }),
2736 ))
2737 }
2738 other => Err(format!("unknown ValueKind variant '{other}'")),
2739 }
2740}
2741
2742#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2751pub struct PathSegment {
2752 pub data: String,
2754 pub spec: String,
2756}
2757
2758#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2762pub struct DataPath {
2763 pub segments: Vec<PathSegment>,
2765 pub data: String,
2767}
2768
2769impl DataPath {
2770 pub fn new(segments: Vec<PathSegment>, data: String) -> Self {
2772 Self { segments, data }
2773 }
2774
2775 pub fn local(data: String) -> Self {
2777 Self {
2778 segments: vec![],
2779 data,
2780 }
2781 }
2782
2783 pub fn input_key(&self) -> String {
2786 let mut s = String::new();
2787 for segment in &self.segments {
2788 s.push_str(&segment.data);
2789 s.push('.');
2790 }
2791 s.push_str(&self.data);
2792 s
2793 }
2794}
2795
2796#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2800pub struct RulePath {
2801 pub segments: Vec<PathSegment>,
2803 pub rule: String,
2805}
2806
2807impl RulePath {
2808 pub fn new(segments: Vec<PathSegment>, rule: String) -> Self {
2810 Self { segments, rule }
2811 }
2812}
2813
2814#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2823pub struct Expression {
2824 pub kind: ExpressionKind,
2825 pub source_location: Option<Source>,
2826}
2827
2828impl Expression {
2829 pub fn new(kind: ExpressionKind, source_location: Source) -> Self {
2830 Self {
2831 kind,
2832 source_location: Some(source_location),
2833 }
2834 }
2835
2836 pub fn with_source(kind: ExpressionKind, source_location: Option<Source>) -> Self {
2838 Self {
2839 kind,
2840 source_location,
2841 }
2842 }
2843
2844 pub fn collect_data_paths(&self, data: &mut std::collections::HashSet<DataPath>) {
2846 self.kind.collect_data_paths(data);
2847 }
2848
2849 pub fn collect_rule_paths(&self, rules: &mut std::collections::HashSet<RulePath>) {
2851 self.kind.collect_rule_paths(rules);
2852 }
2853}
2854
2855#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2857#[serde(rename_all = "snake_case")]
2858pub enum ExpressionKind {
2859 Literal(Box<LiteralValue>),
2861 DataPath(DataPath),
2863 RulePath(RulePath),
2865 LogicalAnd(Arc<Expression>, Arc<Expression>),
2866 LogicalOr(Arc<Expression>, Arc<Expression>),
2867 Arithmetic(Arc<Expression>, ArithmeticComputation, Arc<Expression>),
2868 Comparison(Arc<Expression>, ComparisonComputation, Arc<Expression>),
2869 UnitConversion(Arc<Expression>, SemanticConversionTarget),
2870 LogicalNegation(Arc<Expression>, NegationType),
2871 MathematicalComputation(MathematicalComputation, Arc<Expression>),
2872 Veto(VetoExpression),
2873 Now,
2875 DateRelative(DateRelativeKind, Arc<Expression>),
2877 DateCalendar(DateCalendarKind, CalendarPeriodUnit, Arc<Expression>),
2879 RangeLiteral(Arc<Expression>, Arc<Expression>),
2880 PastFutureRange(DateRelativeKind, Arc<Expression>),
2881 RangeContainment(Arc<Expression>, Arc<Expression>),
2882 ResultIsVeto(Arc<Expression>),
2884 Piecewise(Vec<(Arc<Expression>, Arc<Expression>)>),
2887}
2888
2889impl ExpressionKind {
2890 pub(crate) fn collect_data_paths(&self, data: &mut std::collections::HashSet<DataPath>) {
2892 match self {
2893 ExpressionKind::DataPath(fp) => {
2894 data.insert(fp.clone());
2895 }
2896 ExpressionKind::LogicalAnd(left, right) | ExpressionKind::LogicalOr(left, right) => {
2897 left.collect_data_paths(data);
2898 right.collect_data_paths(data);
2899 }
2900 ExpressionKind::Arithmetic(left, _, right)
2901 | ExpressionKind::Comparison(left, _, right)
2902 | ExpressionKind::RangeLiteral(left, right)
2903 | ExpressionKind::RangeContainment(left, right) => {
2904 left.collect_data_paths(data);
2905 right.collect_data_paths(data);
2906 }
2907 ExpressionKind::UnitConversion(inner, _)
2908 | ExpressionKind::LogicalNegation(inner, _)
2909 | ExpressionKind::MathematicalComputation(_, inner)
2910 | ExpressionKind::PastFutureRange(_, inner) => {
2911 inner.collect_data_paths(data);
2912 }
2913 ExpressionKind::DateRelative(_, date_expr) => {
2914 date_expr.collect_data_paths(data);
2915 }
2916 ExpressionKind::DateCalendar(_, _, date_expr) => {
2917 date_expr.collect_data_paths(data);
2918 }
2919 ExpressionKind::Literal(_)
2920 | ExpressionKind::RulePath(_)
2921 | ExpressionKind::Veto(_)
2922 | ExpressionKind::Now => {}
2923 ExpressionKind::ResultIsVeto(operand) => {
2924 operand.collect_data_paths(data);
2925 }
2926 ExpressionKind::Piecewise(arms) => {
2927 for (condition, result) in arms {
2928 condition.collect_data_paths(data);
2929 result.collect_data_paths(data);
2930 }
2931 }
2932 }
2933 }
2934
2935 pub(crate) fn collect_rule_paths(&self, rules: &mut std::collections::HashSet<RulePath>) {
2937 match self {
2938 ExpressionKind::RulePath(rule_path) => {
2939 rules.insert(rule_path.clone());
2940 }
2941 ExpressionKind::LogicalAnd(left, right) | ExpressionKind::LogicalOr(left, right) => {
2942 left.collect_rule_paths(rules);
2943 right.collect_rule_paths(rules);
2944 }
2945 ExpressionKind::Arithmetic(left, _, right)
2946 | ExpressionKind::Comparison(left, _, right)
2947 | ExpressionKind::RangeLiteral(left, right)
2948 | ExpressionKind::RangeContainment(left, right) => {
2949 left.collect_rule_paths(rules);
2950 right.collect_rule_paths(rules);
2951 }
2952 ExpressionKind::UnitConversion(inner, _)
2953 | ExpressionKind::LogicalNegation(inner, _)
2954 | ExpressionKind::MathematicalComputation(_, inner)
2955 | ExpressionKind::PastFutureRange(_, inner) => {
2956 inner.collect_rule_paths(rules);
2957 }
2958 ExpressionKind::DateRelative(_, date_expr) => {
2959 date_expr.collect_rule_paths(rules);
2960 }
2961 ExpressionKind::DateCalendar(_, _, date_expr) => {
2962 date_expr.collect_rule_paths(rules);
2963 }
2964 ExpressionKind::Literal(_)
2965 | ExpressionKind::DataPath(_)
2966 | ExpressionKind::Veto(_)
2967 | ExpressionKind::Now => {}
2968 ExpressionKind::ResultIsVeto(operand) => {
2969 operand.collect_rule_paths(rules);
2970 }
2971 ExpressionKind::Piecewise(arms) => {
2972 for (condition, result) in arms {
2973 condition.collect_rule_paths(rules);
2974 result.collect_rule_paths(rules);
2975 }
2976 }
2977 }
2978 }
2979}
2980
2981#[derive(Clone, Debug, Serialize, Deserialize)]
2987#[serde(tag = "kind", rename_all = "snake_case")]
2988pub enum TypeDefiningSpec {
2989 Local,
2991 Import { spec: Arc<LemmaSpec> },
2993}
2994
2995#[derive(Clone, Debug, Serialize, Deserialize)]
2997#[serde(rename_all = "snake_case")]
2998pub enum TypeExtends {
2999 Primitive,
3001 Custom {
3004 parent: String,
3005 family: String,
3006 defining_spec: TypeDefiningSpec,
3007 },
3008}
3009
3010impl PartialEq for TypeExtends {
3011 fn eq(&self, other: &Self) -> bool {
3012 match (self, other) {
3013 (TypeExtends::Primitive, TypeExtends::Primitive) => true,
3014 (
3015 TypeExtends::Custom {
3016 parent: lp,
3017 family: lf,
3018 defining_spec: ld,
3019 },
3020 TypeExtends::Custom {
3021 parent: rp,
3022 family: rf,
3023 defining_spec: rd,
3024 },
3025 ) => {
3026 lp == rp
3027 && lf == rf
3028 && match (ld, rd) {
3029 (TypeDefiningSpec::Local, TypeDefiningSpec::Local) => true,
3030 (
3031 TypeDefiningSpec::Import { spec: left },
3032 TypeDefiningSpec::Import { spec: right },
3033 ) => Arc::ptr_eq(left, right),
3034 _ => false,
3035 }
3036 }
3037 _ => false,
3038 }
3039 }
3040}
3041
3042impl Eq for TypeExtends {}
3043
3044impl std::hash::Hash for TypeDefiningSpec {
3045 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
3046 match self {
3047 TypeDefiningSpec::Local => {
3048 0u8.hash(state);
3049 }
3050 TypeDefiningSpec::Import { spec } => {
3051 1u8.hash(state);
3052 Arc::as_ptr(spec).hash(state);
3053 }
3054 }
3055 }
3056}
3057
3058impl std::hash::Hash for TypeExtends {
3059 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
3060 match self {
3061 TypeExtends::Primitive => {
3062 0u8.hash(state);
3063 }
3064 TypeExtends::Custom {
3065 parent,
3066 family,
3067 defining_spec,
3068 } => {
3069 1u8.hash(state);
3070 parent.hash(state);
3071 family.hash(state);
3072 defining_spec.hash(state);
3073 }
3074 }
3075 }
3076}
3077
3078impl TypeExtends {
3079 #[must_use]
3081 pub fn custom_local(parent: String, family: String) -> Self {
3082 TypeExtends::Custom {
3083 parent,
3084 family,
3085 defining_spec: TypeDefiningSpec::Local,
3086 }
3087 }
3088
3089 #[must_use]
3091 pub fn parent_name(&self) -> Option<&str> {
3092 match self {
3093 TypeExtends::Primitive => None,
3094 TypeExtends::Custom { parent, .. } => Some(parent.as_str()),
3095 }
3096 }
3097}
3098
3099#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
3104pub struct LemmaType {
3105 pub name: Option<String>,
3107 #[serde(flatten)]
3112 pub specifications: TypeSpecification,
3113 pub extends: TypeExtends,
3115}
3116
3117impl LemmaType {
3118 pub fn map_quantity<F>(self, f: F) -> Self
3122 where
3123 F: FnOnce(
3124 QuantityUnits,
3125 Option<BaseQuantityVector>,
3126 ) -> (QuantityUnits, Option<BaseQuantityVector>),
3127 {
3128 let LemmaType {
3129 name,
3130 specifications,
3131 extends,
3132 } = self;
3133 let specifications = match specifications {
3134 TypeSpecification::Quantity {
3135 minimum,
3136 maximum,
3137 decimals,
3138 units,
3139 traits,
3140 decomposition,
3141 help,
3142 } => {
3143 let (units, decomposition) = f(units, decomposition);
3144 TypeSpecification::Quantity {
3145 minimum,
3146 maximum,
3147 decimals,
3148 units,
3149 traits,
3150 decomposition,
3151 help,
3152 }
3153 }
3154 other => other,
3155 };
3156 LemmaType {
3157 name,
3158 specifications,
3159 extends,
3160 }
3161 }
3162
3163 pub fn new(name: String, specifications: TypeSpecification, extends: TypeExtends) -> Self {
3165 Self {
3166 name: Some(name),
3167 specifications,
3168 extends,
3169 }
3170 }
3171
3172 pub fn without_name(specifications: TypeSpecification, extends: TypeExtends) -> Self {
3174 Self {
3175 name: None,
3176 specifications,
3177 extends,
3178 }
3179 }
3180
3181 pub fn primitive(specifications: TypeSpecification) -> Self {
3183 Self {
3184 name: None,
3185 specifications,
3186 extends: TypeExtends::Primitive,
3187 }
3188 }
3189
3190 pub fn name(&self) -> String {
3192 self.name
3193 .clone()
3194 .unwrap_or_else(|| self.specifications.to_string())
3195 }
3196
3197 pub fn is_boolean(&self) -> bool {
3199 matches!(&self.specifications, TypeSpecification::Boolean { .. })
3200 }
3201
3202 pub fn matches_primitive_kind(&self, kind: PrimitiveKind) -> bool {
3203 matches!(
3204 (kind, &self.specifications),
3205 (PrimitiveKind::Number, TypeSpecification::Number { .. })
3206 | (PrimitiveKind::Text, TypeSpecification::Text { .. })
3207 | (PrimitiveKind::Boolean, TypeSpecification::Boolean { .. })
3208 | (PrimitiveKind::Date, TypeSpecification::Date { .. })
3209 | (PrimitiveKind::Time, TypeSpecification::Time { .. })
3210 | (PrimitiveKind::Ratio, TypeSpecification::Ratio { .. })
3211 | (PrimitiveKind::Quantity, TypeSpecification::Quantity { .. })
3212 )
3213 }
3214
3215 pub fn is_quantity(&self) -> bool {
3217 matches!(&self.specifications, TypeSpecification::Quantity { .. })
3218 }
3219
3220 pub fn is_quantity_range(&self) -> bool {
3221 matches!(
3222 &self.specifications,
3223 TypeSpecification::QuantityRange { .. }
3224 )
3225 }
3226
3227 pub fn is_number(&self) -> bool {
3229 matches!(&self.specifications, TypeSpecification::Number { .. })
3230 }
3231
3232 pub fn is_number_range(&self) -> bool {
3233 matches!(&self.specifications, TypeSpecification::NumberRange { .. })
3234 }
3235
3236 pub fn is_numeric(&self) -> bool {
3238 matches!(
3239 &self.specifications,
3240 TypeSpecification::Quantity { .. } | TypeSpecification::Number { .. }
3241 )
3242 }
3243
3244 pub fn is_text(&self) -> bool {
3246 matches!(&self.specifications, TypeSpecification::Text { .. })
3247 }
3248
3249 pub fn is_date(&self) -> bool {
3251 matches!(&self.specifications, TypeSpecification::Date { .. })
3252 }
3253
3254 pub fn is_date_range(&self) -> bool {
3255 matches!(&self.specifications, TypeSpecification::DateRange { .. })
3256 }
3257
3258 pub fn is_time_range(&self) -> bool {
3259 matches!(&self.specifications, TypeSpecification::TimeRange { .. })
3260 }
3261
3262 pub fn is_time(&self) -> bool {
3264 matches!(&self.specifications, TypeSpecification::Time { .. })
3265 }
3266
3267 pub fn has_trait_duration(&self) -> bool {
3268 match &self.specifications {
3269 TypeSpecification::Quantity { traits, .. } => traits.contains(&QuantityTrait::Duration),
3270 _ => false,
3271 }
3272 }
3273
3274 pub fn is_duration_like_quantity(&self) -> bool {
3275 if !self.is_quantity() {
3276 return false;
3277 }
3278 if self.has_trait_duration() {
3279 return true;
3280 }
3281 self.is_anonymous_quantity()
3282 && self
3283 .quantity_type_decomposition()
3284 .is_some_and(|d| *d == duration_decomposition())
3285 }
3286
3287 pub fn is_duration_like(&self) -> bool {
3288 self.is_duration_like_quantity()
3289 }
3290
3291 pub fn has_trait_calendar(&self) -> bool {
3292 match &self.specifications {
3293 TypeSpecification::Quantity { traits, .. } => traits.contains(&QuantityTrait::Calendar),
3294 _ => false,
3295 }
3296 }
3297
3298 pub fn is_calendar_like_quantity(&self) -> bool {
3299 if !self.is_quantity() {
3300 return false;
3301 }
3302 if self.has_trait_calendar() {
3303 return true;
3304 }
3305 self.is_anonymous_quantity()
3306 && self
3307 .quantity_type_decomposition()
3308 .is_some_and(|d| *d == calendar_decomposition())
3309 }
3310
3311 pub fn is_calendar_like(&self) -> bool {
3312 self.is_calendar_like_quantity()
3313 }
3314
3315 pub fn is_ratio(&self) -> bool {
3317 matches!(&self.specifications, TypeSpecification::Ratio { .. })
3318 }
3319
3320 pub fn is_ratio_range(&self) -> bool {
3321 matches!(&self.specifications, TypeSpecification::RatioRange { .. })
3322 }
3323
3324 pub fn is_calendar_quantity_range(&self) -> bool {
3325 matches!(
3326 &self.specifications,
3327 TypeSpecification::QuantityRange { decomposition: Some(decomposition), .. }
3328 if *decomposition == calendar_decomposition()
3329 )
3330 }
3331
3332 pub fn is_calendar_like_range(&self) -> bool {
3333 self.is_calendar_quantity_range()
3334 }
3335
3336 pub fn is_range(&self) -> bool {
3337 matches!(
3338 &self.specifications,
3339 TypeSpecification::DateRange { .. }
3340 | TypeSpecification::TimeRange { .. }
3341 | TypeSpecification::NumberRange { .. }
3342 | TypeSpecification::QuantityRange { .. }
3343 | TypeSpecification::RatioRange { .. }
3344 )
3345 }
3346
3347 pub fn vetoed(&self) -> bool {
3349 matches!(&self.specifications, TypeSpecification::Veto { .. })
3350 }
3351
3352 pub fn is_undetermined(&self) -> bool {
3354 matches!(&self.specifications, TypeSpecification::Undetermined)
3355 }
3356
3357 pub fn has_same_base_type(&self, other: &LemmaType) -> bool {
3359 use TypeSpecification::*;
3360 matches!(
3361 (&self.specifications, &other.specifications),
3362 (Boolean { .. }, Boolean { .. })
3363 | (Number { .. }, Number { .. })
3364 | (NumberRange { .. }, NumberRange { .. })
3365 | (Quantity { .. }, Quantity { .. })
3366 | (QuantityRange { .. }, QuantityRange { .. })
3367 | (Text { .. }, Text { .. })
3368 | (Date { .. }, Date { .. })
3369 | (DateRange { .. }, DateRange { .. })
3370 | (Time { .. }, Time { .. })
3371 | (TimeRange { .. }, TimeRange { .. })
3372 | (Ratio { .. }, Ratio { .. })
3373 | (RatioRange { .. }, RatioRange { .. })
3374 | (Veto { .. }, Veto { .. })
3375 | (Undetermined, Undetermined)
3376 )
3377 }
3378
3379 #[must_use]
3381 pub fn quantity_family_name(&self) -> Option<&str> {
3382 if !self.is_quantity() {
3383 return None;
3384 }
3385 match &self.extends {
3386 TypeExtends::Custom { family, .. } => Some(family.as_str()),
3387 TypeExtends::Primitive => self.name.as_deref(),
3388 }
3389 }
3390
3391 #[must_use]
3393 pub fn same_quantity_family(&self, other: &LemmaType) -> bool {
3394 if !self.is_quantity() || !other.is_quantity() {
3395 return false;
3396 }
3397 match (self.quantity_family_name(), other.quantity_family_name()) {
3398 (Some(self_family), Some(other_family)) => self_family == other_family,
3399 _ => false,
3400 }
3401 }
3402
3403 #[must_use]
3404 pub fn compatible_with_anonymous_quantity(&self, other: &LemmaType) -> bool {
3405 if !self.is_quantity() || !other.is_quantity() {
3406 return false;
3407 }
3408 if !self.is_anonymous_quantity() && !other.is_anonymous_quantity() {
3409 return false;
3410 }
3411 match (
3412 self.quantity_type_decomposition(),
3413 other.quantity_type_decomposition(),
3414 ) {
3415 (Some(a), Some(b)) => a == b,
3416 _ => false,
3417 }
3418 }
3419
3420 pub fn veto_type() -> Self {
3422 Self::primitive(TypeSpecification::veto())
3423 }
3424
3425 pub fn undetermined_type() -> Self {
3428 Self::primitive(TypeSpecification::Undetermined)
3429 }
3430
3431 pub fn decimal_places(&self) -> Option<u8> {
3434 match &self.specifications {
3435 TypeSpecification::Number { decimals, .. } => *decimals,
3436 TypeSpecification::Quantity { decimals, .. } => *decimals,
3437 TypeSpecification::Ratio { decimals, .. } => *decimals,
3438 _ => None,
3439 }
3440 }
3441
3442 pub fn try_materialize_rational_as_decimal_string(
3447 &self,
3448 magnitude: &crate::computation::rational::RationalInteger,
3449 ) -> Result<String, crate::computation::rational::NumericFailure> {
3450 use crate::computation::rational::commit_rational_to_decimal;
3451 let decimal = commit_rational_to_decimal(magnitude)?;
3452 Ok(format_committed_decimal_for_api(
3453 decimal,
3454 self.decimal_places(),
3455 ))
3456 }
3457
3458 pub fn try_materialize_quantity_canonical_in_unit(
3460 &self,
3461 canonical_magnitude: &crate::computation::rational::RationalInteger,
3462 unit_name: &str,
3463 ) -> Result<String, crate::computation::rational::NumericFailure> {
3464 use crate::computation::rational::checked_div;
3465 let unit_factor = self.quantity_unit_factor(unit_name);
3466 let magnitude_in_unit = checked_div(canonical_magnitude, unit_factor)?;
3467 self.try_materialize_rational_as_decimal_string(&magnitude_in_unit)
3468 }
3469
3470 pub fn try_materialize_ratio_canonical_in_unit(
3472 &self,
3473 canonical_magnitude: &crate::computation::rational::RationalInteger,
3474 unit_name: &str,
3475 ) -> Result<String, crate::computation::rational::NumericFailure> {
3476 use crate::computation::rational::checked_mul;
3477 let units = match &self.specifications {
3478 TypeSpecification::Ratio { units, .. } => units,
3479 _ => unreachable!(
3480 "BUG: try_materialize_ratio_canonical_in_unit called on non-ratio type {}",
3481 self.name()
3482 ),
3483 };
3484 let ratio_unit = units
3485 .iter()
3486 .find(|unit| unit.name == unit_name)
3487 .unwrap_or_else(|| {
3488 let valid: Vec<&str> = units.iter().map(|unit| unit.name.as_str()).collect();
3489 unreachable!(
3490 "BUG: unknown ratio unit '{}' for type {} (valid: {}); planning must reject invalid units",
3491 unit_name,
3492 self.name(),
3493 valid.join(", ")
3494 )
3495 });
3496 let magnitude_in_unit = checked_mul(canonical_magnitude, &ratio_unit.value)?;
3497 self.try_materialize_rational_as_decimal_string(&magnitude_in_unit)
3498 }
3499
3500 pub fn example_value(&self) -> &'static str {
3502 match &self.specifications {
3503 TypeSpecification::Text { .. } => "\"hello world\"",
3504 TypeSpecification::Quantity { .. } => "12.50 eur",
3505 TypeSpecification::QuantityRange { .. } => "30 kilogram...35 kilogram",
3506 TypeSpecification::Number { .. } => "3.14",
3507 TypeSpecification::NumberRange { .. } => "0...100",
3508 TypeSpecification::Boolean { .. } => "true",
3509 TypeSpecification::Date { .. } => "2023-12-25T14:30:00Z",
3510 TypeSpecification::DateRange { .. } => "2024-01-01...2024-12-31",
3511 TypeSpecification::TimeRange { .. } => "09:00...17:00",
3512 TypeSpecification::Veto { .. } => "veto",
3513 TypeSpecification::Time { .. } => "14:30:00",
3514 TypeSpecification::Ratio { .. } => "50%",
3515 TypeSpecification::RatioRange { .. } => "10%...50%",
3516 TypeSpecification::Undetermined => unreachable!(
3517 "BUG: example_value called on Undetermined sentinel type; this type must never reach user-facing code"
3518 ),
3519 }
3520 }
3521
3522 #[must_use]
3526 pub fn quantity_type_decomposition(&self) -> Option<&BaseQuantityVector> {
3530 match &self.specifications {
3531 TypeSpecification::Quantity { decomposition, .. } => decomposition.as_ref(),
3532 _ => unreachable!(
3533 "BUG: quantity_type_decomposition called on non-quantity type {}",
3534 self.name()
3535 ),
3536 }
3537 }
3538
3539 pub fn is_anonymous_quantity(&self) -> bool {
3542 self.name.is_none() && matches!(&self.specifications, TypeSpecification::Quantity { .. })
3543 }
3544
3545 pub fn anonymous_for_decomposition(decomposition: BaseQuantityVector) -> Self {
3549 Self {
3550 name: None,
3551 specifications: TypeSpecification::Quantity {
3552 minimum: None,
3553 maximum: None,
3554 decimals: None,
3555 units: crate::literals::QuantityUnits::new(),
3556 traits: Vec::new(),
3557 decomposition: Some(decomposition),
3558 help: String::new(),
3559 },
3560 extends: TypeExtends::Primitive,
3561 }
3562 }
3563
3564 #[must_use]
3566 pub fn quantity_unit_names(&self) -> Option<Vec<&str>> {
3567 match &self.specifications {
3568 TypeSpecification::Quantity { units, .. } if !units.is_empty() => {
3569 Some(units.iter().map(|unit| unit.name.as_str()).collect())
3570 }
3571 TypeSpecification::QuantityRange { units, .. } if !units.is_empty() => {
3572 Some(units.iter().map(|unit| unit.name.as_str()).collect())
3573 }
3574 _ => None,
3575 }
3576 }
3577
3578 pub fn quantity_unit_factor(
3580 &self,
3581 unit_name: &str,
3582 ) -> &crate::computation::rational::RationalInteger {
3583 let units = match &self.specifications {
3584 TypeSpecification::Quantity { units, .. } => units,
3585 TypeSpecification::QuantityRange { units, .. } => units,
3586 _ => unreachable!(
3587 "BUG: quantity_unit_factor called with non-quantity type {}; only call during evaluation after planning validated quantity conversion",
3588 self.name()
3589 ),
3590 };
3591 match units.get(unit_name) {
3592 Ok(QuantityUnit { factor, .. }) => factor,
3593 Err(_) => {
3594 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
3595 unreachable!(
3596 "BUG: unknown unit '{}' for quantity type {} (valid: {}); planning must reject invalid conversions with Error",
3597 unit_name,
3598 self.name(),
3599 valid.join(", ")
3600 );
3601 }
3602 }
3603 }
3604
3605 pub fn ratio_unit_factor(
3606 &self,
3607 unit_name: &str,
3608 ) -> &crate::computation::rational::RationalInteger {
3609 let units = match &self.specifications {
3610 TypeSpecification::Ratio { units, .. } => units,
3611 _ => unreachable!(
3612 "BUG: ratio_unit_factor called with non-ratio type {}; only call during evaluation after planning validated ratio conversion",
3613 self.name()
3614 ),
3615 };
3616 match units.get(unit_name) {
3617 Ok(RatioUnit { value, .. }) => value,
3618 Err(_) => {
3619 let valid: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
3620 unreachable!(
3621 "BUG: unknown unit '{}' for ratio type {} (valid: {}); planning must reject invalid conversions with Error",
3622 unit_name,
3623 self.name(),
3624 valid.join(", ")
3625 );
3626 }
3627 }
3628 }
3629}
3630
3631#[derive(Clone, Debug, PartialEq, Eq, Hash)]
3633pub struct LiteralValue {
3634 pub value: ValueKind,
3635 pub lemma_type: Arc<LemmaType>,
3636}
3637
3638impl Serialize for LiteralValue {
3639 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
3640 where
3641 S: serde::Serializer,
3642 {
3643 use serde::ser::SerializeStruct;
3644 let mut state = serializer.serialize_struct("LiteralValue", 3)?;
3645 state.serialize_field("value", &self.value)?;
3646 state.serialize_field("lemma_type", self.lemma_type.as_ref())?;
3647 state.serialize_field("display_value", &self.display_value())?;
3648 state.end()
3649 }
3650}
3651
3652impl<'de> Deserialize<'de> for LiteralValue {
3653 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3654 where
3655 D: serde::Deserializer<'de>,
3656 {
3657 #[derive(Deserialize)]
3658 struct Raw {
3659 value: ValueKind,
3660 lemma_type: LemmaType,
3661 }
3662 let raw = Raw::deserialize(deserializer)?;
3663 Ok(Self {
3664 value: raw.value,
3665 lemma_type: Arc::new(raw.lemma_type),
3666 })
3667 }
3668}
3669
3670impl LiteralValue {
3671 pub fn text(s: String) -> Self {
3672 Self {
3673 value: ValueKind::Text(s),
3674 lemma_type: primitive_text_arc().clone(),
3675 }
3676 }
3677
3678 pub fn text_with_type(s: String, lemma_type: Arc<LemmaType>) -> Self {
3679 Self {
3680 value: ValueKind::Text(s),
3681 lemma_type,
3682 }
3683 }
3684
3685 pub fn number(n: RationalInteger) -> Self {
3686 Self {
3687 value: ValueKind::Number(n),
3688 lemma_type: primitive_number_arc().clone(),
3689 }
3690 }
3691
3692 pub fn number_from_decimal(decimal: Decimal) -> Self {
3693 Self::number(
3694 crate::literals::rational_from_parsed_decimal(decimal)
3695 .expect("BUG: literal number from decimal must lift at boundary"),
3696 )
3697 }
3698
3699 pub fn number_with_type(n: RationalInteger, lemma_type: Arc<LemmaType>) -> Self {
3700 Self {
3701 value: ValueKind::Number(n),
3702 lemma_type,
3703 }
3704 }
3705
3706 pub fn number_with_type_from_decimal(decimal: Decimal, lemma_type: Arc<LemmaType>) -> Self {
3707 Self::number_with_type(
3708 crate::literals::rational_from_parsed_decimal(decimal)
3709 .expect("BUG: literal number from decimal must lift at boundary"),
3710 lemma_type,
3711 )
3712 }
3713
3714 pub fn quantity_with_type(
3718 n: RationalInteger,
3719 unit: String,
3720 lemma_type: Arc<LemmaType>,
3721 ) -> Self {
3722 Self {
3723 value: ValueKind::Quantity(n, vec![(unit, 1)]),
3724 lemma_type,
3725 }
3726 }
3727
3728 pub fn quantity_with_signature(
3731 n: RationalInteger,
3732 signature: Vec<(String, i32)>,
3733 lemma_type: Arc<LemmaType>,
3734 ) -> Self {
3735 Self {
3736 value: ValueKind::Quantity(n, signature),
3737 lemma_type,
3738 }
3739 }
3740
3741 pub fn number_interpreted_as_quantity(value: RationalInteger, unit_name: String) -> Self {
3744 Self {
3745 value: ValueKind::Quantity(value, vec![(unit_name, 1)]),
3746 lemma_type: Arc::new(anonymous_quantity_type()),
3747 }
3748 }
3749
3750 pub fn from_bool(b: bool) -> Self {
3751 Self {
3752 value: ValueKind::Boolean(b),
3753 lemma_type: primitive_boolean_arc().clone(),
3754 }
3755 }
3756
3757 pub fn from_datetime(dt: &crate::parsing::ast::DateTimeValue) -> Self {
3758 Self::date(date_time_to_semantic(dt))
3759 }
3760
3761 #[must_use]
3763 pub fn magnitude_default_for_decimal_prompt(&self) -> Option<String> {
3764 use crate::computation::rational::{checked_mul, rational_to_display_str};
3765 match &self.value {
3766 ValueKind::Number(n) => Some(rational_to_display_str(n)),
3767 ValueKind::Quantity(n, signature) if signature.len() == 1 && signature[0].1 == 1 => {
3768 Some(rational_to_display_str(n))
3769 }
3770 ValueKind::Ratio(n, Some(unit)) if unit == "percent" => {
3771 checked_mul(n, &rational_new(100, 1))
3772 .ok()
3773 .map(|scaled| rational_to_display_str(&scaled))
3774 }
3775 ValueKind::Ratio(n, Some(unit)) if unit == "permille" => {
3776 checked_mul(n, &rational_new(1000, 1))
3777 .ok()
3778 .map(|scaled| rational_to_display_str(&scaled))
3779 }
3780 ValueKind::Ratio(n, _) => Some(rational_to_display_str(n)),
3781 _ => None,
3782 }
3783 }
3784
3785 pub fn date(dt: SemanticDateTime) -> Self {
3786 Self {
3787 value: ValueKind::Date(dt),
3788 lemma_type: primitive_date_arc().clone(),
3789 }
3790 }
3791
3792 pub fn date_with_type(dt: SemanticDateTime, lemma_type: Arc<LemmaType>) -> Self {
3793 Self {
3794 value: ValueKind::Date(dt),
3795 lemma_type,
3796 }
3797 }
3798
3799 pub fn time(t: SemanticTime) -> Self {
3800 Self {
3801 value: ValueKind::Time(t),
3802 lemma_type: primitive_time_arc().clone(),
3803 }
3804 }
3805
3806 pub fn time_with_type(t: SemanticTime, lemma_type: Arc<LemmaType>) -> Self {
3807 Self {
3808 value: ValueKind::Time(t),
3809 lemma_type,
3810 }
3811 }
3812
3813 pub fn calendar(
3814 value: RationalInteger,
3815 unit: SemanticCalendarUnit,
3816 lemma_type: Arc<LemmaType>,
3817 ) -> Self {
3818 Self::quantity_with_type(value, unit.to_string(), lemma_type)
3819 }
3820
3821 pub fn calendar_from_decimal(
3822 value: Decimal,
3823 unit: SemanticCalendarUnit,
3824 lemma_type: Arc<LemmaType>,
3825 ) -> Self {
3826 Self::calendar(
3827 crate::literals::rational_from_parsed_decimal(value)
3828 .expect("BUG: calendar literal from decimal must lift at boundary"),
3829 unit,
3830 lemma_type,
3831 )
3832 }
3833
3834 pub fn calendar_with_type(
3835 value: RationalInteger,
3836 unit: SemanticCalendarUnit,
3837 lemma_type: Arc<LemmaType>,
3838 ) -> Self {
3839 Self::calendar(value, unit, lemma_type)
3840 }
3841
3842 pub fn duration_canonical_seconds(&self) -> RationalInteger {
3844 let ValueKind::Quantity(magnitude, _) = &self.value else {
3845 unreachable!(
3846 "BUG: duration_canonical_seconds called with {:?}",
3847 self.value
3848 );
3849 };
3850 if !self.lemma_type.is_duration_like_quantity() {
3851 unreachable!(
3852 "BUG: duration_canonical_seconds called with type {}",
3853 self.lemma_type.name()
3854 );
3855 }
3856 let factor = self.lemma_type.quantity_unit_factor("second");
3857 checked_div(magnitude, factor).expect("BUG: duration unit factor cannot be zero")
3858 }
3859
3860 pub fn calendar_canonical_months(&self) -> RationalInteger {
3862 let ValueKind::Quantity(magnitude, _) = &self.value else {
3863 unreachable!(
3864 "BUG: calendar_canonical_months called with {:?}",
3865 self.value
3866 );
3867 };
3868 if !self.lemma_type.is_calendar_like() {
3869 unreachable!(
3870 "BUG: calendar_canonical_months called with type {}",
3871 self.lemma_type.name()
3872 );
3873 }
3874 let factor = self.lemma_type.quantity_unit_factor("month");
3875 checked_div(magnitude, factor).expect("BUG: calendar unit factor cannot be zero")
3876 }
3877
3878 pub fn ratio(r: RationalInteger, unit: Option<String>) -> Self {
3879 Self {
3880 value: ValueKind::Ratio(r, unit),
3881 lemma_type: primitive_ratio_arc().clone(),
3882 }
3883 }
3884
3885 pub fn ratio_from_decimal(r: Decimal, unit: Option<String>) -> Self {
3886 Self::ratio(
3887 crate::literals::rational_from_parsed_decimal(r)
3888 .expect("BUG: ratio literal from decimal must lift at boundary"),
3889 unit,
3890 )
3891 }
3892
3893 pub fn ratio_with_type(
3894 r: RationalInteger,
3895 unit: Option<String>,
3896 lemma_type: Arc<LemmaType>,
3897 ) -> Self {
3898 Self {
3899 value: ValueKind::Ratio(r, unit),
3900 lemma_type,
3901 }
3902 }
3903
3904 pub fn range(left: LiteralValue, right: LiteralValue) -> Self {
3905 let specifications =
3906 range_type_specification_from_endpoints(&left.lemma_type, &right.lemma_type)
3907 .unwrap_or_else(|| {
3908 unreachable!(
3909 "BUG: attempted to construct a range literal from incompatible endpoint types"
3910 )
3911 });
3912
3913 Self {
3914 value: ValueKind::Range(Box::new(left), Box::new(right)),
3915 lemma_type: Arc::new(LemmaType::primitive(specifications)),
3916 }
3917 }
3918
3919 pub fn display_value(&self) -> String {
3921 format!("{}", self)
3922 }
3923
3924 pub fn byte_size(&self) -> usize {
3926 format!("{}", self).len()
3927 }
3928
3929 pub fn get_type(&self) -> &LemmaType {
3931 &self.lemma_type
3932 }
3933}
3934
3935#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3937#[serde(rename_all = "snake_case")]
3938pub enum DataValue {
3939 Definition {
3940 schema_type: LemmaType,
3941 #[serde(default, skip_serializing_if = "Option::is_none")]
3942 bound_value: Option<LiteralValue>,
3943 },
3944}
3945
3946impl DataValue {
3947 #[must_use]
3948 pub fn from_bound_literal(value: LiteralValue) -> Self {
3949 let schema_type = value.get_type().clone();
3950 Self::Definition {
3951 schema_type,
3952 bound_value: Some(value),
3953 }
3954 }
3955}
3956
3957#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3959pub struct Data {
3960 pub path: DataPath,
3961 pub value: DataValue,
3962 pub source: Option<Source>,
3963}
3964
3965#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3968#[serde(rename_all = "snake_case", tag = "kind")]
3969pub enum ReferenceTarget {
3970 Data(DataPath),
3971 Rule(RulePath),
3972}
3973
3974#[derive(Clone, Debug, Serialize, Deserialize)]
3976#[serde(rename_all = "snake_case")]
3977pub enum DataDefinition {
3978 Value { value: LiteralValue, source: Source },
3980 TypeDeclaration {
3985 resolved_type: Arc<LemmaType>,
3986 declared_default: Option<ValueKind>,
3987 source: Source,
3988 },
3989 Import {
3991 spec: Arc<crate::parsing::ast::LemmaSpec>,
3992 source: Source,
3993 },
3994 Reference {
4018 target: ReferenceTarget,
4019 resolved_type: Arc<LemmaType>,
4020 local_constraints: Option<Vec<Constraint>>,
4021 local_default: Option<ValueKind>,
4022 source: Source,
4023 },
4024}
4025
4026impl DataDefinition {
4027 pub fn schema_type(&self) -> Option<&LemmaType> {
4029 match self {
4030 DataDefinition::Value { value, .. } => Some(value.lemma_type.as_ref()),
4031 DataDefinition::TypeDeclaration { resolved_type, .. } => Some(resolved_type.as_ref()),
4032 DataDefinition::Reference { resolved_type, .. } => Some(resolved_type.as_ref()),
4033 DataDefinition::Import { .. } => None,
4034 }
4035 }
4036
4037 pub fn value(&self) -> Option<&LiteralValue> {
4041 match self {
4042 DataDefinition::Value { value, .. } => Some(value),
4043 DataDefinition::TypeDeclaration { .. }
4044 | DataDefinition::Import { .. }
4045 | DataDefinition::Reference { .. } => None,
4046 }
4047 }
4048
4049 #[inline]
4053 pub fn bound_value(&self) -> Option<&LiteralValue> {
4054 self.value()
4055 }
4056
4057 pub fn default_suggestion(&self) -> Option<LiteralValue> {
4061 match self {
4062 DataDefinition::TypeDeclaration {
4063 resolved_type,
4064 declared_default: Some(dv),
4065 ..
4066 } => Some(LiteralValue {
4067 value: dv.clone(),
4068 lemma_type: Arc::clone(resolved_type),
4069 }),
4070 DataDefinition::Reference {
4071 resolved_type,
4072 local_default: Some(dv),
4073 ..
4074 } => Some(LiteralValue {
4075 value: dv.clone(),
4076 lemma_type: Arc::clone(resolved_type),
4077 }),
4078 DataDefinition::Value { .. }
4079 | DataDefinition::TypeDeclaration {
4080 declared_default: None,
4081 ..
4082 }
4083 | DataDefinition::Reference {
4084 local_default: None,
4085 ..
4086 }
4087 | DataDefinition::Import { .. } => None,
4088 }
4089 }
4090
4091 pub fn source(&self) -> &Source {
4093 match self {
4094 DataDefinition::Value { source, .. } => source,
4095 DataDefinition::TypeDeclaration { source, .. } => source,
4096 DataDefinition::Import { source, .. } => source,
4097 DataDefinition::Reference { source, .. } => source,
4098 }
4099 }
4100
4101 pub fn reference_target(&self) -> Option<&ReferenceTarget> {
4104 match self {
4105 DataDefinition::Reference { target, .. } => Some(target),
4106 _ => None,
4107 }
4108 }
4109}
4110
4111pub fn number_with_unit_to_value_kind(
4113 magnitude: rust_decimal::Decimal,
4114 unit_name: &str,
4115 lemma_type: &LemmaType,
4116) -> Result<ValueKind, String> {
4117 match &lemma_type.specifications {
4118 TypeSpecification::Ratio { units, .. } => {
4119 use crate::computation::rational::{checked_div, decimal_to_rational};
4120 let unit = units.get(unit_name)?;
4121 let magnitude_rational = decimal_to_rational(magnitude)
4122 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
4123 let canonical_rational = checked_div(&magnitude_rational, &unit.value)
4124 .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
4125 Ok(ValueKind::Ratio(
4126 canonical_rational,
4127 Some(unit.name.clone()),
4128 ))
4129 }
4130 TypeSpecification::Quantity { units, .. } => {
4131 use crate::computation::rational::checked_mul;
4132 let rational = lift_parser_decimal(magnitude)?;
4133 let unit = units.get(unit_name)?;
4134 let canonical = checked_mul(&rational, &unit.factor)
4135 .map_err(|failure| format!("quantity canonicalization overflow: {failure}"))?;
4136 Ok(ValueKind::Quantity(
4137 canonical,
4138 vec![(unit_name.to_string(), 1)],
4139 ))
4140 }
4141 _ => Err(format!(
4142 "Unit '{}' is defined on type '{}' which is not quantity or ratio",
4143 unit_name,
4144 lemma_type.name()
4145 )),
4146 }
4147}
4148
4149pub(crate) fn value_kind_matches_spec(value: &ValueKind, type_spec: &TypeSpecification) -> bool {
4152 matches!(
4153 (type_spec, value),
4154 (TypeSpecification::Number { .. }, ValueKind::Number(_))
4155 | (TypeSpecification::Text { .. }, ValueKind::Text(_))
4156 | (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
4157 | (TypeSpecification::Date { .. }, ValueKind::Date(_))
4158 | (TypeSpecification::Time { .. }, ValueKind::Time(_))
4159 | (
4160 TypeSpecification::Quantity { .. },
4161 ValueKind::Quantity(_, _)
4162 )
4163 | (TypeSpecification::Ratio { .. }, ValueKind::Ratio(_, _))
4164 | (TypeSpecification::Ratio { .. }, ValueKind::Number(_))
4165 | (
4166 TypeSpecification::NumberRange { .. },
4167 ValueKind::Range(_, _)
4168 )
4169 | (TypeSpecification::DateRange { .. }, ValueKind::Range(_, _))
4170 | (TypeSpecification::TimeRange { .. }, ValueKind::Range(_, _))
4171 | (TypeSpecification::RatioRange { .. }, ValueKind::Range(_, _))
4172 | (
4173 TypeSpecification::QuantityRange { .. },
4174 ValueKind::Range(_, _)
4175 )
4176 | (TypeSpecification::Veto { .. }, _)
4177 | (TypeSpecification::Undetermined, _)
4178 )
4179}
4180
4181fn value_kind_tag_for_type(spec: &TypeSpecification) -> &'static str {
4182 match spec {
4183 TypeSpecification::Boolean { .. } => "boolean",
4184 TypeSpecification::Quantity { .. } => "quantity",
4185 TypeSpecification::Number { .. } => "number",
4186 TypeSpecification::NumberRange { .. }
4187 | TypeSpecification::QuantityRange { .. }
4188 | TypeSpecification::DateRange { .. }
4189 | TypeSpecification::TimeRange { .. }
4190 | TypeSpecification::RatioRange { .. } => "range",
4191 TypeSpecification::Ratio { .. } => "ratio",
4192 TypeSpecification::Text { .. } => "text",
4193 TypeSpecification::Date { .. } => "date",
4194 TypeSpecification::Time { .. } => "time",
4195 TypeSpecification::Veto { .. } => "veto",
4196 TypeSpecification::Undetermined => "undetermined",
4197 }
4198}
4199
4200fn parser_value_type_mismatch(
4201 value: &crate::literals::Value,
4202 type_spec: &TypeSpecification,
4203) -> String {
4204 use crate::parsing::ast::AsLemmaSource;
4205 let value_str = format!("{}", AsLemmaSource(value));
4206 let expected = value_kind_tag_for_type(type_spec);
4207 match type_spec {
4208 TypeSpecification::Quantity { units, .. } => {
4209 let unit_hint = units
4210 .iter()
4211 .find(|u| u.factor == crate::computation::rational::rational_one())
4212 .map(|u| u.name.as_str())
4213 .or_else(|| units.iter().next().map(|u| u.name.as_str()))
4214 .unwrap_or("unit");
4215 format!("cannot use {value_str} as {expected}: expected `<n> {unit_hint}`")
4216 }
4217 TypeSpecification::Ratio { units, .. } if !units.is_empty() => {
4218 let unit_hint = units
4219 .iter()
4220 .next()
4221 .map(|u| u.name.as_str())
4222 .unwrap_or("unit");
4223 format!(
4224 "cannot use {value_str} as {expected}: expected `<n> {unit_hint}` or bare ratio"
4225 )
4226 }
4227 _ => format!("cannot use {value_str} as {expected}"),
4228 }
4229}
4230
4231pub fn refresh_quantity_literal_canonical_magnitude(
4236 lit: &mut LiteralValue,
4237 resolved_type: &LemmaType,
4238) {
4239 let ValueKind::Quantity(magnitude, signature) = &mut lit.value else {
4240 return;
4241 };
4242 let (unit_name, exponent) = signature
4243 .first()
4244 .expect("BUG: quantity literal has empty signature during canonical magnitude refresh");
4245 if *exponent != 1 || signature.len() != 1 {
4246 return;
4247 }
4248 let stored_factor = lit.lemma_type.quantity_unit_factor(unit_name);
4249 let resolved_factor = resolved_type.quantity_unit_factor(unit_name);
4250 if stored_factor == resolved_factor {
4251 lit.lemma_type = Arc::new(resolved_type.clone());
4252 return;
4253 }
4254 let scaled = checked_mul(magnitude, resolved_factor)
4255 .expect("BUG: quantity recanonicalization multiply overflow");
4256 *magnitude = checked_div(&scaled, stored_factor)
4257 .expect("BUG: quantity recanonicalization divide failed");
4258 lit.lemma_type = Arc::new(resolved_type.clone());
4259}
4260
4261pub fn parser_value_to_value_kind(
4263 value: &crate::literals::Value,
4264 type_spec: &TypeSpecification,
4265) -> Result<ValueKind, String> {
4266 use crate::computation::rational::decimal_to_rational;
4267 use crate::literals::Value;
4268 match (value, type_spec) {
4269 (Value::NumberWithUnit(magnitude, unit_name), TypeSpecification::Ratio { units, .. }) => {
4270 use crate::computation::rational::checked_div;
4271 let unit = units.get(unit_name.as_str())?;
4272 let magnitude_rational = decimal_to_rational(*magnitude)
4273 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
4274 let canonical_rational = checked_div(&magnitude_rational, &unit.value)
4275 .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
4276 Ok(ValueKind::Ratio(
4277 canonical_rational,
4278 Some(unit.name.clone()),
4279 ))
4280 }
4281 (
4282 Value::NumberWithUnit(magnitude, unit_name),
4283 TypeSpecification::Quantity { units, .. },
4284 ) => {
4285 use crate::computation::rational::checked_mul;
4286 let rational = lift_parser_decimal(*magnitude)?;
4287 let unit = units.get(unit_name.as_str())?;
4288 let canonical = checked_mul(&rational, &unit.factor)
4289 .map_err(|failure| format!("quantity canonicalization overflow: {failure}"))?;
4290 Ok(ValueKind::Quantity(canonical, vec![(unit_name.clone(), 1)]))
4291 }
4292 (Value::NumberWithUnit(_, _), _) => {
4293 Err("number_with_unit literal requires a quantity or ratio type".to_string())
4294 }
4295 (Value::Number(n), TypeSpecification::Number { .. }) => {
4296 Ok(ValueKind::Number(lift_parser_decimal(*n)?))
4297 }
4298 (Value::Number(n), TypeSpecification::Ratio { .. }) => {
4299 let r = decimal_to_rational(*n)
4300 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
4301 Ok(ValueKind::Ratio(r, None))
4302 }
4303 (Value::Text(s), TypeSpecification::Text { .. }) => Ok(ValueKind::Text(s.clone())),
4304 (Value::Boolean(b), TypeSpecification::Boolean { .. }) => Ok(ValueKind::Boolean(b.into())),
4305 (Value::Date(dt), TypeSpecification::Date { .. }) => {
4306 Ok(ValueKind::Date(date_time_to_semantic(dt)))
4307 }
4308 (Value::Time(t), TypeSpecification::Time { .. }) => {
4309 Ok(ValueKind::Time(time_to_semantic(t)))
4310 }
4311 (
4312 Value::Range(left, right),
4313 range_spec @ (TypeSpecification::NumberRange { .. }
4314 | TypeSpecification::DateRange { .. }
4315 | TypeSpecification::TimeRange { .. }
4316 | TypeSpecification::RatioRange { .. }
4317 | TypeSpecification::QuantityRange { .. }),
4318 ) => {
4319 let endpoint = range_element_type_specification(range_spec).ok_or_else(|| {
4320 "BUG: range_element_type_specification missing arm for range type".to_string()
4321 })?;
4322 let left_lit = lift_range_endpoint(left, &endpoint)?;
4323 let right_lit = lift_range_endpoint(right, &endpoint)?;
4324 Ok(ValueKind::Range(Box::new(left_lit), Box::new(right_lit)))
4325 }
4326 (value, type_spec) => Err(parser_value_type_mismatch(value, type_spec)),
4327 }
4328}
4329
4330pub fn value_to_semantic(value: &crate::parsing::ast::Value) -> Result<ValueKind, String> {
4334 use crate::parsing::ast::Value;
4335 Ok(match value {
4336 Value::Number(n) => ValueKind::Number(lift_parser_decimal(*n)?),
4337 Value::Text(s) => ValueKind::Text(s.clone()),
4338 Value::Boolean(b) => ValueKind::Boolean(bool::from(*b)),
4339 Value::Date(dt) => ValueKind::Date(date_time_to_semantic(dt)),
4340 Value::Time(t) => ValueKind::Time(time_to_semantic(t)),
4341 Value::NumberWithUnit(_, _) => {
4342 return Err(
4343 "number_with_unit literal requires type context (quantity or ratio)".to_string(),
4344 );
4345 }
4346 Value::Range(_, _) => literal_value_from_parser_value(value)?.value,
4347 })
4348}
4349
4350pub(crate) fn date_time_to_semantic(dt: &crate::parsing::ast::DateTimeValue) -> SemanticDateTime {
4352 SemanticDateTime {
4353 year: dt.year,
4354 month: dt.month,
4355 day: dt.day,
4356 hour: dt.hour,
4357 minute: dt.minute,
4358 second: dt.second,
4359 microsecond: dt.microsecond,
4360 timezone: dt.timezone.as_ref().map(|tz| SemanticTimezone {
4361 offset_hours: tz.offset_hours,
4362 offset_minutes: tz.offset_minutes,
4363 }),
4364 }
4365}
4366
4367pub(crate) fn time_to_semantic(t: &crate::parsing::ast::TimeValue) -> SemanticTime {
4369 SemanticTime {
4370 hour: t.hour.into(),
4371 minute: t.minute.into(),
4372 second: t.second.into(),
4373 microsecond: t.microsecond,
4374 timezone: t.timezone.as_ref().map(|tz| SemanticTimezone {
4375 offset_hours: tz.offset_hours,
4376 offset_minutes: tz.offset_minutes,
4377 }),
4378 }
4379}
4380
4381pub(crate) fn compare_semantic_dates(
4385 left: &SemanticDateTime,
4386 right: &SemanticDateTime,
4387) -> std::cmp::Ordering {
4388 left.year
4389 .cmp(&right.year)
4390 .then_with(|| left.month.cmp(&right.month))
4391 .then_with(|| left.day.cmp(&right.day))
4392 .then_with(|| left.hour.cmp(&right.hour))
4393 .then_with(|| left.minute.cmp(&right.minute))
4394 .then_with(|| left.second.cmp(&right.second))
4395 .then_with(|| left.microsecond.cmp(&right.microsecond))
4396}
4397
4398pub(crate) fn compare_semantic_times(
4401 left: &SemanticTime,
4402 right: &SemanticTime,
4403) -> std::cmp::Ordering {
4404 left.hour
4405 .cmp(&right.hour)
4406 .then_with(|| left.minute.cmp(&right.minute))
4407 .then_with(|| left.second.cmp(&right.second))
4408 .then_with(|| left.microsecond.cmp(&right.microsecond))
4409}
4410
4411pub fn conversion_target_to_semantic(
4413 ct: &ConversionTarget,
4414 unit_index: Option<&HashMap<String, Arc<LemmaType>>>,
4415) -> Result<SemanticConversionTarget, String> {
4416 match ct {
4417 ConversionTarget::Type(kind) => Ok(SemanticConversionTarget::Type(*kind)),
4418 ConversionTarget::Unit { unit_name } => {
4419 let unit_name = crate::parsing::ast::ascii_lowercase_logical_name(unit_name.clone());
4420 let index = unit_index.ok_or_else(|| format!("Unknown unit '{unit_name}'."))?;
4421 let owning_type = index
4422 .get(&unit_name)
4423 .ok_or_else(|| format!("Unknown unit '{unit_name}'."))?
4424 .clone();
4425 Ok(SemanticConversionTarget::Unit {
4426 unit_name,
4427 owning_type,
4428 })
4429 }
4430 }
4431}
4432
4433static PRIMITIVE_BOOLEAN: OnceLock<Arc<LemmaType>> = OnceLock::new();
4439static PRIMITIVE_NUMBER: OnceLock<Arc<LemmaType>> = OnceLock::new();
4440static PRIMITIVE_TEXT: OnceLock<Arc<LemmaType>> = OnceLock::new();
4441static PRIMITIVE_DATE: OnceLock<Arc<LemmaType>> = OnceLock::new();
4442static PRIMITIVE_DATE_RANGE: OnceLock<Arc<LemmaType>> = OnceLock::new();
4443static PRIMITIVE_TIME: OnceLock<Arc<LemmaType>> = OnceLock::new();
4444static PRIMITIVE_RATIO: OnceLock<Arc<LemmaType>> = OnceLock::new();
4445
4446#[must_use]
4447pub fn primitive_boolean_arc() -> &'static Arc<LemmaType> {
4448 PRIMITIVE_BOOLEAN.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::boolean())))
4449}
4450
4451#[must_use]
4452pub fn primitive_number_arc() -> &'static Arc<LemmaType> {
4453 PRIMITIVE_NUMBER.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::number())))
4454}
4455
4456#[must_use]
4457pub fn primitive_text_arc() -> &'static Arc<LemmaType> {
4458 PRIMITIVE_TEXT.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::text())))
4459}
4460
4461#[must_use]
4462pub fn primitive_date_arc() -> &'static Arc<LemmaType> {
4463 PRIMITIVE_DATE.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::date())))
4464}
4465
4466#[must_use]
4467pub fn primitive_date_range_arc() -> &'static Arc<LemmaType> {
4468 PRIMITIVE_DATE_RANGE
4469 .get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::date_range())))
4470}
4471
4472#[must_use]
4473pub fn primitive_time_arc() -> &'static Arc<LemmaType> {
4474 PRIMITIVE_TIME.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::time())))
4475}
4476
4477#[must_use]
4478pub fn primitive_ratio_arc() -> &'static Arc<LemmaType> {
4479 PRIMITIVE_RATIO.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::ratio())))
4480}
4481
4482#[cfg(test)]
4484static PRIMITIVE_QUANTITY: OnceLock<Arc<LemmaType>> = OnceLock::new();
4485
4486#[cfg(test)]
4487#[must_use]
4488pub fn primitive_boolean() -> &'static LemmaType {
4489 primitive_boolean_arc().as_ref()
4490}
4491
4492#[cfg(test)]
4493#[must_use]
4494pub fn primitive_quantity() -> &'static LemmaType {
4495 primitive_quantity_arc().as_ref()
4496}
4497
4498#[cfg(test)]
4499#[must_use]
4500pub fn primitive_quantity_arc() -> &'static Arc<LemmaType> {
4501 PRIMITIVE_QUANTITY.get_or_init(|| Arc::new(LemmaType::primitive(TypeSpecification::quantity())))
4502}
4503
4504#[cfg(test)]
4505#[must_use]
4506pub fn primitive_number() -> &'static LemmaType {
4507 primitive_number_arc().as_ref()
4508}
4509
4510#[cfg(test)]
4511#[must_use]
4512pub fn primitive_text() -> &'static LemmaType {
4513 primitive_text_arc().as_ref()
4514}
4515
4516#[cfg(test)]
4517#[must_use]
4518pub fn primitive_date() -> &'static LemmaType {
4519 primitive_date_arc().as_ref()
4520}
4521
4522#[cfg(test)]
4523#[must_use]
4524pub fn primitive_time() -> &'static LemmaType {
4525 primitive_time_arc().as_ref()
4526}
4527
4528#[cfg(test)]
4529#[must_use]
4530pub fn primitive_ratio() -> &'static LemmaType {
4531 primitive_ratio_arc().as_ref()
4532}
4533
4534#[must_use]
4536pub fn type_spec_for_primitive(kind: PrimitiveKind) -> TypeSpecification {
4537 match kind {
4538 PrimitiveKind::Boolean => TypeSpecification::boolean(),
4539 PrimitiveKind::Quantity => TypeSpecification::quantity(),
4540 PrimitiveKind::QuantityRange => TypeSpecification::quantity_range(),
4541 PrimitiveKind::Number => TypeSpecification::number(),
4542 PrimitiveKind::NumberRange => TypeSpecification::number_range(),
4543 PrimitiveKind::Percent | PrimitiveKind::Ratio => TypeSpecification::ratio(),
4544 PrimitiveKind::RatioRange => TypeSpecification::ratio_range(),
4545 PrimitiveKind::Text => TypeSpecification::text(),
4546 PrimitiveKind::Date => TypeSpecification::date(),
4547 PrimitiveKind::DateRange => TypeSpecification::date_range(),
4548 PrimitiveKind::Time => TypeSpecification::time(),
4549 PrimitiveKind::TimeRange => TypeSpecification::time_range(),
4550 }
4551}
4552
4553impl fmt::Display for PathSegment {
4558 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4559 write!(f, "{} → {}", self.data, self.spec)
4560 }
4561}
4562
4563impl fmt::Display for DataPath {
4564 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4565 for segment in &self.segments {
4566 write!(f, "{}.", segment)?;
4567 }
4568 write!(f, "{}", self.data)
4569 }
4570}
4571
4572impl fmt::Display for RulePath {
4573 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4574 for segment in &self.segments {
4575 write!(f, "{}.", segment)?;
4576 }
4577 write!(f, "{}", self.rule)
4578 }
4579}
4580
4581impl fmt::Display for LemmaType {
4582 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4583 write!(f, "{}", self.name())
4584 }
4585}
4586
4587fn decimal_places_in_display_value(decimal: &rust_decimal::Decimal) -> u32 {
4588 if decimal.is_integer() {
4589 return 0;
4590 }
4591 decimal.fract().normalize().scale()
4592}
4593
4594fn format_committed_decimal_for_api(
4595 decimal: rust_decimal::Decimal,
4596 decimal_places: Option<u8>,
4597) -> String {
4598 match decimal_places {
4599 Some(decimal_places) => {
4600 let rounded = decimal.round_dp(u32::from(decimal_places));
4601 format!("{:.prec$}", rounded, prec = decimal_places as usize)
4602 }
4603 None => {
4604 let normalized = decimal.normalize();
4605 if normalized.fract().is_zero() {
4606 normalized.trunc().to_string()
4607 } else {
4608 normalized.to_string()
4609 }
4610 }
4611 }
4612}
4613
4614fn format_committed_decimal_for_human_display(
4615 decimal: rust_decimal::Decimal,
4616 decimal_places: Option<u8>,
4617) -> String {
4618 match decimal_places {
4619 Some(decimal_places) => {
4620 let rounded = decimal.round_dp(u32::from(decimal_places));
4621 format!("{:.prec$}", rounded, prec = decimal_places as usize)
4622 }
4623 None => decimal.normalize().to_string(),
4624 }
4625}
4626
4627fn format_rational_for_human_display(
4628 magnitude: &crate::computation::rational::RationalInteger,
4629 decimal_places: Option<u8>,
4630) -> String {
4631 use crate::computation::rational::{commit_rational_to_decimal, rational_to_display_str};
4632 match commit_rational_to_decimal(magnitude) {
4633 Ok(decimal) => format_committed_decimal_for_human_display(decimal, decimal_places),
4634 Err(_) => rational_to_display_str(magnitude),
4635 }
4636}
4637
4638fn format_quantity_canonical_for_display(
4639 canonical: &crate::computation::rational::RationalInteger,
4640 lemma_type: &LemmaType,
4641 signature: &[(String, i32)],
4642) -> String {
4643 use crate::computation::rational::{checked_div, commit_rational_to_decimal};
4644 use rust_decimal::Decimal;
4645
4646 let decimals = lemma_type.decimal_places();
4647
4648 if let TypeSpecification::Quantity { units, .. } = &lemma_type.specifications {
4649 if !units.is_empty() {
4650 if let [(sig_unit, 1)] = signature {
4651 if let Some(unit) = units.iter().find(|u| u.name == *sig_unit) {
4652 let in_unit = checked_div(canonical, &unit.factor)
4653 .expect("BUG: de-canonicalization for quantity display must not fail");
4654 let formatted = format_rational_for_human_display(&in_unit, decimals);
4655 return format!("{} {}", formatted, unit.name);
4656 }
4657 }
4658
4659 struct UnitDisplayCandidate {
4660 unit_name: String,
4661 decimal_places: u32,
4662 under_1000: bool,
4663 abs_magnitude: Decimal,
4664 formatted: String,
4665 }
4666
4667 let mut candidates: Vec<UnitDisplayCandidate> = Vec::with_capacity(units.len());
4668 for unit in units.iter() {
4669 let in_unit = checked_div(canonical, &unit.factor)
4670 .expect("BUG: de-canonicalization for quantity display must not fail");
4671 let formatted = format_rational_for_human_display(&in_unit, decimals);
4672 let abs_magnitude = match commit_rational_to_decimal(&in_unit) {
4673 Ok(decimal) => decimal.abs(),
4674 Err(_) => Decimal::MAX,
4675 };
4676 let decimal_places = match commit_rational_to_decimal(&in_unit) {
4677 Ok(decimal) => decimal_places_in_display_value(&decimal),
4678 Err(_) => u32::MAX,
4679 };
4680 let under_1000 = abs_magnitude < Decimal::from(1000);
4681 candidates.push(UnitDisplayCandidate {
4682 unit_name: unit.name.clone(),
4683 decimal_places,
4684 under_1000,
4685 abs_magnitude,
4686 formatted,
4687 });
4688 }
4689
4690 let pool: Vec<&UnitDisplayCandidate> = {
4691 let under: Vec<_> = candidates.iter().filter(|c| c.under_1000).collect();
4692 if under.is_empty() {
4693 candidates.iter().collect()
4694 } else {
4695 under
4696 }
4697 };
4698 let best = pool
4699 .iter()
4700 .min_by(|left, right| {
4701 left.decimal_places
4702 .cmp(&right.decimal_places)
4703 .then_with(|| left.abs_magnitude.cmp(&right.abs_magnitude))
4704 })
4705 .expect("BUG: quantity type must have at least one declared unit");
4706 return format!("{} {}", best.formatted, best.unit_name);
4707 }
4708 }
4709
4710 let unit_label = match signature {
4711 [] => String::new(),
4712 [(name, 1)] => name.clone(),
4713 _ => format_signature_operator_style(signature),
4714 };
4715 let formatted = format_rational_for_human_display(canonical, decimals);
4716 if unit_label.is_empty() {
4717 formatted
4718 } else {
4719 format!("{formatted} {unit_label}")
4720 }
4721}
4722
4723impl fmt::Display for LiteralValue {
4724 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4725 match &self.value {
4726 ValueKind::Quantity(n, signature) => {
4727 write!(
4728 f,
4729 "{}",
4730 format_quantity_canonical_for_display(n, &self.lemma_type, signature)
4731 )
4732 }
4733 ValueKind::Ratio(_, Some(_unit_name)) => write!(f, "{}", self.value),
4734 ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
4735 _ => write!(f, "{}", self.value),
4736 }
4737 }
4738}
4739
4740#[cfg(test)]
4745mod tests {
4746 use super::*;
4747 use crate::computation::rational::decimal_to_rational;
4748 use crate::literals::DateGranularity;
4749 use crate::literals::Value;
4750 use crate::parsing::ast::{BooleanValue, DateTimeValue, LemmaSpec, PrimitiveKind, TimeValue};
4751 use rust_decimal::Decimal;
4752 use std::str::FromStr;
4753 use std::sync::Arc;
4754
4755 #[test]
4756 fn default_primitive_help_is_goal_oriented() {
4757 let kinds = [
4758 PrimitiveKind::Boolean,
4759 PrimitiveKind::Quantity,
4760 PrimitiveKind::QuantityRange,
4761 PrimitiveKind::Number,
4762 PrimitiveKind::NumberRange,
4763 PrimitiveKind::Percent,
4764 PrimitiveKind::Ratio,
4765 PrimitiveKind::RatioRange,
4766 PrimitiveKind::Text,
4767 PrimitiveKind::Date,
4768 PrimitiveKind::DateRange,
4769 PrimitiveKind::Time,
4770 PrimitiveKind::TimeRange,
4771 ];
4772 for kind in kinds {
4773 let spec = type_spec_for_primitive(kind);
4774 let help = match &spec {
4775 TypeSpecification::Boolean { help, .. }
4776 | TypeSpecification::Number { help, .. }
4777 | TypeSpecification::NumberRange { help }
4778 | TypeSpecification::Text { help, .. }
4779 | TypeSpecification::Quantity { help, .. }
4780 | TypeSpecification::QuantityRange { help, .. }
4781 | TypeSpecification::Ratio { help, .. }
4782 | TypeSpecification::RatioRange { help, .. }
4783 | TypeSpecification::Date { help, .. }
4784 | TypeSpecification::DateRange { help }
4785 | TypeSpecification::TimeRange { help }
4786 | TypeSpecification::Time { help, .. } => help,
4787 TypeSpecification::Veto { .. } | TypeSpecification::Undetermined => {
4788 unreachable!(
4789 "BUG: primitive kind {:?} mapped to non-primitive spec",
4790 kind
4791 )
4792 }
4793 };
4794 assert!(!help.is_empty(), "help for {:?}", kind);
4795 assert!(
4796 !help.to_ascii_lowercase().contains("format:"),
4797 "help for {:?} must not describe syntax: {:?}",
4798 kind,
4799 help
4800 );
4801 assert_eq!(help, default_help_for_primitive(kind));
4802 }
4803 }
4804
4805 #[test]
4806 fn test_negated_comparison() {
4807 assert_eq!(
4808 negated_comparison(ComparisonComputation::LessThan),
4809 ComparisonComputation::GreaterThanOrEqual
4810 );
4811 assert_eq!(
4812 negated_comparison(ComparisonComputation::GreaterThanOrEqual),
4813 ComparisonComputation::LessThan
4814 );
4815 assert_eq!(
4816 negated_comparison(ComparisonComputation::Is),
4817 ComparisonComputation::IsNot
4818 );
4819 assert_eq!(
4820 negated_comparison(ComparisonComputation::IsNot),
4821 ComparisonComputation::Is
4822 );
4823 }
4824
4825 #[test]
4826 fn value_to_semantic_number_is_decimal() {
4827 let kind = value_to_semantic(&Value::Number(Decimal::from(42))).unwrap();
4828 assert!(matches!(kind, ValueKind::Number(d) if d == rational_new(42, 1)));
4829 }
4830
4831 #[test]
4832 fn value_kind_quantity_serializes_with_signature() {
4833 let kind = ValueKind::Quantity(
4834 decimal_to_rational(Decimal::from_str("99.50").unwrap()).unwrap(),
4835 vec![("eur".to_string(), 1)],
4836 );
4837 let json = serde_json::to_value(&kind).unwrap();
4838 assert_eq!(json["quantity"]["value"], "99.5");
4839 assert_eq!(json["quantity"]["signature"][0][0], "eur");
4840 assert_eq!(json["quantity"]["signature"][0][1], 1);
4841 }
4842
4843 #[test]
4844 fn value_kind_quantity_compound_signature_roundtrips() {
4845 let original = ValueKind::Quantity(
4846 decimal_to_rational(Decimal::from_str("4800").unwrap()).unwrap(),
4847 vec![
4848 ("eur".to_string(), 1),
4849 ("hour".to_string(), 1),
4850 ("minute".to_string(), -1),
4851 ],
4852 );
4853 let json = serde_json::to_string(&original).unwrap();
4854 let parsed: ValueKind = serde_json::from_str(&json).unwrap();
4855 assert_eq!(original, parsed);
4856 }
4857
4858 #[test]
4859 fn value_kind_quantity_empty_signature_roundtrips() {
4860 let original = ValueKind::Quantity(
4861 decimal_to_rational(Decimal::from_str("12.5").unwrap()).unwrap(),
4862 Vec::new(),
4863 );
4864 let json = serde_json::to_string(&original).unwrap();
4865 let parsed: ValueKind = serde_json::from_str(&json).unwrap();
4866 assert_eq!(original, parsed);
4867 }
4868
4869 #[test]
4870 fn literal_value_number_serde_not_rational_array() {
4871 let lit = LiteralValue::number_from_decimal(Decimal::from(20));
4872 let json = serde_json::to_value(&lit).unwrap();
4873 let number = json
4874 .get("value")
4875 .and_then(|v| v.get("number"))
4876 .expect("number field");
4877 assert!(number.is_string());
4878 assert_eq!(number.as_str(), Some("20"));
4879 assert!(
4880 !number.is_array(),
4881 "stored number must not serialize as [n,d]"
4882 );
4883 }
4884
4885 #[test]
4886 fn test_literal_value_to_primitive_type() {
4887 let one = rational_new(1, 1);
4888
4889 assert_eq!(LiteralValue::text("".to_string()).lemma_type.name(), "text");
4890 assert_eq!(
4891 LiteralValue::number(one.clone()).lemma_type.name(),
4892 "number"
4893 );
4894 assert_eq!(
4895 LiteralValue::from_bool(bool::from(BooleanValue::True))
4896 .lemma_type
4897 .name(),
4898 "boolean"
4899 );
4900
4901 let dt = DateTimeValue {
4902 year: 2024,
4903 month: 1,
4904 day: 1,
4905 hour: 0,
4906 minute: 0,
4907 second: 0,
4908 microsecond: 0,
4909 timezone: None,
4910
4911 granularity: DateGranularity::Full,
4912 };
4913 assert_eq!(
4914 LiteralValue::date(date_time_to_semantic(&dt))
4915 .lemma_type
4916 .name(),
4917 "date"
4918 );
4919 assert_eq!(
4920 LiteralValue::ratio_from_decimal(Decimal::new(1, 2), Some("percent".to_string()))
4921 .lemma_type
4922 .name(),
4923 "ratio"
4924 );
4925 let dur_type = LemmaType::new(
4926 "duration".to_string(),
4927 TypeSpecification::Quantity {
4928 minimum: None,
4929 maximum: None,
4930 decimals: None,
4931 units: QuantityUnits::from(vec![QuantityUnit {
4932 name: "second".to_string(),
4933 factor: crate::computation::rational::rational_one(),
4934 derived_quantity_factors: Vec::new(),
4935 decomposition: BaseQuantityVector::new(),
4936 minimum: None,
4937 maximum: None,
4938 default_magnitude: None,
4939 }]),
4940 traits: vec![QuantityTrait::Duration],
4941 decomposition: None,
4942 help: String::new(),
4943 },
4944 TypeExtends::Primitive,
4945 );
4946 assert_eq!(
4947 LiteralValue::quantity_with_type(one.clone(), "second".to_string(), Arc::new(dur_type))
4948 .lemma_type
4949 .name(),
4950 "duration"
4951 );
4952 }
4953
4954 #[test]
4955 fn test_type_display() {
4956 let specs = TypeSpecification::text();
4957 let lemma_type = LemmaType::new("name".to_string(), specs, TypeExtends::Primitive);
4958 assert_eq!(format!("{}", lemma_type), "name");
4959 }
4960
4961 #[test]
4962 fn test_type_serialization() {
4963 let specs = TypeSpecification::number();
4964 let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
4965 let serialized = serde_json::to_string(&lemma_type).unwrap();
4966 let deserialized: LemmaType = serde_json::from_str(&serialized).unwrap();
4967 assert_eq!(lemma_type, deserialized);
4968 }
4969
4970 #[test]
4971 fn test_literal_value_display_value() {
4972 let ten = rational_new(10, 1);
4973
4974 assert_eq!(
4975 LiteralValue::text("hello".to_string()).display_value(),
4976 "hello"
4977 );
4978 assert_eq!(LiteralValue::number(ten).display_value(), "10");
4979 assert_eq!(LiteralValue::from_bool(true).display_value(), "true");
4980 assert_eq!(LiteralValue::from_bool(false).display_value(), "false");
4981
4982 let ten_percent_ratio =
4984 LiteralValue::ratio_from_decimal(Decimal::new(1, 1), Some("percent".to_string()));
4985 assert_eq!(ten_percent_ratio.display_value(), "10%");
4986
4987 let time = TimeValue {
4988 hour: 14,
4989 minute: 30,
4990 second: 0,
4991 microsecond: 0,
4992 timezone: None,
4993 };
4994 let time_display = LiteralValue::time(time_to_semantic(&time)).display_value();
4995 assert!(time_display.contains("14"));
4996 assert!(time_display.contains("30"));
4997 }
4998
4999 #[test]
5000 fn test_quantity_display_respects_type_decimals() {
5001 let money_type = LemmaType {
5002 name: Some("money".to_string()),
5003 specifications: TypeSpecification::Quantity {
5004 minimum: None,
5005 maximum: None,
5006 decimals: Some(2),
5007 units: QuantityUnits::from(vec![QuantityUnit {
5008 name: "eur".to_string(),
5009 factor: crate::computation::rational::rational_one(),
5010 derived_quantity_factors: Vec::new(),
5011 decomposition: BaseQuantityVector::new(),
5012 minimum: None,
5013 maximum: None,
5014 default_magnitude: None,
5015 }]),
5016 traits: Vec::new(),
5017 decomposition: None,
5018 help: String::new(),
5019 },
5020 extends: TypeExtends::Primitive,
5021 };
5022 let money_type = Arc::new(money_type);
5023 let val = LiteralValue::quantity_with_type(
5024 decimal_to_rational(Decimal::from_str("1.8").unwrap()).unwrap(),
5025 "eur".to_string(),
5026 money_type.clone(),
5027 );
5028 assert_eq!(val.display_value(), "1.80 eur");
5029 let more_precision = LiteralValue::quantity_with_type(
5030 decimal_to_rational(Decimal::from_str("1.80000").unwrap()).unwrap(),
5031 "eur".to_string(),
5032 money_type,
5033 );
5034 assert_eq!(more_precision.display_value(), "1.80 eur");
5035 let quantity_no_decimals = LemmaType {
5036 name: Some("count".to_string()),
5037 specifications: TypeSpecification::Quantity {
5038 minimum: None,
5039 maximum: None,
5040 decimals: None,
5041 units: QuantityUnits::from(vec![QuantityUnit {
5042 name: "items".to_string(),
5043 factor: crate::computation::rational::rational_one(),
5044 derived_quantity_factors: Vec::new(),
5045 decomposition: BaseQuantityVector::new(),
5046 minimum: None,
5047 maximum: None,
5048 default_magnitude: None,
5049 }]),
5050 traits: Vec::new(),
5051 decomposition: None,
5052 help: String::new(),
5053 },
5054 extends: TypeExtends::Primitive,
5055 };
5056 let val_any = LiteralValue::quantity_with_type(
5057 decimal_to_rational(Decimal::from_str("42.50").unwrap()).unwrap(),
5058 "items".to_string(),
5059 Arc::new(quantity_no_decimals),
5060 );
5061 assert_eq!(val_any.display_value(), "42.5 items");
5062 }
5063
5064 #[test]
5065 fn test_literal_value_time_type() {
5066 let time = TimeValue {
5067 hour: 14,
5068 minute: 30,
5069 second: 0,
5070 microsecond: 0,
5071 timezone: None,
5072 };
5073 let lit = LiteralValue::time(time_to_semantic(&time));
5074 assert_eq!(lit.lemma_type.name(), "time");
5075 }
5076
5077 #[test]
5078 fn test_quantity_family_name_primitive_root() {
5079 let quantity_spec = TypeSpecification::quantity();
5080 let money_primitive = LemmaType::new(
5081 "money".to_string(),
5082 quantity_spec.clone(),
5083 TypeExtends::Primitive,
5084 );
5085 assert_eq!(money_primitive.quantity_family_name(), Some("money"));
5086 }
5087
5088 #[test]
5089 fn test_quantity_family_name_custom() {
5090 let quantity_spec = TypeSpecification::quantity();
5091 let money_custom = LemmaType::new(
5092 "money".to_string(),
5093 quantity_spec,
5094 TypeExtends::custom_local("money".to_string(), "money".to_string()),
5095 );
5096 assert_eq!(money_custom.quantity_family_name(), Some("money"));
5097 }
5098
5099 #[test]
5100 fn test_same_quantity_family_same_name_different_extends() {
5101 let quantity_spec = TypeSpecification::quantity();
5102 let money_primitive = LemmaType::new(
5103 "money".to_string(),
5104 quantity_spec.clone(),
5105 TypeExtends::Primitive,
5106 );
5107 let money_custom = LemmaType::new(
5108 "money".to_string(),
5109 quantity_spec,
5110 TypeExtends::custom_local("money".to_string(), "money".to_string()),
5111 );
5112 assert!(money_primitive.same_quantity_family(&money_custom));
5113 assert!(money_custom.same_quantity_family(&money_primitive));
5114 }
5115
5116 #[test]
5117 fn test_same_quantity_family_parent_and_child() {
5118 let quantity_spec = TypeSpecification::quantity();
5119 let type_x = LemmaType::new(
5120 "x".to_string(),
5121 quantity_spec.clone(),
5122 TypeExtends::Primitive,
5123 );
5124 let type_x2 = LemmaType::new(
5125 "x2".to_string(),
5126 quantity_spec,
5127 TypeExtends::custom_local("x".to_string(), "x".to_string()),
5128 );
5129 assert_eq!(type_x.quantity_family_name(), Some("x"));
5130 assert_eq!(type_x2.quantity_family_name(), Some("x"));
5131 assert!(type_x.same_quantity_family(&type_x2));
5132 assert!(type_x2.same_quantity_family(&type_x));
5133 }
5134
5135 #[test]
5136 fn test_same_quantity_family_siblings() {
5137 let quantity_spec = TypeSpecification::quantity();
5138 let type_x2_a = LemmaType::new(
5139 "x2a".to_string(),
5140 quantity_spec.clone(),
5141 TypeExtends::custom_local("x".to_string(), "x".to_string()),
5142 );
5143 let type_x2_b = LemmaType::new(
5144 "x2b".to_string(),
5145 quantity_spec,
5146 TypeExtends::custom_local("x".to_string(), "x".to_string()),
5147 );
5148 assert!(type_x2_a.same_quantity_family(&type_x2_b));
5149 }
5150
5151 #[test]
5152 fn test_same_quantity_family_different_families() {
5153 let quantity_spec = TypeSpecification::quantity();
5154 let money = LemmaType::new(
5155 "money".to_string(),
5156 quantity_spec.clone(),
5157 TypeExtends::Primitive,
5158 );
5159 let temperature = LemmaType::new(
5160 "temperature".to_string(),
5161 quantity_spec,
5162 TypeExtends::Primitive,
5163 );
5164 assert!(!money.same_quantity_family(&temperature));
5165 assert!(!temperature.same_quantity_family(&money));
5166 }
5167
5168 #[test]
5169 fn test_same_quantity_family_quantity_vs_non_quantity() {
5170 let quantity_spec = TypeSpecification::quantity();
5171 let number_spec = TypeSpecification::number();
5172 let quantity_type =
5173 LemmaType::new("money".to_string(), quantity_spec, TypeExtends::Primitive);
5174 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
5175 assert!(!quantity_type.same_quantity_family(&number_type));
5176 assert!(!number_type.same_quantity_family(&quantity_type));
5177 }
5178
5179 #[test]
5180 fn test_same_quantity_family_anonymous_quantitys_are_not_family_compatible() {
5181 let left = LemmaType::anonymous_for_decomposition(duration_decomposition());
5182 let right = LemmaType::anonymous_for_decomposition(duration_decomposition());
5183
5184 assert!(!left.same_quantity_family(&right));
5185 assert!(left.compatible_with_anonymous_quantity(&right));
5186 }
5187
5188 #[test]
5189 fn test_quantity_family_name_non_quantity_returns_none() {
5190 let number_spec = TypeSpecification::number();
5191 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
5192 assert_eq!(number_type.quantity_family_name(), None);
5193 }
5194
5195 #[test]
5196 fn test_lemma_type_inequality_local_vs_import_same_shape() {
5197 let dep = Arc::new(LemmaSpec::new("dep".to_string()));
5198 let quantity_spec = TypeSpecification::quantity();
5199 let local = LemmaType::new(
5200 "t".to_string(),
5201 quantity_spec.clone(),
5202 TypeExtends::custom_local("money".to_string(), "money".to_string()),
5203 );
5204 let imported = LemmaType::new(
5205 "t".to_string(),
5206 quantity_spec,
5207 TypeExtends::Custom {
5208 parent: "money".to_string(),
5209 family: "money".to_string(),
5210 defining_spec: TypeDefiningSpec::Import {
5211 spec: Arc::clone(&dep),
5212 },
5213 },
5214 );
5215 assert_ne!(local, imported);
5216 }
5217
5218 #[test]
5219 fn test_lemma_type_equality_import_same_arc_pointer_identity() {
5220 let shared_spec = Arc::new(LemmaSpec::new("dep".to_string()));
5224 let quantity_spec = TypeSpecification::quantity();
5225 let left = LemmaType::new(
5226 "t".to_string(),
5227 quantity_spec.clone(),
5228 TypeExtends::Custom {
5229 parent: "money".to_string(),
5230 family: "money".to_string(),
5231 defining_spec: TypeDefiningSpec::Import {
5232 spec: Arc::clone(&shared_spec),
5233 },
5234 },
5235 );
5236 let right = LemmaType::new(
5237 "t".to_string(),
5238 quantity_spec,
5239 TypeExtends::Custom {
5240 parent: "money".to_string(),
5241 family: "money".to_string(),
5242 defining_spec: TypeDefiningSpec::Import {
5243 spec: Arc::clone(&shared_spec),
5244 },
5245 },
5246 );
5247 assert_eq!(left, right);
5248 }
5249
5250 #[test]
5251 fn test_lemma_type_inequality_import_different_arc_pointer() {
5252 let spec_a = Arc::new(LemmaSpec::new("dep".to_string()));
5254 let spec_b = Arc::new(LemmaSpec::new("dep".to_string()));
5255 let quantity_spec = TypeSpecification::quantity();
5256 let left = LemmaType::new(
5257 "t".to_string(),
5258 quantity_spec.clone(),
5259 TypeExtends::Custom {
5260 parent: "money".to_string(),
5261 family: "money".to_string(),
5262 defining_spec: TypeDefiningSpec::Import {
5263 spec: Arc::clone(&spec_a),
5264 },
5265 },
5266 );
5267 let right = LemmaType::new(
5268 "t".to_string(),
5269 quantity_spec,
5270 TypeExtends::Custom {
5271 parent: "money".to_string(),
5272 family: "money".to_string(),
5273 defining_spec: TypeDefiningSpec::Import { spec: spec_b },
5274 },
5275 );
5276 assert_ne!(left, right);
5277 }
5278
5279 fn month_default_arg() -> CommandArg {
5280 CommandArg::Literal(crate::literals::Value::NumberWithUnit(
5281 Decimal::ONE,
5282 "month".to_string(),
5283 ))
5284 }
5285
5286 fn unit_factor_arg(name: &str, factor: i64) -> [CommandArg; 2] {
5287 [
5288 CommandArg::Label(name.to_string()),
5289 CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::from(factor))),
5290 ]
5291 }
5292
5293 #[test]
5294 fn default_calendar_on_text_reports_hint() {
5295 let specs = TypeSpecification::text();
5296 let mut default = None;
5297 let err = specs
5298 .apply_constraint(
5299 "notes",
5300 TypeConstraintCommand::Default,
5301 &[month_default_arg()],
5302 &mut default,
5303 )
5304 .unwrap_err();
5305 assert!(err.contains("Unit 'month' is for calendar data"));
5306 assert!(err.contains("double quotes"));
5307 }
5308
5309 #[test]
5310 fn default_calendar_on_duration_reports_valid_units() {
5311 let mut specs = TypeSpecification::quantity();
5312 specs = specs
5313 .apply_constraint(
5314 "duration",
5315 TypeConstraintCommand::Unit,
5316 &unit_factor_arg("second", 1),
5317 &mut None,
5318 )
5319 .unwrap();
5320 specs = specs
5321 .apply_constraint(
5322 "duration",
5323 TypeConstraintCommand::Unit,
5324 &unit_factor_arg("week", 604_800),
5325 &mut None,
5326 )
5327 .unwrap();
5328 specs = specs
5329 .apply_constraint(
5330 "duration",
5331 TypeConstraintCommand::Trait,
5332 &[CommandArg::Label("duration".to_string())],
5333 &mut None,
5334 )
5335 .unwrap();
5336 let mut default = None;
5337 let err = specs
5338 .apply_constraint(
5339 "duration",
5340 TypeConstraintCommand::Default,
5341 &[month_default_arg()],
5342 &mut default,
5343 )
5344 .unwrap_err();
5345 assert!(err.contains("Unit 'month' is for calendar data"));
5346 assert!(err.contains("Valid 'duration' units are"));
5347 assert!(err.contains("week"));
5348 }
5349
5350 #[test]
5351 fn default_valid_duration_weeks_accepted() {
5352 let mut specs = TypeSpecification::quantity();
5353 specs = specs
5354 .apply_constraint(
5355 "duration",
5356 TypeConstraintCommand::Unit,
5357 &unit_factor_arg("second", 1),
5358 &mut None,
5359 )
5360 .unwrap();
5361 specs = specs
5362 .apply_constraint(
5363 "duration",
5364 TypeConstraintCommand::Unit,
5365 &unit_factor_arg("week", 604_800),
5366 &mut None,
5367 )
5368 .unwrap();
5369 specs = specs
5370 .apply_constraint(
5371 "duration",
5372 TypeConstraintCommand::Trait,
5373 &[CommandArg::Label("duration".to_string())],
5374 &mut None,
5375 )
5376 .unwrap();
5377 let mut default = None;
5378 specs
5379 .apply_constraint(
5380 "duration",
5381 TypeConstraintCommand::Default,
5382 &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
5383 Decimal::from(4),
5384 "week".to_string(),
5385 ))],
5386 &mut default,
5387 )
5388 .unwrap();
5389 assert!(matches!(
5390 default,
5391 Some(RawDefault::Quantity {
5392 unit_name,
5393 ..
5394 }) if unit_name == "week"
5395 ));
5396 }
5397
5398 #[test]
5399 fn default_unknown_unit_on_duration_lists_valid_units() {
5400 let mut specs = TypeSpecification::quantity();
5401 specs = specs
5402 .apply_constraint(
5403 "duration",
5404 TypeConstraintCommand::Unit,
5405 &unit_factor_arg("second", 1),
5406 &mut None,
5407 )
5408 .unwrap();
5409 specs = specs
5410 .apply_constraint(
5411 "duration",
5412 TypeConstraintCommand::Trait,
5413 &[CommandArg::Label("duration".to_string())],
5414 &mut None,
5415 )
5416 .unwrap();
5417 let mut default = None;
5418 let err = specs
5419 .apply_constraint(
5420 "duration",
5421 TypeConstraintCommand::Default,
5422 &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
5423 Decimal::ONE,
5424 "fortnight".to_string(),
5425 ))],
5426 &mut default,
5427 )
5428 .unwrap_err();
5429 assert!(err.contains("fortnight"));
5430 assert!(err.contains("not defined on 'duration'"));
5431 assert!(err.contains("Valid units are"));
5432 }
5433
5434 fn money_quantity_type() -> LemmaType {
5435 LemmaType::new(
5436 "Money".to_string(),
5437 TypeSpecification::Quantity {
5438 minimum: None,
5439 maximum: None,
5440 decimals: None,
5441 units: QuantityUnits::from(vec![
5442 QuantityUnit {
5443 name: "eur".to_string(),
5444 factor: crate::computation::rational::rational_one(),
5445 derived_quantity_factors: Vec::new(),
5446 decomposition: BaseQuantityVector::new(),
5447 minimum: None,
5448 maximum: None,
5449 default_magnitude: None,
5450 },
5451 QuantityUnit {
5452 name: "usd".to_string(),
5453 factor: crate::computation::rational::decimal_to_rational(Decimal::new(
5454 91, 2,
5455 ))
5456 .expect("factor"),
5457 derived_quantity_factors: Vec::new(),
5458 decomposition: BaseQuantityVector::new(),
5459 minimum: None,
5460 maximum: None,
5461 default_magnitude: None,
5462 },
5463 ]),
5464 traits: Vec::new(),
5465 decomposition: None,
5466 help: String::new(),
5467 },
5468 TypeExtends::Primitive,
5469 )
5470 }
5471
5472 #[test]
5473 fn quantity_unit_names_for_named_quantity() {
5474 let money = money_quantity_type();
5475 assert_eq!(money.quantity_unit_names(), Some(vec!["eur", "usd"]));
5476 }
5477
5478 fn sig(pairs: &[(&str, i32)]) -> Vec<(String, i32)> {
5483 pairs.iter().map(|(s, e)| (s.to_string(), *e)).collect()
5484 }
5485
5486 #[test]
5487 fn combine_signatures_multiply_adds_exponents() {
5488 let left = sig(&[("eur", 1)]);
5489 let right = sig(&[("hour", -1)]);
5490 let result = combine_signatures(&left, &right, true);
5491 assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
5492 }
5493
5494 #[test]
5495 fn combine_signatures_divide_subtracts_exponents() {
5496 let left = sig(&[("eur", 1)]);
5497 let right = sig(&[("hour", 1)]);
5498 let result = combine_signatures(&left, &right, false);
5499 assert_eq!(result, sig(&[("eur", 1), ("hour", -1)]));
5500 }
5501
5502 #[test]
5503 fn combine_signatures_cancels_to_empty() {
5504 let left = sig(&[("ce", 1), ("minute", -1)]);
5505 let right = sig(&[("minute", 1)]);
5506 let result = combine_signatures(&left, &right, true);
5507 assert_eq!(result, sig(&[("ce", 1)]));
5509 }
5510
5511 #[test]
5512 fn combine_signatures_output_is_canonical_form() {
5513 let left = sig(&[("eur", 1), ("hour", 1)]);
5514 let right = sig(&[("minute", 1)]);
5515 let result = combine_signatures(&left, &right, false); let expected = sig(&[("eur", 1), ("hour", 1), ("minute", -1)]);
5518 assert_eq!(result, expected);
5519 }
5520
5521 #[test]
5522 fn canonicalize_signature_drops_zero_exponents() {
5523 let sig_with_zero = sig(&[("eur", 1), ("hour", 0), ("minute", -1)]);
5524 let result = canonicalize_signature(&sig_with_zero);
5525 assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
5526 }
5527
5528 #[test]
5529 fn canonicalize_signature_sorts_by_name() {
5530 let unsorted = sig(&[("minute", -1), ("eur", 1)]);
5531 let result = canonicalize_signature(&unsorted);
5532 assert_eq!(result, sig(&[("eur", 1), ("minute", -1)]));
5533 }
5534
5535 #[test]
5541 fn format_signature_operator_style_numerator_only() {
5542 let signature = sig(&[("eur", 1)]);
5543 let result = format_signature_operator_style(&signature);
5544 assert_eq!(result, "eur");
5545 }
5546
5547 #[test]
5548 fn format_signature_operator_style_with_denominator() {
5549 let signature = sig(&[("eur", 1), ("hour", -1)]);
5550 let result = format_signature_operator_style(&signature);
5551 assert_eq!(result, "eur/hour");
5552 }
5553
5554 #[test]
5555 fn format_signature_operator_style_denominator_only() {
5556 let signature = sig(&[("meter", -1)]);
5557 let result = format_signature_operator_style(&signature);
5558 assert_eq!(result, "1/meter");
5559 }
5560
5561 #[test]
5562 fn format_signature_operator_style_with_exponents() {
5563 let signature = sig(&[("meter", 2), ("second", -2)]);
5564 let result = format_signature_operator_style(&signature);
5565 assert_eq!(result, "meter^2/second^2");
5566 }
5567
5568 #[test]
5573 fn calendar_unit_factor_table_completeness() {
5574 for unit in &[SemanticCalendarUnit::Month, SemanticCalendarUnit::Year] {
5577 let name = unit.to_string();
5578 assert!(
5579 calendar_unit_factor(&name).is_some(),
5580 "calendar_unit_factor('{}') must return Some",
5581 name
5582 );
5583 }
5584 }
5585
5586 #[test]
5587 fn semantic_calendar_unit_display_returns_singular() {
5588 assert_eq!(SemanticCalendarUnit::Month.to_string(), "month");
5591 assert_eq!(SemanticCalendarUnit::Year.to_string(), "year");
5592 }
5593
5594 #[test]
5599 fn signature_factor_with_calendar_units() {
5600 use std::collections::HashMap;
5601 let calendar = test_calendar_type_for_signature_factor();
5602 let unit_index: HashMap<String, Arc<LemmaType>> = HashMap::new();
5603 let sig_month_per_year = sig(&[("month", 1), ("year", -1)]);
5606 let factor = signature_factor(&sig_month_per_year, &unit_index, Some(&calendar));
5607 let expected = rational_new(1, 12);
5608 assert_eq!(factor, expected, "month/year factor must be 1/12");
5609 }
5610
5611 fn test_calendar_type_for_signature_factor() -> LemmaType {
5612 use crate::computation::rational::{decimal_to_rational, rational_one};
5613 use crate::literals::{QuantityUnit, QuantityUnits};
5614 use rust_decimal::Decimal;
5615 LemmaType::new(
5616 "calendar".to_string(),
5617 TypeSpecification::Quantity {
5618 minimum: None,
5619 maximum: None,
5620 decimals: None,
5621 units: QuantityUnits::from(vec![
5622 QuantityUnit {
5623 name: "month".to_string(),
5624 factor: rational_one(),
5625 minimum: None,
5626 maximum: None,
5627 default_magnitude: None,
5628 decomposition: calendar_decomposition(),
5629 derived_quantity_factors: Vec::new(),
5630 },
5631 QuantityUnit {
5632 name: "year".to_string(),
5633 factor: decimal_to_rational(Decimal::from(12)).expect("year factor"),
5634 minimum: None,
5635 maximum: None,
5636 default_magnitude: None,
5637 decomposition: calendar_decomposition(),
5638 derived_quantity_factors: Vec::new(),
5639 },
5640 ]),
5641 traits: vec![QuantityTrait::Calendar],
5642 decomposition: Some(calendar_decomposition()),
5643 help: String::new(),
5644 },
5645 TypeExtends::Primitive,
5646 )
5647 }
5648
5649 #[test]
5650 #[should_panic(expected = "BUG: signature_factor called with unresolved unit name")]
5651 fn signature_factor_panics_on_unresolved_name() {
5652 use std::collections::HashMap;
5653 let unit_index: HashMap<String, Arc<LemmaType>> = HashMap::new();
5654 let bad_sig = sig(&[("nonexistent_unit_xyz", 1)]);
5655 signature_factor(&bad_sig, &unit_index, None);
5656 }
5657
5658 #[test]
5659 fn signature_factor_uses_owner_when_expression_index_empty() {
5660 use std::collections::HashMap;
5661 let money = test_money_type_for_signature_factor();
5662 let expression_units: HashMap<String, Arc<LemmaType>> = HashMap::new();
5663 let sig_usd = sig(&[("usd", 1)]);
5664 let factor = signature_factor(&sig_usd, &expression_units, Some(&money));
5665 assert_eq!(factor, rational_new(91, 100));
5666 }
5667
5668 fn test_money_type_for_signature_factor() -> LemmaType {
5669 use crate::computation::rational::decimal_to_rational;
5670 use crate::literals::{QuantityUnit, QuantityUnits};
5671 use rust_decimal::Decimal;
5672 LemmaType::new(
5673 "money".to_string(),
5674 TypeSpecification::Quantity {
5675 minimum: None,
5676 maximum: None,
5677 decimals: Some(2),
5678 units: QuantityUnits::from(vec![
5679 QuantityUnit {
5680 name: "eur".to_string(),
5681 factor: crate::computation::rational::rational_one(),
5682 minimum: None,
5683 maximum: None,
5684 default_magnitude: None,
5685 decomposition: BaseQuantityVector::new(),
5686 derived_quantity_factors: Vec::new(),
5687 },
5688 QuantityUnit {
5689 name: "usd".to_string(),
5690 factor: decimal_to_rational(Decimal::new(91, 2)).expect("usd factor"),
5691 minimum: None,
5692 maximum: None,
5693 default_magnitude: None,
5694 decomposition: BaseQuantityVector::new(),
5695 derived_quantity_factors: Vec::new(),
5696 },
5697 ]),
5698 traits: Vec::new(),
5699 decomposition: None,
5700 help: String::new(),
5701 },
5702 TypeExtends::Primitive,
5703 )
5704 }
5705
5706 fn quantity_type_with_kilogram() -> TypeSpecification {
5707 use crate::computation::rational::rational_one;
5708 use crate::literals::{QuantityUnit, QuantityUnits};
5709 let mut units = QuantityUnits::new();
5710 units.push(QuantityUnit {
5711 name: "kilogram".to_string(),
5712 factor: rational_one(),
5713 minimum: None,
5714 maximum: None,
5715 default_magnitude: None,
5716 decomposition: BaseQuantityVector::new(),
5717 derived_quantity_factors: Vec::new(),
5718 });
5719 TypeSpecification::Quantity {
5720 minimum: None,
5721 maximum: None,
5722 decimals: None,
5723 units,
5724 traits: Vec::new(),
5725 decomposition: None,
5726 help: String::new(),
5727 }
5728 }
5729
5730 #[test]
5731 fn parser_value_to_value_kind_rejects_bare_number_for_quantity() {
5732 let ten = Value::Number(Decimal::from(10));
5733 let err = parser_value_to_value_kind(&ten, &quantity_type_with_kilogram())
5734 .expect_err("bare number must not bind to quantity");
5735 assert!(
5736 err.contains("kilogram"),
5737 "error must hint expected unit, got: {err}"
5738 );
5739 }
5740
5741 #[test]
5742 fn parser_value_to_value_kind_accepts_number_with_unit_for_quantity() {
5743 let ten_kg = Value::NumberWithUnit(Decimal::from(10), "kilogram".to_string());
5744 let kind = parser_value_to_value_kind(&ten_kg, &quantity_type_with_kilogram())
5745 .expect("10 kilogram must bind to quantity");
5746 assert!(matches!(kind, ValueKind::Quantity(_, _)));
5747 }
5748
5749 #[test]
5750 fn parser_value_to_value_kind_accepts_bare_number_for_ratio() {
5751 let ten = Value::Number(Decimal::from(10));
5752 let kind =
5753 parser_value_to_value_kind(&ten, &TypeSpecification::ratio()).expect("number -> ratio");
5754 assert!(matches!(kind, ValueKind::Ratio(_, None)));
5755 }
5756
5757 #[test]
5758 fn value_kind_matches_spec_rejects_number_for_quantity() {
5759 let n = ValueKind::Number(rational_new(10, 1));
5760 assert!(!value_kind_matches_spec(&n, &quantity_type_with_kilogram()));
5761 }
5762
5763 #[test]
5764 fn apply_constraint_rejects_inherited_unit_factor_change() {
5765 let mut specs = TypeSpecification::quantity();
5766 specs = specs
5767 .apply_constraint(
5768 "money",
5769 TypeConstraintCommand::Unit,
5770 &unit_factor_arg("eur", 1),
5771 &mut None,
5772 )
5773 .expect("seed eur");
5774 let err = specs
5775 .apply_constraint(
5776 "money",
5777 TypeConstraintCommand::Unit,
5778 &[
5779 CommandArg::Label("eur".to_string()),
5780 CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::new(11, 1))),
5781 ],
5782 &mut None,
5783 )
5784 .expect_err("must not change inherited unit factor");
5785 assert!(err.contains("eur"), "error must name unit, got: {err}");
5786 assert!(
5787 err.contains("inherited") || err.contains("cannot change"),
5788 "error must reject factor change, got: {err}"
5789 );
5790 }
5791
5792 #[test]
5793 fn apply_constraint_allows_additive_unit_on_inherited_spec() {
5794 let mut specs = TypeSpecification::quantity();
5795 specs = specs
5796 .apply_constraint(
5797 "money",
5798 TypeConstraintCommand::Unit,
5799 &unit_factor_arg("eur", 1),
5800 &mut None,
5801 )
5802 .expect("seed eur");
5803 specs = specs
5804 .apply_constraint(
5805 "money",
5806 TypeConstraintCommand::Unit,
5807 &unit_factor_arg("usd", 1),
5808 &mut None,
5809 )
5810 .expect("add usd");
5811 match &specs {
5812 TypeSpecification::Quantity { units, .. } => assert_eq!(units.len(), 2),
5813 other => panic!("expected Quantity, got {other:?}"),
5814 }
5815 }
5816
5817 #[test]
5818 fn apply_constraint_idempotent_inherited_unit_redeclare() {
5819 let mut specs = TypeSpecification::quantity();
5820 specs = specs
5821 .apply_constraint(
5822 "money",
5823 TypeConstraintCommand::Unit,
5824 &unit_factor_arg("eur", 1),
5825 &mut None,
5826 )
5827 .expect("seed eur");
5828 specs = specs
5829 .apply_constraint(
5830 "money",
5831 TypeConstraintCommand::Unit,
5832 &unit_factor_arg("eur", 1),
5833 &mut None,
5834 )
5835 .expect("idempotent eur");
5836 match &specs {
5837 TypeSpecification::Quantity { units, .. } => {
5838 assert_eq!(units.len(), 1);
5839 assert_eq!(
5840 units.iter().find(|u| u.name == "eur").expect("eur").factor,
5841 crate::computation::rational::rational_one()
5842 );
5843 }
5844 other => panic!("expected Quantity, got {other:?}"),
5845 }
5846 }
5847}