1pub(crate) fn ascii_lowercase_logical_name(name: String) -> String {
18 name.to_ascii_lowercase()
19}
20
21#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
23pub struct Span {
24 pub start: usize,
25 pub end: usize,
26 pub line: usize,
27 pub col: usize,
28}
29
30pub struct DepthTracker {
32 depth: usize,
33 max_depth: usize,
34}
35
36impl DepthTracker {
37 pub fn with_max_depth(max_depth: usize) -> Self {
38 Self {
39 depth: 0,
40 max_depth,
41 }
42 }
43
44 pub fn push_depth(&mut self) -> Result<(), usize> {
46 self.depth += 1;
47 if self.depth > self.max_depth {
48 return Err(self.depth);
49 }
50 Ok(())
51 }
52
53 pub fn pop_depth(&mut self) {
54 if self.depth > 0 {
55 self.depth -= 1;
56 }
57 }
58
59 pub fn max_depth(&self) -> usize {
60 self.max_depth
61 }
62}
63
64impl Default for DepthTracker {
65 fn default() -> Self {
66 Self {
67 depth: 0,
68 max_depth: 5,
69 }
70 }
71}
72
73use crate::parsing::source::Source;
78use rust_decimal::Decimal;
79use serde::Serialize;
80use std::cmp::Ordering;
81use std::fmt;
82use std::hash::{Hash, Hasher};
83use std::sync::Arc;
84
85pub use crate::literals::{BooleanValue, DateTimeValue, TimeValue, TimezoneValue, Value};
86
87#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
88pub enum EffectiveDate {
89 Origin,
90 DateTimeValue(crate::DateTimeValue),
91}
92
93impl EffectiveDate {
94 pub fn as_ref(&self) -> Option<&crate::DateTimeValue> {
95 match self {
96 EffectiveDate::Origin => None,
97 EffectiveDate::DateTimeValue(dt) => Some(dt),
98 }
99 }
100
101 pub fn from_option(opt: Option<crate::DateTimeValue>) -> Self {
102 match opt {
103 None => EffectiveDate::Origin,
104 Some(dt) => EffectiveDate::DateTimeValue(dt),
105 }
106 }
107
108 pub fn to_option(&self) -> Option<crate::DateTimeValue> {
109 match self {
110 EffectiveDate::Origin => None,
111 EffectiveDate::DateTimeValue(dt) => Some(dt.clone()),
112 }
113 }
114
115 pub fn is_origin(&self) -> bool {
116 matches!(self, EffectiveDate::Origin)
117 }
118}
119
120impl PartialOrd for EffectiveDate {
121 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
122 Some(self.cmp(other))
123 }
124}
125
126impl Ord for EffectiveDate {
127 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
129 self.as_ref().cmp(&other.as_ref())
130 }
131}
132
133impl fmt::Display for EffectiveDate {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 match self {
136 EffectiveDate::Origin => Ok(()),
137 EffectiveDate::DateTimeValue(dt) => write!(f, "{}", dt),
138 }
139 }
140}
141
142#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
156pub struct LemmaRepository {
157 pub name: Option<String>,
159 pub dependency: Option<String>,
162 pub start_line: usize,
163 pub source_type: Option<crate::parsing::source::SourceType>,
164}
165
166impl LemmaRepository {
167 #[must_use]
168 pub fn new(name: Option<String>) -> Self {
169 Self {
170 name: name.map(ascii_lowercase_logical_name),
171 dependency: None,
172 start_line: 1,
173 source_type: None,
174 }
175 }
176
177 #[must_use]
178 pub fn with_start_line(mut self, start_line: usize) -> Self {
179 self.start_line = start_line;
180 self
181 }
182
183 #[must_use]
184 pub fn with_source_type(mut self, source_type: crate::parsing::source::SourceType) -> Self {
185 self.source_type = Some(source_type);
186 self
187 }
188
189 #[must_use]
190 pub fn with_dependency(mut self, dependency_id: impl Into<String>) -> Self {
191 self.dependency = Some(dependency_id.into());
192 self
193 }
194}
195
196impl PartialEq for LemmaRepository {
197 fn eq(&self, other: &Self) -> bool {
198 self.name == other.name
199 }
200}
201
202impl Eq for LemmaRepository {}
203
204impl PartialOrd for LemmaRepository {
205 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
206 Some(self.cmp(other))
207 }
208}
209
210impl Ord for LemmaRepository {
211 fn cmp(&self, other: &Self) -> Ordering {
212 self.name.cmp(&other.name)
213 }
214}
215
216impl Hash for LemmaRepository {
217 fn hash<H: Hasher>(&self, state: &mut H) {
218 self.name.hash(state);
219 }
220}
221
222#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
226pub struct RepositoryQualifier {
227 pub name: String,
228}
229
230impl RepositoryQualifier {
231 #[must_use]
232 pub fn new(name: impl Into<String>) -> Self {
233 Self {
234 name: ascii_lowercase_logical_name(name.into()),
235 }
236 }
237
238 #[must_use]
240 pub fn is_registry(&self) -> bool {
241 self.name.starts_with('@')
242 }
243}
244
245impl fmt::Display for RepositoryQualifier {
246 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247 write!(f, "{}", self.name)
248 }
249}
250
251#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
262pub struct LemmaSpec {
263 pub name: String,
264 pub effective_from: EffectiveDate,
265 pub source_type: Option<crate::parsing::source::SourceType>,
266 pub start_line: usize,
267 pub commentary: Option<String>,
268 pub data: Vec<LemmaData>,
269 pub rules: Vec<LemmaRule>,
270 pub meta_fields: Vec<MetaField>,
271}
272
273#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
274pub struct MetaField {
275 pub key: String,
276 pub value: Value,
277 pub source_location: Source,
278}
279
280impl fmt::Display for MetaField {
281 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
282 write!(f, "meta {}: {}", self.key, AsLemmaSource(&self.value))
283 }
284}
285
286#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
287pub struct LemmaData {
288 pub reference: Reference,
289 pub value: DataValue,
290 pub source_location: Source,
291}
292
293#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
299pub struct UnlessClause {
300 pub condition: Expression,
301 pub result: Expression,
302 pub source_location: Source,
303}
304
305#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
307pub struct LemmaRule {
308 pub name: String,
309 pub expression: Expression,
310 pub unless_clauses: Vec<UnlessClause>,
311 pub source_location: Source,
312}
313
314#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
320pub struct Expression {
321 pub kind: ExpressionKind,
322 pub source_location: Option<Source>,
323}
324
325impl Expression {
326 #[must_use]
328 pub fn new(kind: ExpressionKind, source_location: Source) -> Self {
329 Self {
330 kind,
331 source_location: Some(source_location),
332 }
333 }
334}
335
336impl PartialEq for Expression {
338 fn eq(&self, other: &Self) -> bool {
339 self.kind == other.kind
340 }
341}
342
343impl Eq for Expression {}
344
345#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
347#[serde(rename_all = "snake_case")]
348pub enum DateRelativeKind {
349 InPast,
350 InFuture,
351}
352
353#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
355#[serde(rename_all = "snake_case")]
356pub enum DateCalendarKind {
357 Current,
358 Past,
359 Future,
360 NotIn,
361}
362
363#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
365#[serde(rename_all = "snake_case")]
366pub enum CalendarPeriodUnit {
367 Year,
368 Month,
369 Week,
370}
371
372impl CalendarPeriodUnit {
373 #[must_use]
374 pub fn from_keyword(s: &str) -> Option<Self> {
375 match s.trim().to_lowercase().as_str() {
376 "year" => Some(Self::Year),
377 "month" => Some(Self::Month),
378 "week" => Some(Self::Week),
379 _ => None,
380 }
381 }
382}
383
384impl fmt::Display for DateRelativeKind {
385 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
386 match self {
387 DateRelativeKind::InPast => write!(f, "in past"),
388 DateRelativeKind::InFuture => write!(f, "in future"),
389 }
390 }
391}
392
393impl fmt::Display for DateCalendarKind {
394 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
395 match self {
396 DateCalendarKind::Current => write!(f, "in calendar"),
397 DateCalendarKind::Past => write!(f, "in past calendar"),
398 DateCalendarKind::Future => write!(f, "in future calendar"),
399 DateCalendarKind::NotIn => write!(f, "not in calendar"),
400 }
401 }
402}
403
404impl fmt::Display for CalendarPeriodUnit {
405 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
406 match self {
407 CalendarPeriodUnit::Year => write!(f, "year"),
408 CalendarPeriodUnit::Month => write!(f, "month"),
409 CalendarPeriodUnit::Week => write!(f, "week"),
410 }
411 }
412}
413
414#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
416#[serde(rename_all = "snake_case")]
417pub enum ExpressionKind {
418 Literal(Value),
420 Reference(Reference),
422 Now,
424 DateRelative(DateRelativeKind, Arc<Expression>),
427 DateCalendar(DateCalendarKind, CalendarPeriodUnit, Arc<Expression>),
430 RangeLiteral(Arc<Expression>, Arc<Expression>),
432 PastFutureRange(DateRelativeKind, Arc<Expression>),
434 RangeContainment(Arc<Expression>, Arc<Expression>),
436 LogicalAnd(Arc<Expression>, Arc<Expression>),
437 Arithmetic(Arc<Expression>, ArithmeticComputation, Arc<Expression>),
438 Comparison(Arc<Expression>, ComparisonComputation, Arc<Expression>),
439 UnitConversion(Arc<Expression>, ConversionTarget),
440 LogicalNegation(Arc<Expression>, NegationType),
441 MathematicalComputation(MathematicalComputation, Arc<Expression>),
442 Veto(VetoExpression),
443 ResultIsVeto(Arc<Expression>),
445}
446
447#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
457pub struct Reference {
458 pub segments: Vec<String>,
459 pub name: String,
460}
461
462impl Reference {
463 #[must_use]
464 pub fn local(name: String) -> Self {
465 Self {
466 segments: Vec::new(),
467 name: ascii_lowercase_logical_name(name),
468 }
469 }
470
471 #[must_use]
472 pub fn from_path(path: Vec<String>) -> Self {
473 if path.is_empty() {
474 Self {
475 segments: Vec::new(),
476 name: String::new(),
477 }
478 } else {
479 let name = ascii_lowercase_logical_name(path[path.len() - 1].clone());
481 let segments = path[..path.len() - 1]
482 .iter()
483 .map(|segment| ascii_lowercase_logical_name(segment.clone()))
484 .collect();
485 Self { segments, name }
486 }
487 }
488
489 #[must_use]
490 pub fn is_local(&self) -> bool {
491 self.segments.is_empty()
492 }
493
494 #[must_use]
495 pub fn full_path(&self) -> Vec<String> {
496 let mut path = self.segments.clone();
497 path.push(self.name.clone());
498 path
499 }
500}
501
502impl fmt::Display for Reference {
503 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
504 for segment in &self.segments {
505 write!(f, "{}.", segment)?;
506 }
507 write!(f, "{}", self.name)
508 }
509}
510
511#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, serde::Deserialize)]
513#[serde(rename_all = "snake_case")]
514pub enum ArithmeticComputation {
515 Add,
516 Subtract,
517 Multiply,
518 Divide,
519 Modulo,
520 Power,
521}
522
523#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, serde::Deserialize)]
525#[serde(rename_all = "snake_case")]
526pub enum ComparisonComputation {
527 GreaterThan,
528 LessThan,
529 GreaterThanOrEqual,
530 LessThanOrEqual,
531 Is,
532 IsNot,
533}
534
535impl ComparisonComputation {
536 #[must_use]
538 pub fn is_equal(&self) -> bool {
539 matches!(self, ComparisonComputation::Is)
540 }
541
542 #[must_use]
544 pub fn is_not_equal(&self) -> bool {
545 matches!(self, ComparisonComputation::IsNot)
546 }
547}
548
549#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
551#[serde(rename_all = "snake_case")]
552pub enum ConversionTarget {
553 Type(PrimitiveKind),
554 Unit { unit_name: String },
555}
556
557#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
559#[serde(rename_all = "snake_case")]
560pub enum NegationType {
561 Not,
562}
563
564#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
571pub struct VetoExpression {
572 pub message: Option<String>,
573}
574
575#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, serde::Deserialize)]
577#[serde(rename_all = "snake_case")]
578pub enum MathematicalComputation {
579 Sqrt,
580 Sin,
581 Cos,
582 Tan,
583 Asin,
584 Acos,
585 Atan,
586 Log,
587 Exp,
588 Abs,
589 Floor,
590 Ceil,
591 Round,
592}
593
594#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
601pub struct SpecRef {
602 pub repository: Option<RepositoryQualifier>,
605 pub name: String,
607 pub effective: Option<DateTimeValue>,
609 pub repository_span: Option<Span>,
611 pub target_span: Option<Span>,
613}
614
615impl std::fmt::Display for SpecRef {
616 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
617 if let Some(qualifier) = &self.repository {
618 write!(f, "{} ", qualifier)?;
619 }
620 write!(f, "{}", self.name)?;
621 if let Some(d) = &self.effective {
622 write!(f, " {}", d)?;
623 }
624 Ok(())
625 }
626}
627
628impl SpecRef {
629 pub fn same_repository(name: impl Into<String>) -> Self {
631 Self {
632 name: ascii_lowercase_logical_name(name.into()),
633 repository: None,
634 effective: None,
635 repository_span: None,
636 target_span: None,
637 }
638 }
639
640 pub fn cross_repository(name: impl Into<String>, qualifier: RepositoryQualifier) -> Self {
642 Self {
643 name: ascii_lowercase_logical_name(name.into()),
644 repository: Some(qualifier),
645 effective: None,
646 repository_span: None,
647 target_span: None,
648 }
649 }
650
651 pub fn at(&self, effective: &EffectiveDate) -> EffectiveDate {
654 self.effective
655 .clone()
656 .map_or_else(|| effective.clone(), EffectiveDate::DateTimeValue)
657 }
658
659 pub fn resolved_instant(
662 &self,
663 consumer_effective_from: Option<&DateTimeValue>,
664 ) -> Option<DateTimeValue> {
665 self.effective
666 .clone()
667 .or_else(|| consumer_effective_from.cloned())
668 }
669}
670
671#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
679pub struct UnitFactor {
680 pub measure_ref: String,
681 pub exp: i32,
682}
683
684#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
693pub enum UnitArg {
694 Factor(#[serde(with = "crate::literals::decimal_string_serde")] Decimal),
695 Expr(
696 #[serde(with = "crate::literals::decimal_string_serde")] Decimal,
697 Vec<UnitFactor>,
698 ),
699}
700
701impl fmt::Display for UnitArg {
702 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
703 match self {
704 UnitArg::Factor(v) => write!(f, "{}", v),
705 UnitArg::Expr(prefix, factors) => {
706 if *prefix != Decimal::ONE {
707 write!(f, "{} ", prefix)?;
708 }
709 for (index, factor) in factors.iter().enumerate() {
710 if factor.exp == 0 {
711 unreachable!("BUG: unit factor exponent cannot be zero");
712 }
713 if factor.exp > 0 {
714 if index > 0 {
715 write!(f, " * ")?;
716 }
717 write!(f, "{}", factor.measure_ref)?;
718 if factor.exp != 1 {
719 write!(f, "^{}", factor.exp)?;
720 }
721 } else {
722 let denominator_started =
723 factors[..index].iter().any(|prior| prior.exp < 0);
724 if denominator_started {
725 write!(f, " * ")?;
726 } else {
727 write!(f, "/")?;
728 }
729 write!(f, "{}", factor.measure_ref)?;
730 let positive_exp = factor
731 .exp
732 .checked_neg()
733 .expect("BUG: negative unit factor exponent");
734 if positive_exp != 1 {
735 write!(f, "^{}", positive_exp)?;
736 }
737 }
738 }
739 Ok(())
740 }
741 }
742 }
743}
744
745#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
763pub enum CommandArg {
764 Literal(crate::literals::Value),
766 Label(String),
768 UnitExpr(UnitArg),
770}
771
772impl fmt::Display for CommandArg {
773 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
774 match self {
775 CommandArg::Literal(v) => write!(f, "{}", v),
776 CommandArg::Label(s) => write!(f, "{}", s),
777 CommandArg::UnitExpr(unit_arg) => write!(f, "{}", unit_arg),
778 }
779 }
780}
781
782#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
784#[serde(rename_all = "snake_case")]
785pub enum TypeConstraintCommand {
786 Help,
787 Suggest,
788 Fill,
789 Unit,
790 Trait,
791 Minimum,
792 Maximum,
793 Lower,
794 Upper,
795 Decimals,
796 Option,
797 Options,
798 Length,
799}
800
801impl fmt::Display for TypeConstraintCommand {
802 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
803 let s = match self {
804 TypeConstraintCommand::Help => "help",
805 TypeConstraintCommand::Suggest => "suggest",
806 TypeConstraintCommand::Fill => "fill",
807 TypeConstraintCommand::Unit => "unit",
808 TypeConstraintCommand::Trait => "trait",
809 TypeConstraintCommand::Minimum => "minimum",
810 TypeConstraintCommand::Maximum => "maximum",
811 TypeConstraintCommand::Lower => "lower",
812 TypeConstraintCommand::Upper => "upper",
813 TypeConstraintCommand::Decimals => "decimals",
814 TypeConstraintCommand::Option => "option",
815 TypeConstraintCommand::Options => "options",
816 TypeConstraintCommand::Length => "length",
817 };
818 write!(f, "{}", s)
819 }
820}
821
822#[must_use]
824pub fn try_parse_type_constraint_command(s: &str) -> Option<TypeConstraintCommand> {
825 match s.trim() {
827 s if s.eq_ignore_ascii_case("help") => Some(TypeConstraintCommand::Help),
828 s if s.eq_ignore_ascii_case("suggest") => Some(TypeConstraintCommand::Suggest),
829 s if s.eq_ignore_ascii_case("fill") => Some(TypeConstraintCommand::Fill),
830 s if s.eq_ignore_ascii_case("unit") => Some(TypeConstraintCommand::Unit),
831 s if s.eq_ignore_ascii_case("trait") => Some(TypeConstraintCommand::Trait),
832 s if s.eq_ignore_ascii_case("minimum") => Some(TypeConstraintCommand::Minimum),
833 s if s.eq_ignore_ascii_case("maximum") => Some(TypeConstraintCommand::Maximum),
834 s if s.eq_ignore_ascii_case("lower") => Some(TypeConstraintCommand::Lower),
835 s if s.eq_ignore_ascii_case("upper") => Some(TypeConstraintCommand::Upper),
836 s if s.eq_ignore_ascii_case("decimals") => Some(TypeConstraintCommand::Decimals),
837 s if s.eq_ignore_ascii_case("option") => Some(TypeConstraintCommand::Option),
838 s if s.eq_ignore_ascii_case("options") => Some(TypeConstraintCommand::Options),
839 s if s.eq_ignore_ascii_case("length") => Some(TypeConstraintCommand::Length),
840 _ => None,
841 }
842}
843
844#[derive(Debug, Clone, Copy, PartialEq, Eq)]
846pub enum ContinuationShape {
847 Assignment,
848 SpaceSeparated,
849}
850
851impl TypeConstraintCommand {
852 #[must_use]
853 pub fn continuation_shape(self) -> ContinuationShape {
854 match self {
855 TypeConstraintCommand::Unit => ContinuationShape::Assignment,
856 TypeConstraintCommand::Help
857 | TypeConstraintCommand::Suggest
858 | TypeConstraintCommand::Fill
859 | TypeConstraintCommand::Trait
860 | TypeConstraintCommand::Minimum
861 | TypeConstraintCommand::Maximum
862 | TypeConstraintCommand::Lower
863 | TypeConstraintCommand::Upper
864 | TypeConstraintCommand::Decimals
865 | TypeConstraintCommand::Option
866 | TypeConstraintCommand::Options
867 | TypeConstraintCommand::Length => ContinuationShape::SpaceSeparated,
868 }
869 }
870}
871
872#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
874pub struct Constraint {
875 pub command: TypeConstraintCommand,
876 pub args: Vec<CommandArg>,
877 pub source_location: crate::parsing::source::Source,
878 pub deprecated_without_colon: bool,
880}
881
882impl Constraint {
883 #[must_use]
884 pub fn new(
885 command: TypeConstraintCommand,
886 args: Vec<CommandArg>,
887 source_location: crate::parsing::source::Source,
888 ) -> Self {
889 Self {
890 command,
891 args,
892 source_location,
893 deprecated_without_colon: false,
894 }
895 }
896}
897
898#[cfg(test)]
899pub(crate) fn test_constraint(command: TypeConstraintCommand, args: Vec<CommandArg>) -> Constraint {
900 Constraint::new(
901 command,
902 args,
903 crate::parsing::source::Source::new(
904 crate::parsing::source::SourceType::Volatile,
905 Span {
906 start: 0,
907 end: 0,
908 line: 1,
909 col: 0,
910 },
911 ),
912 )
913}
914
915#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
917#[serde(rename_all = "snake_case")]
918pub enum WithRhs {
919 Literal(Value),
920 Reference { target: Reference },
921}
922
923#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
925pub struct UsesBinding {
926 pub path: Reference,
928 pub rhs: WithRhs,
929 pub source_location: Source,
930 pub deprecated_standalone_with: bool,
932}
933
934#[must_use]
936pub fn prefix_reference(alias: &str, relative: &Reference) -> Reference {
937 let mut segments = vec![ascii_lowercase_logical_name(alias.to_string())];
938 segments.extend(relative.segments.iter().cloned());
939 Reference {
940 segments,
941 name: relative.name.clone(),
942 }
943}
944
945#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
946#[serde(rename_all = "snake_case")]
947pub enum DataValue {
949 Definition {
957 base: Option<ParentType>,
958 constraints: Option<Vec<Constraint>>,
959 value: Option<Value>,
960 },
961 Import {
963 spec_ref: SpecRef,
964 bindings: Vec<UsesBinding>,
965 },
966}
967
968impl DataValue {
969 #[must_use]
970 pub fn import(spec_ref: SpecRef) -> Self {
971 Self::Import {
972 spec_ref,
973 bindings: Vec::new(),
974 }
975 }
976
977 #[must_use]
979 pub fn is_definition_literal_only(&self) -> bool {
980 matches!(
981 self,
982 DataValue::Definition {
983 base: None,
984 constraints: None,
985 value: Some(_),
986 }
987 )
988 }
989
990 #[must_use]
992 pub fn definition_needs_type_resolution(&self) -> bool {
993 match self {
994 DataValue::Definition { base: Some(_), .. }
995 | DataValue::Definition {
996 constraints: Some(_),
997 ..
998 } => true,
999 DataValue::Definition {
1000 base: None,
1001 constraints: None,
1002 value: Some(v),
1003 } => !matches!(v, Value::NumberWithUnit(_, _)),
1004 DataValue::Import { .. } | DataValue::Definition { .. } => false,
1005 }
1006 }
1007}
1008
1009fn format_constraint_chain(constraints: &[Constraint]) -> String {
1012 constraints
1013 .iter()
1014 .map(|row| format_constraint_as_source(&row.command, &row.args))
1015 .collect::<Vec<_>>()
1016 .join(" -> ")
1017}
1018
1019impl fmt::Display for DataValue {
1020 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1021 match self {
1022 DataValue::Definition {
1023 base,
1024 constraints,
1025 value,
1026 } => {
1027 if base.is_none() && constraints.is_none() {
1028 return match value {
1029 Some(v) => write!(f, "{}", v),
1030 None => Ok(()),
1031 };
1032 }
1033 let base_str = match base.as_ref() {
1034 Some(b) => format!("{b}"),
1035 None => match value {
1036 Some(v) => {
1037 if let Some(ref constraints_vec) = constraints {
1038 let constraint_str = format_constraint_chain(constraints_vec);
1039 return write!(f, "{v} -> {constraint_str}");
1040 }
1041 return write!(f, "{v}");
1042 }
1043 None => String::new(),
1044 },
1045 };
1046 if let Some(ref constraints_vec) = constraints {
1047 let constraint_str = format_constraint_chain(constraints_vec);
1048 write!(f, "{base_str} -> {constraint_str}")
1049 } else {
1050 write!(f, "{base_str}")
1051 }
1052 }
1053 DataValue::Import {
1054 spec_ref,
1055 bindings: _,
1056 } => {
1057 write!(f, "uses {}", spec_ref)
1058 }
1059 }
1060 }
1061}
1062
1063impl LemmaData {
1064 #[must_use]
1065 pub fn new(reference: Reference, value: DataValue, source_location: Source) -> Self {
1066 Self {
1067 reference,
1068 value,
1069 source_location,
1070 }
1071 }
1072}
1073
1074impl LemmaSpec {
1075 #[must_use]
1076 pub fn new(name: String) -> Self {
1077 Self {
1078 name: ascii_lowercase_logical_name(name),
1079 effective_from: EffectiveDate::Origin,
1080 source_type: None,
1081 start_line: 1,
1082 commentary: None,
1083 data: Vec::new(),
1084 rules: Vec::new(),
1085 meta_fields: Vec::new(),
1086 }
1087 }
1088
1089 pub fn effective_from(&self) -> Option<&DateTimeValue> {
1091 self.effective_from.as_ref()
1092 }
1093
1094 #[must_use]
1095 pub fn with_source_type(mut self, source_type: crate::parsing::source::SourceType) -> Self {
1096 self.source_type = Some(source_type);
1097 self
1098 }
1099
1100 #[must_use]
1101 pub fn with_start_line(mut self, start_line: usize) -> Self {
1102 self.start_line = start_line;
1103 self
1104 }
1105
1106 #[must_use]
1107 pub fn set_commentary(mut self, commentary: String) -> Self {
1108 self.commentary = Some(commentary);
1109 self
1110 }
1111
1112 #[must_use]
1113 pub fn add_data(mut self, data: LemmaData) -> Self {
1114 self.data.push(data);
1115 self
1116 }
1117
1118 #[must_use]
1119 pub fn add_rule(mut self, rule: LemmaRule) -> Self {
1120 self.rules.push(rule);
1121 self
1122 }
1123
1124 #[must_use]
1125 pub fn add_meta_field(mut self, meta: MetaField) -> Self {
1126 self.meta_fields.push(meta);
1127 self
1128 }
1129}
1130
1131impl fmt::Display for LemmaSpec {
1132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1133 write!(f, "spec {}", self.name)?;
1134 if let EffectiveDate::DateTimeValue(ref af) = self.effective_from {
1135 write!(f, " {}", af)?;
1136 }
1137 writeln!(f)?;
1138
1139 if let Some(ref commentary) = self.commentary {
1140 writeln!(f, "\"\"\"")?;
1141 writeln!(f, "{}", commentary)?;
1142 writeln!(f, "\"\"\"")?;
1143 }
1144
1145 if !self.data.is_empty() {
1146 writeln!(f)?;
1147 for data in &self.data {
1148 write!(f, "{}", data)?;
1149 }
1150 }
1151
1152 if !self.rules.is_empty() {
1153 writeln!(f)?;
1154 for (index, rule) in self.rules.iter().enumerate() {
1155 if index > 0 {
1156 writeln!(f)?;
1157 }
1158 write!(f, "{}", rule)?;
1159 }
1160 }
1161
1162 if !self.meta_fields.is_empty() {
1163 writeln!(f)?;
1164 for meta in &self.meta_fields {
1165 writeln!(f, "{}", meta)?;
1166 }
1167 }
1168
1169 Ok(())
1170 }
1171}
1172
1173impl fmt::Display for LemmaData {
1174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1175 writeln!(f, "data {}: {}", self.reference, self.value)
1176 }
1177}
1178
1179impl fmt::Display for LemmaRule {
1180 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1181 write!(f, "rule {}: {}", self.name, self.expression)?;
1182 for unless_clause in &self.unless_clauses {
1183 write!(
1184 f,
1185 "\n unless {} then {}",
1186 unless_clause.condition, unless_clause.result
1187 )?;
1188 }
1189 writeln!(f)?;
1190 Ok(())
1191 }
1192}
1193
1194pub fn expression_precedence(kind: &ExpressionKind) -> u8 {
1202 match kind {
1203 ExpressionKind::LogicalAnd(..) => 2,
1204 ExpressionKind::LogicalNegation(..) => 3,
1205 ExpressionKind::Comparison(..) | ExpressionKind::ResultIsVeto(..) => 4,
1206 ExpressionKind::RangeContainment(..) => 4,
1207 ExpressionKind::DateRelative(..) | ExpressionKind::DateCalendar(..) => 4,
1208 ExpressionKind::Arithmetic(_, op, _) => arithmetic_precedence(op),
1209 ExpressionKind::UnitConversion(..) => 8,
1210 ExpressionKind::RangeLiteral(..) => 9,
1211 ExpressionKind::MathematicalComputation(..) => 10,
1212 ExpressionKind::PastFutureRange(..) => 10,
1213 ExpressionKind::Literal(..)
1214 | ExpressionKind::Reference(..)
1215 | ExpressionKind::Now
1216 | ExpressionKind::Veto(..) => 10,
1217 }
1218}
1219
1220pub fn arithmetic_precedence(op: &ArithmeticComputation) -> u8 {
1222 match op {
1223 ArithmeticComputation::Add | ArithmeticComputation::Subtract => 5,
1224 ArithmeticComputation::Multiply
1225 | ArithmeticComputation::Divide
1226 | ArithmeticComputation::Modulo => 6,
1227 ArithmeticComputation::Power => 7,
1228 }
1229}
1230
1231#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1233pub enum OperandSide {
1234 Left,
1235 Right,
1236}
1237
1238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1240pub enum Associativity {
1241 Left,
1242 Right,
1243}
1244
1245pub fn operand_needs_parentheses(
1251 child_prec: u8,
1252 parent_prec: u8,
1253 side: OperandSide,
1254 parent_assoc: Option<Associativity>,
1255) -> bool {
1256 if child_prec < parent_prec {
1257 return true;
1258 }
1259 if child_prec > parent_prec {
1260 return false;
1261 }
1262 match parent_assoc {
1263 None => false,
1264 Some(Associativity::Left) => matches!(side, OperandSide::Right),
1265 Some(Associativity::Right) => matches!(side, OperandSide::Left),
1266 }
1267}
1268
1269pub fn arithmetic_associativity(op: &ArithmeticComputation) -> Associativity {
1270 match op {
1271 ArithmeticComputation::Power => Associativity::Right,
1272 ArithmeticComputation::Add
1273 | ArithmeticComputation::Subtract
1274 | ArithmeticComputation::Multiply
1275 | ArithmeticComputation::Divide
1276 | ArithmeticComputation::Modulo => Associativity::Left,
1277 }
1278}
1279
1280fn write_expression_child(
1281 f: &mut fmt::Formatter<'_>,
1282 child: &Expression,
1283 parent_prec: u8,
1284 side: OperandSide,
1285 parent_assoc: Option<Associativity>,
1286) -> fmt::Result {
1287 let child_prec = expression_precedence(&child.kind);
1288 if operand_needs_parentheses(child_prec, parent_prec, side, parent_assoc) {
1289 write!(f, "({})", child)
1290 } else {
1291 write!(f, "{}", child)
1292 }
1293}
1294
1295impl fmt::Display for Expression {
1296 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1297 match &self.kind {
1298 ExpressionKind::Literal(lit) => write!(f, "{}", AsLemmaSource(lit)),
1299 ExpressionKind::Reference(r) => write!(f, "{}", r),
1300 ExpressionKind::Arithmetic(left, op, right) => {
1301 let my_prec = expression_precedence(&self.kind);
1302 let assoc = Some(arithmetic_associativity(op));
1303 write_expression_child(f, left, my_prec, OperandSide::Left, assoc)?;
1304 write!(f, " {} ", op)?;
1305 write_expression_child(f, right, my_prec, OperandSide::Right, assoc)
1306 }
1307 ExpressionKind::Comparison(left, op, right) => {
1308 let my_prec = expression_precedence(&self.kind);
1309 write_expression_child(f, left, my_prec, OperandSide::Left, None)?;
1310 write!(f, " {} ", op)?;
1311 write_expression_child(f, right, my_prec, OperandSide::Right, None)
1312 }
1313 ExpressionKind::UnitConversion(value, target) => {
1314 let my_prec = expression_precedence(&self.kind);
1315 write_expression_child(f, value, my_prec, OperandSide::Left, None)?;
1316 write!(f, " as {}", target)
1317 }
1318 ExpressionKind::LogicalNegation(expr, negation) => {
1319 if let (NegationType::Not, ExpressionKind::ResultIsVeto(operand)) =
1320 (negation, &expr.kind)
1321 {
1322 let my_prec = expression_precedence(&self.kind);
1323 write_expression_child(f, operand, my_prec, OperandSide::Left, None)?;
1324 write!(f, " is not veto")
1325 } else {
1326 let my_prec = expression_precedence(&self.kind);
1327 write!(f, "not ")?;
1328 write_expression_child(f, expr, my_prec, OperandSide::Right, None)
1329 }
1330 }
1331 ExpressionKind::ResultIsVeto(operand) => {
1332 let my_prec = expression_precedence(&self.kind);
1333 write_expression_child(f, operand, my_prec, OperandSide::Left, None)?;
1334 write!(f, " is veto")
1335 }
1336 ExpressionKind::LogicalAnd(left, right) => {
1337 let my_prec = expression_precedence(&self.kind);
1338 let assoc = Some(Associativity::Left);
1339 write_expression_child(f, left, my_prec, OperandSide::Left, assoc)?;
1340 write!(f, " and ")?;
1341 write_expression_child(f, right, my_prec, OperandSide::Right, assoc)
1342 }
1343 ExpressionKind::MathematicalComputation(op, operand) => {
1344 let my_prec = expression_precedence(&self.kind);
1345 write!(f, "{} ", op)?;
1346 write_expression_child(f, operand, my_prec, OperandSide::Right, None)
1347 }
1348 ExpressionKind::Veto(veto) => match &veto.message {
1349 Some(msg) => write!(f, "veto {}", quote_lemma_text(msg)),
1350 None => write!(f, "veto"),
1351 },
1352 ExpressionKind::Now => write!(f, "now"),
1353 ExpressionKind::DateRelative(kind, date_expr) => {
1354 write!(f, "{} {}", date_expr, kind)?;
1355 Ok(())
1356 }
1357 ExpressionKind::DateCalendar(kind, unit, date_expr) => {
1358 write!(f, "{} {} {}", date_expr, kind, unit)
1359 }
1360 ExpressionKind::RangeLiteral(left, right) => {
1361 let my_prec = expression_precedence(&self.kind);
1362 write_expression_child(f, left, my_prec, OperandSide::Left, None)?;
1363 write!(f, "...")?;
1364 write_expression_child(f, right, my_prec, OperandSide::Right, None)
1365 }
1366 ExpressionKind::PastFutureRange(kind, offset_expr) => {
1367 match kind {
1368 DateRelativeKind::InPast => write!(f, "past ")?,
1369 DateRelativeKind::InFuture => write!(f, "future ")?,
1370 }
1371 let my_prec = expression_precedence(&self.kind);
1372 write_expression_child(f, offset_expr, my_prec, OperandSide::Right, None)
1373 }
1374 ExpressionKind::RangeContainment(value, range) => {
1375 let my_prec = expression_precedence(&self.kind);
1376 write_expression_child(f, value, my_prec, OperandSide::Left, None)?;
1377 write!(f, " in ")?;
1378 write_expression_child(f, range, my_prec, OperandSide::Right, None)
1379 }
1380 }
1381 }
1382}
1383
1384impl fmt::Display for ConversionTarget {
1385 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1386 match self {
1387 ConversionTarget::Type(kind) => write!(f, "{kind}"),
1388 ConversionTarget::Unit { unit_name } => write!(f, "{unit_name}"),
1389 }
1390 }
1391}
1392
1393impl fmt::Display for ArithmeticComputation {
1394 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1395 match self {
1396 ArithmeticComputation::Add => write!(f, "+"),
1397 ArithmeticComputation::Subtract => write!(f, "-"),
1398 ArithmeticComputation::Multiply => write!(f, "*"),
1399 ArithmeticComputation::Divide => write!(f, "/"),
1400 ArithmeticComputation::Modulo => write!(f, "%"),
1401 ArithmeticComputation::Power => write!(f, "^"),
1402 }
1403 }
1404}
1405
1406impl fmt::Display for ComparisonComputation {
1407 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1408 match self {
1409 ComparisonComputation::GreaterThan => write!(f, ">"),
1410 ComparisonComputation::LessThan => write!(f, "<"),
1411 ComparisonComputation::GreaterThanOrEqual => write!(f, ">="),
1412 ComparisonComputation::LessThanOrEqual => write!(f, "<="),
1413 ComparisonComputation::Is => write!(f, "is"),
1414 ComparisonComputation::IsNot => write!(f, "is not"),
1415 }
1416 }
1417}
1418
1419impl fmt::Display for MathematicalComputation {
1420 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1421 match self {
1422 MathematicalComputation::Sqrt => write!(f, "sqrt"),
1423 MathematicalComputation::Sin => write!(f, "sin"),
1424 MathematicalComputation::Cos => write!(f, "cos"),
1425 MathematicalComputation::Tan => write!(f, "tan"),
1426 MathematicalComputation::Asin => write!(f, "asin"),
1427 MathematicalComputation::Acos => write!(f, "acos"),
1428 MathematicalComputation::Atan => write!(f, "atan"),
1429 MathematicalComputation::Log => write!(f, "log"),
1430 MathematicalComputation::Exp => write!(f, "exp"),
1431 MathematicalComputation::Abs => write!(f, "abs"),
1432 MathematicalComputation::Floor => write!(f, "floor"),
1433 MathematicalComputation::Ceil => write!(f, "ceil"),
1434 MathematicalComputation::Round => write!(f, "round"),
1435 }
1436 }
1437}
1438
1439#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1445#[serde(rename_all = "snake_case")]
1446pub enum PrimitiveKind {
1447 Boolean,
1448 Measure,
1449 MeasureRange,
1450 Number,
1451 NumberRange,
1452 Ratio,
1453 RatioRange,
1454 Text,
1455 Date,
1456 DateRange,
1457 Time,
1458 TimeRange,
1459}
1460
1461impl std::fmt::Display for PrimitiveKind {
1462 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1463 let s = match self {
1464 PrimitiveKind::Boolean => "boolean",
1465 PrimitiveKind::Measure => "measure",
1466 PrimitiveKind::MeasureRange => "measure range",
1467 PrimitiveKind::Number => "number",
1468 PrimitiveKind::NumberRange => "number range",
1469 PrimitiveKind::Ratio => "ratio",
1470 PrimitiveKind::RatioRange => "ratio range",
1471 PrimitiveKind::Text => "text",
1472 PrimitiveKind::Date => "date",
1473 PrimitiveKind::DateRange => "date range",
1474 PrimitiveKind::Time => "time",
1475 PrimitiveKind::TimeRange => "time range",
1476 };
1477 write!(f, "{}", s)
1478 }
1479}
1480
1481#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1486pub enum ParentType {
1487 Primitive {
1488 primitive: PrimitiveKind,
1489 },
1490 Custom {
1491 name: String,
1492 },
1493 Qualified {
1496 spec_alias: String,
1497 inner: Box<ParentType>,
1498 },
1499 Ranged {
1501 inner: Box<ParentType>,
1502 },
1503}
1504
1505impl std::fmt::Display for ParentType {
1506 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1507 match self {
1508 ParentType::Primitive { primitive } => write!(f, "{}", primitive),
1509 ParentType::Custom { name } => write!(f, "{}", name),
1510 ParentType::Qualified { spec_alias, inner } => {
1511 write!(f, "{spec_alias}.{inner}")
1512 }
1513 ParentType::Ranged { inner } => write!(f, "{inner} range"),
1514 }
1515 }
1516}
1517
1518pub struct AsLemmaSource<'a, T: ?Sized>(pub &'a T);
1524
1525pub fn quote_lemma_text(s: &str) -> String {
1528 let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
1529 format!("\"{}\"", escaped)
1530}
1531
1532fn format_decimal_source(n: &Decimal) -> String {
1537 let raw = if n.fract().is_zero() {
1538 n.trunc().to_string()
1539 } else {
1540 n.to_string()
1541 };
1542 group_digits(&raw)
1543}
1544
1545fn group_digits(s: &str) -> String {
1549 let (sign, rest) = if s.starts_with('-') || s.starts_with('+') {
1550 (&s[..1], &s[1..])
1551 } else {
1552 ("", s)
1553 };
1554
1555 let (int_part, frac_part) = match rest.find('.') {
1556 Some(pos) => (&rest[..pos], &rest[pos..]),
1557 None => (rest, ""),
1558 };
1559
1560 if int_part.len() < 4 {
1561 return s.to_string();
1562 }
1563
1564 let mut grouped = String::with_capacity(int_part.len() + int_part.len() / 3);
1565 for (i, ch) in int_part.chars().enumerate() {
1566 let digits_remaining = int_part.len() - i;
1567 if i > 0 && digits_remaining % 3 == 0 {
1568 grouped.push('_');
1569 }
1570 grouped.push(ch);
1571 }
1572
1573 format!("{}{}{}", sign, grouped, frac_part)
1574}
1575
1576impl<'a> fmt::Display for AsLemmaSource<'a, CommandArg> {
1577 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1578 use crate::literals::Value;
1579 match self.0 {
1580 CommandArg::Literal(Value::Text(s)) => write!(f, "{}", quote_lemma_text(s)),
1581 CommandArg::Literal(Value::Number(d)) => {
1582 write!(f, "{}", group_digits(&d.to_string()))
1583 }
1584 CommandArg::Literal(Value::Boolean(bv)) => write!(f, "{}", bv),
1585 CommandArg::Literal(Value::NumberWithUnit(d, unit)) => {
1586 write!(f, "{} {}", group_digits(&d.to_string()), unit)
1587 }
1588 CommandArg::Literal(value @ Value::Range(_, _)) => {
1589 write!(f, "{}", AsLemmaSource(value))
1590 }
1591 CommandArg::Literal(Value::Date(dt)) => write!(f, "{}", dt),
1592 CommandArg::Literal(Value::Time(t)) => write!(f, "{}", t),
1593 CommandArg::Label(s) => write!(f, "{}", s),
1594 CommandArg::UnitExpr(unit_arg) => write!(f, "{}", unit_arg),
1595 }
1596 }
1597}
1598
1599pub(crate) fn format_assignment_continuation(
1601 command: &str,
1602 key: &str,
1603 value: &impl fmt::Display,
1604) -> String {
1605 format!("{} {}: {}", command, key, value)
1606}
1607
1608pub(crate) fn format_constraint_as_source(
1610 cmd: &TypeConstraintCommand,
1611 args: &[CommandArg],
1612) -> String {
1613 if *cmd == TypeConstraintCommand::Unit {
1614 let Some(CommandArg::Label(name)) = args.first() else {
1615 return cmd.to_string();
1616 };
1617 let Some(CommandArg::UnitExpr(unit_arg)) = args.get(1) else {
1618 return format!("{} {}", cmd, name);
1619 };
1620 return format_assignment_continuation("unit", name, unit_arg);
1621 }
1622
1623 if args.is_empty() {
1624 cmd.to_string()
1625 } else {
1626 let args_str: Vec<String> = args
1627 .iter()
1628 .map(|a| format!("{}", AsLemmaSource(a)))
1629 .collect();
1630 format!("{} {}", cmd, args_str.join(" "))
1631 }
1632}
1633
1634fn format_constraints_as_source(constraints: &[Constraint], separator: &str) -> String {
1637 constraints
1638 .iter()
1639 .map(|row| format_constraint_as_source(&row.command, &row.args))
1640 .collect::<Vec<_>>()
1641 .join(separator)
1642}
1643
1644impl<'a> fmt::Display for AsLemmaSource<'a, Value> {
1647 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1648 match self.0 {
1649 Value::Number(n) => write!(f, "{}", format_decimal_source(n)),
1650 Value::Text(s) => write!(f, "{}", quote_lemma_text(s)),
1651 Value::Date(dt) => match dt.granularity {
1652 crate::literals::DateGranularity::Year => write!(f, "{:04}", dt.year),
1653 crate::literals::DateGranularity::YearMonth => {
1654 write!(f, "{:04}-{:02}", dt.year, dt.month)
1655 }
1656 crate::literals::DateGranularity::IsoWeek { iso_year, week } => {
1657 write!(f, "{:04}-W{:02}", iso_year, week)
1658 }
1659 crate::literals::DateGranularity::Full => {
1660 write!(f, "{:04}-{:02}-{:02}", dt.year, dt.month, dt.day)
1661 }
1662 crate::literals::DateGranularity::DateTime => {
1663 write!(
1664 f,
1665 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
1666 dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second
1667 )?;
1668 if let Some(tz) = &dt.timezone {
1669 write!(f, "{}", tz)?;
1670 }
1671 Ok(())
1672 }
1673 },
1674 Value::Time(t) => {
1675 write!(f, "{:02}:{:02}:{:02}", t.hour, t.minute, t.second)?;
1676 if let Some(tz) = &t.timezone {
1677 write!(f, "{}", tz)?;
1678 }
1679 Ok(())
1680 }
1681 Value::Boolean(b) => write!(f, "{}", b),
1682 Value::NumberWithUnit(n, u) => match u.as_str() {
1683 "percent" => write!(f, "{}%", format_decimal_source(n)),
1684 "permille" => write!(f, "{}%%", format_decimal_source(n)),
1685 unit => write!(f, "{} {}", format_decimal_source(n), unit),
1686 },
1687 Value::Range(left, right) => {
1688 write!(
1689 f,
1690 "{}...{}",
1691 AsLemmaSource(left.as_ref()),
1692 AsLemmaSource(right.as_ref())
1693 )
1694 }
1695 }
1696 }
1697}
1698
1699impl<'a> fmt::Display for AsLemmaSource<'a, DataValue> {
1702 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1703 match self.0 {
1704 DataValue::Definition {
1705 base,
1706 constraints,
1707 value,
1708 } => {
1709 if base.is_none() && constraints.is_none() {
1710 if let Some(v) = value {
1711 return write!(f, "{}", AsLemmaSource(v));
1712 }
1713 }
1714 let base_str = match base.as_ref() {
1715 Some(b) => format!("{}", b),
1716 None => match value {
1717 Some(v) => {
1718 if let Some(ref constraints_vec) = constraints {
1719 let constraint_str =
1720 format_constraints_as_source(constraints_vec, " -> ");
1721 return write!(f, "{} -> {}", AsLemmaSource(v), constraint_str);
1722 }
1723 return write!(f, "{}", AsLemmaSource(v));
1724 }
1725 None => String::new(),
1726 },
1727 };
1728 if let Some(ref constraints_vec) = constraints {
1729 let constraint_str = format_constraints_as_source(constraints_vec, " -> ");
1730 write!(f, "{} -> {}", base_str, constraint_str)
1731 } else {
1732 write!(f, "{}", base_str)
1733 }
1734 }
1735 DataValue::Import {
1736 spec_ref,
1737 bindings: _,
1738 } => {
1739 write!(f, "uses {}", spec_ref)
1740 }
1741 }
1742 }
1743}
1744
1745pub(crate) fn canonicalize_value(value: &mut Value) {
1746 if let Value::NumberWithUnit(_, unit) = value {
1747 *unit = ascii_lowercase_logical_name(std::mem::take(unit));
1748 }
1749}
1750
1751pub(crate) fn canonicalize_reference(reference: &mut Reference) {
1752 for segment in &mut reference.segments {
1753 *segment = ascii_lowercase_logical_name(std::mem::take(segment));
1754 }
1755 reference.name = ascii_lowercase_logical_name(std::mem::take(&mut reference.name));
1756}
1757
1758pub(crate) fn canonicalize_spec_ref(spec_ref: &mut SpecRef) {
1759 spec_ref.name = ascii_lowercase_logical_name(std::mem::take(&mut spec_ref.name));
1760 if let Some(qualifier) = spec_ref.repository.as_mut() {
1761 qualifier.name = ascii_lowercase_logical_name(std::mem::take(&mut qualifier.name));
1762 }
1763}
1764
1765pub(crate) fn canonicalize_parent_type(parent: &mut ParentType) {
1766 match parent {
1767 ParentType::Custom { name } => {
1768 *name = ascii_lowercase_logical_name(std::mem::take(name));
1769 }
1770 ParentType::Qualified { spec_alias, inner } => {
1771 *spec_alias = ascii_lowercase_logical_name(std::mem::take(spec_alias));
1772 canonicalize_parent_type(inner);
1773 }
1774 ParentType::Ranged { inner } => {
1775 canonicalize_parent_type(inner);
1776 }
1777 ParentType::Primitive { .. } => {}
1778 }
1779}
1780
1781pub(crate) fn canonicalize_unit_factor(factor: &mut UnitFactor) {
1782 factor.measure_ref = ascii_lowercase_logical_name(std::mem::take(&mut factor.measure_ref));
1783}
1784
1785pub(crate) fn canonicalize_unit_arg(unit_arg: &mut UnitArg) {
1786 if let UnitArg::Expr(_, factors) = unit_arg {
1787 for factor in factors {
1788 canonicalize_unit_factor(factor);
1789 }
1790 }
1791}
1792
1793pub(crate) fn canonicalize_command_arg(command_arg: &mut CommandArg) {
1794 match command_arg {
1795 CommandArg::Literal(value) => canonicalize_value(value),
1796 CommandArg::Label(label) => {
1797 *label = ascii_lowercase_logical_name(std::mem::take(label));
1798 }
1799 CommandArg::UnitExpr(unit_arg) => canonicalize_unit_arg(unit_arg),
1800 }
1801}
1802
1803pub(crate) fn canonicalize_constraints(constraints: &mut [Constraint]) {
1804 for row in constraints {
1805 for arg in &mut row.args {
1806 canonicalize_command_arg(arg);
1807 }
1808 }
1809}
1810
1811pub(crate) fn canonicalize_expression(expression: &mut Expression) {
1812 match &mut expression.kind {
1813 ExpressionKind::Literal(value) => canonicalize_value(value),
1814 ExpressionKind::Reference(reference) => canonicalize_reference(reference),
1815 ExpressionKind::Now => {}
1816 ExpressionKind::DateRelative(_, expression) => {
1817 canonicalize_expression(Arc::make_mut(expression));
1818 }
1819 ExpressionKind::DateCalendar(_, _, expression) => {
1820 canonicalize_expression(Arc::make_mut(expression));
1821 }
1822 ExpressionKind::RangeLiteral(left, right) => {
1823 canonicalize_expression(Arc::make_mut(left));
1824 canonicalize_expression(Arc::make_mut(right));
1825 }
1826 ExpressionKind::PastFutureRange(_, expression) => {
1827 canonicalize_expression(Arc::make_mut(expression));
1828 }
1829 ExpressionKind::RangeContainment(value, range) => {
1830 canonicalize_expression(Arc::make_mut(value));
1831 canonicalize_expression(Arc::make_mut(range));
1832 }
1833 ExpressionKind::LogicalAnd(left, right) => {
1834 canonicalize_expression(Arc::make_mut(left));
1835 canonicalize_expression(Arc::make_mut(right));
1836 }
1837 ExpressionKind::Arithmetic(left, _, right) => {
1838 canonicalize_expression(Arc::make_mut(left));
1839 canonicalize_expression(Arc::make_mut(right));
1840 }
1841 ExpressionKind::Comparison(left, _, right) => {
1842 canonicalize_expression(Arc::make_mut(left));
1843 canonicalize_expression(Arc::make_mut(right));
1844 }
1845 ExpressionKind::UnitConversion(expression, _) => {
1846 canonicalize_expression(Arc::make_mut(expression));
1847 }
1848 ExpressionKind::LogicalNegation(expression, _) => {
1849 canonicalize_expression(Arc::make_mut(expression));
1850 }
1851 ExpressionKind::MathematicalComputation(_, expression) => {
1852 canonicalize_expression(Arc::make_mut(expression));
1853 }
1854 ExpressionKind::Veto(_) => {}
1855 ExpressionKind::ResultIsVeto(expression) => {
1856 canonicalize_expression(Arc::make_mut(expression));
1857 }
1858 }
1859}
1860
1861pub(crate) fn canonicalize_unless_clause(unless_clause: &mut UnlessClause) {
1862 canonicalize_expression(&mut unless_clause.condition);
1863 canonicalize_expression(&mut unless_clause.result);
1864}
1865
1866pub(crate) fn canonicalize_data_value(data_value: &mut DataValue) {
1867 match data_value {
1868 DataValue::Definition {
1869 base,
1870 constraints,
1871 value,
1872 } => {
1873 if let Some(base) = base {
1874 canonicalize_parent_type(base);
1875 }
1876 if let Some(constraints) = constraints {
1877 canonicalize_constraints(constraints);
1878 }
1879 if let Some(value) = value {
1880 canonicalize_value(value);
1881 }
1882 }
1883 DataValue::Import { spec_ref, bindings } => {
1884 canonicalize_spec_ref(spec_ref);
1885 for binding in bindings {
1886 canonicalize_reference(&mut binding.path);
1887 match &mut binding.rhs {
1888 WithRhs::Literal(value) => canonicalize_value(value),
1889 WithRhs::Reference { target } => canonicalize_reference(target),
1890 }
1891 }
1892 }
1893 }
1894}
1895
1896pub(crate) fn canonicalize_lemma_data(data: &mut LemmaData) {
1897 canonicalize_reference(&mut data.reference);
1898 canonicalize_data_value(&mut data.value);
1899}
1900
1901pub(crate) fn canonicalize_lemma_rule(rule: &mut LemmaRule) {
1902 rule.name = ascii_lowercase_logical_name(std::mem::take(&mut rule.name));
1903 canonicalize_expression(&mut rule.expression);
1904 for unless_clause in &mut rule.unless_clauses {
1905 canonicalize_unless_clause(unless_clause);
1906 }
1907}
1908
1909pub(crate) fn canonicalize_lemma_spec(spec: &mut LemmaSpec) {
1910 spec.name = ascii_lowercase_logical_name(std::mem::take(&mut spec.name));
1911 for meta in &mut spec.meta_fields {
1912 meta.key = ascii_lowercase_logical_name(std::mem::take(&mut meta.key));
1913 }
1914 for data in &mut spec.data {
1915 canonicalize_lemma_data(data);
1916 }
1917 for rule in &mut spec.rules {
1918 canonicalize_lemma_rule(rule);
1919 }
1920}
1921
1922pub(crate) fn canonicalize_repository(repository: &mut LemmaRepository) {
1923 if let Some(name) = repository.name.take() {
1924 repository.name = Some(ascii_lowercase_logical_name(name));
1925 }
1926}
1927
1928#[cfg(test)]
1929mod tests {
1930 use super::*;
1931 use crate::literals::DateGranularity;
1932
1933 #[test]
1934 fn test_conversion_target_display() {
1935 assert_eq!(
1936 format!("{}", ConversionTarget::Type(PrimitiveKind::Number)),
1937 "number"
1938 );
1939 }
1940
1941 #[test]
1942 fn test_value_number_with_unit_ratio_display() {
1943 use rust_decimal::Decimal;
1944 use std::str::FromStr;
1945 let percent =
1946 Value::NumberWithUnit(Decimal::from_str("10").unwrap(), "percent".to_string());
1947 assert_eq!(format!("{}", percent), "10%");
1948 let permille =
1949 Value::NumberWithUnit(Decimal::from_str("5").unwrap(), "permille".to_string());
1950 assert_eq!(format!("{}", permille), "5%%");
1951 }
1952
1953 #[test]
1954 fn test_datetime_value_display() {
1955 let dt = DateTimeValue {
1956 year: 2024,
1957 month: 12,
1958 day: 25,
1959 hour: 14,
1960 minute: 30,
1961 second: 45,
1962 microsecond: 0,
1963 timezone: Some(TimezoneValue {
1964 offset_hours: 1,
1965 offset_minutes: 0,
1966 }),
1967
1968 granularity: DateGranularity::DateTime,
1969 };
1970 assert_eq!(format!("{}", dt), "2024-12-25T14:30:45+01:00");
1971 }
1972
1973 #[test]
1974 fn test_datetime_value_display_date_only() {
1975 let dt = DateTimeValue {
1976 year: 2026,
1977 month: 3,
1978 day: 4,
1979 hour: 0,
1980 minute: 0,
1981 second: 0,
1982 microsecond: 0,
1983 timezone: None,
1984
1985 granularity: DateGranularity::Full,
1986 };
1987 assert_eq!(format!("{}", dt), "2026-03-04");
1988 }
1989
1990 #[test]
1991 fn test_datetime_value_display_microseconds() {
1992 let dt = DateTimeValue {
1993 year: 2026,
1994 month: 2,
1995 day: 23,
1996 hour: 14,
1997 minute: 30,
1998 second: 45,
1999 microsecond: 123456,
2000 timezone: Some(TimezoneValue {
2001 offset_hours: 0,
2002 offset_minutes: 0,
2003 }),
2004
2005 granularity: DateGranularity::DateTime,
2006 };
2007 assert_eq!(format!("{}", dt), "2026-02-23T14:30:45.123456Z");
2008 }
2009
2010 #[test]
2011 fn test_datetime_microsecond_in_ordering() {
2012 let a = DateTimeValue {
2013 year: 2026,
2014 month: 1,
2015 day: 1,
2016 hour: 0,
2017 minute: 0,
2018 second: 0,
2019 microsecond: 100,
2020 timezone: None,
2021
2022 granularity: DateGranularity::DateTime,
2023 };
2024 let b = DateTimeValue {
2025 year: 2026,
2026 month: 1,
2027 day: 1,
2028 hour: 0,
2029 minute: 0,
2030 second: 0,
2031 microsecond: 200,
2032 timezone: None,
2033
2034 granularity: DateGranularity::DateTime,
2035 };
2036 assert!(a < b);
2037 }
2038
2039 #[test]
2040 fn test_datetime_parse_iso_week() {
2041 let dt: DateTimeValue = "2026-W01".parse().unwrap();
2042 assert_eq!(dt.year, 2025);
2043 assert_eq!(dt.month, 12);
2044 assert_eq!(dt.day, 29);
2045 assert_eq!(dt.microsecond, 0);
2046 assert_eq!(dt.to_string(), "2026-W01");
2047 assert!(matches!(
2048 dt.granularity,
2049 DateGranularity::IsoWeek {
2050 iso_year: 2026,
2051 week: 1
2052 }
2053 ));
2054 }
2055
2056 #[test]
2057 fn test_negation_types() {
2058 let json = serde_json::to_string(&NegationType::Not).expect("serialize NegationType");
2059 let decoded: NegationType = serde_json::from_str(&json).expect("deserialize NegationType");
2060 assert_eq!(decoded, NegationType::Not);
2061 }
2062
2063 #[test]
2064 fn parent_type_primitive_serde_externally_tagged() {
2065 let p = ParentType::Primitive {
2066 primitive: PrimitiveKind::Number,
2067 };
2068 let json = serde_json::to_string(&p).expect("ParentType::Primitive must serialize");
2069 assert!(json.contains("\"Primitive\"") && json.contains("\"primitive\""));
2070 let back: ParentType = serde_json::from_str(&json).expect("deserialize");
2071 assert_eq!(back, p);
2072 }
2073
2074 fn text_arg(s: &str) -> CommandArg {
2079 CommandArg::Literal(crate::literals::Value::Text(s.to_string()))
2080 }
2081
2082 fn number_arg(s: &str) -> CommandArg {
2083 let d: rust_decimal::Decimal = s.parse().expect("decimal");
2084 CommandArg::Literal(crate::literals::Value::Number(d))
2085 }
2086
2087 fn boolean_arg(b: BooleanValue) -> CommandArg {
2088 CommandArg::Literal(crate::literals::Value::Boolean(b))
2089 }
2090
2091 fn measure_arg(value: &str, unit: &str) -> CommandArg {
2092 let d: rust_decimal::Decimal = value.parse().expect("decimal");
2093 CommandArg::Literal(crate::literals::Value::NumberWithUnit(d, unit.to_string()))
2094 }
2095
2096 fn duration_arg(value: &str, unit: &str) -> CommandArg {
2097 let d: rust_decimal::Decimal = value.parse().expect("decimal");
2098 CommandArg::Literal(crate::literals::Value::NumberWithUnit(d, unit.to_string()))
2099 }
2100
2101 #[test]
2102 fn as_lemma_source_text_default_is_quoted() {
2103 let fv = DataValue::Definition {
2104 base: Some(ParentType::Primitive {
2105 primitive: PrimitiveKind::Text,
2106 }),
2107 constraints: Some(vec![test_constraint(
2108 TypeConstraintCommand::Suggest,
2109 vec![text_arg("single")],
2110 )]),
2111 value: None,
2112 };
2113 assert_eq!(
2114 format!("{}", AsLemmaSource(&fv)),
2115 "text -> suggest \"single\""
2116 );
2117 }
2118
2119 #[test]
2120 fn as_lemma_source_number_default_not_quoted() {
2121 let fv = DataValue::Definition {
2122 base: Some(ParentType::Primitive {
2123 primitive: PrimitiveKind::Number,
2124 }),
2125 constraints: Some(vec![test_constraint(
2126 TypeConstraintCommand::Suggest,
2127 vec![number_arg("10")],
2128 )]),
2129 value: None,
2130 };
2131 assert_eq!(format!("{}", AsLemmaSource(&fv)), "number -> suggest 10");
2132 }
2133
2134 #[test]
2135 fn as_lemma_source_help_always_quoted() {
2136 let fv = DataValue::Definition {
2137 base: Some(ParentType::Primitive {
2138 primitive: PrimitiveKind::Number,
2139 }),
2140 constraints: Some(vec![test_constraint(
2141 TypeConstraintCommand::Help,
2142 vec![text_arg("Enter a measure")],
2143 )]),
2144 value: None,
2145 };
2146 assert_eq!(
2147 format!("{}", AsLemmaSource(&fv)),
2148 "number -> help \"Enter a measure\""
2149 );
2150 }
2151
2152 #[test]
2153 fn as_lemma_source_text_option_quoted() {
2154 let fv = DataValue::Definition {
2155 base: Some(ParentType::Primitive {
2156 primitive: PrimitiveKind::Text,
2157 }),
2158 constraints: Some(vec![
2159 test_constraint(TypeConstraintCommand::Option, vec![text_arg("active")]),
2160 test_constraint(TypeConstraintCommand::Option, vec![text_arg("inactive")]),
2161 ]),
2162 value: None,
2163 };
2164 assert_eq!(
2165 format!("{}", AsLemmaSource(&fv)),
2166 "text -> option \"active\" -> option \"inactive\""
2167 );
2168 }
2169
2170 #[test]
2171 fn as_lemma_source_measure_unit_not_quoted() {
2172 let fv = DataValue::Definition {
2173 base: Some(ParentType::Primitive {
2174 primitive: PrimitiveKind::Measure,
2175 }),
2176 constraints: Some(vec![
2177 test_constraint(
2178 TypeConstraintCommand::Unit,
2179 vec![
2180 CommandArg::Label("eur".to_string()),
2181 CommandArg::UnitExpr(UnitArg::Factor(decimal("1.00"))),
2182 ],
2183 ),
2184 test_constraint(
2185 TypeConstraintCommand::Unit,
2186 vec![
2187 CommandArg::Label("usd".to_string()),
2188 CommandArg::UnitExpr(UnitArg::Factor(decimal("0.91"))),
2189 ],
2190 ),
2191 ]),
2192 value: None,
2193 };
2194 assert_eq!(
2195 format!("{}", AsLemmaSource(&fv)),
2196 "measure -> unit eur: 1.00 -> unit usd: 0.91"
2197 );
2198 }
2199
2200 #[test]
2201 fn as_lemma_source_measure_minimum_with_unit() {
2202 let fv = DataValue::Definition {
2203 base: Some(ParentType::Primitive {
2204 primitive: PrimitiveKind::Measure,
2205 }),
2206 constraints: Some(vec![test_constraint(
2207 TypeConstraintCommand::Minimum,
2208 vec![measure_arg("0", "eur")],
2209 )]),
2210 value: None,
2211 };
2212 assert_eq!(
2213 format!("{}", AsLemmaSource(&fv)),
2214 "measure -> minimum 0 eur"
2215 );
2216 }
2217
2218 #[test]
2219 fn as_lemma_source_boolean_default() {
2220 let fv = DataValue::Definition {
2221 base: Some(ParentType::Primitive {
2222 primitive: PrimitiveKind::Boolean,
2223 }),
2224 constraints: Some(vec![test_constraint(
2225 TypeConstraintCommand::Suggest,
2226 vec![boolean_arg(BooleanValue::True)],
2227 )]),
2228 value: None,
2229 };
2230 assert_eq!(format!("{}", AsLemmaSource(&fv)), "boolean -> suggest true");
2231 }
2232
2233 #[test]
2234 fn as_lemma_source_duration_default() {
2235 let fv = DataValue::Definition {
2236 base: Some(ParentType::Custom {
2237 name: "duration".to_string(),
2238 }),
2239 constraints: Some(vec![test_constraint(
2240 TypeConstraintCommand::Suggest,
2241 vec![duration_arg("40", "hour")],
2242 )]),
2243 value: None,
2244 };
2245 assert_eq!(
2246 format!("{}", AsLemmaSource(&fv)),
2247 "duration -> suggest 40 hour"
2248 );
2249 }
2250
2251 #[test]
2252 fn as_lemma_source_named_type_default_quoted() {
2253 let fv = DataValue::Definition {
2256 base: Some(ParentType::Custom {
2257 name: "filing_status_type".to_string(),
2258 }),
2259 constraints: Some(vec![test_constraint(
2260 TypeConstraintCommand::Suggest,
2261 vec![text_arg("single")],
2262 )]),
2263 value: None,
2264 };
2265 assert_eq!(
2266 format!("{}", AsLemmaSource(&fv)),
2267 "filing_status_type -> suggest \"single\""
2268 );
2269 }
2270
2271 #[test]
2272 fn as_lemma_source_help_escapes_quotes() {
2273 let fv = DataValue::Definition {
2274 base: Some(ParentType::Primitive {
2275 primitive: PrimitiveKind::Text,
2276 }),
2277 constraints: Some(vec![test_constraint(
2278 TypeConstraintCommand::Help,
2279 vec![text_arg("say \"hello\"")],
2280 )]),
2281 value: None,
2282 };
2283 assert_eq!(
2284 format!("{}", AsLemmaSource(&fv)),
2285 "text -> help \"say \\\"hello\\\"\""
2286 );
2287 }
2288
2289 fn unit_arg_expr(prefix: Decimal, factors: &[(&str, i32)]) -> UnitArg {
2290 UnitArg::Expr(
2291 prefix,
2292 factors
2293 .iter()
2294 .map(|(measure_ref, exp)| UnitFactor {
2295 measure_ref: (*measure_ref).to_string(),
2296 exp: *exp,
2297 })
2298 .collect(),
2299 )
2300 }
2301
2302 #[test]
2303 fn unit_arg_display_metre_per_second() {
2304 let arg = unit_arg_expr(Decimal::ONE, &[("meter", 1), ("second", -1)]);
2305 assert_eq!(format!("{arg}"), "meter/second");
2306 assert!(
2307 !format!("{arg}").contains("second^-1"),
2308 "must not print denominator as negative exponent"
2309 );
2310 }
2311
2312 #[test]
2313 fn unit_arg_display_meter_per_second_squared() {
2314 let arg = unit_arg_expr(Decimal::ONE, &[("meter", 1), ("second", -2)]);
2315 assert_eq!(format!("{arg}"), "meter/second^2");
2316 }
2317
2318 #[test]
2319 fn unit_arg_display_kg_times_mps2() {
2320 let arg = unit_arg_expr(Decimal::ONE, &[("kg", 1), ("mps2", 1)]);
2321 assert_eq!(format!("{arg}"), "kg * mps2");
2322 }
2323
2324 #[test]
2325 fn unit_arg_display_numeric_prefix_metre_per_second() {
2326 use std::str::FromStr;
2327 let prefix = Decimal::from_str("3.6").expect("decimal");
2328 let arg = unit_arg_expr(prefix, &[("meter", 1), ("second", -1)]);
2329 assert_eq!(format!("{arg}"), "3.6 meter/second");
2330 }
2331
2332 #[test]
2333 fn unit_arg_display_metre_per_second_times_kg() {
2334 let arg = unit_arg_expr(Decimal::ONE, &[("meter", 1), ("second", -1), ("kg", 1)]);
2335 assert_eq!(format!("{arg}"), "meter/second * kg");
2336 }
2337
2338 #[test]
2339 fn unit_arg_display_kg_meter_per_second_squared() {
2340 let arg = unit_arg_expr(Decimal::ONE, &[("kg", 1), ("meter", 1), ("second", -2)]);
2341 assert_eq!(format!("{arg}"), "kg * meter/second^2");
2342 }
2343
2344 #[test]
2347 fn format_constraint_as_source_unit_factor_uses_assignment_colon() {
2348 let args = vec![
2349 CommandArg::Label("eur".to_string()),
2350 CommandArg::UnitExpr(UnitArg::Factor(decimal("1"))),
2351 ];
2352 assert_eq!(
2353 format_constraint_as_source(&TypeConstraintCommand::Unit, &args),
2354 "unit eur: 1"
2355 );
2356 }
2357
2358 #[test]
2359 fn format_constraint_as_source_unit_compound_uses_assignment_colon() {
2360 let arg = unit_arg_expr(decimal("3.6"), &[("meter", 1), ("second", -1)]);
2361 let args = vec![
2362 CommandArg::Label("kmh".to_string()),
2363 CommandArg::UnitExpr(arg),
2364 ];
2365 assert_eq!(
2366 format_constraint_as_source(&TypeConstraintCommand::Unit, &args),
2367 "unit kmh: 3.6 meter/second"
2368 );
2369 }
2370
2371 #[test]
2372 fn format_constraint_as_source_unit_factor_one_decimal_uses_assignment_colon() {
2373 let args = vec![
2374 CommandArg::Label("eur".to_string()),
2375 CommandArg::UnitExpr(UnitArg::Factor(decimal("1.00"))),
2376 ];
2377 assert_eq!(
2378 format_constraint_as_source(&TypeConstraintCommand::Unit, &args),
2379 "unit eur: 1.00"
2380 );
2381 }
2382
2383 #[test]
2384 fn as_lemma_source_measure_unit_uses_assignment_colon() {
2385 let fv = DataValue::Definition {
2386 base: Some(ParentType::Primitive {
2387 primitive: PrimitiveKind::Measure,
2388 }),
2389 constraints: Some(vec![
2390 test_constraint(
2391 TypeConstraintCommand::Unit,
2392 vec![
2393 CommandArg::Label("eur".to_string()),
2394 CommandArg::UnitExpr(UnitArg::Factor(decimal("1.00"))),
2395 ],
2396 ),
2397 test_constraint(
2398 TypeConstraintCommand::Unit,
2399 vec![
2400 CommandArg::Label("usd".to_string()),
2401 CommandArg::UnitExpr(UnitArg::Factor(decimal("0.91"))),
2402 ],
2403 ),
2404 ]),
2405 value: None,
2406 };
2407 assert_eq!(
2408 format!("{}", AsLemmaSource(&fv)),
2409 "measure -> unit eur: 1.00 -> unit usd: 0.91"
2410 );
2411 }
2412
2413 fn decimal(value: &str) -> rust_decimal::Decimal {
2414 use std::str::FromStr;
2415 rust_decimal::Decimal::from_str(value).expect("decimal literal in test")
2416 }
2417}