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, serde::Serialize, serde::Deserialize)]
264pub struct LemmaSpec {
265 pub name: String,
266 pub effective_from: EffectiveDate,
267 pub source_type: Option<crate::parsing::source::SourceType>,
268 pub start_line: usize,
269 pub commentary: Option<String>,
270 pub data: Vec<LemmaData>,
271 pub rules: Vec<LemmaRule>,
272 pub meta_fields: Vec<MetaField>,
273}
274
275#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
276pub struct MetaField {
277 pub key: String,
278 pub value: MetaValue,
279 pub source_location: Source,
280}
281
282#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
283#[serde(rename_all = "snake_case")]
284pub enum MetaValue {
285 Literal(Value),
286 Unquoted(String),
287}
288
289impl fmt::Display for MetaValue {
290 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291 match self {
292 MetaValue::Literal(v) => write!(f, "{}", v),
293 MetaValue::Unquoted(s) => write!(f, "{}", s),
294 }
295 }
296}
297
298impl fmt::Display for MetaField {
299 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300 write!(f, "meta {}: {}", self.key, self.value)
301 }
302}
303
304#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
305pub struct LemmaData {
306 pub reference: Reference,
307 pub value: DataValue,
308 pub source_location: Source,
309}
310
311#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
317pub struct UnlessClause {
318 pub condition: Expression,
319 pub result: Expression,
320 pub source_location: Source,
321}
322
323#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
325pub struct LemmaRule {
326 pub name: String,
327 pub expression: Expression,
328 pub unless_clauses: Vec<UnlessClause>,
329 pub source_location: Source,
330}
331
332#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
338pub struct Expression {
339 pub kind: ExpressionKind,
340 pub source_location: Option<Source>,
341}
342
343impl Expression {
344 #[must_use]
346 pub fn new(kind: ExpressionKind, source_location: Source) -> Self {
347 Self {
348 kind,
349 source_location: Some(source_location),
350 }
351 }
352}
353
354impl PartialEq for Expression {
356 fn eq(&self, other: &Self) -> bool {
357 self.kind == other.kind
358 }
359}
360
361impl Eq for Expression {}
362
363#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
365#[serde(rename_all = "snake_case")]
366pub enum DateRelativeKind {
367 InPast,
368 InFuture,
369}
370
371#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
373#[serde(rename_all = "snake_case")]
374pub enum DateCalendarKind {
375 Current,
376 Past,
377 Future,
378 NotIn,
379}
380
381#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
383#[serde(rename_all = "snake_case")]
384pub enum CalendarPeriodUnit {
385 Year,
386 Month,
387 Week,
388}
389
390impl CalendarPeriodUnit {
391 #[must_use]
392 pub fn from_keyword(s: &str) -> Option<Self> {
393 match s.trim().to_lowercase().as_str() {
394 "year" => Some(Self::Year),
395 "month" => Some(Self::Month),
396 "week" => Some(Self::Week),
397 _ => None,
398 }
399 }
400}
401
402impl fmt::Display for DateRelativeKind {
403 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404 match self {
405 DateRelativeKind::InPast => write!(f, "in past"),
406 DateRelativeKind::InFuture => write!(f, "in future"),
407 }
408 }
409}
410
411impl fmt::Display for DateCalendarKind {
412 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
413 match self {
414 DateCalendarKind::Current => write!(f, "in calendar"),
415 DateCalendarKind::Past => write!(f, "in past calendar"),
416 DateCalendarKind::Future => write!(f, "in future calendar"),
417 DateCalendarKind::NotIn => write!(f, "not in calendar"),
418 }
419 }
420}
421
422impl fmt::Display for CalendarPeriodUnit {
423 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
424 match self {
425 CalendarPeriodUnit::Year => write!(f, "year"),
426 CalendarPeriodUnit::Month => write!(f, "month"),
427 CalendarPeriodUnit::Week => write!(f, "week"),
428 }
429 }
430}
431
432#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
434#[serde(rename_all = "snake_case")]
435pub enum ExpressionKind {
436 Literal(Value),
438 Reference(Reference),
440 Now,
442 DateRelative(DateRelativeKind, Arc<Expression>),
445 DateCalendar(DateCalendarKind, CalendarPeriodUnit, Arc<Expression>),
448 RangeLiteral(Arc<Expression>, Arc<Expression>),
450 PastFutureRange(DateRelativeKind, Arc<Expression>),
452 RangeContainment(Arc<Expression>, Arc<Expression>),
454 LogicalAnd(Arc<Expression>, Arc<Expression>),
455 Arithmetic(Arc<Expression>, ArithmeticComputation, Arc<Expression>),
456 Comparison(Arc<Expression>, ComparisonComputation, Arc<Expression>),
457 UnitConversion(Arc<Expression>, ConversionTarget),
458 LogicalNegation(Arc<Expression>, NegationType),
459 MathematicalComputation(MathematicalComputation, Arc<Expression>),
460 Veto(VetoExpression),
461 ResultIsVeto(Arc<Expression>),
463}
464
465#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
475pub struct Reference {
476 pub segments: Vec<String>,
477 pub name: String,
478}
479
480impl Reference {
481 #[must_use]
482 pub fn local(name: String) -> Self {
483 Self {
484 segments: Vec::new(),
485 name: ascii_lowercase_logical_name(name),
486 }
487 }
488
489 #[must_use]
490 pub fn from_path(path: Vec<String>) -> Self {
491 if path.is_empty() {
492 Self {
493 segments: Vec::new(),
494 name: String::new(),
495 }
496 } else {
497 let name = ascii_lowercase_logical_name(path[path.len() - 1].clone());
499 let segments = path[..path.len() - 1]
500 .iter()
501 .map(|segment| ascii_lowercase_logical_name(segment.clone()))
502 .collect();
503 Self { segments, name }
504 }
505 }
506
507 #[must_use]
508 pub fn is_local(&self) -> bool {
509 self.segments.is_empty()
510 }
511
512 #[must_use]
513 pub fn full_path(&self) -> Vec<String> {
514 let mut path = self.segments.clone();
515 path.push(self.name.clone());
516 path
517 }
518}
519
520impl fmt::Display for Reference {
521 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
522 for segment in &self.segments {
523 write!(f, "{}.", segment)?;
524 }
525 write!(f, "{}", self.name)
526 }
527}
528
529#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, serde::Deserialize)]
531#[serde(rename_all = "snake_case")]
532pub enum ArithmeticComputation {
533 Add,
534 Subtract,
535 Multiply,
536 Divide,
537 Modulo,
538 Power,
539}
540
541#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, serde::Deserialize)]
543#[serde(rename_all = "snake_case")]
544pub enum ComparisonComputation {
545 GreaterThan,
546 LessThan,
547 GreaterThanOrEqual,
548 LessThanOrEqual,
549 Is,
550 IsNot,
551}
552
553impl ComparisonComputation {
554 #[must_use]
556 pub fn is_equal(&self) -> bool {
557 matches!(self, ComparisonComputation::Is)
558 }
559
560 #[must_use]
562 pub fn is_not_equal(&self) -> bool {
563 matches!(self, ComparisonComputation::IsNot)
564 }
565}
566
567#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
569#[serde(rename_all = "snake_case")]
570pub enum ConversionTarget {
571 Type(PrimitiveKind),
572 Unit { unit_name: String },
573}
574
575#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
577#[serde(rename_all = "snake_case")]
578pub enum NegationType {
579 Not,
580}
581
582#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
589pub struct VetoExpression {
590 pub message: Option<String>,
591}
592
593#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, serde::Deserialize)]
595#[serde(rename_all = "snake_case")]
596pub enum MathematicalComputation {
597 Sqrt,
598 Sin,
599 Cos,
600 Tan,
601 Asin,
602 Acos,
603 Atan,
604 Log,
605 Exp,
606 Abs,
607 Floor,
608 Ceil,
609 Round,
610}
611
612#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
619pub struct SpecRef {
620 pub repository: Option<RepositoryQualifier>,
623 pub name: String,
625 pub effective: Option<DateTimeValue>,
627 #[serde(default, skip_serializing_if = "Option::is_none")]
629 pub repository_span: Option<Span>,
630 #[serde(default, skip_serializing_if = "Option::is_none")]
632 pub target_span: Option<Span>,
633}
634
635impl std::fmt::Display for SpecRef {
636 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
637 if let Some(qualifier) = &self.repository {
638 write!(f, "{} ", qualifier)?;
639 }
640 write!(f, "{}", self.name)?;
641 if let Some(d) = &self.effective {
642 write!(f, " {}", d)?;
643 }
644 Ok(())
645 }
646}
647
648impl SpecRef {
649 pub fn same_repository(name: impl Into<String>) -> Self {
651 Self {
652 name: ascii_lowercase_logical_name(name.into()),
653 repository: None,
654 effective: None,
655 repository_span: None,
656 target_span: None,
657 }
658 }
659
660 pub fn cross_repository(name: impl Into<String>, qualifier: RepositoryQualifier) -> Self {
662 Self {
663 name: ascii_lowercase_logical_name(name.into()),
664 repository: Some(qualifier),
665 effective: None,
666 repository_span: None,
667 target_span: None,
668 }
669 }
670
671 pub fn at(&self, effective: &EffectiveDate) -> EffectiveDate {
674 self.effective
675 .clone()
676 .map_or_else(|| effective.clone(), EffectiveDate::DateTimeValue)
677 }
678
679 pub fn resolved_instant(
682 &self,
683 consumer_effective_from: Option<&DateTimeValue>,
684 ) -> Option<DateTimeValue> {
685 self.effective
686 .clone()
687 .or_else(|| consumer_effective_from.cloned())
688 }
689}
690
691#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
699pub struct UnitFactor {
700 pub measure_ref: String,
701 pub exp: i32,
702}
703
704#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
713pub enum UnitArg {
714 Factor(Decimal),
715 Expr(Decimal, Vec<UnitFactor>),
716}
717
718impl fmt::Display for UnitArg {
719 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
720 match self {
721 UnitArg::Factor(v) => write!(f, "{}", v),
722 UnitArg::Expr(prefix, factors) => {
723 if *prefix != Decimal::ONE {
724 write!(f, "{} ", prefix)?;
725 }
726 for (index, factor) in factors.iter().enumerate() {
727 if factor.exp == 0 {
728 unreachable!("BUG: unit factor exponent cannot be zero");
729 }
730 if factor.exp > 0 {
731 if index > 0 {
732 write!(f, " * ")?;
733 }
734 write!(f, "{}", factor.measure_ref)?;
735 if factor.exp != 1 {
736 write!(f, "^{}", factor.exp)?;
737 }
738 } else {
739 let denominator_started =
740 factors[..index].iter().any(|prior| prior.exp < 0);
741 if denominator_started {
742 write!(f, " * ")?;
743 } else {
744 write!(f, "/")?;
745 }
746 write!(f, "{}", factor.measure_ref)?;
747 let positive_exp = factor
748 .exp
749 .checked_neg()
750 .expect("BUG: negative unit factor exponent");
751 if positive_exp != 1 {
752 write!(f, "^{}", positive_exp)?;
753 }
754 }
755 }
756 Ok(())
757 }
758 }
759 }
760}
761
762#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
780#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
781pub enum CommandArg {
782 Literal(crate::literals::Value),
784 Label(String),
786 UnitExpr(UnitArg),
788}
789
790impl fmt::Display for CommandArg {
791 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
792 match self {
793 CommandArg::Literal(v) => write!(f, "{}", v),
794 CommandArg::Label(s) => write!(f, "{}", s),
795 CommandArg::UnitExpr(unit_arg) => write!(f, "{}", unit_arg),
796 }
797 }
798}
799
800#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
802#[serde(rename_all = "snake_case")]
803pub enum TypeConstraintCommand {
804 Help,
805 Suggest,
806 Unit,
807 Trait,
808 Minimum,
809 Maximum,
810 Lower,
811 Upper,
812 Decimals,
813 Option,
814 Options,
815 Length,
816}
817
818impl fmt::Display for TypeConstraintCommand {
819 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
820 let s = match self {
821 TypeConstraintCommand::Help => "help",
822 TypeConstraintCommand::Suggest => "suggest",
823 TypeConstraintCommand::Unit => "unit",
824 TypeConstraintCommand::Trait => "trait",
825 TypeConstraintCommand::Minimum => "minimum",
826 TypeConstraintCommand::Maximum => "maximum",
827 TypeConstraintCommand::Lower => "lower",
828 TypeConstraintCommand::Upper => "upper",
829 TypeConstraintCommand::Decimals => "decimals",
830 TypeConstraintCommand::Option => "option",
831 TypeConstraintCommand::Options => "options",
832 TypeConstraintCommand::Length => "length",
833 };
834 write!(f, "{}", s)
835 }
836}
837
838#[must_use]
840pub fn try_parse_type_constraint_command(s: &str) -> Option<TypeConstraintCommand> {
841 match s.trim().to_lowercase().as_str() {
842 "help" => Some(TypeConstraintCommand::Help),
843 "suggest" => Some(TypeConstraintCommand::Suggest),
844 "unit" => Some(TypeConstraintCommand::Unit),
845 "trait" => Some(TypeConstraintCommand::Trait),
846 "minimum" => Some(TypeConstraintCommand::Minimum),
847 "maximum" => Some(TypeConstraintCommand::Maximum),
848 "lower" => Some(TypeConstraintCommand::Lower),
849 "upper" => Some(TypeConstraintCommand::Upper),
850 "decimals" => Some(TypeConstraintCommand::Decimals),
851 "option" => Some(TypeConstraintCommand::Option),
852 "options" => Some(TypeConstraintCommand::Options),
853 "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::Trait
873 | TypeConstraintCommand::Minimum
874 | TypeConstraintCommand::Maximum
875 | TypeConstraintCommand::Lower
876 | TypeConstraintCommand::Upper
877 | TypeConstraintCommand::Decimals
878 | TypeConstraintCommand::Option
879 | TypeConstraintCommand::Options
880 | TypeConstraintCommand::Length => ContinuationShape::SpaceSeparated,
881 }
882 }
883}
884
885#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
887pub struct Constraint {
888 pub command: TypeConstraintCommand,
889 pub args: Vec<CommandArg>,
890 pub source_location: crate::parsing::source::Source,
891 #[serde(default, skip_serializing_if = "is_false")]
893 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 #[serde(default, skip_serializing_if = "is_false")]
946 pub deprecated_standalone_with: bool,
947}
948
949fn is_false(value: &bool) -> bool {
950 !*value
951}
952
953#[must_use]
955pub fn prefix_reference(alias: &str, relative: &Reference) -> Reference {
956 let mut segments = vec![ascii_lowercase_logical_name(alias.to_string())];
957 segments.extend(relative.segments.iter().cloned());
958 Reference {
959 segments,
960 name: relative.name.clone(),
961 }
962}
963
964#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
965#[serde(rename_all = "snake_case")]
966pub enum DataValue {
968 Definition {
976 #[serde(default, skip_serializing_if = "Option::is_none")]
977 base: Option<ParentType>,
978 constraints: Option<Vec<Constraint>>,
979 #[serde(default, skip_serializing_if = "Option::is_none")]
980 value: Option<Value>,
981 },
982 Import {
984 spec_ref: SpecRef,
985 bindings: Vec<UsesBinding>,
986 },
987}
988
989impl DataValue {
990 #[must_use]
991 pub fn import(spec_ref: SpecRef) -> Self {
992 Self::Import {
993 spec_ref,
994 bindings: Vec::new(),
995 }
996 }
997
998 #[must_use]
1000 pub fn is_definition_literal_only(&self) -> bool {
1001 matches!(
1002 self,
1003 DataValue::Definition {
1004 base: None,
1005 constraints: None,
1006 value: Some(_),
1007 }
1008 )
1009 }
1010
1011 #[must_use]
1013 pub fn definition_needs_type_resolution(&self) -> bool {
1014 match self {
1015 DataValue::Definition { base: Some(_), .. }
1016 | DataValue::Definition {
1017 constraints: Some(_),
1018 ..
1019 } => true,
1020 DataValue::Definition {
1021 base: None,
1022 constraints: None,
1023 value: Some(v),
1024 } => !matches!(v, Value::NumberWithUnit(_, _)),
1025 DataValue::Import { .. } | DataValue::Definition { .. } => false,
1026 }
1027 }
1028}
1029
1030fn format_constraint_chain(constraints: &[Constraint]) -> String {
1033 constraints
1034 .iter()
1035 .map(|row| format_constraint_as_source(&row.command, &row.args))
1036 .collect::<Vec<_>>()
1037 .join(" -> ")
1038}
1039
1040impl fmt::Display for DataValue {
1041 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1042 match self {
1043 DataValue::Definition {
1044 base,
1045 constraints,
1046 value,
1047 } => {
1048 if base.is_none() && constraints.is_none() {
1049 return match value {
1050 Some(v) => write!(f, "{}", v),
1051 None => Ok(()),
1052 };
1053 }
1054 let base_str = match base.as_ref() {
1055 Some(b) => format!("{b}"),
1056 None => match value {
1057 Some(v) => {
1058 if let Some(ref constraints_vec) = constraints {
1059 let constraint_str = format_constraint_chain(constraints_vec);
1060 return write!(f, "{v} -> {constraint_str}");
1061 }
1062 return write!(f, "{v}");
1063 }
1064 None => String::new(),
1065 },
1066 };
1067 if let Some(ref constraints_vec) = constraints {
1068 let constraint_str = format_constraint_chain(constraints_vec);
1069 write!(f, "{base_str} -> {constraint_str}")
1070 } else {
1071 write!(f, "{base_str}")
1072 }
1073 }
1074 DataValue::Import {
1075 spec_ref,
1076 bindings: _,
1077 } => {
1078 write!(f, "uses {}", spec_ref)
1079 }
1080 }
1081 }
1082}
1083
1084impl LemmaData {
1085 #[must_use]
1086 pub fn new(reference: Reference, value: DataValue, source_location: Source) -> Self {
1087 Self {
1088 reference,
1089 value,
1090 source_location,
1091 }
1092 }
1093}
1094
1095impl LemmaSpec {
1096 #[must_use]
1097 pub fn new(name: String) -> Self {
1098 Self {
1099 name: ascii_lowercase_logical_name(name),
1100 effective_from: EffectiveDate::Origin,
1101 source_type: None,
1102 start_line: 1,
1103 commentary: None,
1104 data: Vec::new(),
1105 rules: Vec::new(),
1106 meta_fields: Vec::new(),
1107 }
1108 }
1109
1110 pub fn effective_from(&self) -> Option<&DateTimeValue> {
1112 self.effective_from.as_ref()
1113 }
1114
1115 #[must_use]
1116 pub fn with_source_type(mut self, source_type: crate::parsing::source::SourceType) -> Self {
1117 self.source_type = Some(source_type);
1118 self
1119 }
1120
1121 #[must_use]
1122 pub fn with_start_line(mut self, start_line: usize) -> Self {
1123 self.start_line = start_line;
1124 self
1125 }
1126
1127 #[must_use]
1128 pub fn set_commentary(mut self, commentary: String) -> Self {
1129 self.commentary = Some(commentary);
1130 self
1131 }
1132
1133 #[must_use]
1134 pub fn add_data(mut self, data: LemmaData) -> Self {
1135 self.data.push(data);
1136 self
1137 }
1138
1139 #[must_use]
1140 pub fn add_rule(mut self, rule: LemmaRule) -> Self {
1141 self.rules.push(rule);
1142 self
1143 }
1144
1145 #[must_use]
1146 pub fn add_meta_field(mut self, meta: MetaField) -> Self {
1147 self.meta_fields.push(meta);
1148 self
1149 }
1150}
1151
1152impl fmt::Display for LemmaSpec {
1153 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1154 write!(f, "spec {}", self.name)?;
1155 if let EffectiveDate::DateTimeValue(ref af) = self.effective_from {
1156 write!(f, " {}", af)?;
1157 }
1158 writeln!(f)?;
1159
1160 if let Some(ref commentary) = self.commentary {
1161 writeln!(f, "\"\"\"")?;
1162 writeln!(f, "{}", commentary)?;
1163 writeln!(f, "\"\"\"")?;
1164 }
1165
1166 if !self.data.is_empty() {
1167 writeln!(f)?;
1168 for data in &self.data {
1169 write!(f, "{}", data)?;
1170 }
1171 }
1172
1173 if !self.rules.is_empty() {
1174 writeln!(f)?;
1175 for (index, rule) in self.rules.iter().enumerate() {
1176 if index > 0 {
1177 writeln!(f)?;
1178 }
1179 write!(f, "{}", rule)?;
1180 }
1181 }
1182
1183 if !self.meta_fields.is_empty() {
1184 writeln!(f)?;
1185 for meta in &self.meta_fields {
1186 writeln!(f, "{}", meta)?;
1187 }
1188 }
1189
1190 Ok(())
1191 }
1192}
1193
1194impl fmt::Display for LemmaData {
1195 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1196 writeln!(f, "data {}: {}", self.reference, self.value)
1197 }
1198}
1199
1200impl fmt::Display for LemmaRule {
1201 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1202 write!(f, "rule {}: {}", self.name, self.expression)?;
1203 for unless_clause in &self.unless_clauses {
1204 write!(
1205 f,
1206 "\n unless {} then {}",
1207 unless_clause.condition, unless_clause.result
1208 )?;
1209 }
1210 writeln!(f)?;
1211 Ok(())
1212 }
1213}
1214
1215pub fn expression_precedence(kind: &ExpressionKind) -> u8 {
1223 match kind {
1224 ExpressionKind::LogicalAnd(..) => 2,
1225 ExpressionKind::LogicalNegation(..) => 3,
1226 ExpressionKind::Comparison(..) | ExpressionKind::ResultIsVeto(..) => 4,
1227 ExpressionKind::RangeContainment(..) => 4,
1228 ExpressionKind::DateRelative(..) | ExpressionKind::DateCalendar(..) => 4,
1229 ExpressionKind::Arithmetic(_, op, _) => arithmetic_precedence(op),
1230 ExpressionKind::UnitConversion(..) => 8,
1231 ExpressionKind::RangeLiteral(..) => 9,
1232 ExpressionKind::MathematicalComputation(..) => 10,
1233 ExpressionKind::PastFutureRange(..) => 10,
1234 ExpressionKind::Literal(..)
1235 | ExpressionKind::Reference(..)
1236 | ExpressionKind::Now
1237 | ExpressionKind::Veto(..) => 10,
1238 }
1239}
1240
1241pub fn arithmetic_precedence(op: &ArithmeticComputation) -> u8 {
1243 match op {
1244 ArithmeticComputation::Add | ArithmeticComputation::Subtract => 5,
1245 ArithmeticComputation::Multiply
1246 | ArithmeticComputation::Divide
1247 | ArithmeticComputation::Modulo => 6,
1248 ArithmeticComputation::Power => 7,
1249 }
1250}
1251
1252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1254pub enum OperandSide {
1255 Left,
1256 Right,
1257}
1258
1259#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1261pub enum Associativity {
1262 Left,
1263 Right,
1264}
1265
1266pub fn operand_needs_parentheses(
1272 child_prec: u8,
1273 parent_prec: u8,
1274 side: OperandSide,
1275 parent_assoc: Option<Associativity>,
1276) -> bool {
1277 if child_prec < parent_prec {
1278 return true;
1279 }
1280 if child_prec > parent_prec {
1281 return false;
1282 }
1283 match parent_assoc {
1284 None => false,
1285 Some(Associativity::Left) => matches!(side, OperandSide::Right),
1286 Some(Associativity::Right) => matches!(side, OperandSide::Left),
1287 }
1288}
1289
1290pub fn arithmetic_associativity(op: &ArithmeticComputation) -> Associativity {
1291 match op {
1292 ArithmeticComputation::Power => Associativity::Right,
1293 ArithmeticComputation::Add
1294 | ArithmeticComputation::Subtract
1295 | ArithmeticComputation::Multiply
1296 | ArithmeticComputation::Divide
1297 | ArithmeticComputation::Modulo => Associativity::Left,
1298 }
1299}
1300
1301fn write_expression_child(
1302 f: &mut fmt::Formatter<'_>,
1303 child: &Expression,
1304 parent_prec: u8,
1305 side: OperandSide,
1306 parent_assoc: Option<Associativity>,
1307) -> fmt::Result {
1308 let child_prec = expression_precedence(&child.kind);
1309 if operand_needs_parentheses(child_prec, parent_prec, side, parent_assoc) {
1310 write!(f, "({})", child)
1311 } else {
1312 write!(f, "{}", child)
1313 }
1314}
1315
1316impl fmt::Display for Expression {
1317 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1318 match &self.kind {
1319 ExpressionKind::Literal(lit) => write!(f, "{}", AsLemmaSource(lit)),
1320 ExpressionKind::Reference(r) => write!(f, "{}", r),
1321 ExpressionKind::Arithmetic(left, op, right) => {
1322 let my_prec = expression_precedence(&self.kind);
1323 let assoc = Some(arithmetic_associativity(op));
1324 write_expression_child(f, left, my_prec, OperandSide::Left, assoc)?;
1325 write!(f, " {} ", op)?;
1326 write_expression_child(f, right, my_prec, OperandSide::Right, assoc)
1327 }
1328 ExpressionKind::Comparison(left, op, right) => {
1329 let my_prec = expression_precedence(&self.kind);
1330 write_expression_child(f, left, my_prec, OperandSide::Left, None)?;
1331 write!(f, " {} ", op)?;
1332 write_expression_child(f, right, my_prec, OperandSide::Right, None)
1333 }
1334 ExpressionKind::UnitConversion(value, target) => {
1335 let my_prec = expression_precedence(&self.kind);
1336 write_expression_child(f, value, my_prec, OperandSide::Left, None)?;
1337 write!(f, " as {}", target)
1338 }
1339 ExpressionKind::LogicalNegation(expr, negation) => {
1340 if let (NegationType::Not, ExpressionKind::ResultIsVeto(operand)) =
1341 (negation, &expr.kind)
1342 {
1343 let my_prec = expression_precedence(&self.kind);
1344 write_expression_child(f, operand, my_prec, OperandSide::Left, None)?;
1345 write!(f, " is not veto")
1346 } else {
1347 let my_prec = expression_precedence(&self.kind);
1348 write!(f, "not ")?;
1349 write_expression_child(f, expr, my_prec, OperandSide::Right, None)
1350 }
1351 }
1352 ExpressionKind::ResultIsVeto(operand) => {
1353 let my_prec = expression_precedence(&self.kind);
1354 write_expression_child(f, operand, my_prec, OperandSide::Left, None)?;
1355 write!(f, " is veto")
1356 }
1357 ExpressionKind::LogicalAnd(left, right) => {
1358 let my_prec = expression_precedence(&self.kind);
1359 let assoc = Some(Associativity::Left);
1360 write_expression_child(f, left, my_prec, OperandSide::Left, assoc)?;
1361 write!(f, " and ")?;
1362 write_expression_child(f, right, my_prec, OperandSide::Right, assoc)
1363 }
1364 ExpressionKind::MathematicalComputation(op, operand) => {
1365 let my_prec = expression_precedence(&self.kind);
1366 write!(f, "{} ", op)?;
1367 write_expression_child(f, operand, my_prec, OperandSide::Right, None)
1368 }
1369 ExpressionKind::Veto(veto) => match &veto.message {
1370 Some(msg) => write!(f, "veto {}", quote_lemma_text(msg)),
1371 None => write!(f, "veto"),
1372 },
1373 ExpressionKind::Now => write!(f, "now"),
1374 ExpressionKind::DateRelative(kind, date_expr) => {
1375 write!(f, "{} {}", date_expr, kind)?;
1376 Ok(())
1377 }
1378 ExpressionKind::DateCalendar(kind, unit, date_expr) => {
1379 write!(f, "{} {} {}", date_expr, kind, unit)
1380 }
1381 ExpressionKind::RangeLiteral(left, right) => {
1382 let my_prec = expression_precedence(&self.kind);
1383 write_expression_child(f, left, my_prec, OperandSide::Left, None)?;
1384 write!(f, "...")?;
1385 write_expression_child(f, right, my_prec, OperandSide::Right, None)
1386 }
1387 ExpressionKind::PastFutureRange(kind, offset_expr) => {
1388 match kind {
1389 DateRelativeKind::InPast => write!(f, "past ")?,
1390 DateRelativeKind::InFuture => write!(f, "future ")?,
1391 }
1392 let my_prec = expression_precedence(&self.kind);
1393 write_expression_child(f, offset_expr, my_prec, OperandSide::Right, None)
1394 }
1395 ExpressionKind::RangeContainment(value, range) => {
1396 let my_prec = expression_precedence(&self.kind);
1397 write_expression_child(f, value, my_prec, OperandSide::Left, None)?;
1398 write!(f, " in ")?;
1399 write_expression_child(f, range, my_prec, OperandSide::Right, None)
1400 }
1401 }
1402 }
1403}
1404
1405impl fmt::Display for ConversionTarget {
1406 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1407 match self {
1408 ConversionTarget::Type(kind) => write!(f, "{kind}"),
1409 ConversionTarget::Unit { unit_name } => write!(f, "{unit_name}"),
1410 }
1411 }
1412}
1413
1414impl fmt::Display for ArithmeticComputation {
1415 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1416 match self {
1417 ArithmeticComputation::Add => write!(f, "+"),
1418 ArithmeticComputation::Subtract => write!(f, "-"),
1419 ArithmeticComputation::Multiply => write!(f, "*"),
1420 ArithmeticComputation::Divide => write!(f, "/"),
1421 ArithmeticComputation::Modulo => write!(f, "%"),
1422 ArithmeticComputation::Power => write!(f, "^"),
1423 }
1424 }
1425}
1426
1427impl fmt::Display for ComparisonComputation {
1428 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1429 match self {
1430 ComparisonComputation::GreaterThan => write!(f, ">"),
1431 ComparisonComputation::LessThan => write!(f, "<"),
1432 ComparisonComputation::GreaterThanOrEqual => write!(f, ">="),
1433 ComparisonComputation::LessThanOrEqual => write!(f, "<="),
1434 ComparisonComputation::Is => write!(f, "is"),
1435 ComparisonComputation::IsNot => write!(f, "is not"),
1436 }
1437 }
1438}
1439
1440impl fmt::Display for MathematicalComputation {
1441 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1442 match self {
1443 MathematicalComputation::Sqrt => write!(f, "sqrt"),
1444 MathematicalComputation::Sin => write!(f, "sin"),
1445 MathematicalComputation::Cos => write!(f, "cos"),
1446 MathematicalComputation::Tan => write!(f, "tan"),
1447 MathematicalComputation::Asin => write!(f, "asin"),
1448 MathematicalComputation::Acos => write!(f, "acos"),
1449 MathematicalComputation::Atan => write!(f, "atan"),
1450 MathematicalComputation::Log => write!(f, "log"),
1451 MathematicalComputation::Exp => write!(f, "exp"),
1452 MathematicalComputation::Abs => write!(f, "abs"),
1453 MathematicalComputation::Floor => write!(f, "floor"),
1454 MathematicalComputation::Ceil => write!(f, "ceil"),
1455 MathematicalComputation::Round => write!(f, "round"),
1456 }
1457 }
1458}
1459
1460#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1466#[serde(rename_all = "snake_case")]
1467pub enum PrimitiveKind {
1468 Boolean,
1469 Measure,
1470 MeasureRange,
1471 Number,
1472 NumberRange,
1473 Ratio,
1474 RatioRange,
1475 Text,
1476 Date,
1477 DateRange,
1478 Time,
1479 TimeRange,
1480}
1481
1482impl std::fmt::Display for PrimitiveKind {
1483 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1484 let s = match self {
1485 PrimitiveKind::Boolean => "boolean",
1486 PrimitiveKind::Measure => "measure",
1487 PrimitiveKind::MeasureRange => "measure range",
1488 PrimitiveKind::Number => "number",
1489 PrimitiveKind::NumberRange => "number range",
1490 PrimitiveKind::Ratio => "ratio",
1491 PrimitiveKind::RatioRange => "ratio range",
1492 PrimitiveKind::Text => "text",
1493 PrimitiveKind::Date => "date",
1494 PrimitiveKind::DateRange => "date range",
1495 PrimitiveKind::Time => "time",
1496 PrimitiveKind::TimeRange => "time range",
1497 };
1498 write!(f, "{}", s)
1499 }
1500}
1501
1502#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1507#[serde(tag = "kind", rename_all = "snake_case")]
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, MetaValue> {
1724 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1725 match self.0 {
1726 MetaValue::Literal(v) => write!(f, "{}", AsLemmaSource(v)),
1727 MetaValue::Unquoted(s) => write!(f, "{}", s),
1728 }
1729 }
1730}
1731
1732impl<'a> fmt::Display for AsLemmaSource<'a, DataValue> {
1733 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1734 match self.0 {
1735 DataValue::Definition {
1736 base,
1737 constraints,
1738 value,
1739 } => {
1740 if base.is_none() && constraints.is_none() {
1741 if let Some(v) = value {
1742 return write!(f, "{}", AsLemmaSource(v));
1743 }
1744 }
1745 let base_str = match base.as_ref() {
1746 Some(b) => format!("{}", b),
1747 None => match value {
1748 Some(v) => {
1749 if let Some(ref constraints_vec) = constraints {
1750 let constraint_str =
1751 format_constraints_as_source(constraints_vec, " -> ");
1752 return write!(f, "{} -> {}", AsLemmaSource(v), constraint_str);
1753 }
1754 return write!(f, "{}", AsLemmaSource(v));
1755 }
1756 None => String::new(),
1757 },
1758 };
1759 if let Some(ref constraints_vec) = constraints {
1760 let constraint_str = format_constraints_as_source(constraints_vec, " -> ");
1761 write!(f, "{} -> {}", base_str, constraint_str)
1762 } else {
1763 write!(f, "{}", base_str)
1764 }
1765 }
1766 DataValue::Import {
1767 spec_ref,
1768 bindings: _,
1769 } => {
1770 write!(f, "uses {}", spec_ref)
1771 }
1772 }
1773 }
1774}
1775
1776pub(crate) fn canonicalize_value(value: &mut Value) {
1777 if let Value::NumberWithUnit(_, unit) = value {
1778 *unit = ascii_lowercase_logical_name(std::mem::take(unit));
1779 }
1780}
1781
1782pub(crate) fn canonicalize_reference(reference: &mut Reference) {
1783 for segment in &mut reference.segments {
1784 *segment = ascii_lowercase_logical_name(std::mem::take(segment));
1785 }
1786 reference.name = ascii_lowercase_logical_name(std::mem::take(&mut reference.name));
1787}
1788
1789pub(crate) fn canonicalize_spec_ref(spec_ref: &mut SpecRef) {
1790 spec_ref.name = ascii_lowercase_logical_name(std::mem::take(&mut spec_ref.name));
1791 if let Some(qualifier) = spec_ref.repository.as_mut() {
1792 qualifier.name = ascii_lowercase_logical_name(std::mem::take(&mut qualifier.name));
1793 }
1794}
1795
1796pub(crate) fn canonicalize_parent_type(parent: &mut ParentType) {
1797 match parent {
1798 ParentType::Custom { name } => {
1799 *name = ascii_lowercase_logical_name(std::mem::take(name));
1800 }
1801 ParentType::Qualified { spec_alias, inner } => {
1802 *spec_alias = ascii_lowercase_logical_name(std::mem::take(spec_alias));
1803 canonicalize_parent_type(inner);
1804 }
1805 ParentType::Ranged { inner } => {
1806 canonicalize_parent_type(inner);
1807 }
1808 ParentType::Primitive { .. } => {}
1809 }
1810}
1811
1812pub(crate) fn canonicalize_unit_factor(factor: &mut UnitFactor) {
1813 factor.measure_ref = ascii_lowercase_logical_name(std::mem::take(&mut factor.measure_ref));
1814}
1815
1816pub(crate) fn canonicalize_unit_arg(unit_arg: &mut UnitArg) {
1817 if let UnitArg::Expr(_, factors) = unit_arg {
1818 for factor in factors {
1819 canonicalize_unit_factor(factor);
1820 }
1821 }
1822}
1823
1824pub(crate) fn canonicalize_command_arg(command_arg: &mut CommandArg) {
1825 match command_arg {
1826 CommandArg::Literal(value) => canonicalize_value(value),
1827 CommandArg::Label(label) => {
1828 *label = ascii_lowercase_logical_name(std::mem::take(label));
1829 }
1830 CommandArg::UnitExpr(unit_arg) => canonicalize_unit_arg(unit_arg),
1831 }
1832}
1833
1834pub(crate) fn canonicalize_constraints(constraints: &mut [Constraint]) {
1835 for row in constraints {
1836 for arg in &mut row.args {
1837 canonicalize_command_arg(arg);
1838 }
1839 }
1840}
1841
1842pub(crate) fn canonicalize_expression(expression: &mut Expression) {
1843 match &mut expression.kind {
1844 ExpressionKind::Literal(value) => canonicalize_value(value),
1845 ExpressionKind::Reference(reference) => canonicalize_reference(reference),
1846 ExpressionKind::Now => {}
1847 ExpressionKind::DateRelative(_, expression) => {
1848 canonicalize_expression(Arc::make_mut(expression));
1849 }
1850 ExpressionKind::DateCalendar(_, _, expression) => {
1851 canonicalize_expression(Arc::make_mut(expression));
1852 }
1853 ExpressionKind::RangeLiteral(left, right) => {
1854 canonicalize_expression(Arc::make_mut(left));
1855 canonicalize_expression(Arc::make_mut(right));
1856 }
1857 ExpressionKind::PastFutureRange(_, expression) => {
1858 canonicalize_expression(Arc::make_mut(expression));
1859 }
1860 ExpressionKind::RangeContainment(value, range) => {
1861 canonicalize_expression(Arc::make_mut(value));
1862 canonicalize_expression(Arc::make_mut(range));
1863 }
1864 ExpressionKind::LogicalAnd(left, right) => {
1865 canonicalize_expression(Arc::make_mut(left));
1866 canonicalize_expression(Arc::make_mut(right));
1867 }
1868 ExpressionKind::Arithmetic(left, _, right) => {
1869 canonicalize_expression(Arc::make_mut(left));
1870 canonicalize_expression(Arc::make_mut(right));
1871 }
1872 ExpressionKind::Comparison(left, _, right) => {
1873 canonicalize_expression(Arc::make_mut(left));
1874 canonicalize_expression(Arc::make_mut(right));
1875 }
1876 ExpressionKind::UnitConversion(expression, _) => {
1877 canonicalize_expression(Arc::make_mut(expression));
1878 }
1879 ExpressionKind::LogicalNegation(expression, _) => {
1880 canonicalize_expression(Arc::make_mut(expression));
1881 }
1882 ExpressionKind::MathematicalComputation(_, expression) => {
1883 canonicalize_expression(Arc::make_mut(expression));
1884 }
1885 ExpressionKind::Veto(_) => {}
1886 ExpressionKind::ResultIsVeto(expression) => {
1887 canonicalize_expression(Arc::make_mut(expression));
1888 }
1889 }
1890}
1891
1892pub(crate) fn canonicalize_unless_clause(unless_clause: &mut UnlessClause) {
1893 canonicalize_expression(&mut unless_clause.condition);
1894 canonicalize_expression(&mut unless_clause.result);
1895}
1896
1897pub(crate) fn canonicalize_data_value(data_value: &mut DataValue) {
1898 match data_value {
1899 DataValue::Definition {
1900 base,
1901 constraints,
1902 value,
1903 } => {
1904 if let Some(base) = base {
1905 canonicalize_parent_type(base);
1906 }
1907 if let Some(constraints) = constraints {
1908 canonicalize_constraints(constraints);
1909 }
1910 if let Some(value) = value {
1911 canonicalize_value(value);
1912 }
1913 }
1914 DataValue::Import { spec_ref, bindings } => {
1915 canonicalize_spec_ref(spec_ref);
1916 for binding in bindings {
1917 canonicalize_reference(&mut binding.path);
1918 match &mut binding.rhs {
1919 WithRhs::Literal(value) => canonicalize_value(value),
1920 WithRhs::Reference { target } => canonicalize_reference(target),
1921 }
1922 }
1923 }
1924 }
1925}
1926
1927pub(crate) fn canonicalize_lemma_data(data: &mut LemmaData) {
1928 canonicalize_reference(&mut data.reference);
1929 canonicalize_data_value(&mut data.value);
1930}
1931
1932pub(crate) fn canonicalize_lemma_rule(rule: &mut LemmaRule) {
1933 rule.name = ascii_lowercase_logical_name(std::mem::take(&mut rule.name));
1934 canonicalize_expression(&mut rule.expression);
1935 for unless_clause in &mut rule.unless_clauses {
1936 canonicalize_unless_clause(unless_clause);
1937 }
1938}
1939
1940pub(crate) fn canonicalize_lemma_spec(spec: &mut LemmaSpec) {
1941 spec.name = ascii_lowercase_logical_name(std::mem::take(&mut spec.name));
1942 for meta in &mut spec.meta_fields {
1943 meta.key = ascii_lowercase_logical_name(std::mem::take(&mut meta.key));
1944 }
1945 for data in &mut spec.data {
1946 canonicalize_lemma_data(data);
1947 }
1948 for rule in &mut spec.rules {
1949 canonicalize_lemma_rule(rule);
1950 }
1951}
1952
1953pub(crate) fn canonicalize_repository(repository: &mut LemmaRepository) {
1954 if let Some(name) = repository.name.take() {
1955 repository.name = Some(ascii_lowercase_logical_name(name));
1956 }
1957}
1958
1959#[cfg(test)]
1960mod tests {
1961 use super::*;
1962 use crate::literals::DateGranularity;
1963
1964 #[test]
1965 fn test_conversion_target_display() {
1966 assert_eq!(
1967 format!("{}", ConversionTarget::Type(PrimitiveKind::Number)),
1968 "number"
1969 );
1970 }
1971
1972 #[test]
1973 fn test_value_number_with_unit_ratio_display() {
1974 use rust_decimal::Decimal;
1975 use std::str::FromStr;
1976 let percent =
1977 Value::NumberWithUnit(Decimal::from_str("10").unwrap(), "percent".to_string());
1978 assert_eq!(format!("{}", percent), "10%");
1979 let permille =
1980 Value::NumberWithUnit(Decimal::from_str("5").unwrap(), "permille".to_string());
1981 assert_eq!(format!("{}", permille), "5%%");
1982 }
1983
1984 #[test]
1985 fn test_datetime_value_display() {
1986 let dt = DateTimeValue {
1987 year: 2024,
1988 month: 12,
1989 day: 25,
1990 hour: 14,
1991 minute: 30,
1992 second: 45,
1993 microsecond: 0,
1994 timezone: Some(TimezoneValue {
1995 offset_hours: 1,
1996 offset_minutes: 0,
1997 }),
1998
1999 granularity: DateGranularity::DateTime,
2000 };
2001 assert_eq!(format!("{}", dt), "2024-12-25T14:30:45+01:00");
2002 }
2003
2004 #[test]
2005 fn test_datetime_value_display_date_only() {
2006 let dt = DateTimeValue {
2007 year: 2026,
2008 month: 3,
2009 day: 4,
2010 hour: 0,
2011 minute: 0,
2012 second: 0,
2013 microsecond: 0,
2014 timezone: None,
2015
2016 granularity: DateGranularity::Full,
2017 };
2018 assert_eq!(format!("{}", dt), "2026-03-04");
2019 }
2020
2021 #[test]
2022 fn test_datetime_value_display_microseconds() {
2023 let dt = DateTimeValue {
2024 year: 2026,
2025 month: 2,
2026 day: 23,
2027 hour: 14,
2028 minute: 30,
2029 second: 45,
2030 microsecond: 123456,
2031 timezone: Some(TimezoneValue {
2032 offset_hours: 0,
2033 offset_minutes: 0,
2034 }),
2035
2036 granularity: DateGranularity::DateTime,
2037 };
2038 assert_eq!(format!("{}", dt), "2026-02-23T14:30:45.123456Z");
2039 }
2040
2041 #[test]
2042 fn test_datetime_microsecond_in_ordering() {
2043 let a = DateTimeValue {
2044 year: 2026,
2045 month: 1,
2046 day: 1,
2047 hour: 0,
2048 minute: 0,
2049 second: 0,
2050 microsecond: 100,
2051 timezone: None,
2052
2053 granularity: DateGranularity::DateTime,
2054 };
2055 let b = DateTimeValue {
2056 year: 2026,
2057 month: 1,
2058 day: 1,
2059 hour: 0,
2060 minute: 0,
2061 second: 0,
2062 microsecond: 200,
2063 timezone: None,
2064
2065 granularity: DateGranularity::DateTime,
2066 };
2067 assert!(a < b);
2068 }
2069
2070 #[test]
2071 fn test_datetime_parse_iso_week() {
2072 let dt: DateTimeValue = "2026-W01".parse().unwrap();
2073 assert_eq!(dt.year, 2025);
2074 assert_eq!(dt.month, 12);
2075 assert_eq!(dt.day, 29);
2076 assert_eq!(dt.microsecond, 0);
2077 assert_eq!(dt.to_string(), "2026-W01");
2078 assert!(matches!(
2079 dt.granularity,
2080 DateGranularity::IsoWeek {
2081 iso_year: 2026,
2082 week: 1
2083 }
2084 ));
2085 }
2086
2087 #[test]
2088 fn test_negation_types() {
2089 let json = serde_json::to_string(&NegationType::Not).expect("serialize NegationType");
2090 let decoded: NegationType = serde_json::from_str(&json).expect("deserialize NegationType");
2091 assert_eq!(decoded, NegationType::Not);
2092 }
2093
2094 #[test]
2095 fn parent_type_primitive_serde_internally_tagged() {
2096 let p = ParentType::Primitive {
2097 primitive: PrimitiveKind::Number,
2098 };
2099 let json = serde_json::to_string(&p).expect("ParentType::Primitive must serialize");
2100 assert!(json.contains("\"kind\"") && json.contains("\"primitive\""));
2101 let back: ParentType = serde_json::from_str(&json).expect("deserialize");
2102 assert_eq!(back, p);
2103 }
2104
2105 fn text_arg(s: &str) -> CommandArg {
2110 CommandArg::Literal(crate::literals::Value::Text(s.to_string()))
2111 }
2112
2113 fn number_arg(s: &str) -> CommandArg {
2114 let d: rust_decimal::Decimal = s.parse().expect("decimal");
2115 CommandArg::Literal(crate::literals::Value::Number(d))
2116 }
2117
2118 fn boolean_arg(b: BooleanValue) -> CommandArg {
2119 CommandArg::Literal(crate::literals::Value::Boolean(b))
2120 }
2121
2122 fn measure_arg(value: &str, unit: &str) -> CommandArg {
2123 let d: rust_decimal::Decimal = value.parse().expect("decimal");
2124 CommandArg::Literal(crate::literals::Value::NumberWithUnit(d, unit.to_string()))
2125 }
2126
2127 fn duration_arg(value: &str, unit: &str) -> CommandArg {
2128 let d: rust_decimal::Decimal = value.parse().expect("decimal");
2129 CommandArg::Literal(crate::literals::Value::NumberWithUnit(d, unit.to_string()))
2130 }
2131
2132 #[test]
2133 fn as_lemma_source_text_default_is_quoted() {
2134 let fv = DataValue::Definition {
2135 base: Some(ParentType::Primitive {
2136 primitive: PrimitiveKind::Text,
2137 }),
2138 constraints: Some(vec![test_constraint(
2139 TypeConstraintCommand::Suggest,
2140 vec![text_arg("single")],
2141 )]),
2142 value: None,
2143 };
2144 assert_eq!(
2145 format!("{}", AsLemmaSource(&fv)),
2146 "text -> suggest \"single\""
2147 );
2148 }
2149
2150 #[test]
2151 fn as_lemma_source_number_default_not_quoted() {
2152 let fv = DataValue::Definition {
2153 base: Some(ParentType::Primitive {
2154 primitive: PrimitiveKind::Number,
2155 }),
2156 constraints: Some(vec![test_constraint(
2157 TypeConstraintCommand::Suggest,
2158 vec![number_arg("10")],
2159 )]),
2160 value: None,
2161 };
2162 assert_eq!(format!("{}", AsLemmaSource(&fv)), "number -> suggest 10");
2163 }
2164
2165 #[test]
2166 fn as_lemma_source_help_always_quoted() {
2167 let fv = DataValue::Definition {
2168 base: Some(ParentType::Primitive {
2169 primitive: PrimitiveKind::Number,
2170 }),
2171 constraints: Some(vec![test_constraint(
2172 TypeConstraintCommand::Help,
2173 vec![text_arg("Enter a measure")],
2174 )]),
2175 value: None,
2176 };
2177 assert_eq!(
2178 format!("{}", AsLemmaSource(&fv)),
2179 "number -> help \"Enter a measure\""
2180 );
2181 }
2182
2183 #[test]
2184 fn as_lemma_source_text_option_quoted() {
2185 let fv = DataValue::Definition {
2186 base: Some(ParentType::Primitive {
2187 primitive: PrimitiveKind::Text,
2188 }),
2189 constraints: Some(vec![
2190 test_constraint(TypeConstraintCommand::Option, vec![text_arg("active")]),
2191 test_constraint(TypeConstraintCommand::Option, vec![text_arg("inactive")]),
2192 ]),
2193 value: None,
2194 };
2195 assert_eq!(
2196 format!("{}", AsLemmaSource(&fv)),
2197 "text -> option \"active\" -> option \"inactive\""
2198 );
2199 }
2200
2201 #[test]
2202 fn as_lemma_source_measure_unit_not_quoted() {
2203 let fv = DataValue::Definition {
2204 base: Some(ParentType::Primitive {
2205 primitive: PrimitiveKind::Measure,
2206 }),
2207 constraints: Some(vec![
2208 test_constraint(
2209 TypeConstraintCommand::Unit,
2210 vec![
2211 CommandArg::Label("eur".to_string()),
2212 CommandArg::UnitExpr(UnitArg::Factor(decimal("1.00"))),
2213 ],
2214 ),
2215 test_constraint(
2216 TypeConstraintCommand::Unit,
2217 vec![
2218 CommandArg::Label("usd".to_string()),
2219 CommandArg::UnitExpr(UnitArg::Factor(decimal("0.91"))),
2220 ],
2221 ),
2222 ]),
2223 value: None,
2224 };
2225 assert_eq!(
2226 format!("{}", AsLemmaSource(&fv)),
2227 "measure -> unit eur: 1.00 -> unit usd: 0.91"
2228 );
2229 }
2230
2231 #[test]
2232 fn as_lemma_source_measure_minimum_with_unit() {
2233 let fv = DataValue::Definition {
2234 base: Some(ParentType::Primitive {
2235 primitive: PrimitiveKind::Measure,
2236 }),
2237 constraints: Some(vec![test_constraint(
2238 TypeConstraintCommand::Minimum,
2239 vec![measure_arg("0", "eur")],
2240 )]),
2241 value: None,
2242 };
2243 assert_eq!(
2244 format!("{}", AsLemmaSource(&fv)),
2245 "measure -> minimum 0 eur"
2246 );
2247 }
2248
2249 #[test]
2250 fn as_lemma_source_boolean_default() {
2251 let fv = DataValue::Definition {
2252 base: Some(ParentType::Primitive {
2253 primitive: PrimitiveKind::Boolean,
2254 }),
2255 constraints: Some(vec![test_constraint(
2256 TypeConstraintCommand::Suggest,
2257 vec![boolean_arg(BooleanValue::True)],
2258 )]),
2259 value: None,
2260 };
2261 assert_eq!(format!("{}", AsLemmaSource(&fv)), "boolean -> suggest true");
2262 }
2263
2264 #[test]
2265 fn as_lemma_source_duration_default() {
2266 let fv = DataValue::Definition {
2267 base: Some(ParentType::Custom {
2268 name: "duration".to_string(),
2269 }),
2270 constraints: Some(vec![test_constraint(
2271 TypeConstraintCommand::Suggest,
2272 vec![duration_arg("40", "hour")],
2273 )]),
2274 value: None,
2275 };
2276 assert_eq!(
2277 format!("{}", AsLemmaSource(&fv)),
2278 "duration -> suggest 40 hour"
2279 );
2280 }
2281
2282 #[test]
2283 fn as_lemma_source_named_type_default_quoted() {
2284 let fv = DataValue::Definition {
2287 base: Some(ParentType::Custom {
2288 name: "filing_status_type".to_string(),
2289 }),
2290 constraints: Some(vec![test_constraint(
2291 TypeConstraintCommand::Suggest,
2292 vec![text_arg("single")],
2293 )]),
2294 value: None,
2295 };
2296 assert_eq!(
2297 format!("{}", AsLemmaSource(&fv)),
2298 "filing_status_type -> suggest \"single\""
2299 );
2300 }
2301
2302 #[test]
2303 fn as_lemma_source_help_escapes_quotes() {
2304 let fv = DataValue::Definition {
2305 base: Some(ParentType::Primitive {
2306 primitive: PrimitiveKind::Text,
2307 }),
2308 constraints: Some(vec![test_constraint(
2309 TypeConstraintCommand::Help,
2310 vec![text_arg("say \"hello\"")],
2311 )]),
2312 value: None,
2313 };
2314 assert_eq!(
2315 format!("{}", AsLemmaSource(&fv)),
2316 "text -> help \"say \\\"hello\\\"\""
2317 );
2318 }
2319
2320 fn unit_arg_expr(prefix: Decimal, factors: &[(&str, i32)]) -> UnitArg {
2321 UnitArg::Expr(
2322 prefix,
2323 factors
2324 .iter()
2325 .map(|(measure_ref, exp)| UnitFactor {
2326 measure_ref: (*measure_ref).to_string(),
2327 exp: *exp,
2328 })
2329 .collect(),
2330 )
2331 }
2332
2333 #[test]
2334 fn unit_arg_display_metre_per_second() {
2335 let arg = unit_arg_expr(Decimal::ONE, &[("meter", 1), ("second", -1)]);
2336 assert_eq!(format!("{arg}"), "meter/second");
2337 assert!(
2338 !format!("{arg}").contains("second^-1"),
2339 "must not print denominator as negative exponent"
2340 );
2341 }
2342
2343 #[test]
2344 fn unit_arg_display_meter_per_second_squared() {
2345 let arg = unit_arg_expr(Decimal::ONE, &[("meter", 1), ("second", -2)]);
2346 assert_eq!(format!("{arg}"), "meter/second^2");
2347 }
2348
2349 #[test]
2350 fn unit_arg_display_kg_times_mps2() {
2351 let arg = unit_arg_expr(Decimal::ONE, &[("kg", 1), ("mps2", 1)]);
2352 assert_eq!(format!("{arg}"), "kg * mps2");
2353 }
2354
2355 #[test]
2356 fn unit_arg_display_numeric_prefix_metre_per_second() {
2357 use std::str::FromStr;
2358 let prefix = Decimal::from_str("3.6").expect("decimal");
2359 let arg = unit_arg_expr(prefix, &[("meter", 1), ("second", -1)]);
2360 assert_eq!(format!("{arg}"), "3.6 meter/second");
2361 }
2362
2363 #[test]
2364 fn unit_arg_display_metre_per_second_times_kg() {
2365 let arg = unit_arg_expr(Decimal::ONE, &[("meter", 1), ("second", -1), ("kg", 1)]);
2366 assert_eq!(format!("{arg}"), "meter/second * kg");
2367 }
2368
2369 #[test]
2370 fn unit_arg_display_kg_meter_per_second_squared() {
2371 let arg = unit_arg_expr(Decimal::ONE, &[("kg", 1), ("meter", 1), ("second", -2)]);
2372 assert_eq!(format!("{arg}"), "kg * meter/second^2");
2373 }
2374
2375 #[test]
2378 fn format_constraint_as_source_unit_factor_uses_assignment_colon() {
2379 let args = vec![
2380 CommandArg::Label("eur".to_string()),
2381 CommandArg::UnitExpr(UnitArg::Factor(decimal("1"))),
2382 ];
2383 assert_eq!(
2384 format_constraint_as_source(&TypeConstraintCommand::Unit, &args),
2385 "unit eur: 1"
2386 );
2387 }
2388
2389 #[test]
2390 fn format_constraint_as_source_unit_compound_uses_assignment_colon() {
2391 let arg = unit_arg_expr(decimal("3.6"), &[("meter", 1), ("second", -1)]);
2392 let args = vec![
2393 CommandArg::Label("kmh".to_string()),
2394 CommandArg::UnitExpr(arg),
2395 ];
2396 assert_eq!(
2397 format_constraint_as_source(&TypeConstraintCommand::Unit, &args),
2398 "unit kmh: 3.6 meter/second"
2399 );
2400 }
2401
2402 #[test]
2403 fn format_constraint_as_source_unit_factor_one_decimal_uses_assignment_colon() {
2404 let args = vec![
2405 CommandArg::Label("eur".to_string()),
2406 CommandArg::UnitExpr(UnitArg::Factor(decimal("1.00"))),
2407 ];
2408 assert_eq!(
2409 format_constraint_as_source(&TypeConstraintCommand::Unit, &args),
2410 "unit eur: 1.00"
2411 );
2412 }
2413
2414 #[test]
2415 fn as_lemma_source_measure_unit_uses_assignment_colon() {
2416 let fv = DataValue::Definition {
2417 base: Some(ParentType::Primitive {
2418 primitive: PrimitiveKind::Measure,
2419 }),
2420 constraints: Some(vec![
2421 test_constraint(
2422 TypeConstraintCommand::Unit,
2423 vec![
2424 CommandArg::Label("eur".to_string()),
2425 CommandArg::UnitExpr(UnitArg::Factor(decimal("1.00"))),
2426 ],
2427 ),
2428 test_constraint(
2429 TypeConstraintCommand::Unit,
2430 vec![
2431 CommandArg::Label("usd".to_string()),
2432 CommandArg::UnitExpr(UnitArg::Factor(decimal("0.91"))),
2433 ],
2434 ),
2435 ]),
2436 value: None,
2437 };
2438 assert_eq!(
2439 format!("{}", AsLemmaSource(&fv)),
2440 "measure -> unit eur: 1.00 -> unit usd: 0.91"
2441 );
2442 }
2443
2444 fn decimal(value: &str) -> rust_decimal::Decimal {
2445 use std::str::FromStr;
2446 rust_decimal::Decimal::from_str(value).expect("decimal literal in test")
2447 }
2448}