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