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