1pub use crate::parsing::ast::{
9 ArithmeticComputation, ComparisonComputation, MathematicalComputation, NegationType, Span,
10 VetoExpression,
11};
12pub use crate::parsing::source::Source;
13
14#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum LogicalComputation {
18 And,
19 Or,
20 Not,
21}
22
23#[must_use]
25pub fn negated_comparison(op: ComparisonComputation) -> ComparisonComputation {
26 match op {
27 ComparisonComputation::LessThan => ComparisonComputation::GreaterThanOrEqual,
28 ComparisonComputation::LessThanOrEqual => ComparisonComputation::GreaterThan,
29 ComparisonComputation::GreaterThan => ComparisonComputation::LessThanOrEqual,
30 ComparisonComputation::GreaterThanOrEqual => ComparisonComputation::LessThan,
31 ComparisonComputation::Is => ComparisonComputation::IsNot,
32 ComparisonComputation::IsNot => ComparisonComputation::Is,
33 }
34}
35
36use crate::computation::rational::RationalInteger;
38use crate::parsing::ast::Constraint;
39use crate::parsing::ast::{
40 BooleanValue, CalendarPeriodUnit, CalendarUnit, CommandArg, ConversionTarget, DateCalendarKind,
41 DateRelativeKind, DateTimeValue, LemmaSpec, PrimitiveKind, TimeValue, TypeConstraintCommand,
42};
43use crate::Error;
44use rust_decimal::Decimal;
45use serde::{Deserialize, Deserializer, Serialize, Serializer};
46use std::collections::HashMap;
47use std::fmt;
48use std::hash::Hash;
49use std::str::FromStr;
50use std::sync::{Arc, OnceLock};
51
52pub use crate::literals::{BaseQuantityVector, QuantityUnit, QuantityUnits, RatioUnit, RatioUnits};
59
60pub fn combine_decompositions(
63 left: &BaseQuantityVector,
64 right: &BaseQuantityVector,
65 is_multiply: bool,
66) -> BaseQuantityVector {
67 let mut result = left.clone();
68 for (dim, &exp) in right {
69 let delta = if is_multiply { exp } else { -exp };
70 let entry = result.entry(dim.clone()).or_insert(0);
71 *entry += delta;
72 if *entry == 0 {
73 result.remove(dim);
74 }
75 }
76 result
77}
78
79pub const DURATION_DIMENSION: &str = "duration";
80pub const CALENDAR_DIMENSION: &str = "calendar";
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
83#[serde(rename_all = "snake_case")]
84pub enum QuantityTrait {
85 Duration,
86}
87
88pub fn duration_decomposition() -> BaseQuantityVector {
89 [(DURATION_DIMENSION.to_string(), 1i32)]
90 .into_iter()
91 .collect()
92}
93
94pub fn calendar_decomposition() -> BaseQuantityVector {
95 [(CALENDAR_DIMENSION.to_string(), 1i32)]
96 .into_iter()
97 .collect()
98}
99
100mod stored_quantity_declared_bound_serde {
101 use super::RationalInteger;
102 use crate::computation::rational::commit_rational_to_decimal;
103 use rust_decimal::Decimal;
104 use serde::{Deserialize, Deserializer, Serialize, Serializer};
105
106 fn lift(decimal: Decimal) -> Result<RationalInteger, String> {
107 crate::computation::rational::decimal_to_rational(decimal)
108 .map_err(|failure| failure.to_string())
109 }
110
111 pub mod option {
112 use super::*;
113
114 pub fn serialize<S: Serializer>(
115 value: &Option<(RationalInteger, String)>,
116 serializer: S,
117 ) -> Result<S::Ok, S::Error> {
118 match value {
119 None => serializer.serialize_none(),
120 Some((magnitude, unit_name)) => {
121 let decimal =
122 commit_rational_to_decimal(magnitude).map_err(serde::ser::Error::custom)?;
123 (decimal, unit_name.as_str()).serialize(serializer)
124 }
125 }
126 }
127
128 pub fn deserialize<'de, D: Deserializer<'de>>(
129 deserializer: D,
130 ) -> Result<Option<(RationalInteger, String)>, D::Error> {
131 let parsed: Option<(Decimal, String)> = Option::deserialize(deserializer)?;
132 parsed
133 .map(|(decimal, unit_name)| lift(decimal).map(|magnitude| (magnitude, unit_name)))
134 .transpose()
135 .map_err(serde::de::Error::custom)
136 }
137 }
138}
139
140mod stored_calendar_bound_serde {
141 use super::{RationalInteger, SemanticCalendarUnit};
142 use crate::computation::rational::commit_rational_to_decimal;
143 use rust_decimal::Decimal;
144 use serde::{Deserialize, Deserializer, Serialize, Serializer};
145
146 fn lift(decimal: Decimal) -> Result<RationalInteger, String> {
147 crate::computation::rational::decimal_to_rational(decimal)
148 .map_err(|failure| failure.to_string())
149 }
150
151 pub mod option {
152 use super::*;
153
154 pub fn serialize<S: Serializer>(
155 value: &Option<(RationalInteger, SemanticCalendarUnit)>,
156 serializer: S,
157 ) -> Result<S::Ok, S::Error> {
158 match value {
159 None => serializer.serialize_none(),
160 Some((magnitude, unit)) => {
161 let decimal =
162 commit_rational_to_decimal(magnitude).map_err(serde::ser::Error::custom)?;
163 (decimal, unit).serialize(serializer)
164 }
165 }
166 }
167
168 pub fn deserialize<'de, D: Deserializer<'de>>(
169 deserializer: D,
170 ) -> Result<Option<(RationalInteger, SemanticCalendarUnit)>, D::Error> {
171 let parsed: Option<(Decimal, SemanticCalendarUnit)> =
172 Option::deserialize(deserializer)?;
173 parsed
174 .map(|(decimal, unit)| lift(decimal).map(|magnitude| (magnitude, unit)))
175 .transpose()
176 .map_err(serde::de::Error::custom)
177 }
178 }
179}
180
181#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
182#[serde(tag = "kind", rename_all = "lowercase")]
183pub enum TypeSpecification {
184 Boolean {
185 help: String,
186 },
187 Quantity {
188 #[serde(with = "stored_quantity_declared_bound_serde::option", default)]
189 minimum: Option<(RationalInteger, String)>,
190 #[serde(with = "stored_quantity_declared_bound_serde::option", default)]
191 maximum: Option<(RationalInteger, String)>,
192 decimals: Option<u8>,
193 units: QuantityUnits,
194 #[serde(default)]
195 traits: Vec<QuantityTrait>,
196 #[serde(default)]
200 decomposition: BaseQuantityVector,
201 #[serde(default)]
204 canonical_unit: String,
205 help: String,
206 },
207 Number {
208 #[serde(with = "crate::literals::stored_rational_serde::option", default)]
209 minimum: Option<RationalInteger>,
210 #[serde(with = "crate::literals::stored_rational_serde::option", default)]
211 maximum: Option<RationalInteger>,
212 decimals: Option<u8>,
213 help: String,
214 },
215 NumberRange {
216 help: String,
217 },
218 Ratio {
219 #[serde(with = "crate::literals::stored_rational_serde::option", default)]
220 minimum: Option<RationalInteger>,
221 #[serde(with = "crate::literals::stored_rational_serde::option", default)]
222 maximum: Option<RationalInteger>,
223 decimals: Option<u8>,
224 units: RatioUnits,
225 help: String,
226 },
227 RatioRange {
228 units: RatioUnits,
229 help: String,
230 },
231 Text {
232 length: Option<usize>,
233 options: Vec<String>,
234 help: String,
235 },
236 Date {
237 minimum: Option<DateTimeValue>,
238 maximum: Option<DateTimeValue>,
239 help: String,
240 },
241 DateRange {
242 help: String,
243 },
244 Time {
245 minimum: Option<TimeValue>,
246 maximum: Option<TimeValue>,
247 help: String,
248 },
249 Calendar {
250 #[serde(with = "stored_calendar_bound_serde::option", default)]
251 minimum: Option<(RationalInteger, SemanticCalendarUnit)>,
252 #[serde(with = "stored_calendar_bound_serde::option", default)]
253 maximum: Option<(RationalInteger, SemanticCalendarUnit)>,
254 help: String,
255 },
256 CalendarRange {
257 help: String,
258 },
259 QuantityRange {
260 units: QuantityUnits,
261 #[serde(default)]
262 decomposition: BaseQuantityVector,
263 #[serde(default)]
264 canonical_unit: String,
265 help: String,
266 },
267 Veto {
268 message: Option<String>,
269 },
270 Undetermined,
274}
275
276fn require_literal<'a>(
282 args: &'a [CommandArg],
283 cmd: &str,
284) -> Result<&'a crate::literals::Value, String> {
285 let arg = args
286 .first()
287 .ok_or_else(|| format!("{} requires an argument", cmd))?;
288 match arg {
289 CommandArg::Literal(v) => Ok(v),
290 CommandArg::Label(name) => Err(format!(
291 "{} requires a literal value, got identifier '{}'",
292 cmd, name
293 )),
294 CommandArg::UnitExpr(_) => Err(format!(
295 "{} requires a literal value, got a unit expression (only valid for 'unit' command)",
296 cmd
297 )),
298 }
299}
300
301fn apply_type_help_command(help: &mut String, args: &[CommandArg]) -> Result<(), String> {
302 match require_literal(args, "help")? {
303 crate::literals::Value::Text(s) => {
304 *help = s.clone();
305 Ok(())
306 }
307 other => Err(format!(
308 "help requires a text literal (quoted string), got {}",
309 value_kind_name(other)
310 )),
311 }
312}
313
314fn calendar_unit_singular_label(unit: &crate::literals::CalendarUnit) -> &'static str {
315 match unit {
316 crate::literals::CalendarUnit::Month => "month",
317 crate::literals::CalendarUnit::Year => "year",
318 }
319}
320
321fn format_quantity_units_list(units: &QuantityUnits) -> String {
322 units
323 .iter()
324 .map(|u| u.name.as_str())
325 .collect::<Vec<_>>()
326 .join(", ")
327}
328
329#[derive(Debug, Clone, Copy, PartialEq, Eq)]
331pub(crate) enum DefaultExpectation {
332 QuantityUnits,
333 Text,
334 Number,
335 Boolean,
336 Date,
337 Time,
338 Ratio,
339 NumberRange,
340 DateRange,
341 QuantityRange,
342 RatioRange,
343 CalendarRange,
344}
345
346pub(crate) fn default_value_mismatch_error(
347 calendar_unit: &crate::literals::CalendarUnit,
348 type_name: &str,
349 expectation: DefaultExpectation,
350 quantity_units: Option<&QuantityUnits>,
351) -> String {
352 let unit_label = calendar_unit_singular_label(calendar_unit);
353 let first = format!("Unit '{unit_label}' is for calendar data.");
354 match expectation {
355 DefaultExpectation::QuantityUnits => {
356 let list = quantity_units
357 .map(format_quantity_units_list)
358 .unwrap_or_default();
359 format!("{first} Valid '{type_name}' units are: {list}.")
360 }
361 DefaultExpectation::Text => format!(
362 "{first} Please provide a text value in double quotes, for example `-> default \"my default value\"`."
363 ),
364 DefaultExpectation::Number => format!(
365 "{first} Please provide a number, for example `-> default 42`."
366 ),
367 DefaultExpectation::Boolean => format!(
368 "{first} Please provide true or false, for example `-> default true`."
369 ),
370 DefaultExpectation::Date => format!(
371 "{first} Please provide a date, for example `-> default 2024-06-15`."
372 ),
373 DefaultExpectation::Time => format!(
374 "{first} Please provide a time, for example `-> default 09:00:00`."
375 ),
376 DefaultExpectation::Ratio | DefaultExpectation::RatioRange => format!(
377 "{first} Please provide a ratio, for example `-> default 25%`."
378 ),
379 DefaultExpectation::NumberRange => format!(
380 "{first} Please provide a number range, for example `-> default 10...100`."
381 ),
382 DefaultExpectation::DateRange => format!(
383 "{first} Please provide a date range, for example `-> default 2024-01-01...2024-12-31`."
384 ),
385 DefaultExpectation::QuantityRange => format!(
386 "{first} Please provide a range with units valid for '{type_name}', for example `-> default 30 kilogram...35 kilogram`."
387 ),
388 DefaultExpectation::CalendarRange => format!(
389 "{first} Please provide a calendar range, for example `-> default 18 years...67 years`."
390 ),
391 }
392}
393
394fn quantity_default_unit_error(unit: &str, type_name: &str, units: &QuantityUnits) -> String {
395 format!(
396 "Unit '{unit}' is not defined on '{type_name}'. Valid '{type_name}' units are: {}.",
397 format_quantity_units_list(units)
398 )
399}
400
401fn quantity_default_wrong_shape_error(type_name: &str, traits: &[QuantityTrait]) -> String {
402 let example = if traits.contains(&QuantityTrait::Duration) {
403 "4 weeks"
404 } else {
405 "30 kilogram"
406 };
407 format!(
408 "Please provide a value with a unit valid for '{type_name}', for example `-> default {example}`."
409 )
410}
411
412fn validate_quantity_default_literal(
413 args: &[CommandArg],
414 type_name: &str,
415 units: &QuantityUnits,
416 traits: &[QuantityTrait],
417) -> Result<ValueKind, String> {
418 let (magnitude, unit_name) = match args {
419 [CommandArg::Literal(crate::literals::Value::NumberWithUnit(m, u))] => (*m, u.as_str()),
420 _ => return Err(quantity_default_wrong_shape_error(type_name, traits)),
421 };
422 if units.get(unit_name).is_err() {
423 return Err(quantity_default_unit_error(unit_name, type_name, units));
424 }
425 Ok(ValueKind::Quantity(
426 lift_parser_decimal(magnitude)?,
427 unit_name.to_string(),
428 BaseQuantityVector::new(),
429 ))
430}
431
432fn reject_calendar_for_default(
433 value: &crate::literals::Value,
434 type_name: &str,
435 expectation: DefaultExpectation,
436 quantity_units: Option<&QuantityUnits>,
437) -> Result<(), String> {
438 if let crate::literals::Value::Calendar(_, unit) = value {
439 return Err(default_value_mismatch_error(
440 unit,
441 type_name,
442 expectation,
443 quantity_units,
444 ));
445 }
446 Ok(())
447}
448
449fn value_kind_name(v: &crate::literals::Value) -> &'static str {
451 use crate::literals::Value;
452 match v {
453 Value::Number(_) => "number",
454 Value::NumberWithUnit(_, _) => "number_with_unit",
455 Value::Text(_) => "text",
456 Value::Date(_) => "date",
457 Value::Time(_) => "time",
458 Value::Boolean(_) => "boolean",
459 Value::Calendar(_, _) => "calendar",
460 Value::Range(_, _) => "range",
461 }
462}
463
464fn require_default_range_endpoints<'a>(
465 args: &'a [CommandArg],
466 type_name: &str,
467 expectation: DefaultExpectation,
468 quantity_units: Option<&QuantityUnits>,
469) -> Result<(&'a crate::literals::Value, &'a crate::literals::Value), String> {
470 match require_literal(args, "default")? {
471 crate::literals::Value::Calendar(_, unit) => Err(default_value_mismatch_error(
472 unit,
473 type_name,
474 expectation,
475 quantity_units,
476 )),
477 crate::literals::Value::Range(left, right) => Ok((left.as_ref(), right.as_ref())),
478 _ => Err(match expectation {
479 DefaultExpectation::NumberRange => {
480 "Please provide a number range, for example `-> default 10...100`.".to_string()
481 }
482 DefaultExpectation::DateRange => {
483 "Please provide a date range, for example `-> default 2024-01-01...2024-12-31`."
484 .to_string()
485 }
486 DefaultExpectation::RatioRange => {
487 "Please provide a ratio range, for example `-> default 10%...50%`.".to_string()
488 }
489 DefaultExpectation::QuantityRange => format!(
490 "Please provide a range with units valid for '{type_name}', for example `-> default 30 kilogram...35 kilogram`."
491 ),
492 DefaultExpectation::CalendarRange => {
493 "Please provide a calendar range, for example `-> default 18 years...67 years`."
494 .to_string()
495 }
496 _ => unreachable!("BUG: require_default_range_endpoints called with non-range expectation"),
497 }),
498 }
499}
500
501fn lift_parser_decimal(decimal: rust_decimal::Decimal) -> Result<RationalInteger, String> {
502 crate::computation::rational::decimal_to_rational(decimal)
503 .map_err(|failure| format!("literal failed rational lift: {failure}"))
504}
505
506pub fn range_element_type_specification(
509 range_spec: &TypeSpecification,
510) -> Option<TypeSpecification> {
511 match range_spec {
512 TypeSpecification::NumberRange { .. } => Some(TypeSpecification::number()),
513 TypeSpecification::QuantityRange {
514 units,
515 decomposition,
516 canonical_unit,
517 ..
518 } => Some(TypeSpecification::Quantity {
519 minimum: None,
520 maximum: None,
521 decimals: None,
522 units: units.clone(),
523 traits: Vec::new(),
524 decomposition: decomposition.clone(),
525 canonical_unit: canonical_unit.clone(),
526 help: String::new(),
527 }),
528 TypeSpecification::DateRange { .. } => Some(TypeSpecification::date()),
529 TypeSpecification::CalendarRange { .. } => Some(TypeSpecification::calendar()),
530 TypeSpecification::RatioRange { units, .. } => Some(TypeSpecification::Ratio {
531 minimum: None,
532 maximum: None,
533 decimals: None,
534 units: units.clone(),
535 help: String::new(),
536 }),
537 _ => None,
538 }
539}
540
541fn lift_range_endpoint(
545 value: &crate::parsing::ast::Value,
546 element_spec: &TypeSpecification,
547) -> Result<LiteralValue, String> {
548 use crate::parsing::ast::Value;
549 let kind = match value {
550 Value::NumberWithUnit(_, _) => parser_value_to_value_kind(value, element_spec)?,
551 _ => value_to_semantic(value)?,
552 };
553 Ok(LiteralValue {
554 value: kind,
555 lemma_type: LemmaType::primitive(element_spec.clone()),
556 })
557}
558
559fn literal_value_from_parser_value(
560 value: &crate::parsing::ast::Value,
561) -> Result<LiteralValue, String> {
562 use crate::parsing::ast::Value;
563
564 match value {
565 Value::Number(n) => Ok(LiteralValue::number(lift_parser_decimal(*n)?)),
566 Value::NumberWithUnit(n, unit) => Ok(LiteralValue::number_interpreted_as_quantity(
567 lift_parser_decimal(*n)?,
568 unit.clone(),
569 )),
570 Value::Text(s) => Ok(LiteralValue::text(s.clone())),
571 Value::Date(dt) => Ok(LiteralValue::date(date_time_to_semantic(dt))),
572 Value::Time(t) => Ok(LiteralValue::time(time_to_semantic(t))),
573 Value::Boolean(b) => Ok(LiteralValue::from_bool(bool::from(*b))),
574 Value::Calendar(n, unit) => Ok(LiteralValue::calendar(
575 lift_parser_decimal(*n)?,
576 calendar_unit_to_semantic(unit),
577 )),
578 Value::Range(left, right) => {
579 let left = literal_value_from_parser_value(left)?;
580 let right = literal_value_from_parser_value(right)?;
581 let compatible = match (
582 &left.lemma_type.specifications,
583 &right.lemma_type.specifications,
584 ) {
585 (TypeSpecification::Date { .. }, TypeSpecification::Date { .. }) => true,
586 (TypeSpecification::Number { .. }, TypeSpecification::Number { .. }) => true,
587 (TypeSpecification::Quantity { .. }, TypeSpecification::Quantity { .. }) => {
588 left.lemma_type.same_quantity_family(&right.lemma_type)
589 || left
590 .lemma_type
591 .compatible_with_anonymous_quantity(&right.lemma_type)
592 || right
593 .lemma_type
594 .compatible_with_anonymous_quantity(&left.lemma_type)
595 }
596 (TypeSpecification::Ratio { .. }, TypeSpecification::Ratio { .. }) => true,
597 (TypeSpecification::Calendar { .. }, TypeSpecification::Calendar { .. }) => true,
598 _ => false,
599 };
600 if !compatible {
601 return Err(format!(
602 "range endpoints must have the same supported base type, got {} and {}",
603 left.lemma_type.name(),
604 right.lemma_type.name()
605 ));
606 }
607 Ok(LiteralValue::range(left, right))
608 }
609 }
610}
611
612fn decimal_to_u8(d: RationalInteger, ctx: &str) -> Result<u8, String> {
614 if *d.denom() != 1 {
615 return Err(format!(
616 "{} requires a whole number, got fractional value",
617 ctx
618 ));
619 }
620 u8::try_from(*d.numer()).map_err(|_| format!("{} value out of range for u8", ctx))
621}
622
623fn decimal_to_usize(d: RationalInteger, ctx: &str) -> Result<usize, String> {
625 if *d.denom() != 1 {
626 return Err(format!(
627 "{} requires a whole number, got fractional value",
628 ctx
629 ));
630 }
631 usize::try_from(*d.numer()).map_err(|_| format!("{} value out of range for usize", ctx))
632}
633
634fn ratio_bound_to_canonical_rational(
640 args: &[CommandArg],
641 cmd: &str,
642 units: &RatioUnits,
643) -> Result<RationalInteger, String> {
644 use crate::computation::rational::{checked_div, decimal_to_rational};
645 let lit = require_literal(args, cmd)?;
646 match lit {
647 crate::literals::Value::NumberWithUnit(magnitude, unit_name) => {
648 let unit = units.get(unit_name.as_str())?;
649 let magnitude_rational = decimal_to_rational(*magnitude)
650 .map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
651 checked_div(&magnitude_rational, &unit.value)
652 .map_err(|failure| format!("{cmd}: unit conversion failed: {failure}"))
653 }
654 other => Err(format!(
655 "{cmd} requires a ratio literal with a unit, got {}",
656 value_kind_name(other)
657 )),
658 }
659}
660
661fn require_decimal_literal(args: &[CommandArg], cmd: &str) -> Result<RationalInteger, String> {
662 use crate::computation::rational::decimal_to_rational;
663 match require_literal(args, cmd)? {
664 crate::literals::Value::Number(d) => decimal_to_rational(*d)
665 .map_err(|failure| format!("{} literal failed rational lift: {}", cmd, failure)),
666 other => Err(format!(
667 "{} requires a number literal, got {}",
668 cmd,
669 value_kind_name(other)
670 )),
671 }
672}
673
674enum UnitConstraintField {
675 Minimum,
676 Maximum,
677 DefaultMagnitude,
678}
679
680fn quantity_declared_bound_to_canonical(
681 magnitude: &RationalInteger,
682 unit_name: &str,
683 units: &QuantityUnits,
684 type_name: &str,
685 command: &str,
686) -> Result<RationalInteger, String> {
687 use crate::computation::rational::checked_mul;
688 let unit = units.get(unit_name).map_err(|_| {
689 format!(
690 "Unit '{unit_name}' is not defined on '{type_name}'. Valid units are: {}.",
691 format_quantity_units_list(units)
692 )
693 })?;
694 checked_mul(magnitude, &unit.factor)
695 .map_err(|failure| format!("{command}: unit conversion overflow: {failure}"))
696}
697
698fn parse_quantity_declared_bound(
699 args: &[CommandArg],
700 cmd: &str,
701 units: &QuantityUnits,
702 type_name: &str,
703) -> Result<(RationalInteger, String), String> {
704 use crate::computation::rational::decimal_to_rational;
705 let lit = require_literal(args, cmd)?;
706 let (magnitude, unit_name) = match lit {
707 crate::literals::Value::NumberWithUnit(n, unit) => (*n, unit.clone()),
708 other => {
709 return Err(format!(
710 "{cmd} requires a quantity literal with a unit, got {}",
711 value_kind_name(other)
712 ));
713 }
714 };
715 units.get(unit_name.as_str()).map_err(|_| {
716 format!(
717 "Unit '{unit_name}' is not defined on '{type_name}'. Valid units are: {}.",
718 format_quantity_units_list(units)
719 )
720 })?;
721 let magnitude_rational = decimal_to_rational(magnitude)
722 .map_err(|failure| format!("{cmd} literal failed rational lift: {failure}"))?;
723 Ok((magnitude_rational, unit_name))
724}
725
726fn sync_quantity_units_from_canonical(
727 units: &mut QuantityUnits,
728 canonical: &RationalInteger,
729 field: UnitConstraintField,
730) -> Result<(), String> {
731 use crate::computation::rational::checked_div;
732 for unit in &mut units.0 {
733 let magnitude = checked_div(canonical, &unit.factor).map_err(|failure| {
734 format!(
735 "cannot derive per-unit constraint for unit '{}': {failure}",
736 unit.name
737 )
738 })?;
739 match field {
740 UnitConstraintField::Minimum => unit.minimum = Some(magnitude),
741 UnitConstraintField::Maximum => unit.maximum = Some(magnitude),
742 UnitConstraintField::DefaultMagnitude => unit.default_magnitude = Some(magnitude),
743 }
744 }
745 Ok(())
746}
747
748fn sync_ratio_units_from_canonical(
749 units: &mut RatioUnits,
750 canonical: &RationalInteger,
751 field: UnitConstraintField,
752) -> Result<(), String> {
753 use crate::computation::rational::checked_mul;
754 for unit in &mut units.0 {
755 let magnitude = checked_mul(canonical, &unit.value).map_err(|failure| {
756 format!(
757 "cannot derive per-unit constraint for ratio unit '{}': {failure}",
758 unit.name
759 )
760 })?;
761 match field {
762 UnitConstraintField::Minimum => unit.minimum = Some(magnitude),
763 UnitConstraintField::Maximum => unit.maximum = Some(magnitude),
764 UnitConstraintField::DefaultMagnitude => unit.default_magnitude = Some(magnitude),
765 }
766 }
767 Ok(())
768}
769
770fn sync_quantity_default_units(
771 units: &mut QuantityUnits,
772 default: &ValueKind,
773 type_name: &str,
774) -> Result<(), String> {
775 use crate::computation::rational::checked_mul;
776 let ValueKind::Quantity(magnitude, unit_name, _) = default else {
777 return Ok(());
778 };
779 let unit = units.get(unit_name).map_err(|_| {
780 format!("Default unit '{unit_name}' is not defined on quantity type '{type_name}'.")
781 })?;
782 let canonical = checked_mul(magnitude, &unit.factor)
783 .map_err(|failure| format!("default: unit conversion overflow: {failure}"))?;
784 sync_quantity_units_from_canonical(units, &canonical, UnitConstraintField::DefaultMagnitude)
785}
786
787pub(crate) fn finalize_quantity_unit_constraint_magnitudes(
788 specification: &mut TypeSpecification,
789 declared_default: Option<&ValueKind>,
790 type_name: &str,
791) -> Result<(), String> {
792 let TypeSpecification::Quantity {
793 minimum,
794 maximum,
795 units,
796 ..
797 } = specification
798 else {
799 return Ok(());
800 };
801
802 if let Some((magnitude, unit_name)) = minimum.clone() {
803 let canonical = quantity_declared_bound_to_canonical(
804 &magnitude, &unit_name, units, type_name, "minimum",
805 )?;
806 sync_quantity_units_from_canonical(units, &canonical, UnitConstraintField::Minimum)?;
807 }
808 if let Some((magnitude, unit_name)) = maximum.clone() {
809 let canonical = quantity_declared_bound_to_canonical(
810 &magnitude, &unit_name, units, type_name, "maximum",
811 )?;
812 sync_quantity_units_from_canonical(units, &canonical, UnitConstraintField::Maximum)?;
813 }
814 if let Some(default) = declared_default {
815 sync_quantity_default_units(units, default, type_name)?;
816 }
817 Ok(())
818}
819
820pub(crate) fn quantity_declared_bound_canonical(
821 bound: &(RationalInteger, String),
822 units: &QuantityUnits,
823 type_name: &str,
824 command: &str,
825) -> Result<RationalInteger, String> {
826 let (magnitude, unit_name) = bound;
827 quantity_declared_bound_to_canonical(magnitude, unit_name, units, type_name, command)
828}
829
830fn sync_ratio_default_units(units: &mut RatioUnits, default: &ValueKind) -> Result<(), String> {
831 let ValueKind::Ratio(canonical, _) = default else {
832 return Ok(());
833 };
834 sync_ratio_units_from_canonical(units, canonical, UnitConstraintField::DefaultMagnitude)
835}
836
837fn option_name(arg: &CommandArg, cmd: &str) -> Result<String, String> {
843 match arg {
844 CommandArg::Literal(crate::literals::Value::Text(s)) => Ok(s.clone()),
845 CommandArg::Label(name) => Ok(name.clone()),
846 CommandArg::Literal(other) => Err(format!(
847 "{} requires a text literal or identifier, got {}",
848 cmd,
849 value_kind_name(other)
850 )),
851 CommandArg::UnitExpr(_) => Err(format!(
852 "{} requires a text literal or identifier, got a unit expression",
853 cmd
854 )),
855 }
856}
857
858fn label_name(arg: &CommandArg, cmd: &str) -> Result<String, String> {
859 match arg {
860 CommandArg::Label(name) => Ok(name.clone()),
861 CommandArg::Literal(other) => Err(format!(
862 "{} requires an identifier, got {}",
863 cmd,
864 value_kind_name(other)
865 )),
866 CommandArg::UnitExpr(_) => Err(format!(
867 "{} requires an identifier, got a unit expression",
868 cmd
869 )),
870 }
871}
872
873fn quantity_trait_name(quantity_trait: QuantityTrait) -> &'static str {
874 match quantity_trait {
875 QuantityTrait::Duration => "duration",
876 }
877}
878
879fn parse_quantity_trait(args: &[CommandArg]) -> Result<QuantityTrait, String> {
880 if args.len() != 1 {
881 return Err("trait requires exactly one identifier argument".to_string());
882 }
883 match label_name(&args[0], "trait")?
884 .trim()
885 .to_lowercase()
886 .as_str()
887 {
888 "duration" => Ok(QuantityTrait::Duration),
889 other => Err(format!("Unknown quantity trait '{}'", other)),
890 }
891}
892
893fn validate_duration_trait_requirements(units: &QuantityUnits) -> Result<(), String> {
894 let second_unit = units
895 .iter()
896 .find(|unit| unit.name == "second")
897 .ok_or_else(|| {
898 "trait duration requires a canonical 'second' unit declared before 'trait duration'"
899 .to_string()
900 })?;
901 if !second_unit.is_canonical_factor() {
902 return Err("trait duration requires unit second 1".to_string());
903 }
904 Ok(())
905}
906
907fn require_date_literal(args: &[CommandArg], cmd: &str) -> Result<DateTimeValue, String> {
909 match require_literal(args, cmd)? {
910 crate::literals::Value::Date(dt) => Ok(dt.clone()),
911 other => Err(format!(
912 "{} requires a date literal (e.g. 2024-01-01), got {}",
913 cmd,
914 value_kind_name(other)
915 )),
916 }
917}
918
919fn require_time_literal(args: &[CommandArg], cmd: &str) -> Result<TimeValue, String> {
921 match require_literal(args, cmd)? {
922 crate::literals::Value::Time(t) => Ok(t.clone()),
923 other => Err(format!(
924 "{} requires a time literal (e.g. 12:30:00), got {}",
925 cmd,
926 value_kind_name(other)
927 )),
928 }
929}
930
931fn require_calendar_literal(
932 args: &[CommandArg],
933 cmd: &str,
934) -> Result<(RationalInteger, CalendarUnit), String> {
935 match require_literal(args, cmd)? {
936 crate::literals::Value::Calendar(d, unit) => {
937 lift_parser_decimal(*d).map(|value| (value, unit.clone()))
938 }
939 other => Err(format!(
940 "{} requires a calendar literal (e.g. 1 month), got {}",
941 cmd,
942 value_kind_name(other)
943 )),
944 }
945}
946
947#[must_use]
949pub fn default_help_for_primitive(kind: PrimitiveKind) -> &'static str {
950 use PrimitiveKind::*;
951 match kind {
952 Boolean => "Whether this holds (true or false).",
953 Number => "A dimensionless number.",
954 NumberRange => "The lower and upper bound of the number range.",
955 Text => "A text value.",
956 Quantity => "A numeric amount in one of this type's units.",
957 QuantityRange => "The lower and upper bound of the quantity range in the same unit.",
958 Ratio | Percent => "A ratio in one of this type's units (e.g. percent).",
959 RatioRange => "The lower and upper bound of the ratio range.",
960 Date => "A date, or a date and time with optional timezone.",
961 DateRange => "The start date and end date of the date range.",
962 Time => "A time of day, with optional timezone.",
963 Calendar => "A length in years or months.",
964 CalendarRange => "The lower and upper bound of the calendar range in years or months.",
965 }
966}
967
968impl TypeSpecification {
969 pub fn boolean() -> Self {
970 TypeSpecification::Boolean {
971 help: default_help_for_primitive(PrimitiveKind::Boolean).to_string(),
972 }
973 }
974 pub fn quantity() -> Self {
975 TypeSpecification::Quantity {
976 minimum: None,
977 maximum: None,
978 decimals: None,
979 units: QuantityUnits::new(),
980 traits: Vec::new(),
981 decomposition: BaseQuantityVector::new(),
982 canonical_unit: String::new(),
983 help: default_help_for_primitive(PrimitiveKind::Quantity).to_string(),
984 }
985 }
986 pub fn number() -> Self {
987 TypeSpecification::Number {
988 minimum: None,
989 maximum: None,
990 decimals: None,
991 help: default_help_for_primitive(PrimitiveKind::Number).to_string(),
992 }
993 }
994 pub fn number_range() -> Self {
995 TypeSpecification::NumberRange {
996 help: default_help_for_primitive(PrimitiveKind::NumberRange).to_string(),
997 }
998 }
999 pub fn ratio() -> Self {
1000 TypeSpecification::Ratio {
1001 minimum: None,
1002 maximum: None,
1003 decimals: None,
1004 units: RatioUnits(vec![
1005 RatioUnit {
1006 name: "percent".to_string(),
1007 value: crate::computation::rational::RationalInteger::new(100, 1),
1008 minimum: None,
1009 maximum: None,
1010 default_magnitude: None,
1011 },
1012 RatioUnit {
1013 name: "permille".to_string(),
1014 value: crate::computation::rational::RationalInteger::new(1000, 1),
1015 minimum: None,
1016 maximum: None,
1017 default_magnitude: None,
1018 },
1019 ]),
1020 help: default_help_for_primitive(PrimitiveKind::Ratio).to_string(),
1021 }
1022 }
1023 pub fn ratio_range() -> Self {
1024 TypeSpecification::RatioRange {
1025 units: match TypeSpecification::ratio() {
1026 TypeSpecification::Ratio { units, .. } => units,
1027 _ => unreachable!("BUG: ratio constructor must return a ratio type"),
1028 },
1029 help: default_help_for_primitive(PrimitiveKind::RatioRange).to_string(),
1030 }
1031 }
1032 pub fn text() -> Self {
1033 TypeSpecification::Text {
1034 length: None,
1035 options: vec![],
1036 help: default_help_for_primitive(PrimitiveKind::Text).to_string(),
1037 }
1038 }
1039 pub fn date() -> Self {
1040 TypeSpecification::Date {
1041 minimum: None,
1042 maximum: None,
1043 help: default_help_for_primitive(PrimitiveKind::Date).to_string(),
1044 }
1045 }
1046 pub fn date_range() -> Self {
1047 TypeSpecification::DateRange {
1048 help: default_help_for_primitive(PrimitiveKind::DateRange).to_string(),
1049 }
1050 }
1051 pub fn time() -> Self {
1052 TypeSpecification::Time {
1053 minimum: None,
1054 maximum: None,
1055 help: default_help_for_primitive(PrimitiveKind::Time).to_string(),
1056 }
1057 }
1058 pub fn calendar() -> Self {
1059 TypeSpecification::Calendar {
1060 minimum: None,
1061 maximum: None,
1062 help: default_help_for_primitive(PrimitiveKind::Calendar).to_string(),
1063 }
1064 }
1065 pub fn calendar_range() -> Self {
1066 TypeSpecification::CalendarRange {
1067 help: default_help_for_primitive(PrimitiveKind::CalendarRange).to_string(),
1068 }
1069 }
1070 pub fn quantity_range() -> Self {
1071 TypeSpecification::QuantityRange {
1072 units: QuantityUnits::new(),
1073 decomposition: BaseQuantityVector::new(),
1074 canonical_unit: String::new(),
1075 help: default_help_for_primitive(PrimitiveKind::QuantityRange).to_string(),
1076 }
1077 }
1078 pub fn veto() -> Self {
1079 TypeSpecification::Veto { message: None }
1080 }
1081
1082 pub fn apply_constraint(
1090 mut self,
1091 type_name: &str,
1092 command: TypeConstraintCommand,
1093 args: &[CommandArg],
1094 declared_default: &mut Option<ValueKind>,
1095 ) -> Result<Self, String> {
1096 if command == TypeConstraintCommand::Trait
1097 && !matches!(&self, TypeSpecification::Quantity { .. })
1098 {
1099 return Err("trait command is only valid on quantity types".to_string());
1100 }
1101 match &mut self {
1102 TypeSpecification::Boolean { help } => match command {
1103 TypeConstraintCommand::Help => {
1104 apply_type_help_command(help, args)?;
1105 }
1106 TypeConstraintCommand::Default => {
1107 let lit = require_literal(args, "default")?;
1108 reject_calendar_for_default(lit, type_name, DefaultExpectation::Boolean, None)?;
1109 match lit {
1110 crate::literals::Value::Boolean(bv) => {
1111 *declared_default = Some(ValueKind::Boolean(bool::from(bv)));
1112 }
1113 _ => {
1114 return Err(
1115 "Please provide true or false, for example `-> default true`."
1116 .to_string(),
1117 );
1118 }
1119 }
1120 }
1121 other => {
1122 return Err(format!(
1123 "Invalid command '{}' for boolean type. Valid commands: help, default",
1124 other
1125 ));
1126 }
1127 },
1128 TypeSpecification::Quantity {
1129 decimals,
1130 minimum,
1131 maximum,
1132 units,
1133 traits,
1134 help,
1135 ..
1136 } => match command {
1137 TypeConstraintCommand::Decimals => {
1138 let d = require_decimal_literal(args, "decimals")?;
1139 *decimals = Some(decimal_to_u8(d, "decimals")?);
1140 }
1141 TypeConstraintCommand::Unit => {
1142 let (unit_name, value, derived_quantity_factors) = match args {
1143 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
1144 (name.clone(), *v, Vec::new())
1145 }
1146 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Expr(
1147 prefix,
1148 factors,
1149 ))] => {
1150 let raw: Vec<(String, i32)> = factors
1151 .iter()
1152 .map(|f| (f.quantity_ref.clone(), f.exp))
1153 .collect();
1154 (name.clone(), *prefix, raw)
1155 }
1156 _ => {
1157 return Err(
1158 "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')"
1159 .to_string(),
1160 );
1161 }
1162 };
1163 if let Some(u) = units.0.iter_mut().find(|u| u.name == unit_name) {
1164 u.factor = crate::computation::rational::decimal_to_rational(value)
1165 .map_err(|failure| failure.to_string())?;
1166 u.derived_quantity_factors = derived_quantity_factors;
1167 } else {
1168 units.0.push(QuantityUnit::from_decimal_factor(
1169 unit_name,
1170 value,
1171 derived_quantity_factors,
1172 )?);
1173 }
1174 }
1175 TypeConstraintCommand::Trait => {
1176 let quantity_trait = parse_quantity_trait(args)?;
1177 if traits.contains(&quantity_trait) {
1178 return Err(format!(
1179 "Duplicate trait '{}' for quantity type.",
1180 quantity_trait_name(quantity_trait)
1181 ));
1182 }
1183 if quantity_trait == QuantityTrait::Duration {
1184 validate_duration_trait_requirements(units)?;
1185 }
1186 traits.push(quantity_trait);
1187 }
1188 TypeConstraintCommand::Minimum => {
1189 *minimum = Some(parse_quantity_declared_bound(
1190 args, "minimum", units, type_name,
1191 )?);
1192 }
1193 TypeConstraintCommand::Maximum => {
1194 *maximum = Some(parse_quantity_declared_bound(
1195 args, "maximum", units, type_name,
1196 )?);
1197 }
1198 TypeConstraintCommand::Help => {
1199 apply_type_help_command(help, args)?;
1200 }
1201 TypeConstraintCommand::Default => {
1202 let lit = require_literal(args, "default")?;
1203 reject_calendar_for_default(
1204 lit,
1205 type_name,
1206 DefaultExpectation::QuantityUnits,
1207 Some(units),
1208 )?;
1209 let default =
1210 validate_quantity_default_literal(args, type_name, units, traits)?;
1211 *declared_default = Some(default);
1212 }
1213 _ => {
1214 return Err(format!(
1215 "Invalid command '{}' for quantity type. Valid commands: unit, trait, minimum, maximum, decimals, help, default",
1216 command
1217 ));
1218 }
1219 },
1220 TypeSpecification::Number {
1221 decimals,
1222 minimum,
1223 maximum,
1224 help,
1225 } => match command {
1226 TypeConstraintCommand::Decimals => {
1227 let d = require_decimal_literal(args, "decimals")?;
1228 *decimals = Some(decimal_to_u8(d, "decimals")?);
1229 }
1230 TypeConstraintCommand::Unit => {
1231 return Err(
1232 "Invalid command 'unit' for number type. Number types are dimensionless and cannot have units. Use 'quantity' type instead.".to_string()
1233 );
1234 }
1235 TypeConstraintCommand::Minimum => {
1236 *minimum = Some(require_decimal_literal(args, "minimum")?);
1237 }
1238 TypeConstraintCommand::Maximum => {
1239 *maximum = Some(require_decimal_literal(args, "maximum")?);
1240 }
1241 TypeConstraintCommand::Help => {
1242 apply_type_help_command(help, args)?;
1243 }
1244 TypeConstraintCommand::Default => {
1245 let lit = require_literal(args, "default")?;
1246 reject_calendar_for_default(lit, type_name, DefaultExpectation::Number, None)?;
1247 match lit {
1248 crate::literals::Value::Number(d) => {
1249 *declared_default = Some(ValueKind::Number(lift_parser_decimal(*d)?));
1250 }
1251 _ => {
1252 return Err(
1253 "Please provide a number, for example `-> default 42`.".to_string()
1254 );
1255 }
1256 }
1257 }
1258 _ => {
1259 return Err(format!(
1260 "Invalid command '{}' for number type. Valid commands: minimum, maximum, decimals, help, default",
1261 command
1262 ));
1263 }
1264 },
1265 TypeSpecification::NumberRange { help } => match command {
1266 TypeConstraintCommand::Help => {
1267 apply_type_help_command(help, args)?;
1268 }
1269 TypeConstraintCommand::Default => {
1270 let (left, right) = require_default_range_endpoints(
1271 args,
1272 type_name,
1273 DefaultExpectation::NumberRange,
1274 None,
1275 )?;
1276 let left = literal_value_from_parser_value(left)?;
1277 let right = literal_value_from_parser_value(right)?;
1278 if !left.lemma_type.is_number() || !right.lemma_type.is_number() {
1279 return Err(
1280 "Please provide a number range, for example `-> default 10...100`."
1281 .to_string(),
1282 );
1283 }
1284 *declared_default = Some(ValueKind::Range(Box::new(left), Box::new(right)));
1285 }
1286 _ => {
1287 return Err(format!(
1288 "Invalid command '{}' for number range type. Valid commands: help, default",
1289 command
1290 ));
1291 }
1292 },
1293 TypeSpecification::Ratio {
1294 decimals,
1295 minimum,
1296 maximum,
1297 units,
1298 help,
1299 } => match command {
1300 TypeConstraintCommand::Decimals => {
1301 let d = require_decimal_literal(args, "decimals")?;
1302 *decimals = Some(decimal_to_u8(d, "decimals")?);
1303 }
1304 TypeConstraintCommand::Unit => {
1305 let (unit_name, value_dec) = match args {
1306 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
1307 (name.clone(), *v)
1308 }
1309 _ => {
1310 return Err(
1311 "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."
1312 .to_string(),
1313 );
1314 }
1315 };
1316 let value = crate::computation::rational::decimal_to_rational(value_dec)
1317 .map_err(|failure| {
1318 format!(
1319 "ratio unit value is not exactly representable as a rational: {}",
1320 failure
1321 )
1322 })?;
1323 if let Some(u) = units.0.iter_mut().find(|u| u.name == unit_name) {
1324 u.value = value;
1325 } else {
1326 units.0.push(RatioUnit {
1327 name: unit_name,
1328 value,
1329 minimum: None,
1330 maximum: None,
1331 default_magnitude: None,
1332 });
1333 }
1334 }
1335 TypeConstraintCommand::Minimum => {
1336 let canonical = ratio_bound_to_canonical_rational(args, "minimum", units)?;
1337 sync_ratio_units_from_canonical(
1338 units,
1339 &canonical,
1340 UnitConstraintField::Minimum,
1341 )?;
1342 *minimum = Some(canonical);
1343 }
1344 TypeConstraintCommand::Maximum => {
1345 let canonical = ratio_bound_to_canonical_rational(args, "maximum", units)?;
1346 sync_ratio_units_from_canonical(
1347 units,
1348 &canonical,
1349 UnitConstraintField::Maximum,
1350 )?;
1351 *maximum = Some(canonical);
1352 }
1353 TypeConstraintCommand::Help => {
1354 apply_type_help_command(help, args)?;
1355 }
1356 TypeConstraintCommand::Default => {
1357 let lit = require_literal(args, "default")?;
1358 reject_calendar_for_default(lit, type_name, DefaultExpectation::Ratio, None)?;
1359 let default = match lit {
1360 crate::literals::Value::Number(d) => {
1361 ValueKind::Ratio(lift_parser_decimal(*d)?, None)
1362 }
1363 crate::literals::Value::NumberWithUnit(_, _) => {
1364 let element_spec = TypeSpecification::Ratio {
1365 decimals: *decimals,
1366 minimum: *minimum,
1367 maximum: *maximum,
1368 units: units.clone(),
1369 help: help.clone(),
1370 };
1371 parser_value_to_value_kind(lit, &element_spec)?
1372 }
1373 _ => {
1374 return Err("Please provide a ratio value, for example `-> default 0.25` or `-> default 25%`.".to_string());
1375 }
1376 };
1377 sync_ratio_default_units(units, &default)?;
1378 *declared_default = Some(default);
1379 }
1380 _ => {
1381 return Err(format!(
1382 "Invalid command '{}' for ratio type. Valid commands: unit, minimum, maximum, decimals, help, default",
1383 command
1384 ));
1385 }
1386 },
1387 TypeSpecification::RatioRange { units, help } => {
1388 match command {
1389 TypeConstraintCommand::Unit => {
1390 let (unit_name, value_dec) = match args {
1391 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
1392 (name.clone(), *v)
1393 }
1394 _ => {
1395 return Err(
1396 "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."
1397 .to_string(),
1398 );
1399 }
1400 };
1401 let value = crate::computation::rational::decimal_to_rational(value_dec)
1402 .map_err(|e| format!("ratio unit value is not exactly representable as a rational: {e}"))?;
1403 if let Some(u) = units.0.iter_mut().find(|u| u.name == unit_name) {
1404 u.value = value;
1405 } else {
1406 units.0.push(RatioUnit {
1407 name: unit_name,
1408 value,
1409 minimum: None,
1410 maximum: None,
1411 default_magnitude: None,
1412 });
1413 }
1414 }
1415 TypeConstraintCommand::Help => {
1416 apply_type_help_command(help, args)?;
1417 }
1418 TypeConstraintCommand::Default => {
1419 let (left, right) = require_default_range_endpoints(
1420 args,
1421 type_name,
1422 DefaultExpectation::RatioRange,
1423 None,
1424 )?;
1425 let element_spec = TypeSpecification::Ratio {
1426 decimals: None,
1427 minimum: None,
1428 maximum: None,
1429 units: units.clone(),
1430 help: String::new(),
1431 };
1432 let left = lift_range_endpoint(left, &element_spec)?;
1433 let right = lift_range_endpoint(right, &element_spec)?;
1434 if !left.lemma_type.is_ratio() || !right.lemma_type.is_ratio() {
1435 return Err(
1436 "Please provide a ratio range, for example `-> default 10%...50%`."
1437 .to_string(),
1438 );
1439 }
1440 *declared_default = Some(ValueKind::Range(Box::new(left), Box::new(right)));
1441 }
1442 _ => {
1443 return Err(format!(
1444 "Invalid command '{}' for ratio range type. Valid commands: unit, help, default",
1445 command
1446 ));
1447 }
1448 }
1449 }
1450 TypeSpecification::Text {
1451 length,
1452 options,
1453 help,
1454 } => match command {
1455 TypeConstraintCommand::Option => {
1456 if args.len() != 1 {
1457 return Err("option takes exactly one argument".to_string());
1458 }
1459 options.push(option_name(&args[0], "option")?);
1460 }
1461 TypeConstraintCommand::Options => {
1462 let mut collected = Vec::with_capacity(args.len());
1463 for arg in args {
1464 collected.push(option_name(arg, "options")?);
1465 }
1466 *options = collected;
1467 }
1468 TypeConstraintCommand::Length => {
1469 let d = require_decimal_literal(args, "length")?;
1470 *length = Some(decimal_to_usize(d, "length")?);
1471 }
1472 TypeConstraintCommand::Help => {
1473 apply_type_help_command(help, args)?;
1474 }
1475 TypeConstraintCommand::Default => {
1476 let lit = require_literal(args, "default")?;
1477 reject_calendar_for_default(lit, type_name, DefaultExpectation::Text, None)?;
1478 match lit {
1479 crate::literals::Value::Text(s) => {
1480 *declared_default = Some(ValueKind::Text(s.clone()));
1481 }
1482 _ => {
1483 return Err(
1484 "Please provide a text value in double quotes, for example `-> default \"my default value\"`."
1485 .to_string(),
1486 );
1487 }
1488 }
1489 }
1490 _ => {
1491 return Err(format!(
1492 "Invalid command '{}' for text type. Valid commands: options, length, help, default",
1493 command
1494 ));
1495 }
1496 },
1497 TypeSpecification::Date {
1498 minimum,
1499 maximum,
1500 help,
1501 } => match command {
1502 TypeConstraintCommand::Minimum => {
1503 let dt = require_date_literal(args, "minimum")?;
1504 *minimum = Some(dt);
1505 }
1506 TypeConstraintCommand::Maximum => {
1507 let dt = require_date_literal(args, "maximum")?;
1508 *maximum = Some(dt);
1509 }
1510 TypeConstraintCommand::Help => {
1511 apply_type_help_command(help, args)?;
1512 }
1513 TypeConstraintCommand::Default => {
1514 let lit = require_literal(args, "default")?;
1515 reject_calendar_for_default(lit, type_name, DefaultExpectation::Date, None)?;
1516 match lit {
1517 crate::literals::Value::Date(dt) => {
1518 *declared_default = Some(ValueKind::Date(date_time_to_semantic(dt)));
1519 }
1520 _ => {
1521 return Err(
1522 "Please provide a date, for example `-> default 2024-06-15`."
1523 .to_string(),
1524 );
1525 }
1526 }
1527 }
1528 _ => {
1529 return Err(format!(
1530 "Invalid command '{}' for date type. Valid commands: minimum, maximum, help, default",
1531 command
1532 ));
1533 }
1534 },
1535 TypeSpecification::DateRange { help } => match command {
1536 TypeConstraintCommand::Help => {
1537 apply_type_help_command(help, args)?;
1538 }
1539 TypeConstraintCommand::Default => {
1540 let (left, right) = require_default_range_endpoints(
1541 args,
1542 type_name,
1543 DefaultExpectation::DateRange,
1544 None,
1545 )?;
1546 let left = literal_value_from_parser_value(left)?;
1547 let right = literal_value_from_parser_value(right)?;
1548 if !left.lemma_type.is_date() || !right.lemma_type.is_date() {
1549 return Err(
1550 "Please provide a date range, for example `-> default 2024-01-01...2024-12-31`."
1551 .to_string(),
1552 );
1553 }
1554 *declared_default = Some(ValueKind::Range(Box::new(left), Box::new(right)));
1555 }
1556 _ => {
1557 return Err(format!(
1558 "Invalid command '{}' for date range type. Valid commands: help, default",
1559 command
1560 ));
1561 }
1562 },
1563 TypeSpecification::Time {
1564 minimum,
1565 maximum,
1566 help,
1567 } => match command {
1568 TypeConstraintCommand::Minimum => {
1569 let t = require_time_literal(args, "minimum")?;
1570 *minimum = Some(t);
1571 }
1572 TypeConstraintCommand::Maximum => {
1573 let t = require_time_literal(args, "maximum")?;
1574 *maximum = Some(t);
1575 }
1576 TypeConstraintCommand::Help => {
1577 apply_type_help_command(help, args)?;
1578 }
1579 TypeConstraintCommand::Default => {
1580 let lit = require_literal(args, "default")?;
1581 reject_calendar_for_default(lit, type_name, DefaultExpectation::Time, None)?;
1582 match lit {
1583 crate::literals::Value::Time(t) => {
1584 *declared_default = Some(ValueKind::Time(time_to_semantic(t)));
1585 }
1586 _ => {
1587 return Err(
1588 "Please provide a time, for example `-> default 09:00:00`."
1589 .to_string(),
1590 );
1591 }
1592 }
1593 }
1594 _ => {
1595 return Err(format!(
1596 "Invalid command '{}' for time type. Valid commands: minimum, maximum, help, default",
1597 command
1598 ));
1599 }
1600 },
1601 TypeSpecification::Calendar {
1602 minimum,
1603 maximum,
1604 help,
1605 } => match command {
1606 TypeConstraintCommand::Help => {
1607 apply_type_help_command(help, args)?;
1608 }
1609 TypeConstraintCommand::Minimum => {
1610 let (value, unit) = require_calendar_literal(args, "minimum")?;
1611 *minimum = Some((value, calendar_unit_to_semantic(&unit)));
1612 }
1613 TypeConstraintCommand::Maximum => {
1614 let (value, unit) = require_calendar_literal(args, "maximum")?;
1615 *maximum = Some((value, calendar_unit_to_semantic(&unit)));
1616 }
1617 TypeConstraintCommand::Default => {
1618 let (value, unit) = require_calendar_literal(args, "default")?;
1619 *declared_default =
1620 Some(ValueKind::Calendar(value, calendar_unit_to_semantic(&unit)));
1621 }
1622 _ => {
1623 return Err(format!(
1624 "Invalid command '{}' for calendar type. Valid commands: minimum, maximum, help, default",
1625 command
1626 ));
1627 }
1628 },
1629 TypeSpecification::CalendarRange { help } => match command {
1630 TypeConstraintCommand::Help => {
1631 apply_type_help_command(help, args)?;
1632 }
1633 TypeConstraintCommand::Default => {
1634 let (left, right) = require_default_range_endpoints(
1635 args,
1636 type_name,
1637 DefaultExpectation::CalendarRange,
1638 None,
1639 )?;
1640 let left = literal_value_from_parser_value(left)?;
1641 let right = literal_value_from_parser_value(right)?;
1642 if !left.lemma_type.is_calendar() || !right.lemma_type.is_calendar() {
1643 return Err(
1644 "Please provide a calendar range, for example `-> default 18 years...67 years`."
1645 .to_string(),
1646 );
1647 }
1648 *declared_default = Some(ValueKind::Range(Box::new(left), Box::new(right)));
1649 }
1650 _ => {
1651 return Err(format!(
1652 "Invalid command '{}' for calendar range type. Valid commands: help, default",
1653 command
1654 ));
1655 }
1656 },
1657 TypeSpecification::QuantityRange { units, help, .. } => match command {
1658 TypeConstraintCommand::Unit => {
1659 let (unit_name, value, derived_quantity_factors) = match args {
1660 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(v))] => {
1661 (name.clone(), *v, Vec::new())
1662 }
1663 [CommandArg::Label(name), CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Expr(
1664 prefix,
1665 factors,
1666 ))] => {
1667 let raw: Vec<(String, i32)> = factors
1668 .iter()
1669 .map(|f| (f.quantity_ref.clone(), f.exp))
1670 .collect();
1671 (name.clone(), *prefix, raw)
1672 }
1673 _ => {
1674 return Err(
1675 "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')"
1676 .to_string(),
1677 );
1678 }
1679 };
1680 if let Some(u) = units.0.iter_mut().find(|u| u.name == unit_name) {
1681 u.factor = crate::computation::rational::decimal_to_rational(value)
1682 .map_err(|failure| failure.to_string())?;
1683 u.derived_quantity_factors = derived_quantity_factors;
1684 } else {
1685 units.0.push(QuantityUnit::from_decimal_factor(
1686 unit_name,
1687 value,
1688 derived_quantity_factors,
1689 )?);
1690 }
1691 }
1692 TypeConstraintCommand::Help => {
1693 apply_type_help_command(help, args)?;
1694 }
1695 TypeConstraintCommand::Default => {
1696 let (left, right) = require_default_range_endpoints(
1697 args,
1698 type_name,
1699 DefaultExpectation::QuantityRange,
1700 Some(units),
1701 )?;
1702 let left = literal_value_from_parser_value(left)?;
1703 let right = literal_value_from_parser_value(right)?;
1704 if !left.lemma_type.is_quantity() || !right.lemma_type.is_quantity() {
1705 return Err(format!(
1706 "Please provide a range with units valid for '{type_name}', for example `-> default 30 kilogram...35 kilogram`."
1707 ));
1708 }
1709 *declared_default = Some(ValueKind::Range(Box::new(left), Box::new(right)));
1710 }
1711 _ => {
1712 return Err(format!(
1713 "Invalid command '{}' for quantity range type. Valid commands: unit, help, default",
1714 command
1715 ));
1716 }
1717 },
1718 TypeSpecification::Veto { .. } => {
1719 return Err(format!(
1720 "Invalid command '{}' for veto type. Veto is not a user-declarable type and cannot have constraints",
1721 command
1722 ));
1723 }
1724 TypeSpecification::Undetermined => {
1725 return Err(format!(
1726 "Invalid command '{}' for undetermined sentinel type. Undetermined is an internal type used during type inference and cannot have constraints",
1727 command
1728 ));
1729 }
1730 }
1731 Ok(self)
1732 }
1733}
1734
1735pub fn parse_number_unit(
1738 value_str: &str,
1739 type_spec: &TypeSpecification,
1740) -> Result<crate::parsing::ast::Value, String> {
1741 use crate::literals::{NumberWithUnit, RatioLiteral};
1742 use crate::parsing::ast::Value;
1743
1744 let trimmed = value_str.trim();
1745 match type_spec {
1746 TypeSpecification::Quantity { units, .. } => {
1747 if units.is_empty() {
1748 unreachable!(
1749 "BUG: Quantity type has no units; should have been validated during planning"
1750 );
1751 }
1752 match trimmed.parse::<NumberWithUnit>() {
1753 Ok(n) => {
1754 let unit = units.get(&n.1).map_err(|e| e.to_string())?;
1755 Ok(Value::NumberWithUnit(n.0, unit.name.clone()))
1756 }
1757 Err(e) => {
1758 if trimmed.split_whitespace().count() == 1 && !trimmed.is_empty() {
1759 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
1760 let example_unit = units
1761 .iter()
1762 .next()
1763 .expect("BUG: units non-empty after guard")
1764 .name
1765 .as_str();
1766 Err(format!(
1767 "Quantity value must include a unit, for example: '{} {}'. Valid units: {}.",
1768 trimmed,
1769 example_unit,
1770 valid.join(", ")
1771 ))
1772 } else {
1773 Err(e)
1774 }
1775 }
1776 }
1777 }
1778 TypeSpecification::Ratio { units, .. } => {
1779 if units.is_empty() {
1780 unreachable!(
1781 "BUG: Ratio type has no units; should have been validated during planning"
1782 );
1783 }
1784 match trimmed.parse::<RatioLiteral>()? {
1785 RatioLiteral::Bare(_) => {
1786 Err("Ratio value requires a unit (e.g. '50%', '500 basis_points').".to_string())
1787 }
1788 RatioLiteral::Percent(n) => {
1789 let unit = units.get("percent").map_err(|e| e.to_string())?;
1790 Ok(Value::NumberWithUnit(n, unit.name.clone()))
1791 }
1792 RatioLiteral::Permille(n) => {
1793 let unit = units.get("permille").map_err(|e| e.to_string())?;
1794 Ok(Value::NumberWithUnit(n, unit.name.clone()))
1795 }
1796 RatioLiteral::Named { value, unit } => {
1797 let resolved = units.get(&unit).map_err(|e| e.to_string())?;
1798 Ok(Value::NumberWithUnit(value, resolved.name.clone()))
1799 }
1800 }
1801 }
1802 _ => Err("parse_number_unit only accepts Quantity or Ratio type".to_string()),
1803 }
1804}
1805
1806pub fn parse_data_value_from_json(
1808 value: &serde_json::Value,
1809 type_spec: &TypeSpecification,
1810 lemma_type: &LemmaType,
1811 source: &Source,
1812) -> Result<LiteralValue, Error> {
1813 let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
1814
1815 let kind = if let Some(s) = value.as_str() {
1816 let parsed = parse_value_from_string(s, type_spec, source)?;
1817 parser_value_to_value_kind(&parsed, type_spec).map_err(to_err)?
1818 } else if let Some(b) = value.as_bool() {
1819 if !matches!(type_spec, TypeSpecification::Boolean { .. }) {
1820 return Err(to_err(format!(
1821 "JSON boolean is only valid for boolean data, not {}",
1822 value_kind_tag_for_type(type_spec)
1823 )));
1824 }
1825 ValueKind::Boolean(b)
1826 } else if let Some(n) = value.as_number() {
1827 let parsed = parse_value_from_string(&n.to_string(), type_spec, source)?;
1828 parser_value_to_value_kind(&parsed, type_spec).map_err(to_err)?
1829 } else if let Some(obj) = value.as_object() {
1830 if obj.len() == 2 && obj.contains_key("value") && obj.contains_key("unit") {
1831 let tagged = serde_json::json!({ value_kind_tag_for_type(type_spec): value });
1832 serde_json::from_value::<ValueKind>(tagged).map_err(|e| to_err(e.to_string()))?
1833 } else {
1834 serde_json::from_value::<ValueKind>(value.clone()).map_err(|e| to_err(e.to_string()))?
1835 }
1836 } else {
1837 return Err(to_err("unsupported JSON value for data input".to_string()));
1838 };
1839
1840 Ok(LiteralValue {
1841 value: kind,
1842 lemma_type: lemma_type.clone(),
1843 })
1844}
1845
1846fn value_kind_tag_for_type(spec: &TypeSpecification) -> &'static str {
1847 match spec {
1848 TypeSpecification::Boolean { .. } => "boolean",
1849 TypeSpecification::Quantity { .. } => "quantity",
1850 TypeSpecification::Number { .. } => "number",
1851 TypeSpecification::NumberRange { .. }
1852 | TypeSpecification::QuantityRange { .. }
1853 | TypeSpecification::DateRange { .. }
1854 | TypeSpecification::RatioRange { .. }
1855 | TypeSpecification::CalendarRange { .. } => "range",
1856 TypeSpecification::Ratio { .. } => "ratio",
1857 TypeSpecification::Text { .. } => "text",
1858 TypeSpecification::Date { .. } => "date",
1859 TypeSpecification::Time { .. } => "time",
1860 TypeSpecification::Calendar { .. } => "calendar",
1861 TypeSpecification::Veto { .. } => "veto",
1862 TypeSpecification::Undetermined => "undetermined",
1863 }
1864}
1865
1866pub fn parse_value_from_string(
1869 value_str: &str,
1870 type_spec: &TypeSpecification,
1871 source: &Source,
1872) -> Result<crate::parsing::ast::Value, Error> {
1873 use crate::parsing::ast::Value;
1874
1875 let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
1876
1877 let parse_range_value = |element_spec: TypeSpecification| -> Result<Value, Error> {
1878 let (left_str, right_str) = value_str.split_once("...").ok_or_else(|| {
1879 to_err("Range value must use '...' between the two endpoints".to_string())
1880 })?;
1881 if left_str.trim().is_empty() || right_str.trim().is_empty() {
1882 return Err(to_err(
1883 "Range value must contain a non-empty left and right endpoint".to_string(),
1884 ));
1885 }
1886 let left = parse_value_from_string(left_str.trim(), &element_spec, source)?;
1887 let right = parse_value_from_string(right_str.trim(), &element_spec, source)?;
1888 Ok(Value::Range(Box::new(left), Box::new(right)))
1889 };
1890
1891 match type_spec {
1892 TypeSpecification::Text { .. } => value_str
1893 .parse::<crate::literals::TextLiteral>()
1894 .map(|t| Value::Text(t.0))
1895 .map_err(to_err),
1896 TypeSpecification::Number { .. } => value_str
1897 .parse::<crate::literals::NumberLiteral>()
1898 .map(|n| Value::Number(n.0))
1899 .map_err(to_err),
1900 TypeSpecification::Quantity { .. } => {
1901 parse_number_unit(value_str, type_spec).map_err(to_err)
1902 }
1903 TypeSpecification::Boolean { .. } => value_str
1904 .parse::<BooleanValue>()
1905 .map(Value::Boolean)
1906 .map_err(to_err),
1907 TypeSpecification::Date { .. } => {
1908 let date = value_str.parse::<DateTimeValue>().map_err(to_err)?;
1909 Ok(Value::Date(date))
1910 }
1911 TypeSpecification::Time { .. } => {
1912 let time = value_str.parse::<TimeValue>().map_err(to_err)?;
1913 Ok(Value::Time(time))
1914 }
1915 TypeSpecification::Calendar { .. } => value_str
1916 .parse::<crate::literals::CalendarLiteral>()
1917 .map(|d| Value::Calendar(d.0, d.1))
1918 .map_err(to_err),
1919 TypeSpecification::Ratio { .. } => {
1920 parse_number_unit(value_str, type_spec).map_err(to_err)
1921 }
1922 TypeSpecification::NumberRange { .. }
1923 | TypeSpecification::QuantityRange { .. }
1924 | TypeSpecification::DateRange { .. }
1925 | TypeSpecification::CalendarRange { .. }
1926 | TypeSpecification::RatioRange { .. } => {
1927 let element_spec = range_element_type_specification(type_spec).unwrap_or_else(|| {
1928 unreachable!("BUG: range_element_type_specification missing arm for known range type")
1929 });
1930 parse_range_value(element_spec)
1931 }
1932 TypeSpecification::Veto { .. } => Err(to_err(
1933 "Veto type cannot be parsed from string".to_string(),
1934 )),
1935 TypeSpecification::Undetermined => unreachable!(
1936 "BUG: parse_value_from_string called with Undetermined sentinel type; this type exists only during type inference"
1937 ),
1938 }
1939}
1940
1941#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1946#[serde(rename_all = "snake_case")]
1947pub enum SemanticCalendarUnit {
1948 Month,
1949 Year,
1950}
1951
1952impl fmt::Display for SemanticCalendarUnit {
1953 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1954 let s = match self {
1955 SemanticCalendarUnit::Month => "months",
1956 SemanticCalendarUnit::Year => "years",
1957 };
1958 write!(f, "{}", s)
1959 }
1960}
1961
1962#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1966#[serde(rename_all = "snake_case")]
1967pub enum SemanticConversionTarget {
1968 Calendar(SemanticCalendarUnit),
1969 QuantityUnit(String),
1970 RatioUnit(String),
1971 Number,
1973}
1974
1975impl fmt::Display for SemanticConversionTarget {
1976 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1977 match self {
1978 SemanticConversionTarget::Calendar(u) => write!(f, "{}", u),
1979 SemanticConversionTarget::QuantityUnit(s) => write!(f, "{}", s),
1980 SemanticConversionTarget::RatioUnit(s) => write!(f, "{}", s),
1981 SemanticConversionTarget::Number => write!(f, "number"),
1982 }
1983 }
1984}
1985
1986#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1988pub struct SemanticTimezone {
1989 pub offset_hours: i8,
1990 pub offset_minutes: u8,
1991}
1992
1993impl fmt::Display for SemanticTimezone {
1994 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1995 if self.offset_hours == 0 && self.offset_minutes == 0 {
1996 write!(f, "Z")
1997 } else {
1998 let sign = if self.offset_hours >= 0 { "+" } else { "-" };
1999 let hours = self.offset_hours.abs();
2000 write!(f, "{}{:02}:{:02}", sign, hours, self.offset_minutes)
2001 }
2002 }
2003}
2004
2005#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2007pub struct SemanticTime {
2008 pub hour: u32,
2009 pub minute: u32,
2010 pub second: u32,
2011 pub microsecond: u32,
2012 pub timezone: Option<SemanticTimezone>,
2013}
2014
2015impl fmt::Display for SemanticTime {
2016 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2017 write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
2018 if self.microsecond != 0 {
2019 write!(f, ".{:06}", self.microsecond)?;
2020 }
2021 if let Some(timezone) = &self.timezone {
2022 write!(f, "{}", timezone)?;
2023 }
2024 Ok(())
2025 }
2026}
2027
2028#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2030pub struct SemanticDateTime {
2031 pub year: i32,
2032 pub month: u32,
2033 pub day: u32,
2034 pub hour: u32,
2035 pub minute: u32,
2036 pub second: u32,
2037 #[serde(default)]
2038 pub microsecond: u32,
2039 pub timezone: Option<SemanticTimezone>,
2040}
2041
2042impl fmt::Display for SemanticDateTime {
2043 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2044 let has_time = self.hour != 0
2045 || self.minute != 0
2046 || self.second != 0
2047 || self.microsecond != 0
2048 || self.timezone.is_some();
2049 if !has_time {
2050 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
2051 } else {
2052 write!(
2053 f,
2054 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
2055 self.year, self.month, self.day, self.hour, self.minute, self.second
2056 )?;
2057 if self.microsecond != 0 {
2058 write!(f, ".{:06}", self.microsecond)?;
2059 }
2060 if let Some(tz) = &self.timezone {
2061 write!(f, "{}", tz)?;
2062 }
2063 Ok(())
2064 }
2065 }
2066}
2067
2068#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2071pub enum ValueKind {
2072 Number(RationalInteger),
2073 Quantity(RationalInteger, String, BaseQuantityVector),
2081 Text(String),
2082 Date(SemanticDateTime),
2083 Time(SemanticTime),
2084 Boolean(bool),
2085 Calendar(RationalInteger, SemanticCalendarUnit),
2087 Ratio(RationalInteger, Option<String>),
2089 Range(Box<LiteralValue>, Box<LiteralValue>),
2090}
2091
2092fn format_rational_magnitude_for_display(rational: &RationalInteger) -> String {
2093 crate::computation::rational::rational_to_display_str(rational)
2094}
2095
2096fn format_number_with_unit_for_display(rational: &RationalInteger, unit: &str) -> String {
2097 use crate::computation::rational::{commit_rational_to_decimal, rational_to_display_str};
2098 use crate::parsing::ast::Value;
2099 match commit_rational_to_decimal(rational) {
2100 Ok(decimal) => format!("{}", Value::NumberWithUnit(decimal, unit.to_string())),
2101 Err(_) => format!("{} {}", rational_to_display_str(rational), unit),
2102 }
2103}
2104
2105impl fmt::Display for ValueKind {
2106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2107 use crate::computation::rational::{checked_mul, rational_to_display_str};
2108 match self {
2109 ValueKind::Number(rational) => {
2110 write!(f, "{}", format_rational_magnitude_for_display(rational))
2111 }
2112 ValueKind::Quantity(rational, unit, _decomp) => {
2113 write!(f, "{}", format_number_with_unit_for_display(rational, unit))
2114 }
2115 ValueKind::Text(s) => write!(f, "{}", crate::parsing::ast::Value::Text(s.clone())),
2116 ValueKind::Ratio(rational, unit) => match unit.as_deref() {
2117 Some("percent") => {
2118 let display = match checked_mul(rational, &RationalInteger::new(100, 1)) {
2119 Ok(scaled) => format_number_with_unit_for_display(&scaled, "percent"),
2120 Err(_) => format!("{} percent", rational_to_display_str(rational)),
2121 };
2122 write!(f, "{}", display)
2123 }
2124 Some("permille") => {
2125 let display = match checked_mul(rational, &RationalInteger::new(1000, 1)) {
2126 Ok(scaled) => format_number_with_unit_for_display(&scaled, "permille"),
2127 Err(_) => format!("{} permille", rational_to_display_str(rational)),
2128 };
2129 write!(f, "{}", display)
2130 }
2131 Some(unit_name) => {
2132 write!(
2133 f,
2134 "{}",
2135 format_number_with_unit_for_display(rational, unit_name)
2136 )
2137 }
2138 None => write!(f, "{}", format_rational_magnitude_for_display(rational)),
2139 },
2140 ValueKind::Date(dt) => write!(f, "{}", dt),
2141 ValueKind::Time(t) => write!(
2142 f,
2143 "{}",
2144 crate::parsing::ast::Value::Time(crate::parsing::ast::TimeValue {
2145 hour: t.hour as u8,
2146 minute: t.minute as u8,
2147 second: t.second as u8,
2148 microsecond: t.microsecond,
2149 timezone: t
2150 .timezone
2151 .as_ref()
2152 .map(|tz| crate::parsing::ast::TimezoneValue {
2153 offset_hours: tz.offset_hours,
2154 offset_minutes: tz.offset_minutes,
2155 }),
2156 })
2157 ),
2158 ValueKind::Boolean(b) => write!(f, "{}", b),
2159 ValueKind::Calendar(rational, unit) => write!(
2160 f,
2161 "{} {}",
2162 format_rational_magnitude_for_display(rational),
2163 unit
2164 ),
2165 ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
2166 }
2167 }
2168}
2169
2170fn decimal_from_serialized_str(s: &str) -> Result<Decimal, String> {
2171 Decimal::from_str(s.trim()).map_err(|e| format!("invalid decimal '{s}': {e}"))
2172}
2173
2174#[derive(Serialize, Deserialize)]
2175struct SerializedValueUnit {
2176 value: String,
2177 unit: String,
2178}
2179
2180#[derive(Serialize, Deserialize)]
2181struct SerializedRange {
2182 from: ValueKind,
2183 to: ValueKind,
2184}
2185
2186impl Serialize for ValueKind {
2187 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2188 use serde::ser::SerializeMap;
2189 let mut map = serializer.serialize_map(Some(1))?;
2190 match self {
2191 ValueKind::Number(rational) => {
2192 map.serialize_entry(
2193 "number",
2194 &crate::literals::rational_to_serialized_str(rational)
2195 .map_err(serde::ser::Error::custom)?,
2196 )?;
2197 }
2198 ValueKind::Quantity(rational, unit, _) => {
2199 map.serialize_entry(
2200 "quantity",
2201 &SerializedValueUnit {
2202 value: crate::literals::rational_to_serialized_str(rational)
2203 .map_err(serde::ser::Error::custom)?,
2204 unit: unit.clone(),
2205 },
2206 )?;
2207 }
2208 ValueKind::Text(s) => {
2209 map.serialize_entry("text", s)?;
2210 }
2211 ValueKind::Date(dt) => {
2212 map.serialize_entry("date", dt)?;
2213 }
2214 ValueKind::Time(t) => {
2215 map.serialize_entry("time", t)?;
2216 }
2217 ValueKind::Boolean(b) => {
2218 map.serialize_entry("boolean", b)?;
2219 }
2220 ValueKind::Calendar(rational, unit) => {
2221 map.serialize_entry(
2222 "calendar",
2223 &SerializedValueUnit {
2224 value: crate::literals::rational_to_serialized_str(rational)
2225 .map_err(serde::ser::Error::custom)?,
2226 unit: unit.to_string(),
2227 },
2228 )?;
2229 }
2230 ValueKind::Ratio(rational, unit) => {
2231 map.serialize_entry(
2232 "ratio",
2233 &SerializedValueUnit {
2234 value: crate::literals::rational_to_serialized_str(rational)
2235 .map_err(serde::ser::Error::custom)?,
2236 unit: unit.clone().unwrap_or_default(),
2237 },
2238 )?;
2239 }
2240 ValueKind::Range(left, right) => {
2241 map.serialize_entry(
2242 "range",
2243 &SerializedRange {
2244 from: left.value.clone(),
2245 to: right.value.clone(),
2246 },
2247 )?;
2248 }
2249 }
2250 map.end()
2251 }
2252}
2253
2254impl<'de> Deserialize<'de> for ValueKind {
2255 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2256 let map = <serde_json::Map<String, serde_json::Value>>::deserialize(deserializer)?;
2257 if map.len() != 1 {
2258 return Err(serde::de::Error::custom(format!(
2259 "ValueKind must have exactly one variant key, got {}",
2260 map.len()
2261 )));
2262 }
2263 let (tag, payload) = map.into_iter().next().expect("BUG: len checked");
2264 deserialize_value_kind_variant(&tag, payload).map_err(serde::de::Error::custom)
2265 }
2266}
2267
2268fn deserialize_value_kind_variant(
2269 tag: &str,
2270 payload: serde_json::Value,
2271) -> Result<ValueKind, String> {
2272 match tag {
2273 "number" => {
2274 let s = payload
2275 .as_str()
2276 .ok_or_else(|| "number must be a JSON string".to_string())?;
2277 let decimal = decimal_from_serialized_str(s)?;
2278 Ok(ValueKind::Number(
2279 crate::literals::rational_from_parsed_decimal(decimal)?,
2280 ))
2281 }
2282 "quantity" => {
2283 let pair: SerializedValueUnit =
2284 serde_json::from_value(payload).map_err(|e| e.to_string())?;
2285 let decimal = decimal_from_serialized_str(&pair.value)?;
2286 Ok(ValueKind::Quantity(
2287 crate::literals::rational_from_parsed_decimal(decimal)?,
2288 pair.unit,
2289 BaseQuantityVector::new(),
2290 ))
2291 }
2292 "ratio" => {
2293 let pair: SerializedValueUnit =
2294 serde_json::from_value(payload).map_err(|e| e.to_string())?;
2295 let unit = if pair.unit.is_empty() {
2296 None
2297 } else {
2298 Some(pair.unit)
2299 };
2300 let decimal = decimal_from_serialized_str(&pair.value)?;
2301 Ok(ValueKind::Ratio(
2302 crate::literals::rational_from_parsed_decimal(decimal)?,
2303 unit,
2304 ))
2305 }
2306 "calendar" => {
2307 let pair: SerializedValueUnit =
2308 serde_json::from_value(payload).map_err(|e| e.to_string())?;
2309 let unit = match pair.unit.as_str() {
2310 "months" => SemanticCalendarUnit::Month,
2311 "years" => SemanticCalendarUnit::Year,
2312 other => {
2313 return Err(format!(
2314 "unknown calendar unit '{other}' (expected 'months' or 'years')"
2315 ));
2316 }
2317 };
2318 let decimal = decimal_from_serialized_str(&pair.value)?;
2319 Ok(ValueKind::Calendar(
2320 crate::literals::rational_from_parsed_decimal(decimal)?,
2321 unit,
2322 ))
2323 }
2324 "text" => {
2325 let s = payload
2326 .as_str()
2327 .ok_or_else(|| "text must be a JSON string".to_string())?;
2328 Ok(ValueKind::Text(s.to_string()))
2329 }
2330 "date" => {
2331 let dt: SemanticDateTime =
2332 serde_json::from_value(payload).map_err(|e| e.to_string())?;
2333 Ok(ValueKind::Date(dt))
2334 }
2335 "time" => {
2336 let t: SemanticTime = serde_json::from_value(payload).map_err(|e| e.to_string())?;
2337 Ok(ValueKind::Time(t))
2338 }
2339 "boolean" => {
2340 let b = payload
2341 .as_bool()
2342 .ok_or_else(|| "boolean must be a JSON bool".to_string())?;
2343 Ok(ValueKind::Boolean(b))
2344 }
2345 "range" => {
2346 let range: SerializedRange =
2347 serde_json::from_value(payload).map_err(|e| e.to_string())?;
2348 Ok(ValueKind::Range(
2349 Box::new(LiteralValue {
2350 value: range.from,
2351 lemma_type: primitive_number().clone(),
2352 }),
2353 Box::new(LiteralValue {
2354 value: range.to,
2355 lemma_type: primitive_number().clone(),
2356 }),
2357 ))
2358 }
2359 other => Err(format!("unknown ValueKind variant '{other}'")),
2360 }
2361}
2362
2363#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2372pub struct PathSegment {
2373 pub data: String,
2375 pub spec: String,
2377}
2378
2379#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2383pub struct DataPath {
2384 pub segments: Vec<PathSegment>,
2386 pub data: String,
2388}
2389
2390impl DataPath {
2391 pub fn new(segments: Vec<PathSegment>, data: String) -> Self {
2393 Self { segments, data }
2394 }
2395
2396 pub fn local(data: String) -> Self {
2398 Self {
2399 segments: vec![],
2400 data,
2401 }
2402 }
2403
2404 pub fn input_key(&self) -> String {
2407 let mut s = String::new();
2408 for segment in &self.segments {
2409 s.push_str(&segment.data);
2410 s.push('.');
2411 }
2412 s.push_str(&self.data);
2413 s
2414 }
2415}
2416
2417#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
2421pub struct RulePath {
2422 pub segments: Vec<PathSegment>,
2424 pub rule: String,
2426}
2427
2428impl RulePath {
2429 pub fn new(segments: Vec<PathSegment>, rule: String) -> Self {
2431 Self { segments, rule }
2432 }
2433}
2434
2435#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2444pub struct Expression {
2445 pub kind: ExpressionKind,
2446 pub source_location: Option<Source>,
2447}
2448
2449impl Expression {
2450 pub fn new(kind: ExpressionKind, source_location: Source) -> Self {
2451 Self {
2452 kind,
2453 source_location: Some(source_location),
2454 }
2455 }
2456
2457 pub fn with_source(kind: ExpressionKind, source_location: Option<Source>) -> Self {
2459 Self {
2460 kind,
2461 source_location,
2462 }
2463 }
2464
2465 pub fn collect_data_paths(&self, data: &mut std::collections::HashSet<DataPath>) {
2467 self.kind.collect_data_paths(data);
2468 }
2469}
2470
2471#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2473#[serde(rename_all = "snake_case")]
2474pub enum ExpressionKind {
2475 Literal(Box<LiteralValue>),
2477 DataPath(DataPath),
2479 RulePath(RulePath),
2481 LogicalAnd(Arc<Expression>, Arc<Expression>),
2482 LogicalOr(Arc<Expression>, Arc<Expression>),
2483 Arithmetic(Arc<Expression>, ArithmeticComputation, Arc<Expression>),
2484 Comparison(Arc<Expression>, ComparisonComputation, Arc<Expression>),
2485 UnitConversion(Arc<Expression>, SemanticConversionTarget),
2486 LogicalNegation(Arc<Expression>, NegationType),
2487 MathematicalComputation(MathematicalComputation, Arc<Expression>),
2488 Veto(VetoExpression),
2489 Now,
2491 DateRelative(DateRelativeKind, Arc<Expression>),
2493 DateCalendar(DateCalendarKind, CalendarPeriodUnit, Arc<Expression>),
2495 RangeLiteral(Arc<Expression>, Arc<Expression>),
2496 PastFutureRange(DateRelativeKind, Arc<Expression>),
2497 RangeContainment(Arc<Expression>, Arc<Expression>),
2498 ResultIsVeto(Arc<Expression>),
2500}
2501
2502impl ExpressionKind {
2503 pub(crate) fn collect_data_paths(&self, data: &mut std::collections::HashSet<DataPath>) {
2505 match self {
2506 ExpressionKind::DataPath(fp) => {
2507 data.insert(fp.clone());
2508 }
2509 ExpressionKind::LogicalAnd(left, right) | ExpressionKind::LogicalOr(left, right) => {
2510 left.collect_data_paths(data);
2511 right.collect_data_paths(data);
2512 }
2513 ExpressionKind::Arithmetic(left, _, right)
2514 | ExpressionKind::Comparison(left, _, right)
2515 | ExpressionKind::RangeLiteral(left, right)
2516 | ExpressionKind::RangeContainment(left, right) => {
2517 left.collect_data_paths(data);
2518 right.collect_data_paths(data);
2519 }
2520 ExpressionKind::UnitConversion(inner, _)
2521 | ExpressionKind::LogicalNegation(inner, _)
2522 | ExpressionKind::MathematicalComputation(_, inner)
2523 | ExpressionKind::PastFutureRange(_, inner) => {
2524 inner.collect_data_paths(data);
2525 }
2526 ExpressionKind::DateRelative(_, date_expr) => {
2527 date_expr.collect_data_paths(data);
2528 }
2529 ExpressionKind::DateCalendar(_, _, date_expr) => {
2530 date_expr.collect_data_paths(data);
2531 }
2532 ExpressionKind::Literal(_)
2533 | ExpressionKind::RulePath(_)
2534 | ExpressionKind::Veto(_)
2535 | ExpressionKind::Now => {}
2536 ExpressionKind::ResultIsVeto(operand) => {
2537 operand.collect_data_paths(data);
2538 }
2539 }
2540 }
2541}
2542
2543#[derive(Clone, Debug, Serialize, Deserialize)]
2549#[serde(tag = "kind", rename_all = "snake_case")]
2550pub enum TypeDefiningSpec {
2551 Local,
2553 Import { spec: Arc<LemmaSpec> },
2555}
2556
2557#[derive(Clone, Debug, Serialize, Deserialize)]
2559#[serde(rename_all = "snake_case")]
2560pub enum TypeExtends {
2561 Primitive,
2563 Custom {
2566 parent: String,
2567 family: String,
2568 defining_spec: TypeDefiningSpec,
2569 },
2570}
2571
2572impl PartialEq for TypeExtends {
2573 fn eq(&self, other: &Self) -> bool {
2574 match (self, other) {
2575 (TypeExtends::Primitive, TypeExtends::Primitive) => true,
2576 (
2577 TypeExtends::Custom {
2578 parent: lp,
2579 family: lf,
2580 defining_spec: ld,
2581 },
2582 TypeExtends::Custom {
2583 parent: rp,
2584 family: rf,
2585 defining_spec: rd,
2586 },
2587 ) => {
2588 lp == rp
2589 && lf == rf
2590 && match (ld, rd) {
2591 (TypeDefiningSpec::Local, TypeDefiningSpec::Local) => true,
2592 (
2593 TypeDefiningSpec::Import { spec: left },
2594 TypeDefiningSpec::Import { spec: right },
2595 ) => Arc::ptr_eq(left, right),
2596 _ => false,
2597 }
2598 }
2599 _ => false,
2600 }
2601 }
2602}
2603
2604impl Eq for TypeExtends {}
2605
2606impl std::hash::Hash for TypeDefiningSpec {
2607 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
2608 match self {
2609 TypeDefiningSpec::Local => {
2610 0u8.hash(state);
2611 }
2612 TypeDefiningSpec::Import { spec } => {
2613 1u8.hash(state);
2614 Arc::as_ptr(spec).hash(state);
2615 }
2616 }
2617 }
2618}
2619
2620impl std::hash::Hash for TypeExtends {
2621 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
2622 match self {
2623 TypeExtends::Primitive => {
2624 0u8.hash(state);
2625 }
2626 TypeExtends::Custom {
2627 parent,
2628 family,
2629 defining_spec,
2630 } => {
2631 1u8.hash(state);
2632 parent.hash(state);
2633 family.hash(state);
2634 defining_spec.hash(state);
2635 }
2636 }
2637 }
2638}
2639
2640impl TypeExtends {
2641 #[must_use]
2643 pub fn custom_local(parent: String, family: String) -> Self {
2644 TypeExtends::Custom {
2645 parent,
2646 family,
2647 defining_spec: TypeDefiningSpec::Local,
2648 }
2649 }
2650
2651 #[must_use]
2653 pub fn parent_name(&self) -> Option<&str> {
2654 match self {
2655 TypeExtends::Primitive => None,
2656 TypeExtends::Custom { parent, .. } => Some(parent.as_str()),
2657 }
2658 }
2659}
2660
2661#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
2666pub struct LemmaType {
2667 pub name: Option<String>,
2669 #[serde(flatten)]
2674 pub specifications: TypeSpecification,
2675 pub extends: TypeExtends,
2677}
2678
2679impl LemmaType {
2680 pub fn new(name: String, specifications: TypeSpecification, extends: TypeExtends) -> Self {
2682 Self {
2683 name: Some(name),
2684 specifications,
2685 extends,
2686 }
2687 }
2688
2689 pub fn without_name(specifications: TypeSpecification, extends: TypeExtends) -> Self {
2691 Self {
2692 name: None,
2693 specifications,
2694 extends,
2695 }
2696 }
2697
2698 pub fn primitive(specifications: TypeSpecification) -> Self {
2700 Self {
2701 name: None,
2702 specifications,
2703 extends: TypeExtends::Primitive,
2704 }
2705 }
2706
2707 pub fn name(&self) -> String {
2709 self.name.clone().unwrap_or_else(|| {
2710 match &self.specifications {
2711 TypeSpecification::Boolean { .. } => "boolean",
2712 TypeSpecification::Quantity { .. } => "quantity",
2713 TypeSpecification::QuantityRange { .. } => "quantity range",
2714 TypeSpecification::Number { .. } => "number",
2715 TypeSpecification::NumberRange { .. } => "number range",
2716 TypeSpecification::Text { .. } => "text",
2717 TypeSpecification::Date { .. } => "date",
2718 TypeSpecification::DateRange { .. } => "date range",
2719 TypeSpecification::Time { .. } => "time",
2720 TypeSpecification::Calendar { .. } => "calendar",
2721 TypeSpecification::CalendarRange { .. } => "calendar range",
2722 TypeSpecification::Ratio { .. } => "ratio",
2723 TypeSpecification::RatioRange { .. } => "ratio range",
2724 TypeSpecification::Veto { .. } => "veto",
2725 TypeSpecification::Undetermined => "undetermined",
2726 }
2727 .to_string()
2728 })
2729 }
2730
2731 pub fn is_boolean(&self) -> bool {
2733 matches!(&self.specifications, TypeSpecification::Boolean { .. })
2734 }
2735
2736 pub fn is_quantity(&self) -> bool {
2738 matches!(&self.specifications, TypeSpecification::Quantity { .. })
2739 }
2740
2741 pub fn is_quantity_range(&self) -> bool {
2742 matches!(
2743 &self.specifications,
2744 TypeSpecification::QuantityRange { .. }
2745 )
2746 }
2747
2748 pub fn is_number(&self) -> bool {
2750 matches!(&self.specifications, TypeSpecification::Number { .. })
2751 }
2752
2753 pub fn is_number_range(&self) -> bool {
2754 matches!(&self.specifications, TypeSpecification::NumberRange { .. })
2755 }
2756
2757 pub fn is_numeric(&self) -> bool {
2759 matches!(
2760 &self.specifications,
2761 TypeSpecification::Quantity { .. } | TypeSpecification::Number { .. }
2762 )
2763 }
2764
2765 pub fn is_text(&self) -> bool {
2767 matches!(&self.specifications, TypeSpecification::Text { .. })
2768 }
2769
2770 pub fn is_date(&self) -> bool {
2772 matches!(&self.specifications, TypeSpecification::Date { .. })
2773 }
2774
2775 pub fn is_date_range(&self) -> bool {
2776 matches!(&self.specifications, TypeSpecification::DateRange { .. })
2777 }
2778
2779 pub fn is_time(&self) -> bool {
2781 matches!(&self.specifications, TypeSpecification::Time { .. })
2782 }
2783
2784 pub fn has_trait_duration(&self) -> bool {
2785 match &self.specifications {
2786 TypeSpecification::Quantity { traits, .. } => traits.contains(&QuantityTrait::Duration),
2787 _ => false,
2788 }
2789 }
2790
2791 pub fn is_duration_like_quantity(&self) -> bool {
2792 if !self.is_quantity() {
2793 return false;
2794 }
2795 if self.has_trait_duration() {
2796 return true;
2797 }
2798 self.is_anonymous_quantity()
2799 && self.quantity_type_decomposition() == &duration_decomposition()
2800 }
2801
2802 pub fn is_duration_like(&self) -> bool {
2803 self.is_duration_like_quantity()
2804 }
2805
2806 pub fn is_calendar(&self) -> bool {
2808 matches!(&self.specifications, TypeSpecification::Calendar { .. })
2809 }
2810
2811 pub fn is_ratio(&self) -> bool {
2813 matches!(&self.specifications, TypeSpecification::Ratio { .. })
2814 }
2815
2816 pub fn is_ratio_range(&self) -> bool {
2817 matches!(&self.specifications, TypeSpecification::RatioRange { .. })
2818 }
2819
2820 pub fn is_calendar_range(&self) -> bool {
2821 matches!(
2822 &self.specifications,
2823 TypeSpecification::CalendarRange { .. }
2824 )
2825 }
2826
2827 pub fn is_range(&self) -> bool {
2828 matches!(
2829 &self.specifications,
2830 TypeSpecification::DateRange { .. }
2831 | TypeSpecification::NumberRange { .. }
2832 | TypeSpecification::QuantityRange { .. }
2833 | TypeSpecification::RatioRange { .. }
2834 | TypeSpecification::CalendarRange { .. }
2835 )
2836 }
2837
2838 pub fn vetoed(&self) -> bool {
2840 matches!(&self.specifications, TypeSpecification::Veto { .. })
2841 }
2842
2843 pub fn is_undetermined(&self) -> bool {
2845 matches!(&self.specifications, TypeSpecification::Undetermined)
2846 }
2847
2848 pub fn has_same_base_type(&self, other: &LemmaType) -> bool {
2850 use TypeSpecification::*;
2851 matches!(
2852 (&self.specifications, &other.specifications),
2853 (Boolean { .. }, Boolean { .. })
2854 | (Number { .. }, Number { .. })
2855 | (NumberRange { .. }, NumberRange { .. })
2856 | (Quantity { .. }, Quantity { .. })
2857 | (QuantityRange { .. }, QuantityRange { .. })
2858 | (Text { .. }, Text { .. })
2859 | (Date { .. }, Date { .. })
2860 | (DateRange { .. }, DateRange { .. })
2861 | (Time { .. }, Time { .. })
2862 | (Calendar { .. }, Calendar { .. })
2863 | (CalendarRange { .. }, CalendarRange { .. })
2864 | (Ratio { .. }, Ratio { .. })
2865 | (RatioRange { .. }, RatioRange { .. })
2866 | (Veto { .. }, Veto { .. })
2867 | (Undetermined, Undetermined)
2868 )
2869 }
2870
2871 #[must_use]
2873 pub fn quantity_family_name(&self) -> Option<&str> {
2874 if !self.is_quantity() {
2875 return None;
2876 }
2877 match &self.extends {
2878 TypeExtends::Custom { family, .. } => Some(family.as_str()),
2879 TypeExtends::Primitive => self.name.as_deref(),
2880 }
2881 }
2882
2883 #[must_use]
2885 pub fn same_quantity_family(&self, other: &LemmaType) -> bool {
2886 if !self.is_quantity() || !other.is_quantity() {
2887 return false;
2888 }
2889 match (self.quantity_family_name(), other.quantity_family_name()) {
2890 (Some(self_family), Some(other_family)) => self_family == other_family,
2891 _ => false,
2892 }
2893 }
2894
2895 #[must_use]
2896 pub fn compatible_with_anonymous_quantity(&self, other: &LemmaType) -> bool {
2897 if !self.is_quantity() || !other.is_quantity() {
2898 return false;
2899 }
2900 if !self.is_anonymous_quantity() && !other.is_anonymous_quantity() {
2901 return false;
2902 }
2903 let self_decomposition = self.quantity_type_decomposition();
2904 let other_decomposition = other.quantity_type_decomposition();
2905 !self_decomposition.is_empty() && self_decomposition == other_decomposition
2906 }
2907
2908 pub fn veto_type() -> Self {
2910 Self::primitive(TypeSpecification::veto())
2911 }
2912
2913 pub fn undetermined_type() -> Self {
2916 Self::primitive(TypeSpecification::Undetermined)
2917 }
2918
2919 pub fn decimal_places(&self) -> Option<u8> {
2922 match &self.specifications {
2923 TypeSpecification::Number { decimals, .. } => *decimals,
2924 TypeSpecification::Quantity { decimals, .. } => *decimals,
2925 TypeSpecification::Ratio { decimals, .. } => *decimals,
2926 _ => None,
2927 }
2928 }
2929
2930 pub fn example_value(&self) -> &'static str {
2932 match &self.specifications {
2933 TypeSpecification::Text { .. } => "\"hello world\"",
2934 TypeSpecification::Quantity { .. } => "12.50 eur",
2935 TypeSpecification::QuantityRange { .. } => "30 kilogram...35 kilogram",
2936 TypeSpecification::Number { .. } => "3.14",
2937 TypeSpecification::NumberRange { .. } => "0...100",
2938 TypeSpecification::Boolean { .. } => "true",
2939 TypeSpecification::Date { .. } => "2023-12-25T14:30:00Z",
2940 TypeSpecification::DateRange { .. } => "2024-01-01...2024-12-31",
2941 TypeSpecification::Veto { .. } => "veto",
2942 TypeSpecification::Time { .. } => "14:30:00",
2943 TypeSpecification::Calendar { .. } => "6 months",
2944 TypeSpecification::CalendarRange { .. } => "18 years...67 years",
2945 TypeSpecification::Ratio { .. } => "50%",
2946 TypeSpecification::RatioRange { .. } => "10%...50%",
2947 TypeSpecification::Undetermined => unreachable!(
2948 "BUG: example_value called on Undetermined sentinel type; this type must never reach user-facing code"
2949 ),
2950 }
2951 }
2952
2953 #[must_use]
2957 pub fn quantity_type_decomposition(&self) -> &BaseQuantityVector {
2962 match &self.specifications {
2963 TypeSpecification::Quantity { decomposition, .. } => decomposition,
2964 _ => unreachable!(
2965 "BUG: quantity_type_decomposition called on non-quantity type {}",
2966 self.name()
2967 ),
2968 }
2969 }
2970
2971 pub fn is_anonymous_quantity(&self) -> bool {
2974 self.name.is_none() && matches!(&self.specifications, TypeSpecification::Quantity { .. })
2975 }
2976
2977 pub fn anonymous_for_decomposition(decomposition: BaseQuantityVector) -> Self {
2980 Self {
2981 name: None,
2982 specifications: TypeSpecification::Quantity {
2983 minimum: None,
2984 maximum: None,
2985 decimals: None,
2986 units: crate::literals::QuantityUnits::new(),
2987 traits: Vec::new(),
2988 decomposition,
2989 canonical_unit: String::new(),
2990 help: String::new(),
2991 },
2992 extends: TypeExtends::Primitive,
2993 }
2994 }
2995
2996 #[must_use]
2998 pub fn quantity_unit_names(&self) -> Option<Vec<&str>> {
2999 if !self.is_quantity() || self.is_anonymous_quantity() {
3000 return None;
3001 }
3002 match &self.specifications {
3003 TypeSpecification::Quantity { units, .. } => {
3004 Some(units.iter().map(|unit| unit.name.as_str()).collect())
3005 }
3006 _ => None,
3007 }
3008 }
3009
3010 pub fn validate_quantity_result_unit(&self, target_unit: &str) -> Result<(), String> {
3014 let target_unit =
3015 crate::parsing::ast::ascii_lowercase_logical_name(target_unit.to_string());
3016 let units = match &self.specifications {
3017 TypeSpecification::Quantity { units, .. } => units,
3018 _ => {
3019 return Err(format!(
3020 "Cannot convert {} to quantity unit '{}'.",
3021 self.name(),
3022 target_unit
3023 ));
3024 }
3025 };
3026 if self.is_anonymous_quantity() {
3027 return Err(format!(
3028 "Cannot convert {} to quantity unit '{}'.",
3029 self.name(),
3030 target_unit
3031 ));
3032 }
3033 let matched = units.get(&target_unit)?;
3034 if crate::computation::rational::rational_is_zero(&matched.factor) {
3035 return Err(format!(
3036 "Unit '{}' has a zero conversion factor in quantity type {}.",
3037 matched.name,
3038 self.name()
3039 ));
3040 }
3041 Ok(())
3042 }
3043
3044 fn validate_ratio_result_unit(&self, target_unit: &str) -> Result<(), String> {
3045 let target_unit =
3046 crate::parsing::ast::ascii_lowercase_logical_name(target_unit.to_string());
3047 let units = match &self.specifications {
3048 TypeSpecification::Ratio { units, .. } => units,
3049 _ => {
3050 return Err(format!(
3051 "Cannot convert {} to ratio unit '{}'.",
3052 self.name(),
3053 target_unit
3054 ));
3055 }
3056 };
3057 let matched = units.get(&target_unit)?;
3058 if crate::computation::rational::rational_is_zero(&matched.value) {
3059 return Err(format!(
3060 "Unit '{}' has a zero conversion value in ratio type {}.",
3061 matched.name,
3062 self.name()
3063 ));
3064 }
3065 Ok(())
3066 }
3067
3068 pub fn validate_rule_result_unit_conversion(
3073 &self,
3074 target_unit: &str,
3075 unit_index: &std::collections::HashMap<String, LemmaType>,
3076 spec_name: &str,
3077 ) -> Result<SemanticConversionTarget, String> {
3078 if self.is_ratio() {
3079 self.validate_ratio_result_unit(target_unit)?;
3080 match unit_index.get(target_unit) {
3081 Some(target_type) if target_type.is_ratio() => {}
3082 Some(_) => {
3083 return Err(format!(
3084 "Unit '{}' does not belong to a ratio type.",
3085 target_unit
3086 ));
3087 }
3088 None => {
3089 return Err(format!(
3090 "Unknown unit '{}': no ratio type in spec '{}' owns this unit.",
3091 target_unit, spec_name
3092 ));
3093 }
3094 }
3095 return Ok(SemanticConversionTarget::RatioUnit(target_unit.to_string()));
3096 }
3097
3098 if !self.is_quantity() {
3099 return Err(format!(
3100 "Cannot convert {} to unit '{}': requires quantity or ratio result type.",
3101 self.name(),
3102 target_unit
3103 ));
3104 }
3105
3106 if self.is_anonymous_quantity() {
3107 let target_type = unit_index.get(target_unit).ok_or_else(|| {
3108 format!(
3109 "Unknown unit '{}': no quantity type in spec '{}' owns this unit.",
3110 target_unit, spec_name
3111 )
3112 })?;
3113 let source_decomp = self.quantity_type_decomposition();
3114 let target_decomp = match &target_type.specifications {
3115 TypeSpecification::Quantity { decomposition, .. } => decomposition,
3116 _ => {
3117 return Err(format!(
3118 "Unit '{}' does not belong to a quantity type.",
3119 target_unit
3120 ));
3121 }
3122 };
3123 if source_decomp != target_decomp {
3124 let target_quantity_family = target_type
3125 .quantity_family_name()
3126 .map(str::to_string)
3127 .unwrap_or_else(|| target_type.name().to_string());
3128 return Err(format!(
3129 "Cannot cast to '{}' (quantity '{}'): source dimensions {:?} do not \
3130 match target dimensions {:?}. The intermediate result has a different \
3131 physical quantity than the target type.",
3132 target_unit, target_quantity_family, source_decomp, target_decomp
3133 ));
3134 }
3135 target_type.validate_quantity_result_unit(target_unit)?;
3136 return Ok(SemanticConversionTarget::QuantityUnit(
3137 target_unit.to_string(),
3138 ));
3139 }
3140
3141 self.validate_quantity_result_unit(target_unit)?;
3142 if unit_index.get(target_unit).is_none() {
3143 return Err(format!(
3144 "Unknown unit '{}': no quantity type in spec '{}' owns this unit.",
3145 target_unit, spec_name
3146 ));
3147 }
3148 Ok(SemanticConversionTarget::QuantityUnit(
3149 target_unit.to_string(),
3150 ))
3151 }
3152
3153 pub fn quantity_unit_factor(
3154 &self,
3155 unit_name: &str,
3156 ) -> &crate::computation::rational::RationalInteger {
3157 use crate::computation::rational::rational_one;
3158 use std::sync::LazyLock;
3159 static EMPTY_UNIT_FACTOR: LazyLock<crate::computation::rational::RationalInteger> =
3160 LazyLock::new(rational_one);
3161 if unit_name.is_empty() {
3162 return &EMPTY_UNIT_FACTOR;
3163 }
3164 let units = match &self.specifications {
3165 TypeSpecification::Quantity { units, .. } => units,
3166 _ => unreachable!(
3167 "BUG: quantity_unit_factor called with non-quantity type {}; only call during evaluation after planning validated quantity conversion",
3168 self.name()
3169 ),
3170 };
3171 match units.get(unit_name) {
3172 Ok(QuantityUnit { factor, .. }) => factor,
3173 Err(_) => {
3174 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
3175 unreachable!(
3176 "BUG: unknown unit '{}' for quantity type {} (valid: {}); planning must reject invalid conversions with Error",
3177 unit_name,
3178 self.name(),
3179 valid.join(", ")
3180 );
3181 }
3182 }
3183 }
3184
3185 pub fn ratio_unit_factor(
3186 &self,
3187 unit_name: &str,
3188 ) -> &crate::computation::rational::RationalInteger {
3189 let units = match &self.specifications {
3190 TypeSpecification::Ratio { units, .. } => units,
3191 _ => unreachable!(
3192 "BUG: ratio_unit_factor called with non-ratio type {}; only call during evaluation after planning validated ratio conversion",
3193 self.name()
3194 ),
3195 };
3196 match units.get(unit_name) {
3197 Ok(RatioUnit { value, .. }) => value,
3198 Err(_) => {
3199 let valid: Vec<&str> = units.0.iter().map(|u| u.name.as_str()).collect();
3200 unreachable!(
3201 "BUG: unknown unit '{}' for ratio type {} (valid: {}); planning must reject invalid conversions with Error",
3202 unit_name,
3203 self.name(),
3204 valid.join(", ")
3205 );
3206 }
3207 }
3208 }
3209}
3210
3211#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize)]
3213pub struct LiteralValue {
3214 pub value: ValueKind,
3215 pub lemma_type: LemmaType,
3216}
3217
3218impl Serialize for LiteralValue {
3219 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
3220 where
3221 S: serde::Serializer,
3222 {
3223 use serde::ser::SerializeStruct;
3224 let mut state = serializer.serialize_struct("LiteralValue", 3)?;
3225 state.serialize_field("value", &self.value)?;
3226 state.serialize_field("lemma_type", &self.lemma_type)?;
3227 state.serialize_field("display_value", &self.display_value())?;
3228 state.end()
3229 }
3230}
3231
3232impl LiteralValue {
3233 pub fn text(s: String) -> Self {
3234 Self {
3235 value: ValueKind::Text(s),
3236 lemma_type: primitive_text().clone(),
3237 }
3238 }
3239
3240 pub fn text_with_type(s: String, lemma_type: LemmaType) -> Self {
3241 Self {
3242 value: ValueKind::Text(s),
3243 lemma_type,
3244 }
3245 }
3246
3247 pub fn number(n: RationalInteger) -> Self {
3248 Self {
3249 value: ValueKind::Number(n),
3250 lemma_type: primitive_number().clone(),
3251 }
3252 }
3253
3254 pub fn number_from_decimal(decimal: Decimal) -> Self {
3255 Self::number(
3256 crate::literals::rational_from_parsed_decimal(decimal)
3257 .expect("BUG: literal number from decimal must lift at boundary"),
3258 )
3259 }
3260
3261 pub fn number_with_type(n: RationalInteger, lemma_type: LemmaType) -> Self {
3262 Self {
3263 value: ValueKind::Number(n),
3264 lemma_type,
3265 }
3266 }
3267
3268 pub fn number_with_type_from_decimal(decimal: Decimal, lemma_type: LemmaType) -> Self {
3269 Self::number_with_type(
3270 crate::literals::rational_from_parsed_decimal(decimal)
3271 .expect("BUG: literal number from decimal must lift at boundary"),
3272 lemma_type,
3273 )
3274 }
3275
3276 pub fn quantity_with_type(n: RationalInteger, unit: String, lemma_type: LemmaType) -> Self {
3277 Self {
3278 value: ValueKind::Quantity(n, unit, BaseQuantityVector::new()),
3279 lemma_type,
3280 }
3281 }
3282
3283 pub fn quantity_anonymous(n: RationalInteger, decomposition: BaseQuantityVector) -> Self {
3286 let lemma_type = LemmaType {
3287 name: None,
3288 specifications: TypeSpecification::Quantity {
3289 minimum: None,
3290 maximum: None,
3291 decimals: None,
3292 units: crate::literals::QuantityUnits::new(),
3293 traits: Vec::new(),
3294 decomposition: decomposition.clone(),
3295 canonical_unit: String::new(),
3296 help: String::new(),
3297 },
3298 extends: TypeExtends::Primitive,
3299 };
3300 Self {
3301 value: ValueKind::Quantity(n, String::new(), decomposition),
3302 lemma_type,
3303 }
3304 }
3305
3306 pub fn number_interpreted_as_quantity(value: RationalInteger, unit_name: String) -> Self {
3309 let lemma_type = LemmaType {
3310 name: None,
3311 specifications: TypeSpecification::Quantity {
3312 minimum: None,
3313 maximum: None,
3314 decimals: None,
3315 units: QuantityUnits::from(vec![QuantityUnit {
3316 name: unit_name.clone(),
3317 factor: crate::computation::rational::rational_one(),
3318 derived_quantity_factors: Vec::new(),
3319 decomposition: BaseQuantityVector::new(),
3320 minimum: None,
3321 maximum: None,
3322 default_magnitude: None,
3323 }]),
3324 traits: Vec::new(),
3325 decomposition: BaseQuantityVector::new(),
3326 canonical_unit: unit_name.clone(),
3327 help: default_help_for_primitive(PrimitiveKind::Quantity).to_string(),
3328 },
3329 extends: TypeExtends::Primitive,
3330 };
3331 Self {
3332 value: ValueKind::Quantity(value, unit_name, BaseQuantityVector::new()),
3333 lemma_type,
3334 }
3335 }
3336
3337 pub fn from_bool(b: bool) -> Self {
3338 Self {
3339 value: ValueKind::Boolean(b),
3340 lemma_type: primitive_boolean().clone(),
3341 }
3342 }
3343
3344 pub fn date(dt: SemanticDateTime) -> Self {
3345 Self {
3346 value: ValueKind::Date(dt),
3347 lemma_type: primitive_date().clone(),
3348 }
3349 }
3350
3351 pub fn date_with_type(dt: SemanticDateTime, lemma_type: LemmaType) -> Self {
3352 Self {
3353 value: ValueKind::Date(dt),
3354 lemma_type,
3355 }
3356 }
3357
3358 pub fn time(t: SemanticTime) -> Self {
3359 Self {
3360 value: ValueKind::Time(t),
3361 lemma_type: primitive_time().clone(),
3362 }
3363 }
3364
3365 pub fn time_with_type(t: SemanticTime, lemma_type: LemmaType) -> Self {
3366 Self {
3367 value: ValueKind::Time(t),
3368 lemma_type,
3369 }
3370 }
3371
3372 pub fn calendar(value: RationalInteger, unit: SemanticCalendarUnit) -> Self {
3373 Self {
3374 value: ValueKind::Calendar(value, unit),
3375 lemma_type: primitive_calendar().clone(),
3376 }
3377 }
3378
3379 pub fn calendar_from_decimal(value: Decimal, unit: SemanticCalendarUnit) -> Self {
3380 Self::calendar(
3381 crate::literals::rational_from_parsed_decimal(value)
3382 .expect("BUG: calendar literal from decimal must lift at boundary"),
3383 unit,
3384 )
3385 }
3386
3387 pub fn calendar_with_type(
3388 value: RationalInteger,
3389 unit: SemanticCalendarUnit,
3390 lemma_type: LemmaType,
3391 ) -> Self {
3392 Self {
3393 value: ValueKind::Calendar(value, unit),
3394 lemma_type,
3395 }
3396 }
3397
3398 pub fn ratio(r: RationalInteger, unit: Option<String>) -> Self {
3399 Self {
3400 value: ValueKind::Ratio(r, unit),
3401 lemma_type: primitive_ratio().clone(),
3402 }
3403 }
3404
3405 pub fn ratio_from_decimal(r: Decimal, unit: Option<String>) -> Self {
3406 Self::ratio(
3407 crate::literals::rational_from_parsed_decimal(r)
3408 .expect("BUG: ratio literal from decimal must lift at boundary"),
3409 unit,
3410 )
3411 }
3412
3413 pub fn ratio_with_type(
3414 r: RationalInteger,
3415 unit: Option<String>,
3416 lemma_type: LemmaType,
3417 ) -> Self {
3418 Self {
3419 value: ValueKind::Ratio(r, unit),
3420 lemma_type,
3421 }
3422 }
3423
3424 pub fn range(left: LiteralValue, right: LiteralValue) -> Self {
3425 let specifications = match (
3426 &left.lemma_type.specifications,
3427 &right.lemma_type.specifications,
3428 ) {
3429 (TypeSpecification::Date { .. }, TypeSpecification::Date { .. }) => {
3430 TypeSpecification::date_range()
3431 }
3432 (TypeSpecification::Number { .. }, TypeSpecification::Number { .. }) => {
3433 TypeSpecification::number_range()
3434 }
3435 (
3436 TypeSpecification::Quantity {
3437 units,
3438 decomposition,
3439 canonical_unit,
3440 ..
3441 },
3442 TypeSpecification::Quantity { .. },
3443 ) if left.lemma_type.same_quantity_family(&right.lemma_type) => {
3444 let mut spec = TypeSpecification::quantity_range();
3445 if let TypeSpecification::QuantityRange {
3446 units: range_units,
3447 decomposition: range_decomposition,
3448 canonical_unit: range_canonical_unit,
3449 ..
3450 } = &mut spec
3451 {
3452 *range_units = units.clone();
3453 *range_decomposition = decomposition.clone();
3454 *range_canonical_unit = canonical_unit.clone();
3455 }
3456 spec
3457 }
3458 (TypeSpecification::Ratio { units, .. }, TypeSpecification::Ratio { .. }) => {
3459 let mut spec = TypeSpecification::ratio_range();
3460 if let TypeSpecification::RatioRange {
3461 units: range_units, ..
3462 } = &mut spec
3463 {
3464 *range_units = units.clone();
3465 }
3466 spec
3467 }
3468 (TypeSpecification::Calendar { .. }, TypeSpecification::Calendar { .. }) => {
3469 TypeSpecification::calendar_range()
3470 }
3471 _ => unreachable!(
3472 "BUG: attempted to construct a range literal from incompatible endpoint types"
3473 ),
3474 };
3475
3476 Self {
3477 value: ValueKind::Range(Box::new(left), Box::new(right)),
3478 lemma_type: LemmaType::primitive(specifications),
3479 }
3480 }
3481
3482 pub fn display_value(&self) -> String {
3484 format!("{}", self)
3485 }
3486
3487 pub fn byte_size(&self) -> usize {
3489 format!("{}", self).len()
3490 }
3491
3492 pub fn get_type(&self) -> &LemmaType {
3494 &self.lemma_type
3495 }
3496}
3497
3498#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3500#[serde(rename_all = "snake_case")]
3501pub enum DataValue {
3502 Definition {
3503 schema_type: LemmaType,
3504 #[serde(default, skip_serializing_if = "Option::is_none")]
3505 bound_value: Option<LiteralValue>,
3506 },
3507}
3508
3509impl DataValue {
3510 #[must_use]
3511 pub fn from_bound_literal(value: LiteralValue) -> Self {
3512 let schema_type = value.get_type().clone();
3513 Self::Definition {
3514 schema_type,
3515 bound_value: Some(value),
3516 }
3517 }
3518}
3519
3520#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3522pub struct Data {
3523 pub path: DataPath,
3524 pub value: DataValue,
3525 pub source: Option<Source>,
3526}
3527
3528#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3531#[serde(rename_all = "snake_case", tag = "kind")]
3532pub enum ReferenceTarget {
3533 Data(DataPath),
3534 Rule(RulePath),
3535}
3536
3537#[derive(Clone, Debug, Serialize, Deserialize)]
3539#[serde(rename_all = "snake_case")]
3540pub enum DataDefinition {
3541 Value { value: LiteralValue, source: Source },
3543 TypeDeclaration {
3548 resolved_type: LemmaType,
3549 declared_default: Option<ValueKind>,
3550 source: Source,
3551 },
3552 Import {
3554 spec: Arc<crate::parsing::ast::LemmaSpec>,
3555 source: Source,
3556 },
3557 Reference {
3583 target: ReferenceTarget,
3584 resolved_type: LemmaType,
3585 local_constraints: Option<Vec<Constraint>>,
3586 local_default: Option<ValueKind>,
3587 source: Source,
3588 },
3589}
3590
3591impl DataDefinition {
3592 pub fn schema_type(&self) -> Option<&LemmaType> {
3594 match self {
3595 DataDefinition::Value { value, .. } => Some(&value.lemma_type),
3596 DataDefinition::TypeDeclaration { resolved_type, .. } => Some(resolved_type),
3597 DataDefinition::Reference { resolved_type, .. } => Some(resolved_type),
3598 DataDefinition::Import { .. } => None,
3599 }
3600 }
3601
3602 pub fn value(&self) -> Option<&LiteralValue> {
3606 match self {
3607 DataDefinition::Value { value, .. } => Some(value),
3608 DataDefinition::TypeDeclaration { .. }
3609 | DataDefinition::Import { .. }
3610 | DataDefinition::Reference { .. } => None,
3611 }
3612 }
3613
3614 #[inline]
3618 pub fn bound_value(&self) -> Option<&LiteralValue> {
3619 self.value()
3620 }
3621
3622 pub fn default_suggestion(&self) -> Option<LiteralValue> {
3627 match self {
3628 DataDefinition::TypeDeclaration {
3629 resolved_type,
3630 declared_default: Some(dv),
3631 ..
3632 } => Some(LiteralValue {
3633 value: dv.clone(),
3634 lemma_type: resolved_type.clone(),
3635 }),
3636 DataDefinition::Reference {
3637 resolved_type,
3638 local_default: Some(dv),
3639 ..
3640 } => Some(LiteralValue {
3641 value: dv.clone(),
3642 lemma_type: resolved_type.clone(),
3643 }),
3644 DataDefinition::Value { .. }
3645 | DataDefinition::TypeDeclaration { .. }
3646 | DataDefinition::Reference { .. }
3647 | DataDefinition::Import { .. } => None,
3648 }
3649 }
3650
3651 pub fn source(&self) -> &Source {
3653 match self {
3654 DataDefinition::Value { source, .. } => source,
3655 DataDefinition::TypeDeclaration { source, .. } => source,
3656 DataDefinition::Import { source, .. } => source,
3657 DataDefinition::Reference { source, .. } => source,
3658 }
3659 }
3660
3661 pub fn reference_target(&self) -> Option<&ReferenceTarget> {
3664 match self {
3665 DataDefinition::Reference { target, .. } => Some(target),
3666 _ => None,
3667 }
3668 }
3669}
3670
3671pub fn number_with_unit_to_value_kind(
3673 magnitude: rust_decimal::Decimal,
3674 unit_name: &str,
3675 lemma_type: &LemmaType,
3676) -> Result<ValueKind, String> {
3677 match &lemma_type.specifications {
3678 TypeSpecification::Ratio { units, .. } => {
3679 use crate::computation::rational::{checked_div, decimal_to_rational};
3680 let unit = units.get(unit_name)?;
3681 let magnitude_rational = decimal_to_rational(magnitude)
3682 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
3683 let canonical_rational = checked_div(&magnitude_rational, &unit.value)
3684 .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
3685 Ok(ValueKind::Ratio(
3686 canonical_rational,
3687 Some(unit.name.clone()),
3688 ))
3689 }
3690 TypeSpecification::Quantity { .. } => Ok(ValueKind::Quantity(
3691 lift_parser_decimal(magnitude)?,
3692 unit_name.to_string(),
3693 BaseQuantityVector::new(),
3694 )),
3695 _ => Err(format!(
3696 "Unit '{}' is defined on type '{}' which is not quantity or ratio",
3697 unit_name,
3698 lemma_type.name()
3699 )),
3700 }
3701}
3702
3703pub fn parser_value_to_value_kind(
3705 value: &crate::literals::Value,
3706 type_spec: &TypeSpecification,
3707) -> Result<ValueKind, String> {
3708 use crate::literals::Value;
3709 match (value, type_spec) {
3710 (Value::NumberWithUnit(magnitude, unit_name), TypeSpecification::Ratio { units, .. }) => {
3711 use crate::computation::rational::{checked_div, decimal_to_rational};
3712 let unit = units.get(unit_name.as_str())?;
3713 let magnitude_rational = decimal_to_rational(*magnitude)
3714 .map_err(|failure| format!("ratio literal failed rational lift: {failure}"))?;
3715 let canonical_rational = checked_div(&magnitude_rational, &unit.value)
3716 .map_err(|failure| format!("ratio literal: unit conversion failed: {failure}"))?;
3717 Ok(ValueKind::Ratio(
3718 canonical_rational,
3719 Some(unit.name.clone()),
3720 ))
3721 }
3722 (Value::NumberWithUnit(magnitude, unit_name), TypeSpecification::Quantity { .. }) => {
3723 Ok(ValueKind::Quantity(
3724 lift_parser_decimal(*magnitude)?,
3725 unit_name.clone(),
3726 BaseQuantityVector::new(),
3727 ))
3728 }
3729 (Value::NumberWithUnit(_, _), _) => {
3730 Err("number_with_unit literal requires a quantity or ratio type".to_string())
3731 }
3732 _ => value_to_semantic(value),
3733 }
3734}
3735
3736pub fn value_to_semantic(value: &crate::parsing::ast::Value) -> Result<ValueKind, String> {
3740 use crate::parsing::ast::Value;
3741 Ok(match value {
3742 Value::Number(n) => ValueKind::Number(lift_parser_decimal(*n)?),
3743 Value::Text(s) => ValueKind::Text(s.clone()),
3744 Value::Boolean(b) => ValueKind::Boolean(bool::from(*b)),
3745 Value::Date(dt) => ValueKind::Date(date_time_to_semantic(dt)),
3746 Value::Time(t) => ValueKind::Time(time_to_semantic(t)),
3747 Value::Calendar(n, u) => {
3748 ValueKind::Calendar(lift_parser_decimal(*n)?, calendar_unit_to_semantic(u))
3749 }
3750 Value::NumberWithUnit(_, _) => {
3751 return Err(
3752 "number_with_unit literal requires type context (quantity or ratio)".to_string(),
3753 );
3754 }
3755 Value::Range(left, right) => ValueKind::Range(
3756 Box::new(literal_value_from_parser_value(left)?),
3757 Box::new(literal_value_from_parser_value(right)?),
3758 ),
3759 })
3760}
3761
3762pub(crate) fn date_time_to_semantic(dt: &crate::parsing::ast::DateTimeValue) -> SemanticDateTime {
3764 SemanticDateTime {
3765 year: dt.year,
3766 month: dt.month,
3767 day: dt.day,
3768 hour: dt.hour,
3769 minute: dt.minute,
3770 second: dt.second,
3771 microsecond: dt.microsecond,
3772 timezone: dt.timezone.as_ref().map(|tz| SemanticTimezone {
3773 offset_hours: tz.offset_hours,
3774 offset_minutes: tz.offset_minutes,
3775 }),
3776 }
3777}
3778
3779pub(crate) fn time_to_semantic(t: &crate::parsing::ast::TimeValue) -> SemanticTime {
3781 SemanticTime {
3782 hour: t.hour.into(),
3783 minute: t.minute.into(),
3784 second: t.second.into(),
3785 microsecond: t.microsecond,
3786 timezone: t.timezone.as_ref().map(|tz| SemanticTimezone {
3787 offset_hours: tz.offset_hours,
3788 offset_minutes: tz.offset_minutes,
3789 }),
3790 }
3791}
3792
3793pub(crate) fn compare_semantic_dates(
3797 left: &SemanticDateTime,
3798 right: &SemanticDateTime,
3799) -> std::cmp::Ordering {
3800 left.year
3801 .cmp(&right.year)
3802 .then_with(|| left.month.cmp(&right.month))
3803 .then_with(|| left.day.cmp(&right.day))
3804 .then_with(|| left.hour.cmp(&right.hour))
3805 .then_with(|| left.minute.cmp(&right.minute))
3806 .then_with(|| left.second.cmp(&right.second))
3807 .then_with(|| left.microsecond.cmp(&right.microsecond))
3808}
3809
3810pub(crate) fn compare_semantic_times(
3813 left: &SemanticTime,
3814 right: &SemanticTime,
3815) -> std::cmp::Ordering {
3816 left.hour
3817 .cmp(&right.hour)
3818 .then_with(|| left.minute.cmp(&right.minute))
3819 .then_with(|| left.second.cmp(&right.second))
3820 .then_with(|| left.microsecond.cmp(&right.microsecond))
3821}
3822
3823pub(crate) fn calendar_unit_to_semantic(
3824 u: &crate::parsing::ast::CalendarUnit,
3825) -> SemanticCalendarUnit {
3826 use crate::parsing::ast::CalendarUnit as CU;
3827 match u {
3828 CU::Month => SemanticCalendarUnit::Month,
3829 CU::Year => SemanticCalendarUnit::Year,
3830 }
3831}
3832
3833pub fn conversion_target_to_semantic(
3839 ct: &ConversionTarget,
3840 unit_index: Option<&HashMap<String, LemmaType>>,
3841) -> Result<SemanticConversionTarget, String> {
3842 match ct {
3843 ConversionTarget::Calendar(u) => Ok(SemanticConversionTarget::Calendar(
3844 calendar_unit_to_semantic(u),
3845 )),
3846 ConversionTarget::Type(PrimitiveKind::Number) => Ok(SemanticConversionTarget::Number),
3847 ConversionTarget::Type(kind) => Err(format!(
3848 "Type conversion to '{:?}' is not yet supported.",
3849 kind
3850 )),
3851 ConversionTarget::Unit(name) => {
3852 let index = unit_index.ok_or_else(|| {
3853 "Unit conversion requires type resolution; unit index not available.".to_string()
3854 })?;
3855 let lemma_type = index.get(name).ok_or_else(|| {
3856 format!(
3857 "Unknown unit '{}'. Unit must be defined by a quantity or ratio type.",
3858 name
3859 )
3860 })?;
3861 if lemma_type.is_ratio() {
3862 Ok(SemanticConversionTarget::RatioUnit(name.clone()))
3863 } else if lemma_type.is_quantity() {
3864 Ok(SemanticConversionTarget::QuantityUnit(name.clone()))
3865 } else {
3866 Err(format!(
3867 "Unit '{}' is not a ratio or quantity type; cannot use it in conversion.",
3868 name
3869 ))
3870 }
3871 }
3872 }
3873}
3874
3875static PRIMITIVE_BOOLEAN: OnceLock<LemmaType> = OnceLock::new();
3881static PRIMITIVE_QUANTITY: OnceLock<LemmaType> = OnceLock::new();
3882static PRIMITIVE_QUANTITY_RANGE: OnceLock<LemmaType> = OnceLock::new();
3883static PRIMITIVE_NUMBER: OnceLock<LemmaType> = OnceLock::new();
3884static PRIMITIVE_NUMBER_RANGE: OnceLock<LemmaType> = OnceLock::new();
3885static PRIMITIVE_TEXT: OnceLock<LemmaType> = OnceLock::new();
3886static PRIMITIVE_DATE: OnceLock<LemmaType> = OnceLock::new();
3887static PRIMITIVE_DATE_RANGE: OnceLock<LemmaType> = OnceLock::new();
3888static PRIMITIVE_TIME: OnceLock<LemmaType> = OnceLock::new();
3889static PRIMITIVE_CALENDAR: OnceLock<LemmaType> = OnceLock::new();
3890static PRIMITIVE_RATIO: OnceLock<LemmaType> = OnceLock::new();
3891static PRIMITIVE_RATIO_RANGE: OnceLock<LemmaType> = OnceLock::new();
3892static PRIMITIVE_CALENDAR_RANGE: OnceLock<LemmaType> = OnceLock::new();
3893
3894#[must_use]
3896pub fn primitive_boolean() -> &'static LemmaType {
3897 PRIMITIVE_BOOLEAN.get_or_init(|| LemmaType::primitive(TypeSpecification::boolean()))
3898}
3899
3900#[must_use]
3901pub fn primitive_quantity() -> &'static LemmaType {
3902 PRIMITIVE_QUANTITY.get_or_init(|| LemmaType::primitive(TypeSpecification::quantity()))
3903}
3904
3905#[must_use]
3906pub fn primitive_quantity_range() -> &'static LemmaType {
3907 PRIMITIVE_QUANTITY_RANGE
3908 .get_or_init(|| LemmaType::primitive(TypeSpecification::quantity_range()))
3909}
3910
3911#[must_use]
3912pub fn primitive_number() -> &'static LemmaType {
3913 PRIMITIVE_NUMBER.get_or_init(|| LemmaType::primitive(TypeSpecification::number()))
3914}
3915
3916#[must_use]
3917pub fn primitive_number_range() -> &'static LemmaType {
3918 PRIMITIVE_NUMBER_RANGE.get_or_init(|| LemmaType::primitive(TypeSpecification::number_range()))
3919}
3920
3921#[must_use]
3922pub fn primitive_text() -> &'static LemmaType {
3923 PRIMITIVE_TEXT.get_or_init(|| LemmaType::primitive(TypeSpecification::text()))
3924}
3925
3926#[must_use]
3927pub fn primitive_date() -> &'static LemmaType {
3928 PRIMITIVE_DATE.get_or_init(|| LemmaType::primitive(TypeSpecification::date()))
3929}
3930
3931#[must_use]
3932pub fn primitive_date_range() -> &'static LemmaType {
3933 PRIMITIVE_DATE_RANGE.get_or_init(|| LemmaType::primitive(TypeSpecification::date_range()))
3934}
3935
3936#[must_use]
3937pub fn primitive_time() -> &'static LemmaType {
3938 PRIMITIVE_TIME.get_or_init(|| LemmaType::primitive(TypeSpecification::time()))
3939}
3940
3941#[must_use]
3942pub fn primitive_calendar() -> &'static LemmaType {
3943 PRIMITIVE_CALENDAR.get_or_init(|| LemmaType::primitive(TypeSpecification::calendar()))
3944}
3945
3946#[must_use]
3947pub fn primitive_ratio() -> &'static LemmaType {
3948 PRIMITIVE_RATIO.get_or_init(|| LemmaType::primitive(TypeSpecification::ratio()))
3949}
3950
3951#[must_use]
3952pub fn primitive_ratio_range() -> &'static LemmaType {
3953 PRIMITIVE_RATIO_RANGE.get_or_init(|| LemmaType::primitive(TypeSpecification::ratio_range()))
3954}
3955
3956#[must_use]
3957pub fn primitive_calendar_range() -> &'static LemmaType {
3958 PRIMITIVE_CALENDAR_RANGE
3959 .get_or_init(|| LemmaType::primitive(TypeSpecification::calendar_range()))
3960}
3961
3962#[must_use]
3964pub fn type_spec_for_primitive(kind: PrimitiveKind) -> TypeSpecification {
3965 match kind {
3966 PrimitiveKind::Boolean => TypeSpecification::boolean(),
3967 PrimitiveKind::Quantity => TypeSpecification::quantity(),
3968 PrimitiveKind::QuantityRange => TypeSpecification::quantity_range(),
3969 PrimitiveKind::Number => TypeSpecification::number(),
3970 PrimitiveKind::NumberRange => TypeSpecification::number_range(),
3971 PrimitiveKind::Percent | PrimitiveKind::Ratio => TypeSpecification::ratio(),
3972 PrimitiveKind::RatioRange => TypeSpecification::ratio_range(),
3973 PrimitiveKind::Text => TypeSpecification::text(),
3974 PrimitiveKind::Date => TypeSpecification::date(),
3975 PrimitiveKind::DateRange => TypeSpecification::date_range(),
3976 PrimitiveKind::Time => TypeSpecification::time(),
3977 PrimitiveKind::Calendar => TypeSpecification::calendar(),
3978 PrimitiveKind::CalendarRange => TypeSpecification::calendar_range(),
3979 }
3980}
3981
3982impl fmt::Display for PathSegment {
3987 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3988 write!(f, "{} → {}", self.data, self.spec)
3989 }
3990}
3991
3992impl fmt::Display for DataPath {
3993 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3994 for segment in &self.segments {
3995 write!(f, "{}.", segment)?;
3996 }
3997 write!(f, "{}", self.data)
3998 }
3999}
4000
4001impl fmt::Display for RulePath {
4002 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4003 for segment in &self.segments {
4004 write!(f, "{}.", segment)?;
4005 }
4006 write!(f, "{}", self.rule)
4007 }
4008}
4009
4010impl fmt::Display for LemmaType {
4011 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4012 write!(f, "{}", self.name())
4013 }
4014}
4015
4016impl fmt::Display for LiteralValue {
4017 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4018 use crate::computation::rational::{commit_rational_to_decimal, rational_to_display_str};
4019 match &self.value {
4020 ValueKind::Quantity(n, u, _decomp) => {
4021 if let TypeSpecification::Quantity { decimals, .. } =
4022 &self.lemma_type.specifications
4023 {
4024 let s = match commit_rational_to_decimal(n) {
4025 Ok(decimal) => match decimals {
4026 Some(dp) => {
4027 let rounded = decimal.round_dp(u32::from(*dp));
4028 format!("{:.prec$}", rounded, prec = *dp as usize)
4029 }
4030 None => decimal.normalize().to_string(),
4031 },
4032 Err(_) => rational_to_display_str(n),
4033 };
4034 return write!(f, "{} {}", s, u);
4035 }
4036 write!(f, "{}", self.value)
4037 }
4038 ValueKind::Ratio(_, Some(_unit_name)) => write!(f, "{}", self.value),
4039 ValueKind::Range(left, right) => write!(f, "{}...{}", left, right),
4040 _ => write!(f, "{}", self.value),
4041 }
4042 }
4043}
4044
4045#[cfg(test)]
4050mod tests {
4051 use super::*;
4052 use crate::computation::rational::{decimal_to_rational, RationalInteger};
4053 use crate::literals::Value;
4054 use crate::parsing::ast::{BooleanValue, DateTimeValue, LemmaSpec, PrimitiveKind, TimeValue};
4055 use rust_decimal::Decimal;
4056 use std::str::FromStr;
4057 use std::sync::Arc;
4058
4059 #[test]
4060 fn default_primitive_help_is_goal_oriented() {
4061 let kinds = [
4062 PrimitiveKind::Boolean,
4063 PrimitiveKind::Quantity,
4064 PrimitiveKind::QuantityRange,
4065 PrimitiveKind::Number,
4066 PrimitiveKind::NumberRange,
4067 PrimitiveKind::Percent,
4068 PrimitiveKind::Ratio,
4069 PrimitiveKind::RatioRange,
4070 PrimitiveKind::Text,
4071 PrimitiveKind::Date,
4072 PrimitiveKind::DateRange,
4073 PrimitiveKind::Time,
4074 PrimitiveKind::Calendar,
4075 PrimitiveKind::CalendarRange,
4076 ];
4077 for kind in kinds {
4078 let spec = type_spec_for_primitive(kind);
4079 let help = match &spec {
4080 TypeSpecification::Boolean { help, .. }
4081 | TypeSpecification::Number { help, .. }
4082 | TypeSpecification::NumberRange { help }
4083 | TypeSpecification::Text { help, .. }
4084 | TypeSpecification::Quantity { help, .. }
4085 | TypeSpecification::QuantityRange { help, .. }
4086 | TypeSpecification::Ratio { help, .. }
4087 | TypeSpecification::RatioRange { help, .. }
4088 | TypeSpecification::Date { help, .. }
4089 | TypeSpecification::DateRange { help }
4090 | TypeSpecification::Time { help, .. }
4091 | TypeSpecification::Calendar { help, .. }
4092 | TypeSpecification::CalendarRange { help } => help,
4093 TypeSpecification::Veto { .. } | TypeSpecification::Undetermined => {
4094 unreachable!(
4095 "BUG: primitive kind {:?} mapped to non-primitive spec",
4096 kind
4097 )
4098 }
4099 };
4100 assert!(!help.is_empty(), "help for {:?}", kind);
4101 assert!(
4102 !help.to_ascii_lowercase().contains("format:"),
4103 "help for {:?} must not describe syntax: {:?}",
4104 kind,
4105 help
4106 );
4107 assert_eq!(help, default_help_for_primitive(kind));
4108 }
4109 }
4110
4111 #[test]
4112 fn test_negated_comparison() {
4113 assert_eq!(
4114 negated_comparison(ComparisonComputation::LessThan),
4115 ComparisonComputation::GreaterThanOrEqual
4116 );
4117 assert_eq!(
4118 negated_comparison(ComparisonComputation::GreaterThanOrEqual),
4119 ComparisonComputation::LessThan
4120 );
4121 assert_eq!(
4122 negated_comparison(ComparisonComputation::Is),
4123 ComparisonComputation::IsNot
4124 );
4125 assert_eq!(
4126 negated_comparison(ComparisonComputation::IsNot),
4127 ComparisonComputation::Is
4128 );
4129 }
4130
4131 #[test]
4132 fn value_to_semantic_number_is_decimal() {
4133 let kind = value_to_semantic(&Value::Number(Decimal::from(42))).unwrap();
4134 assert!(matches!(kind, ValueKind::Number(d) if d == RationalInteger::new(42, 1)));
4135 }
4136
4137 #[test]
4138 fn parse_data_value_from_json_accepts_json_number_for_number_type() {
4139 use crate::parsing::ast::Span;
4140 use crate::parsing::source::SourceType;
4141 let source = Source::new(
4142 SourceType::Volatile,
4143 Span {
4144 start: 0,
4145 end: 0,
4146 line: 1,
4147 col: 0,
4148 },
4149 );
4150 let ty = primitive_number();
4151 let lit =
4152 parse_data_value_from_json(&serde_json::json!(42), &ty.specifications, ty, &source)
4153 .unwrap();
4154 assert!(matches!(lit.value, ValueKind::Number(d) if d == RationalInteger::new(42, 1)));
4155 let lit =
4156 parse_data_value_from_json(&serde_json::json!(1.5), &ty.specifications, ty, &source)
4157 .unwrap();
4158 assert!(
4159 matches!(lit.value, ValueKind::Number(d) if d == decimal_to_rational(Decimal::from_str("1.5").unwrap()).unwrap())
4160 );
4161 }
4162
4163 #[test]
4164 fn parse_data_value_from_json_rejects_bare_json_number_for_quantity() {
4165 use crate::literals::{QuantityUnit, QuantityUnits};
4166 use crate::parsing::ast::Span;
4167 use crate::parsing::source::SourceType;
4168 let source = Source::new(
4169 SourceType::Volatile,
4170 Span {
4171 start: 0,
4172 end: 0,
4173 line: 1,
4174 col: 0,
4175 },
4176 );
4177 let spec = TypeSpecification::Quantity {
4178 minimum: None,
4179 maximum: None,
4180 decimals: None,
4181 units: QuantityUnits::from(vec![QuantityUnit {
4182 name: "eur".to_string(),
4183 factor: crate::computation::rational::rational_one(),
4184 derived_quantity_factors: Vec::new(),
4185 decomposition: BaseQuantityVector::new(),
4186 minimum: None,
4187 maximum: None,
4188 default_magnitude: None,
4189 }]),
4190 traits: Vec::new(),
4191 decomposition: BaseQuantityVector::new(),
4192 canonical_unit: "eur".to_string(),
4193 help: String::new(),
4194 };
4195 let ty = LemmaType::primitive(spec);
4196 assert!(parse_data_value_from_json(
4197 &serde_json::json!(100),
4198 &ty.specifications,
4199 &ty,
4200 &source,
4201 )
4202 .is_err());
4203 }
4204
4205 #[test]
4206 fn value_kind_quantity_serializes_as_value_unit_object() {
4207 let kind = ValueKind::Quantity(
4208 decimal_to_rational(Decimal::from_str("99.50").unwrap()).unwrap(),
4209 "eur".to_string(),
4210 BaseQuantityVector::new(),
4211 );
4212 let json = serde_json::to_value(&kind).unwrap();
4213 assert_eq!(json["quantity"]["value"], "99.5");
4214 assert_eq!(json["quantity"]["unit"], "eur");
4215 }
4216
4217 #[test]
4218 fn literal_value_number_serde_not_rational_array() {
4219 let lit = LiteralValue::number_from_decimal(Decimal::from(20));
4220 let json = serde_json::to_value(&lit).unwrap();
4221 let number = json
4222 .get("value")
4223 .and_then(|v| v.get("number"))
4224 .expect("number field");
4225 assert!(number.is_string());
4226 assert_eq!(number.as_str(), Some("20"));
4227 assert!(
4228 !number.is_array(),
4229 "stored number must not serialize as [n,d]"
4230 );
4231 }
4232
4233 #[test]
4234 fn test_literal_value_to_primitive_type() {
4235 let one = RationalInteger::new(1, 1);
4236
4237 assert_eq!(LiteralValue::text("".to_string()).lemma_type.name(), "text");
4238 assert_eq!(LiteralValue::number(one).lemma_type.name(), "number");
4239 assert_eq!(
4240 LiteralValue::from_bool(bool::from(BooleanValue::True))
4241 .lemma_type
4242 .name(),
4243 "boolean"
4244 );
4245
4246 let dt = DateTimeValue {
4247 year: 2024,
4248 month: 1,
4249 day: 1,
4250 hour: 0,
4251 minute: 0,
4252 second: 0,
4253 microsecond: 0,
4254 timezone: None,
4255 };
4256 assert_eq!(
4257 LiteralValue::date(date_time_to_semantic(&dt))
4258 .lemma_type
4259 .name(),
4260 "date"
4261 );
4262 assert_eq!(
4263 LiteralValue::ratio_from_decimal(Decimal::new(1, 2), Some("percent".to_string()))
4264 .lemma_type
4265 .name(),
4266 "ratio"
4267 );
4268 let dur_type = LemmaType::new(
4269 "duration".to_string(),
4270 TypeSpecification::Quantity {
4271 minimum: None,
4272 maximum: None,
4273 decimals: None,
4274 units: QuantityUnits::from(vec![QuantityUnit {
4275 name: "second".to_string(),
4276 factor: crate::computation::rational::rational_one(),
4277 derived_quantity_factors: Vec::new(),
4278 decomposition: BaseQuantityVector::new(),
4279 minimum: None,
4280 maximum: None,
4281 default_magnitude: None,
4282 }]),
4283 traits: vec![QuantityTrait::Duration],
4284 decomposition: BaseQuantityVector::new(),
4285 canonical_unit: "second".to_string(),
4286 help: String::new(),
4287 },
4288 TypeExtends::Primitive,
4289 );
4290 assert_eq!(
4291 LiteralValue::quantity_with_type(one, "second".to_string(), dur_type)
4292 .lemma_type
4293 .name(),
4294 "duration"
4295 );
4296 }
4297
4298 #[test]
4299 fn test_type_display() {
4300 let specs = TypeSpecification::text();
4301 let lemma_type = LemmaType::new("name".to_string(), specs, TypeExtends::Primitive);
4302 assert_eq!(format!("{}", lemma_type), "name");
4303 }
4304
4305 #[test]
4306 fn test_type_serialization() {
4307 let specs = TypeSpecification::number();
4308 let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
4309 let serialized = serde_json::to_string(&lemma_type).unwrap();
4310 let deserialized: LemmaType = serde_json::from_str(&serialized).unwrap();
4311 assert_eq!(lemma_type, deserialized);
4312 }
4313
4314 #[test]
4315 fn test_literal_value_display_value() {
4316 let ten = RationalInteger::new(10, 1);
4317
4318 assert_eq!(
4319 LiteralValue::text("hello".to_string()).display_value(),
4320 "hello"
4321 );
4322 assert_eq!(LiteralValue::number(ten).display_value(), "10");
4323 assert_eq!(LiteralValue::from_bool(true).display_value(), "true");
4324 assert_eq!(LiteralValue::from_bool(false).display_value(), "false");
4325
4326 let ten_percent_ratio =
4328 LiteralValue::ratio_from_decimal(Decimal::new(1, 1), Some("percent".to_string()));
4329 assert_eq!(ten_percent_ratio.display_value(), "10%");
4330
4331 let time = TimeValue {
4332 hour: 14,
4333 minute: 30,
4334 second: 0,
4335 microsecond: 0,
4336 timezone: None,
4337 };
4338 let time_display = LiteralValue::time(time_to_semantic(&time)).display_value();
4339 assert!(time_display.contains("14"));
4340 assert!(time_display.contains("30"));
4341 }
4342
4343 #[test]
4344 fn test_quantity_display_respects_type_decimals() {
4345 let money_type = LemmaType {
4346 name: Some("money".to_string()),
4347 specifications: TypeSpecification::Quantity {
4348 minimum: None,
4349 maximum: None,
4350 decimals: Some(2),
4351 units: QuantityUnits::from(vec![QuantityUnit {
4352 name: "eur".to_string(),
4353 factor: crate::computation::rational::rational_one(),
4354 derived_quantity_factors: Vec::new(),
4355 decomposition: BaseQuantityVector::new(),
4356 minimum: None,
4357 maximum: None,
4358 default_magnitude: None,
4359 }]),
4360 traits: Vec::new(),
4361 decomposition: BaseQuantityVector::new(),
4362 canonical_unit: "eur".to_string(),
4363 help: String::new(),
4364 },
4365 extends: TypeExtends::Primitive,
4366 };
4367 let val = LiteralValue::quantity_with_type(
4368 decimal_to_rational(Decimal::from_str("1.8").unwrap()).unwrap(),
4369 "eur".to_string(),
4370 money_type.clone(),
4371 );
4372 assert_eq!(val.display_value(), "1.80 eur");
4373 let more_precision = LiteralValue::quantity_with_type(
4374 decimal_to_rational(Decimal::from_str("1.80000").unwrap()).unwrap(),
4375 "eur".to_string(),
4376 money_type,
4377 );
4378 assert_eq!(more_precision.display_value(), "1.80 eur");
4379 let quantity_no_decimals = LemmaType {
4380 name: Some("count".to_string()),
4381 specifications: TypeSpecification::Quantity {
4382 minimum: None,
4383 maximum: None,
4384 decimals: None,
4385 units: QuantityUnits::from(vec![QuantityUnit {
4386 name: "items".to_string(),
4387 factor: crate::computation::rational::rational_one(),
4388 derived_quantity_factors: Vec::new(),
4389 decomposition: BaseQuantityVector::new(),
4390 minimum: None,
4391 maximum: None,
4392 default_magnitude: None,
4393 }]),
4394 traits: Vec::new(),
4395 decomposition: BaseQuantityVector::new(),
4396 canonical_unit: "items".to_string(),
4397 help: String::new(),
4398 },
4399 extends: TypeExtends::Primitive,
4400 };
4401 let val_any = LiteralValue::quantity_with_type(
4402 decimal_to_rational(Decimal::from_str("42.50").unwrap()).unwrap(),
4403 "items".to_string(),
4404 quantity_no_decimals,
4405 );
4406 assert_eq!(val_any.display_value(), "42.5 items");
4407 }
4408
4409 #[test]
4410 fn test_literal_value_time_type() {
4411 let time = TimeValue {
4412 hour: 14,
4413 minute: 30,
4414 second: 0,
4415 microsecond: 0,
4416 timezone: None,
4417 };
4418 let lit = LiteralValue::time(time_to_semantic(&time));
4419 assert_eq!(lit.lemma_type.name(), "time");
4420 }
4421
4422 #[test]
4423 fn test_quantity_family_name_primitive_root() {
4424 let quantity_spec = TypeSpecification::quantity();
4425 let money_primitive = LemmaType::new(
4426 "money".to_string(),
4427 quantity_spec.clone(),
4428 TypeExtends::Primitive,
4429 );
4430 assert_eq!(money_primitive.quantity_family_name(), Some("money"));
4431 }
4432
4433 #[test]
4434 fn test_quantity_family_name_custom() {
4435 let quantity_spec = TypeSpecification::quantity();
4436 let money_custom = LemmaType::new(
4437 "money".to_string(),
4438 quantity_spec,
4439 TypeExtends::custom_local("money".to_string(), "money".to_string()),
4440 );
4441 assert_eq!(money_custom.quantity_family_name(), Some("money"));
4442 }
4443
4444 #[test]
4445 fn test_same_quantity_family_same_name_different_extends() {
4446 let quantity_spec = TypeSpecification::quantity();
4447 let money_primitive = LemmaType::new(
4448 "money".to_string(),
4449 quantity_spec.clone(),
4450 TypeExtends::Primitive,
4451 );
4452 let money_custom = LemmaType::new(
4453 "money".to_string(),
4454 quantity_spec,
4455 TypeExtends::custom_local("money".to_string(), "money".to_string()),
4456 );
4457 assert!(money_primitive.same_quantity_family(&money_custom));
4458 assert!(money_custom.same_quantity_family(&money_primitive));
4459 }
4460
4461 #[test]
4462 fn test_same_quantity_family_parent_and_child() {
4463 let quantity_spec = TypeSpecification::quantity();
4464 let type_x = LemmaType::new(
4465 "x".to_string(),
4466 quantity_spec.clone(),
4467 TypeExtends::Primitive,
4468 );
4469 let type_x2 = LemmaType::new(
4470 "x2".to_string(),
4471 quantity_spec,
4472 TypeExtends::custom_local("x".to_string(), "x".to_string()),
4473 );
4474 assert_eq!(type_x.quantity_family_name(), Some("x"));
4475 assert_eq!(type_x2.quantity_family_name(), Some("x"));
4476 assert!(type_x.same_quantity_family(&type_x2));
4477 assert!(type_x2.same_quantity_family(&type_x));
4478 }
4479
4480 #[test]
4481 fn test_same_quantity_family_siblings() {
4482 let quantity_spec = TypeSpecification::quantity();
4483 let type_x2_a = LemmaType::new(
4484 "x2a".to_string(),
4485 quantity_spec.clone(),
4486 TypeExtends::custom_local("x".to_string(), "x".to_string()),
4487 );
4488 let type_x2_b = LemmaType::new(
4489 "x2b".to_string(),
4490 quantity_spec,
4491 TypeExtends::custom_local("x".to_string(), "x".to_string()),
4492 );
4493 assert!(type_x2_a.same_quantity_family(&type_x2_b));
4494 }
4495
4496 #[test]
4497 fn test_same_quantity_family_different_families() {
4498 let quantity_spec = TypeSpecification::quantity();
4499 let money = LemmaType::new(
4500 "money".to_string(),
4501 quantity_spec.clone(),
4502 TypeExtends::Primitive,
4503 );
4504 let temperature = LemmaType::new(
4505 "temperature".to_string(),
4506 quantity_spec,
4507 TypeExtends::Primitive,
4508 );
4509 assert!(!money.same_quantity_family(&temperature));
4510 assert!(!temperature.same_quantity_family(&money));
4511 }
4512
4513 #[test]
4514 fn test_same_quantity_family_quantity_vs_non_quantity() {
4515 let quantity_spec = TypeSpecification::quantity();
4516 let number_spec = TypeSpecification::number();
4517 let quantity_type =
4518 LemmaType::new("money".to_string(), quantity_spec, TypeExtends::Primitive);
4519 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
4520 assert!(!quantity_type.same_quantity_family(&number_type));
4521 assert!(!number_type.same_quantity_family(&quantity_type));
4522 }
4523
4524 #[test]
4525 fn test_same_quantity_family_anonymous_quantitys_are_not_family_compatible() {
4526 let left = LemmaType::anonymous_for_decomposition(duration_decomposition());
4527 let right = LemmaType::anonymous_for_decomposition(duration_decomposition());
4528
4529 assert!(!left.same_quantity_family(&right));
4530 assert!(left.compatible_with_anonymous_quantity(&right));
4531 }
4532
4533 #[test]
4534 fn test_quantity_family_name_non_quantity_returns_none() {
4535 let number_spec = TypeSpecification::number();
4536 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
4537 assert_eq!(number_type.quantity_family_name(), None);
4538 }
4539
4540 #[test]
4541 fn test_lemma_type_inequality_local_vs_import_same_shape() {
4542 let dep = Arc::new(LemmaSpec::new("dep".to_string()));
4543 let quantity_spec = TypeSpecification::quantity();
4544 let local = LemmaType::new(
4545 "t".to_string(),
4546 quantity_spec.clone(),
4547 TypeExtends::custom_local("money".to_string(), "money".to_string()),
4548 );
4549 let imported = LemmaType::new(
4550 "t".to_string(),
4551 quantity_spec,
4552 TypeExtends::Custom {
4553 parent: "money".to_string(),
4554 family: "money".to_string(),
4555 defining_spec: TypeDefiningSpec::Import {
4556 spec: Arc::clone(&dep),
4557 },
4558 },
4559 );
4560 assert_ne!(local, imported);
4561 }
4562
4563 #[test]
4564 fn test_lemma_type_equality_import_same_arc_pointer_identity() {
4565 let shared_spec = Arc::new(LemmaSpec::new("dep".to_string()));
4569 let quantity_spec = TypeSpecification::quantity();
4570 let left = LemmaType::new(
4571 "t".to_string(),
4572 quantity_spec.clone(),
4573 TypeExtends::Custom {
4574 parent: "money".to_string(),
4575 family: "money".to_string(),
4576 defining_spec: TypeDefiningSpec::Import {
4577 spec: Arc::clone(&shared_spec),
4578 },
4579 },
4580 );
4581 let right = LemmaType::new(
4582 "t".to_string(),
4583 quantity_spec,
4584 TypeExtends::Custom {
4585 parent: "money".to_string(),
4586 family: "money".to_string(),
4587 defining_spec: TypeDefiningSpec::Import {
4588 spec: Arc::clone(&shared_spec),
4589 },
4590 },
4591 );
4592 assert_eq!(left, right);
4593 }
4594
4595 #[test]
4596 fn test_lemma_type_inequality_import_different_arc_pointer() {
4597 let spec_a = Arc::new(LemmaSpec::new("dep".to_string()));
4599 let spec_b = Arc::new(LemmaSpec::new("dep".to_string()));
4600 let quantity_spec = TypeSpecification::quantity();
4601 let left = LemmaType::new(
4602 "t".to_string(),
4603 quantity_spec.clone(),
4604 TypeExtends::Custom {
4605 parent: "money".to_string(),
4606 family: "money".to_string(),
4607 defining_spec: TypeDefiningSpec::Import {
4608 spec: Arc::clone(&spec_a),
4609 },
4610 },
4611 );
4612 let right = LemmaType::new(
4613 "t".to_string(),
4614 quantity_spec,
4615 TypeExtends::Custom {
4616 parent: "money".to_string(),
4617 family: "money".to_string(),
4618 defining_spec: TypeDefiningSpec::Import { spec: spec_b },
4619 },
4620 );
4621 assert_ne!(left, right);
4622 }
4623
4624 fn month_default_arg() -> CommandArg {
4625 CommandArg::Literal(crate::literals::Value::Calendar(
4626 Decimal::ONE,
4627 crate::literals::CalendarUnit::Month,
4628 ))
4629 }
4630
4631 fn unit_factor_arg(name: &str, factor: i64) -> [CommandArg; 2] {
4632 [
4633 CommandArg::Label(name.to_string()),
4634 CommandArg::UnitExpr(crate::parsing::ast::UnitArg::Factor(Decimal::from(factor))),
4635 ]
4636 }
4637
4638 #[test]
4639 fn default_calendar_on_text_reports_hint() {
4640 let specs = TypeSpecification::text();
4641 let mut default = None;
4642 let err = specs
4643 .apply_constraint(
4644 "notes",
4645 TypeConstraintCommand::Default,
4646 &[month_default_arg()],
4647 &mut default,
4648 )
4649 .unwrap_err();
4650 assert!(err.contains("Unit 'month' is for calendar data"));
4651 assert!(err.contains("double quotes"));
4652 }
4653
4654 #[test]
4655 fn default_calendar_on_duration_reports_valid_units() {
4656 let mut specs = TypeSpecification::quantity();
4657 specs = specs
4658 .apply_constraint(
4659 "duration",
4660 TypeConstraintCommand::Unit,
4661 &unit_factor_arg("second", 1),
4662 &mut None,
4663 )
4664 .unwrap();
4665 specs = specs
4666 .apply_constraint(
4667 "duration",
4668 TypeConstraintCommand::Unit,
4669 &unit_factor_arg("week", 604_800),
4670 &mut None,
4671 )
4672 .unwrap();
4673 specs = specs
4674 .apply_constraint(
4675 "duration",
4676 TypeConstraintCommand::Trait,
4677 &[CommandArg::Label("duration".to_string())],
4678 &mut None,
4679 )
4680 .unwrap();
4681 let mut default = None;
4682 let err = specs
4683 .apply_constraint(
4684 "duration",
4685 TypeConstraintCommand::Default,
4686 &[month_default_arg()],
4687 &mut default,
4688 )
4689 .unwrap_err();
4690 assert!(err.contains("Unit 'month' is for calendar data"));
4691 assert!(err.contains("Valid 'duration' units are"));
4692 assert!(err.contains("week"));
4693 }
4694
4695 #[test]
4696 fn default_valid_duration_weeks_accepted() {
4697 let mut specs = TypeSpecification::quantity();
4698 specs = specs
4699 .apply_constraint(
4700 "duration",
4701 TypeConstraintCommand::Unit,
4702 &unit_factor_arg("second", 1),
4703 &mut None,
4704 )
4705 .unwrap();
4706 specs = specs
4707 .apply_constraint(
4708 "duration",
4709 TypeConstraintCommand::Unit,
4710 &unit_factor_arg("week", 604_800),
4711 &mut None,
4712 )
4713 .unwrap();
4714 specs = specs
4715 .apply_constraint(
4716 "duration",
4717 TypeConstraintCommand::Trait,
4718 &[CommandArg::Label("duration".to_string())],
4719 &mut None,
4720 )
4721 .unwrap();
4722 let mut default = None;
4723 specs
4724 .apply_constraint(
4725 "duration",
4726 TypeConstraintCommand::Default,
4727 &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
4728 Decimal::from(4),
4729 "week".to_string(),
4730 ))],
4731 &mut default,
4732 )
4733 .unwrap();
4734 assert!(matches!(default, Some(ValueKind::Quantity(_, unit, _)) if unit == "week"));
4735 }
4736
4737 #[test]
4738 fn default_unknown_unit_on_duration_lists_valid_units() {
4739 let mut specs = TypeSpecification::quantity();
4740 specs = specs
4741 .apply_constraint(
4742 "duration",
4743 TypeConstraintCommand::Unit,
4744 &unit_factor_arg("second", 1),
4745 &mut None,
4746 )
4747 .unwrap();
4748 specs = specs
4749 .apply_constraint(
4750 "duration",
4751 TypeConstraintCommand::Trait,
4752 &[CommandArg::Label("duration".to_string())],
4753 &mut None,
4754 )
4755 .unwrap();
4756 let mut default = None;
4757 let err = specs
4758 .apply_constraint(
4759 "duration",
4760 TypeConstraintCommand::Default,
4761 &[CommandArg::Literal(crate::literals::Value::NumberWithUnit(
4762 Decimal::ONE,
4763 "fortnight".to_string(),
4764 ))],
4765 &mut default,
4766 )
4767 .unwrap_err();
4768 assert!(err.contains("fortnight"));
4769 assert!(err.contains("Valid 'duration' units are"));
4770 }
4771
4772 fn money_quantity_type() -> LemmaType {
4773 LemmaType::new(
4774 "Money".to_string(),
4775 TypeSpecification::Quantity {
4776 minimum: None,
4777 maximum: None,
4778 decimals: None,
4779 units: QuantityUnits::from(vec![
4780 QuantityUnit {
4781 name: "eur".to_string(),
4782 factor: crate::computation::rational::rational_one(),
4783 derived_quantity_factors: Vec::new(),
4784 decomposition: BaseQuantityVector::new(),
4785 minimum: None,
4786 maximum: None,
4787 default_magnitude: None,
4788 },
4789 QuantityUnit {
4790 name: "usd".to_string(),
4791 factor: crate::computation::rational::decimal_to_rational(Decimal::new(
4792 91, 2,
4793 ))
4794 .expect("factor"),
4795 derived_quantity_factors: Vec::new(),
4796 decomposition: BaseQuantityVector::new(),
4797 minimum: None,
4798 maximum: None,
4799 default_magnitude: None,
4800 },
4801 ]),
4802 traits: Vec::new(),
4803 decomposition: BaseQuantityVector::new(),
4804 canonical_unit: "eur".to_string(),
4805 help: String::new(),
4806 },
4807 TypeExtends::Primitive,
4808 )
4809 }
4810
4811 #[test]
4812 fn validate_rule_result_unit_conversion_requires_unit_index_entry() {
4813 let money = money_quantity_type();
4814 let mut index = std::collections::HashMap::new();
4815 index.insert("eur".to_string(), money.clone());
4816 let err = money
4817 .validate_rule_result_unit_conversion("usd", &index, "pricing")
4818 .expect_err("usd missing from index");
4819 assert!(err.contains("Unknown unit 'usd'"), "got: {err}");
4820 }
4821
4822 #[test]
4823 fn validate_rule_result_unit_conversion_accepts_declared_unit_in_index() {
4824 let money = money_quantity_type();
4825 let mut index = std::collections::HashMap::new();
4826 index.insert("eur".to_string(), money.clone());
4827 index.insert("usd".to_string(), money.clone());
4828 money
4829 .validate_rule_result_unit_conversion("usd", &index, "pricing")
4830 .unwrap();
4831 }
4832
4833 #[test]
4834 fn validate_quantity_result_unit_accepts_declared_unit() {
4835 let money = money_quantity_type();
4836 money.validate_quantity_result_unit("usd").unwrap();
4837 money.validate_quantity_result_unit("EUR").unwrap();
4838 }
4839
4840 #[test]
4841 fn validate_quantity_result_unit_lists_valid_units() {
4842 let money = money_quantity_type();
4843 let err = money
4844 .validate_quantity_result_unit("gbp")
4845 .expect_err("gbp not declared");
4846 assert!(err.contains("Valid units: eur, usd"), "got: {err}");
4847 }
4848
4849 #[test]
4850 fn validate_quantity_result_unit_rejects_zero_factor() {
4851 let mut money = money_quantity_type();
4852 if let TypeSpecification::Quantity { units, .. } = &mut money.specifications {
4853 units.push(QuantityUnit {
4854 name: "zero".to_string(),
4855 factor: crate::computation::rational::RationalInteger::new(0, 1),
4856 derived_quantity_factors: Vec::new(),
4857 decomposition: BaseQuantityVector::new(),
4858 minimum: None,
4859 maximum: None,
4860 default_magnitude: None,
4861 });
4862 }
4863 let err = money
4864 .validate_quantity_result_unit("zero")
4865 .expect_err("zero factor");
4866 assert!(err.contains("zero conversion factor"), "got: {err}");
4867 }
4868
4869 #[test]
4870 fn validate_quantity_result_unit_rejects_non_quantity() {
4871 let number = primitive_number().clone();
4872 let err = number
4873 .validate_quantity_result_unit("eur")
4874 .expect_err("number is not quantity");
4875 assert!(
4876 err.contains("Cannot convert number to quantity unit"),
4877 "got: {err}"
4878 );
4879 }
4880
4881 #[test]
4882 fn validate_quantity_result_unit_rejects_anonymous_quantity() {
4883 let mut decomposition = BaseQuantityVector::new();
4884 decomposition.insert("mass".to_string(), 1);
4885 let anonymous = LemmaType::anonymous_for_decomposition(decomposition);
4886 let err = anonymous
4887 .validate_quantity_result_unit("kilogram")
4888 .expect_err("anonymous");
4889 assert!(
4890 err.contains("Cannot convert quantity to quantity unit"),
4891 "got: {err}"
4892 );
4893 }
4894
4895 #[test]
4896 fn quantity_unit_names_for_named_quantity() {
4897 let money = money_quantity_type();
4898 assert_eq!(money.quantity_unit_names(), Some(vec!["eur", "usd"]));
4899 }
4900}