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