1#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
19pub struct Span {
20 pub start: usize,
21 pub end: usize,
22 pub line: usize,
23 pub col: usize,
24}
25
26impl Span {
27 pub fn from_pest_span(span: pest::Span) -> Self {
28 let (line, col) = span.start_pos().line_col();
29 Self {
30 start: span.start(),
31 end: span.end(),
32 line,
33 col,
34 }
35 }
36}
37
38pub struct DepthTracker {
40 depth: usize,
41 max_depth: usize,
42}
43
44impl DepthTracker {
45 pub fn with_max_depth(max_depth: usize) -> Self {
46 Self {
47 depth: 0,
48 max_depth,
49 }
50 }
51
52 pub fn push_depth(&mut self) -> Result<(), String> {
53 self.depth += 1;
54 if self.depth > self.max_depth {
55 return Err(format!(
56 "Expression depth {} exceeds maximum of {}",
57 self.depth, self.max_depth
58 ));
59 }
60 Ok(())
61 }
62
63 pub fn pop_depth(&mut self) {
64 if self.depth > 0 {
65 self.depth -= 1;
66 }
67 }
68
69 pub fn max_depth(&self) -> usize {
70 self.max_depth
71 }
72}
73
74impl Default for DepthTracker {
75 fn default() -> Self {
76 Self {
77 depth: 0,
78 max_depth: 100,
79 }
80 }
81}
82
83use crate::parsing::source::Source;
88use chrono::{Datelike, Timelike};
89use rust_decimal::Decimal;
90use serde::Serialize;
91use std::cmp::Ordering;
92use std::fmt;
93use std::hash::{Hash, Hasher};
94use std::sync::Arc;
95
96#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
99pub struct LemmaSpec {
100 pub name: String,
102 pub effective_from: Option<DateTimeValue>,
103 pub attribute: Option<String>,
104 pub start_line: usize,
105 pub commentary: Option<String>,
106 pub types: Vec<TypeDef>,
107 pub facts: Vec<LemmaFact>,
108 pub rules: Vec<LemmaRule>,
109 pub meta_fields: Vec<MetaField>,
110}
111
112#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
113pub struct MetaField {
114 pub key: String,
115 pub value: MetaValue,
116 pub source_location: Source,
117}
118
119#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
120#[serde(rename_all = "snake_case")]
121pub enum MetaValue {
122 Literal(Value),
123 Unquoted(String),
124}
125
126impl fmt::Display for MetaValue {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 match self {
129 MetaValue::Literal(v) => write!(f, "{}", v),
130 MetaValue::Unquoted(s) => write!(f, "{}", s),
131 }
132 }
133}
134
135impl fmt::Display for MetaField {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 write!(f, "meta {}: {}", self.key, self.value)
138 }
139}
140
141#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
142pub struct LemmaFact {
143 pub reference: Reference,
144 pub value: FactValue,
145 pub source_location: Source,
146}
147
148#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
154pub struct UnlessClause {
155 pub condition: Expression,
156 pub result: Expression,
157 pub source_location: Source,
158}
159
160#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
162pub struct LemmaRule {
163 pub name: String,
164 pub expression: Expression,
165 pub unless_clauses: Vec<UnlessClause>,
166 pub source_location: Source,
167}
168
169#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
175pub struct Expression {
176 pub kind: ExpressionKind,
177 pub source_location: Option<Source>,
178}
179
180impl Expression {
181 #[must_use]
183 pub fn new(kind: ExpressionKind, source_location: Source) -> Self {
184 Self {
185 kind,
186 source_location: Some(source_location),
187 }
188 }
189
190 pub fn get_source_text(
194 &self,
195 sources: &std::collections::HashMap<String, String>,
196 ) -> Option<String> {
197 let loc = self.source_location.as_ref()?;
198 sources
199 .get(&loc.attribute)
200 .and_then(|source| loc.extract_text(source))
201 }
202}
203
204impl PartialEq for Expression {
206 fn eq(&self, other: &Self) -> bool {
207 self.kind == other.kind
208 }
209}
210
211impl Eq for Expression {}
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
215#[serde(rename_all = "snake_case")]
216pub enum DateRelativeKind {
217 InPast,
218 InFuture,
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
223#[serde(rename_all = "snake_case")]
224pub enum DateCalendarKind {
225 Current,
226 Past,
227 Future,
228 NotIn,
229}
230
231#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
233#[serde(rename_all = "snake_case")]
234pub enum CalendarUnit {
235 Year,
236 Month,
237 Week,
238}
239
240impl fmt::Display for DateRelativeKind {
241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242 match self {
243 DateRelativeKind::InPast => write!(f, "in past"),
244 DateRelativeKind::InFuture => write!(f, "in future"),
245 }
246 }
247}
248
249impl fmt::Display for DateCalendarKind {
250 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251 match self {
252 DateCalendarKind::Current => write!(f, "in calendar"),
253 DateCalendarKind::Past => write!(f, "in past calendar"),
254 DateCalendarKind::Future => write!(f, "in future calendar"),
255 DateCalendarKind::NotIn => write!(f, "not in calendar"),
256 }
257 }
258}
259
260impl fmt::Display for CalendarUnit {
261 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262 match self {
263 CalendarUnit::Year => write!(f, "year"),
264 CalendarUnit::Month => write!(f, "month"),
265 CalendarUnit::Week => write!(f, "week"),
266 }
267 }
268}
269
270#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
272#[serde(rename_all = "snake_case")]
273pub enum ExpressionKind {
274 Literal(Value),
276 Reference(Reference),
278 UnresolvedUnitLiteral(Decimal, String),
281 Now,
283 DateRelative(DateRelativeKind, Arc<Expression>, Option<Arc<Expression>>),
286 DateCalendar(DateCalendarKind, CalendarUnit, Arc<Expression>),
289 LogicalAnd(Arc<Expression>, Arc<Expression>),
290 Arithmetic(Arc<Expression>, ArithmeticComputation, Arc<Expression>),
291 Comparison(Arc<Expression>, ComparisonComputation, Arc<Expression>),
292 UnitConversion(Arc<Expression>, ConversionTarget),
293 LogicalNegation(Arc<Expression>, NegationType),
294 MathematicalComputation(MathematicalComputation, Arc<Expression>),
295 Veto(VetoExpression),
296}
297
298#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
308pub struct Reference {
309 pub segments: Vec<String>,
310 pub name: String,
311}
312
313impl Reference {
314 #[must_use]
315 pub fn local(name: String) -> Self {
316 Self {
317 segments: Vec::new(),
318 name,
319 }
320 }
321
322 #[must_use]
323 pub fn from_path(path: Vec<String>) -> Self {
324 if path.is_empty() {
325 Self {
326 segments: Vec::new(),
327 name: String::new(),
328 }
329 } else {
330 let name = path[path.len() - 1].clone();
332 let segments = path[..path.len() - 1].to_vec();
333 Self { segments, name }
334 }
335 }
336
337 #[must_use]
338 pub fn is_local(&self) -> bool {
339 self.segments.is_empty()
340 }
341
342 #[must_use]
343 pub fn full_path(&self) -> Vec<String> {
344 let mut path = self.segments.clone();
345 path.push(self.name.clone());
346 path
347 }
348}
349
350impl fmt::Display for Reference {
351 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
352 for segment in &self.segments {
353 write!(f, "{}.", segment)?;
354 }
355 write!(f, "{}", self.name)
356 }
357}
358
359#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, serde::Deserialize)]
361#[serde(rename_all = "snake_case")]
362pub enum ArithmeticComputation {
363 Add,
364 Subtract,
365 Multiply,
366 Divide,
367 Modulo,
368 Power,
369}
370
371impl ArithmeticComputation {
372 #[must_use]
374 pub fn symbol(&self) -> &'static str {
375 match self {
376 ArithmeticComputation::Add => "+",
377 ArithmeticComputation::Subtract => "-",
378 ArithmeticComputation::Multiply => "*",
379 ArithmeticComputation::Divide => "/",
380 ArithmeticComputation::Modulo => "%",
381 ArithmeticComputation::Power => "^",
382 }
383 }
384}
385
386#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, serde::Deserialize)]
388#[serde(rename_all = "snake_case")]
389pub enum ComparisonComputation {
390 GreaterThan,
391 LessThan,
392 GreaterThanOrEqual,
393 LessThanOrEqual,
394 Equal,
395 NotEqual,
396 Is,
397 IsNot,
398}
399
400impl ComparisonComputation {
401 #[must_use]
403 pub fn symbol(&self) -> &'static str {
404 match self {
405 ComparisonComputation::GreaterThan => ">",
406 ComparisonComputation::LessThan => "<",
407 ComparisonComputation::GreaterThanOrEqual => ">=",
408 ComparisonComputation::LessThanOrEqual => "<=",
409 ComparisonComputation::Equal => "==",
410 ComparisonComputation::NotEqual => "!=",
411 ComparisonComputation::Is => "is",
412 ComparisonComputation::IsNot => "is not",
413 }
414 }
415
416 #[must_use]
418 pub fn is_equal(&self) -> bool {
419 matches!(
420 self,
421 ComparisonComputation::Equal | ComparisonComputation::Is
422 )
423 }
424
425 #[must_use]
427 pub fn is_not_equal(&self) -> bool {
428 matches!(
429 self,
430 ComparisonComputation::NotEqual | ComparisonComputation::IsNot
431 )
432 }
433}
434
435#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
438#[serde(rename_all = "snake_case")]
439pub enum ConversionTarget {
440 Duration(DurationUnit),
441 Unit(String),
442}
443
444#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
446pub enum NegationType {
447 Not,
448}
449
450#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
458pub struct VetoExpression {
459 pub message: Option<String>,
460}
461
462#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, serde::Deserialize)]
464#[serde(rename_all = "snake_case")]
465pub enum MathematicalComputation {
466 Sqrt,
467 Sin,
468 Cos,
469 Tan,
470 Asin,
471 Acos,
472 Atan,
473 Log,
474 Exp,
475 Abs,
476 Floor,
477 Ceil,
478 Round,
479}
480
481#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
486pub struct SpecRef {
487 pub name: String,
489 pub is_registry: bool,
491 pub hash_pin: Option<String>,
493 pub effective: Option<DateTimeValue>,
495}
496
497impl std::fmt::Display for SpecRef {
498 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
499 if self.is_registry {
500 write!(f, "@{}", self.name)?;
501 } else {
502 write!(f, "{}", self.name)?;
503 }
504 if let Some(ref h) = self.hash_pin {
505 write!(f, "~{}", h)?;
506 }
507 if let Some(ref d) = self.effective {
508 write!(f, " {}", d)?;
509 }
510 Ok(())
511 }
512}
513
514impl SpecRef {
515 pub fn local(name: impl Into<String>) -> Self {
517 Self {
518 name: name.into(),
519 is_registry: false,
520 hash_pin: None,
521 effective: None,
522 }
523 }
524
525 pub fn registry(name: impl Into<String>) -> Self {
527 Self {
528 name: name.into(),
529 is_registry: true,
530 hash_pin: None,
531 effective: None,
532 }
533 }
534
535 pub fn resolution_key(&self) -> String {
536 self.name.clone()
537 }
538}
539
540#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
549#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
550pub enum CommandArg {
551 Number(String),
553 Boolean(String),
555 Text(String),
558 Label(String),
560}
561
562impl CommandArg {
563 pub fn value(&self) -> &str {
568 match self {
569 CommandArg::Number(s)
570 | CommandArg::Boolean(s)
571 | CommandArg::Text(s)
572 | CommandArg::Label(s) => s,
573 }
574 }
575}
576
577impl fmt::Display for CommandArg {
578 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
579 write!(f, "{}", self.value())
580 }
581}
582
583pub type Constraint = (String, Vec<CommandArg>);
585
586#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
587#[serde(rename_all = "snake_case")]
588pub enum FactValue {
590 Literal(Value),
592 SpecReference(SpecRef),
594 TypeDeclaration {
596 base: String,
597 constraints: Option<Vec<Constraint>>,
598 from: Option<SpecRef>,
599 },
600}
601
602#[derive(
605 Debug,
606 Clone,
607 PartialEq,
608 Eq,
609 Hash,
610 Serialize,
611 serde::Deserialize,
612 strum_macros::EnumString,
613 strum_macros::Display,
614)]
615#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
616pub enum BooleanValue {
617 True,
618 False,
619 Yes,
620 No,
621 Accept,
622 Reject,
623}
624
625impl From<BooleanValue> for bool {
626 fn from(value: BooleanValue) -> bool {
627 match value {
628 BooleanValue::True | BooleanValue::Yes | BooleanValue::Accept => true,
629 BooleanValue::False | BooleanValue::No | BooleanValue::Reject => false,
630 }
631 }
632}
633
634impl From<&BooleanValue> for bool {
635 fn from(value: &BooleanValue) -> bool {
636 match value {
637 BooleanValue::True | BooleanValue::Yes | BooleanValue::Accept => true,
638 BooleanValue::False | BooleanValue::No | BooleanValue::Reject => false,
639 }
640 }
641}
642
643impl From<bool> for BooleanValue {
644 fn from(value: bool) -> BooleanValue {
645 if value {
646 BooleanValue::True
647 } else {
648 BooleanValue::False
649 }
650 }
651}
652
653impl std::ops::Not for BooleanValue {
654 type Output = BooleanValue;
655
656 fn not(self) -> Self::Output {
657 if self.into() {
658 BooleanValue::False
659 } else {
660 BooleanValue::True
661 }
662 }
663}
664
665impl std::ops::Not for &BooleanValue {
666 type Output = BooleanValue;
667
668 fn not(self) -> Self::Output {
669 if self.into() {
670 BooleanValue::False
671 } else {
672 BooleanValue::True
673 }
674 }
675}
676
677#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, serde::Deserialize)]
679#[serde(rename_all = "snake_case")]
680pub enum Value {
681 Number(Decimal),
682 Scale(Decimal, String),
683 Text(String),
684 Date(DateTimeValue),
685 Time(TimeValue),
686 Boolean(BooleanValue),
687 Duration(Decimal, DurationUnit),
688 Ratio(Decimal, Option<String>),
689}
690
691impl fmt::Display for Value {
692 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
693 match self {
694 Value::Number(n) => write!(f, "{}", n),
695 Value::Text(s) => write!(f, "{}", s),
696 Value::Date(dt) => write!(f, "{}", dt),
697 Value::Boolean(b) => write!(f, "{}", b),
698 Value::Time(time) => write!(f, "{}", time),
699 Value::Scale(n, u) => write!(f, "{} {}", n, u),
700 Value::Duration(n, u) => write!(f, "{} {}", n, u),
701 Value::Ratio(n, u) => match u.as_deref() {
702 Some("percent") => {
703 let display_value = *n * Decimal::from(100);
704 let norm = display_value.normalize();
705 let s = if norm.fract().is_zero() {
706 norm.trunc().to_string()
707 } else {
708 norm.to_string()
709 };
710 write!(f, "{}%", s)
711 }
712 Some("permille") => {
713 let display_value = *n * Decimal::from(1000);
714 let norm = display_value.normalize();
715 let s = if norm.fract().is_zero() {
716 norm.trunc().to_string()
717 } else {
718 norm.to_string()
719 };
720 write!(f, "{}%%", s)
721 }
722 Some(unit) => {
723 let norm = n.normalize();
724 let s = if norm.fract().is_zero() {
725 norm.trunc().to_string()
726 } else {
727 norm.to_string()
728 };
729 write!(f, "{} {}", s, unit)
730 }
731 None => {
732 let norm = n.normalize();
733 let s = if norm.fract().is_zero() {
734 norm.trunc().to_string()
735 } else {
736 norm.to_string()
737 };
738 write!(f, "{}", s)
739 }
740 },
741 }
742 }
743}
744
745impl fmt::Display for FactValue {
746 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
747 match self {
748 FactValue::Literal(v) => write!(f, "{}", v),
749 FactValue::SpecReference(spec_ref) => {
750 if spec_ref.is_registry {
751 write!(f, "spec @{}", spec_ref.name)
752 } else {
753 write!(f, "spec {}", spec_ref.name)
754 }
755 }
756 FactValue::TypeDeclaration {
757 base,
758 constraints,
759 from,
760 } => {
761 let base_str = if let Some(from_spec) = from {
762 format!("{} from {}", base, from_spec)
763 } else {
764 base.clone()
765 };
766 if let Some(ref constraints_vec) = constraints {
767 let constraint_str = constraints_vec
768 .iter()
769 .map(|(cmd, args)| {
770 let args_str: Vec<&str> = args.iter().map(|a| a.value()).collect();
771 let joined = args_str.join(" ");
772 if joined.is_empty() {
773 cmd.clone()
774 } else {
775 format!("{} {}", cmd, joined)
776 }
777 })
778 .collect::<Vec<_>>()
779 .join(" -> ");
780 write!(f, "[{} -> {}]", base_str, constraint_str)
781 } else {
782 write!(f, "[{}]", base_str)
783 }
784 }
785 }
786 }
787}
788
789#[derive(
791 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, serde::Deserialize,
792)]
793pub struct TimeValue {
794 pub hour: u8,
795 pub minute: u8,
796 pub second: u8,
797 pub timezone: Option<TimezoneValue>,
798}
799
800#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, serde::Deserialize)]
802pub struct TimezoneValue {
803 pub offset_hours: i8,
804 pub offset_minutes: u8,
805}
806
807#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, serde::Deserialize)]
809pub struct DateTimeValue {
810 pub year: i32,
811 pub month: u32,
812 pub day: u32,
813 pub hour: u32,
814 pub minute: u32,
815 pub second: u32,
816 #[serde(default)]
817 pub microsecond: u32,
818 pub timezone: Option<TimezoneValue>,
819}
820
821impl DateTimeValue {
822 pub fn now() -> Self {
823 let now = chrono::Local::now();
824 let offset_secs = now.offset().local_minus_utc();
825 Self {
826 year: now.year(),
827 month: now.month(),
828 day: now.day(),
829 hour: now.time().hour(),
830 minute: now.time().minute(),
831 second: now.time().second(),
832 microsecond: now.time().nanosecond() / 1000 % 1_000_000,
833 timezone: Some(TimezoneValue {
834 offset_hours: (offset_secs / 3600) as i8,
835 offset_minutes: ((offset_secs.abs() % 3600) / 60) as u8,
836 }),
837 }
838 }
839
840 pub fn parse(s: &str) -> Option<Self> {
848 if let Some(dtv) = crate::parsing::literals::parse_datetime_str(s) {
849 return Some(dtv);
850 }
851 if let Some(week_val) = Self::parse_iso_week(s) {
852 return Some(week_val);
853 }
854 if let Ok(ym) = chrono::NaiveDate::parse_from_str(&format!("{}-01", s), "%Y-%m-%d") {
855 return Some(Self {
856 year: ym.year(),
857 month: ym.month(),
858 day: 1,
859 hour: 0,
860 minute: 0,
861 second: 0,
862 microsecond: 0,
863 timezone: None,
864 });
865 }
866 if let Ok(year) = s.parse::<i32>() {
867 if (1..=9999).contains(&year) {
868 return Some(Self {
869 year,
870 month: 1,
871 day: 1,
872 hour: 0,
873 minute: 0,
874 second: 0,
875 microsecond: 0,
876 timezone: None,
877 });
878 }
879 }
880 None
881 }
882
883 fn parse_iso_week(s: &str) -> Option<Self> {
886 let parts: Vec<&str> = s.split("-W").collect();
887 if parts.len() != 2 {
888 return None;
889 }
890 let year: i32 = parts[0].parse().ok()?;
891 let week: u32 = parts[1].parse().ok()?;
892 if week == 0 || week > 53 {
893 return None;
894 }
895 let date = chrono::NaiveDate::from_isoywd_opt(year, week, chrono::Weekday::Mon)?;
896 Some(Self {
897 year: date.year(),
898 month: date.month(),
899 day: date.day(),
900 hour: 0,
901 minute: 0,
902 second: 0,
903 microsecond: 0,
904 timezone: None,
905 })
906 }
907}
908
909macro_rules! impl_unit_serialize {
911 ($($unit_type:ty),+) => {
912 $(
913 impl Serialize for $unit_type {
914 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
915 where
916 S: serde::Serializer,
917 {
918 serializer.serialize_str(&self.to_string())
919 }
920 }
921 )+
922 };
923}
924
925impl_unit_serialize!(DurationUnit);
926
927#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Deserialize, strum_macros::EnumString)]
928#[strum(serialize_all = "lowercase")]
929pub enum DurationUnit {
930 Year,
931 Month,
932 Week,
933 Day,
934 Hour,
935 Minute,
936 Second,
937 Millisecond,
938 Microsecond,
939}
940
941impl fmt::Display for DurationUnit {
942 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
943 let s = match self {
944 DurationUnit::Year => "years",
945 DurationUnit::Month => "months",
946 DurationUnit::Week => "weeks",
947 DurationUnit::Day => "days",
948 DurationUnit::Hour => "hours",
949 DurationUnit::Minute => "minutes",
950 DurationUnit::Second => "seconds",
951 DurationUnit::Millisecond => "milliseconds",
952 DurationUnit::Microsecond => "microseconds",
953 };
954 write!(f, "{}", s)
955 }
956}
957
958impl LemmaFact {
961 #[must_use]
962 pub fn new(reference: Reference, value: FactValue, source_location: Source) -> Self {
963 Self {
964 reference,
965 value,
966 source_location,
967 }
968 }
969}
970
971impl LemmaSpec {
972 #[must_use]
973 pub fn new(name: String) -> Self {
974 Self {
975 name,
976 effective_from: None,
977 attribute: None,
978 start_line: 1,
979 commentary: None,
980 types: Vec::new(),
981 facts: Vec::new(),
982 rules: Vec::new(),
983 meta_fields: Vec::new(),
984 }
985 }
986
987 pub fn effective_from(&self) -> Option<&DateTimeValue> {
989 self.effective_from.as_ref()
990 }
991
992 #[must_use]
993 pub fn with_attribute(mut self, attribute: String) -> Self {
994 self.attribute = Some(attribute);
995 self
996 }
997
998 #[must_use]
999 pub fn with_start_line(mut self, start_line: usize) -> Self {
1000 self.start_line = start_line;
1001 self
1002 }
1003
1004 #[must_use]
1005 pub fn set_commentary(mut self, commentary: String) -> Self {
1006 self.commentary = Some(commentary);
1007 self
1008 }
1009
1010 #[must_use]
1011 pub fn add_fact(mut self, fact: LemmaFact) -> Self {
1012 self.facts.push(fact);
1013 self
1014 }
1015
1016 #[must_use]
1017 pub fn add_rule(mut self, rule: LemmaRule) -> Self {
1018 self.rules.push(rule);
1019 self
1020 }
1021
1022 #[must_use]
1023 pub fn add_type(mut self, type_def: TypeDef) -> Self {
1024 self.types.push(type_def);
1025 self
1026 }
1027
1028 #[must_use]
1029 pub fn add_meta_field(mut self, meta: MetaField) -> Self {
1030 self.meta_fields.push(meta);
1031 self
1032 }
1033}
1034
1035impl PartialEq for LemmaSpec {
1036 fn eq(&self, other: &Self) -> bool {
1037 self.name == other.name && self.effective_from() == other.effective_from()
1038 }
1039}
1040
1041impl Eq for LemmaSpec {}
1042
1043impl PartialOrd for LemmaSpec {
1044 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1045 Some(self.cmp(other))
1046 }
1047}
1048
1049impl Ord for LemmaSpec {
1050 fn cmp(&self, other: &Self) -> Ordering {
1051 (self.name.as_str(), self.effective_from())
1052 .cmp(&(other.name.as_str(), other.effective_from()))
1053 }
1054}
1055
1056impl Hash for LemmaSpec {
1057 fn hash<H: Hasher>(&self, state: &mut H) {
1058 self.name.hash(state);
1059 match self.effective_from() {
1060 Some(d) => d.hash(state),
1061 None => 0u8.hash(state),
1062 }
1063 }
1064}
1065
1066impl fmt::Display for LemmaSpec {
1067 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1068 write!(f, "spec {}", self.name)?;
1069 if let Some(ref af) = self.effective_from {
1070 write!(f, " {}", af)?;
1071 }
1072 writeln!(f)?;
1073
1074 if let Some(ref commentary) = self.commentary {
1075 writeln!(f, "\"\"\"")?;
1076 writeln!(f, "{}", commentary)?;
1077 writeln!(f, "\"\"\"")?;
1078 }
1079
1080 let named_types: Vec<_> = self
1081 .types
1082 .iter()
1083 .filter(|t| !matches!(t, TypeDef::Inline { .. }))
1084 .collect();
1085 if !named_types.is_empty() {
1086 writeln!(f)?;
1087 for (index, type_def) in named_types.iter().enumerate() {
1088 if index > 0 {
1089 writeln!(f)?;
1090 }
1091 write!(f, "{}", type_def)?;
1092 writeln!(f)?;
1093 }
1094 }
1095
1096 if !self.facts.is_empty() {
1097 writeln!(f)?;
1098 for fact in &self.facts {
1099 write!(f, "{}", fact)?;
1100 }
1101 }
1102
1103 if !self.rules.is_empty() {
1104 writeln!(f)?;
1105 for (index, rule) in self.rules.iter().enumerate() {
1106 if index > 0 {
1107 writeln!(f)?;
1108 }
1109 write!(f, "{}", rule)?;
1110 }
1111 }
1112
1113 if !self.meta_fields.is_empty() {
1114 writeln!(f)?;
1115 for meta in &self.meta_fields {
1116 writeln!(f, "{}", meta)?;
1117 }
1118 }
1119
1120 Ok(())
1121 }
1122}
1123
1124impl fmt::Display for LemmaFact {
1125 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1126 writeln!(f, "fact {}: {}", self.reference, self.value)
1127 }
1128}
1129
1130impl fmt::Display for LemmaRule {
1131 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1132 write!(f, "rule {}: {}", self.name, self.expression)?;
1133 for unless_clause in &self.unless_clauses {
1134 write!(
1135 f,
1136 "\n unless {} then {}",
1137 unless_clause.condition, unless_clause.result
1138 )?;
1139 }
1140 writeln!(f)?;
1141 Ok(())
1142 }
1143}
1144
1145pub fn expression_precedence(kind: &ExpressionKind) -> u8 {
1150 match kind {
1151 ExpressionKind::LogicalAnd(..) => 2,
1152 ExpressionKind::LogicalNegation(..) => 3,
1153 ExpressionKind::Comparison(..) => 4,
1154 ExpressionKind::UnitConversion(..) => 4,
1155 ExpressionKind::Arithmetic(_, op, _) => match op {
1156 ArithmeticComputation::Add | ArithmeticComputation::Subtract => 5,
1157 ArithmeticComputation::Multiply
1158 | ArithmeticComputation::Divide
1159 | ArithmeticComputation::Modulo => 6,
1160 ArithmeticComputation::Power => 7,
1161 },
1162 ExpressionKind::MathematicalComputation(..) => 8,
1163 ExpressionKind::DateRelative(..) | ExpressionKind::DateCalendar(..) => 4,
1164 ExpressionKind::Literal(..)
1165 | ExpressionKind::Reference(..)
1166 | ExpressionKind::UnresolvedUnitLiteral(..)
1167 | ExpressionKind::Now
1168 | ExpressionKind::Veto(..) => 10,
1169 }
1170}
1171
1172fn write_expression_child(
1173 f: &mut fmt::Formatter<'_>,
1174 child: &Expression,
1175 parent_prec: u8,
1176) -> fmt::Result {
1177 let child_prec = expression_precedence(&child.kind);
1178 if child_prec < parent_prec {
1179 write!(f, "({})", child)
1180 } else {
1181 write!(f, "{}", child)
1182 }
1183}
1184
1185impl fmt::Display for Expression {
1186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1187 match &self.kind {
1188 ExpressionKind::Literal(lit) => write!(f, "{}", lit),
1189 ExpressionKind::Reference(r) => write!(f, "{}", r),
1190 ExpressionKind::Arithmetic(left, op, right) => {
1191 let my_prec = expression_precedence(&self.kind);
1192 write_expression_child(f, left, my_prec)?;
1193 write!(f, " {} ", op)?;
1194 write_expression_child(f, right, my_prec)
1195 }
1196 ExpressionKind::Comparison(left, op, right) => {
1197 let my_prec = expression_precedence(&self.kind);
1198 write_expression_child(f, left, my_prec)?;
1199 write!(f, " {} ", op)?;
1200 write_expression_child(f, right, my_prec)
1201 }
1202 ExpressionKind::UnitConversion(value, target) => {
1203 let my_prec = expression_precedence(&self.kind);
1204 write_expression_child(f, value, my_prec)?;
1205 write!(f, " in {}", target)
1206 }
1207 ExpressionKind::LogicalNegation(expr, _) => {
1208 let my_prec = expression_precedence(&self.kind);
1209 write!(f, "not ")?;
1210 write_expression_child(f, expr, my_prec)
1211 }
1212 ExpressionKind::LogicalAnd(left, right) => {
1213 let my_prec = expression_precedence(&self.kind);
1214 write_expression_child(f, left, my_prec)?;
1215 write!(f, " and ")?;
1216 write_expression_child(f, right, my_prec)
1217 }
1218 ExpressionKind::MathematicalComputation(op, operand) => {
1219 let my_prec = expression_precedence(&self.kind);
1220 write!(f, "{} ", op)?;
1221 write_expression_child(f, operand, my_prec)
1222 }
1223 ExpressionKind::Veto(veto) => match &veto.message {
1224 Some(msg) => write!(f, "veto {}", quote_lemma_text(msg)),
1225 None => write!(f, "veto"),
1226 },
1227 ExpressionKind::UnresolvedUnitLiteral(number, unit_name) => {
1228 write!(f, "{} {}", format_decimal_source(number), unit_name)
1229 }
1230 ExpressionKind::Now => write!(f, "now"),
1231 ExpressionKind::DateRelative(kind, date_expr, tolerance) => {
1232 write!(f, "{} {}", date_expr, kind)?;
1233 if let Some(tol) = tolerance {
1234 write!(f, " {}", tol)?;
1235 }
1236 Ok(())
1237 }
1238 ExpressionKind::DateCalendar(kind, unit, date_expr) => {
1239 write!(f, "{} {} {}", date_expr, kind, unit)
1240 }
1241 }
1242 }
1243}
1244
1245impl fmt::Display for ConversionTarget {
1246 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1247 match self {
1248 ConversionTarget::Duration(unit) => write!(f, "{}", unit),
1249 ConversionTarget::Unit(unit) => write!(f, "{}", unit),
1250 }
1251 }
1252}
1253
1254impl fmt::Display for ArithmeticComputation {
1255 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1256 match self {
1257 ArithmeticComputation::Add => write!(f, "+"),
1258 ArithmeticComputation::Subtract => write!(f, "-"),
1259 ArithmeticComputation::Multiply => write!(f, "*"),
1260 ArithmeticComputation::Divide => write!(f, "/"),
1261 ArithmeticComputation::Modulo => write!(f, "%"),
1262 ArithmeticComputation::Power => write!(f, "^"),
1263 }
1264 }
1265}
1266
1267impl fmt::Display for ComparisonComputation {
1268 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1269 match self {
1270 ComparisonComputation::GreaterThan => write!(f, ">"),
1271 ComparisonComputation::LessThan => write!(f, "<"),
1272 ComparisonComputation::GreaterThanOrEqual => write!(f, ">="),
1273 ComparisonComputation::LessThanOrEqual => write!(f, "<="),
1274 ComparisonComputation::Equal => write!(f, "=="),
1275 ComparisonComputation::NotEqual => write!(f, "!="),
1276 ComparisonComputation::Is => write!(f, "is"),
1277 ComparisonComputation::IsNot => write!(f, "is not"),
1278 }
1279 }
1280}
1281
1282impl fmt::Display for MathematicalComputation {
1283 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1284 match self {
1285 MathematicalComputation::Sqrt => write!(f, "sqrt"),
1286 MathematicalComputation::Sin => write!(f, "sin"),
1287 MathematicalComputation::Cos => write!(f, "cos"),
1288 MathematicalComputation::Tan => write!(f, "tan"),
1289 MathematicalComputation::Asin => write!(f, "asin"),
1290 MathematicalComputation::Acos => write!(f, "acos"),
1291 MathematicalComputation::Atan => write!(f, "atan"),
1292 MathematicalComputation::Log => write!(f, "log"),
1293 MathematicalComputation::Exp => write!(f, "exp"),
1294 MathematicalComputation::Abs => write!(f, "abs"),
1295 MathematicalComputation::Floor => write!(f, "floor"),
1296 MathematicalComputation::Ceil => write!(f, "ceil"),
1297 MathematicalComputation::Round => write!(f, "round"),
1298 }
1299 }
1300}
1301
1302impl fmt::Display for TimeValue {
1303 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1304 write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)
1305 }
1306}
1307
1308impl fmt::Display for TimezoneValue {
1309 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1310 if self.offset_hours == 0 && self.offset_minutes == 0 {
1311 write!(f, "Z")
1312 } else {
1313 let sign = if self.offset_hours >= 0 { "+" } else { "-" };
1314 let hours = self.offset_hours.abs();
1315 write!(f, "{}{:02}:{:02}", sign, hours, self.offset_minutes)
1316 }
1317 }
1318}
1319
1320impl fmt::Display for DateTimeValue {
1321 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1322 let has_time = self.hour != 0
1323 || self.minute != 0
1324 || self.second != 0
1325 || self.microsecond != 0
1326 || self.timezone.is_some();
1327 if !has_time {
1328 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
1329 } else {
1330 write!(
1331 f,
1332 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
1333 self.year, self.month, self.day, self.hour, self.minute, self.second
1334 )?;
1335 if self.microsecond != 0 {
1336 write!(f, ".{:06}", self.microsecond)?;
1337 }
1338 if let Some(tz) = &self.timezone {
1339 write!(f, "{}", tz)?;
1340 }
1341 Ok(())
1342 }
1343 }
1344}
1345
1346#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1351#[serde(tag = "kind", rename_all = "snake_case")]
1352pub enum TypeDef {
1353 Regular {
1354 source_location: Source,
1355 name: String,
1356 parent: String,
1357 constraints: Option<Vec<Constraint>>,
1358 },
1359 Import {
1360 source_location: Source,
1361 name: String,
1362 source_type: String,
1363 from: SpecRef,
1364 constraints: Option<Vec<Constraint>>,
1365 },
1366 Inline {
1367 source_location: Source,
1368 parent: String,
1369 constraints: Option<Vec<Constraint>>,
1370 fact_ref: Reference,
1371 from: Option<SpecRef>,
1372 },
1373}
1374
1375impl TypeDef {
1376 pub fn source_location(&self) -> &Source {
1377 match self {
1378 TypeDef::Regular {
1379 source_location, ..
1380 }
1381 | TypeDef::Import {
1382 source_location, ..
1383 }
1384 | TypeDef::Inline {
1385 source_location, ..
1386 } => source_location,
1387 }
1388 }
1389
1390 pub fn name(&self) -> &str {
1391 match self {
1392 TypeDef::Regular { name, .. } | TypeDef::Import { name, .. } => name,
1393 TypeDef::Inline { parent, .. } => parent,
1394 }
1395 }
1396}
1397
1398impl fmt::Display for TypeDef {
1399 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1400 match self {
1401 TypeDef::Regular {
1402 name,
1403 parent,
1404 constraints,
1405 ..
1406 } => {
1407 write!(f, "type {}: {}", name, parent)?;
1408 if let Some(constraints) = constraints {
1409 for (cmd, args) in constraints {
1410 write!(f, "\n -> {}", cmd)?;
1411 for arg in args {
1412 write!(f, " {}", arg.value())?;
1413 }
1414 }
1415 }
1416 Ok(())
1417 }
1418 TypeDef::Import {
1419 name,
1420 from,
1421 constraints,
1422 ..
1423 } => {
1424 write!(f, "type {} from {}", name, from)?;
1425 if let Some(constraints) = constraints {
1426 for (cmd, args) in constraints {
1427 write!(f, " -> {}", cmd)?;
1428 for arg in args {
1429 write!(f, " {}", arg.value())?;
1430 }
1431 }
1432 }
1433 Ok(())
1434 }
1435 TypeDef::Inline { .. } => Ok(()),
1436 }
1437 }
1438}
1439
1440pub struct AsLemmaSource<'a, T: ?Sized>(pub &'a T);
1455
1456pub fn quote_lemma_text(s: &str) -> String {
1459 let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
1460 format!("\"{}\"", escaped)
1461}
1462
1463fn format_decimal_source(n: &Decimal) -> String {
1466 let norm = n.normalize();
1467 if norm.fract().is_zero() {
1468 norm.trunc().to_string()
1469 } else {
1470 norm.to_string()
1471 }
1472}
1473
1474impl<'a> fmt::Display for AsLemmaSource<'a, CommandArg> {
1477 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1478 match self.0 {
1479 CommandArg::Text(s) => write!(f, "{}", quote_lemma_text(s)),
1480 CommandArg::Number(s) | CommandArg::Boolean(s) | CommandArg::Label(s) => {
1481 write!(f, "{}", s)
1482 }
1483 }
1484 }
1485}
1486
1487fn format_constraint_as_source(cmd: &str, args: &[CommandArg]) -> String {
1492 if args.is_empty() {
1493 cmd.to_string()
1494 } else {
1495 let args_str: Vec<String> = args
1496 .iter()
1497 .map(|a| format!("{}", AsLemmaSource(a)))
1498 .collect();
1499 format!("{} {}", cmd, args_str.join(" "))
1500 }
1501}
1502
1503fn format_constraints_as_source(
1506 constraints: &[(String, Vec<CommandArg>)],
1507 separator: &str,
1508) -> String {
1509 constraints
1510 .iter()
1511 .map(|(cmd, args)| format_constraint_as_source(cmd, args))
1512 .collect::<Vec<_>>()
1513 .join(separator)
1514}
1515
1516impl<'a> fmt::Display for AsLemmaSource<'a, FactValue> {
1519 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1520 match self.0 {
1521 FactValue::Literal(v) => write!(f, "{}", AsLemmaSource(v)),
1522 FactValue::SpecReference(spec_ref) => {
1523 if spec_ref.is_registry {
1524 write!(f, "spec @{}", spec_ref.name)
1525 } else {
1526 write!(f, "spec {}", spec_ref.name)
1527 }
1528 }
1529 FactValue::TypeDeclaration {
1530 base,
1531 constraints,
1532 from,
1533 } => {
1534 let base_str = if let Some(from_spec) = from {
1535 format!("{} from {}", base, from_spec)
1536 } else {
1537 base.clone()
1538 };
1539 if let Some(ref constraints_vec) = constraints {
1540 let constraint_str = format_constraints_as_source(constraints_vec, " -> ");
1541 write!(f, "[{} -> {}]", base_str, constraint_str)
1542 } else {
1543 write!(f, "[{}]", base_str)
1544 }
1545 }
1546 }
1547 }
1548}
1549
1550impl<'a> fmt::Display for AsLemmaSource<'a, Value> {
1553 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1554 match self.0 {
1555 Value::Number(n) => write!(f, "{}", format_decimal_source(n)),
1556 Value::Text(s) => write!(f, "{}", quote_lemma_text(s)),
1557 Value::Date(dt) => {
1558 let is_date_only =
1559 dt.hour == 0 && dt.minute == 0 && dt.second == 0 && dt.timezone.is_none();
1560 if is_date_only {
1561 write!(f, "{:04}-{:02}-{:02}", dt.year, dt.month, dt.day)
1562 } else {
1563 write!(
1564 f,
1565 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
1566 dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second
1567 )?;
1568 if let Some(tz) = &dt.timezone {
1569 write!(f, "{}", tz)?;
1570 }
1571 Ok(())
1572 }
1573 }
1574 Value::Time(t) => {
1575 write!(f, "{:02}:{:02}:{:02}", t.hour, t.minute, t.second)?;
1576 if let Some(tz) = &t.timezone {
1577 write!(f, "{}", tz)?;
1578 }
1579 Ok(())
1580 }
1581 Value::Boolean(b) => write!(f, "{}", b),
1582 Value::Scale(n, u) => write!(f, "{} {}", format_decimal_source(n), u),
1583 Value::Duration(n, u) => write!(f, "{} {}", format_decimal_source(n), u),
1584 Value::Ratio(n, unit) => match unit.as_deref() {
1585 Some("percent") => {
1586 let display_value = *n * Decimal::from(100);
1587 write!(f, "{}%", format_decimal_source(&display_value))
1588 }
1589 Some("permille") => {
1590 let display_value = *n * Decimal::from(1000);
1591 write!(f, "{}%%", format_decimal_source(&display_value))
1592 }
1593 Some(unit_name) => write!(f, "{} {}", format_decimal_source(n), unit_name),
1594 None => write!(f, "{}", format_decimal_source(n)),
1595 },
1596 }
1597 }
1598}
1599
1600impl<'a> fmt::Display for AsLemmaSource<'a, MetaValue> {
1603 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1604 match self.0 {
1605 MetaValue::Literal(v) => write!(f, "{}", AsLemmaSource(v)),
1606 MetaValue::Unquoted(s) => write!(f, "{}", s),
1607 }
1608 }
1609}
1610
1611impl<'a> fmt::Display for AsLemmaSource<'a, TypeDef> {
1614 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1615 match self.0 {
1616 TypeDef::Regular {
1617 name,
1618 parent,
1619 constraints,
1620 ..
1621 } => {
1622 write!(f, "type {}: {}", name, parent)?;
1623 if let Some(constraints) = constraints {
1624 for (cmd, args) in constraints {
1625 write!(f, "\n -> {}", format_constraint_as_source(cmd, args))?;
1626 }
1627 }
1628 Ok(())
1629 }
1630 TypeDef::Import {
1631 name,
1632 from,
1633 constraints,
1634 ..
1635 } => {
1636 write!(f, "type {} from {}", name, from)?;
1637 if let Some(constraints) = constraints {
1638 for (cmd, args) in constraints {
1639 write!(f, " -> {}", format_constraint_as_source(cmd, args))?;
1640 }
1641 }
1642 Ok(())
1643 }
1644 TypeDef::Inline { .. } => Ok(()),
1645 }
1646 }
1647}
1648
1649#[cfg(test)]
1650mod tests {
1651 use super::*;
1652
1653 #[test]
1654 fn test_duration_unit_display() {
1655 assert_eq!(format!("{}", DurationUnit::Second), "seconds");
1656 assert_eq!(format!("{}", DurationUnit::Minute), "minutes");
1657 assert_eq!(format!("{}", DurationUnit::Hour), "hours");
1658 assert_eq!(format!("{}", DurationUnit::Day), "days");
1659 assert_eq!(format!("{}", DurationUnit::Week), "weeks");
1660 assert_eq!(format!("{}", DurationUnit::Millisecond), "milliseconds");
1661 assert_eq!(format!("{}", DurationUnit::Microsecond), "microseconds");
1662 }
1663
1664 #[test]
1665 fn test_conversion_target_display() {
1666 assert_eq!(
1667 format!("{}", ConversionTarget::Duration(DurationUnit::Hour)),
1668 "hours"
1669 );
1670 assert_eq!(
1671 format!("{}", ConversionTarget::Unit("usd".to_string())),
1672 "usd"
1673 );
1674 }
1675
1676 #[test]
1677 fn test_value_ratio_display() {
1678 use rust_decimal::Decimal;
1679 use std::str::FromStr;
1680 let percent = Value::Ratio(
1681 Decimal::from_str("0.10").unwrap(),
1682 Some("percent".to_string()),
1683 );
1684 assert_eq!(format!("{}", percent), "10%");
1685 let permille = Value::Ratio(
1686 Decimal::from_str("0.005").unwrap(),
1687 Some("permille".to_string()),
1688 );
1689 assert_eq!(format!("{}", permille), "5%%");
1690 }
1691
1692 #[test]
1693 fn test_datetime_value_display() {
1694 let dt = DateTimeValue {
1695 year: 2024,
1696 month: 12,
1697 day: 25,
1698 hour: 14,
1699 minute: 30,
1700 second: 45,
1701 microsecond: 0,
1702 timezone: Some(TimezoneValue {
1703 offset_hours: 1,
1704 offset_minutes: 0,
1705 }),
1706 };
1707 assert_eq!(format!("{}", dt), "2024-12-25T14:30:45+01:00");
1708 }
1709
1710 #[test]
1711 fn test_datetime_value_display_date_only() {
1712 let dt = DateTimeValue {
1713 year: 2026,
1714 month: 3,
1715 day: 4,
1716 hour: 0,
1717 minute: 0,
1718 second: 0,
1719 microsecond: 0,
1720 timezone: None,
1721 };
1722 assert_eq!(format!("{}", dt), "2026-03-04");
1723 }
1724
1725 #[test]
1726 fn test_datetime_value_display_microseconds() {
1727 let dt = DateTimeValue {
1728 year: 2026,
1729 month: 2,
1730 day: 23,
1731 hour: 14,
1732 minute: 30,
1733 second: 45,
1734 microsecond: 123456,
1735 timezone: Some(TimezoneValue {
1736 offset_hours: 0,
1737 offset_minutes: 0,
1738 }),
1739 };
1740 assert_eq!(format!("{}", dt), "2026-02-23T14:30:45.123456Z");
1741 }
1742
1743 #[test]
1744 fn test_datetime_microsecond_in_ordering() {
1745 let a = DateTimeValue {
1746 year: 2026,
1747 month: 1,
1748 day: 1,
1749 hour: 0,
1750 minute: 0,
1751 second: 0,
1752 microsecond: 100,
1753 timezone: None,
1754 };
1755 let b = DateTimeValue {
1756 year: 2026,
1757 month: 1,
1758 day: 1,
1759 hour: 0,
1760 minute: 0,
1761 second: 0,
1762 microsecond: 200,
1763 timezone: None,
1764 };
1765 assert!(a < b);
1766 }
1767
1768 #[test]
1769 fn test_datetime_parse_iso_week() {
1770 let dt = DateTimeValue::parse("2026-W01").unwrap();
1771 assert_eq!(dt.year, 2025);
1772 assert_eq!(dt.month, 12);
1773 assert_eq!(dt.day, 29);
1774 assert_eq!(dt.microsecond, 0);
1775 }
1776
1777 #[test]
1778 fn test_time_value_display() {
1779 let time = TimeValue {
1780 hour: 14,
1781 minute: 30,
1782 second: 45,
1783 timezone: Some(TimezoneValue {
1784 offset_hours: -5,
1785 offset_minutes: 30,
1786 }),
1787 };
1788 let display = format!("{}", time);
1789 assert!(display.contains("14"));
1790 assert!(display.contains("30"));
1791 assert!(display.contains("45"));
1792 }
1793
1794 #[test]
1795 fn test_timezone_value() {
1796 let tz_positive = TimezoneValue {
1797 offset_hours: 5,
1798 offset_minutes: 30,
1799 };
1800 assert_eq!(tz_positive.offset_hours, 5);
1801 assert_eq!(tz_positive.offset_minutes, 30);
1802
1803 let tz_negative = TimezoneValue {
1804 offset_hours: -8,
1805 offset_minutes: 0,
1806 };
1807 assert_eq!(tz_negative.offset_hours, -8);
1808 }
1809
1810 #[test]
1811 fn test_negation_types() {
1812 let json = serde_json::to_string(&NegationType::Not).expect("serialize NegationType");
1813 let decoded: NegationType = serde_json::from_str(&json).expect("deserialize NegationType");
1814 assert_eq!(decoded, NegationType::Not);
1815 }
1816
1817 #[test]
1818 fn test_veto_expression() {
1819 let veto_with_message = VetoExpression {
1820 message: Some("Must be over 18".to_string()),
1821 };
1822 assert_eq!(
1823 veto_with_message.message,
1824 Some("Must be over 18".to_string())
1825 );
1826
1827 let veto_without_message = VetoExpression { message: None };
1828 assert!(veto_without_message.message.is_none());
1829 }
1830
1831 #[test]
1840 fn as_lemma_source_text_default_is_quoted() {
1841 let fv = FactValue::TypeDeclaration {
1842 base: "text".to_string(),
1843 constraints: Some(vec![(
1844 "default".to_string(),
1845 vec![CommandArg::Text("single".to_string())],
1846 )]),
1847 from: None,
1848 };
1849 assert_eq!(
1850 format!("{}", AsLemmaSource(&fv)),
1851 "[text -> default \"single\"]"
1852 );
1853 }
1854
1855 #[test]
1856 fn as_lemma_source_number_default_not_quoted() {
1857 let fv = FactValue::TypeDeclaration {
1858 base: "number".to_string(),
1859 constraints: Some(vec![(
1860 "default".to_string(),
1861 vec![CommandArg::Number("10".to_string())],
1862 )]),
1863 from: None,
1864 };
1865 assert_eq!(format!("{}", AsLemmaSource(&fv)), "[number -> default 10]");
1866 }
1867
1868 #[test]
1869 fn as_lemma_source_help_always_quoted() {
1870 let fv = FactValue::TypeDeclaration {
1871 base: "number".to_string(),
1872 constraints: Some(vec![(
1873 "help".to_string(),
1874 vec![CommandArg::Text("Enter a quantity".to_string())],
1875 )]),
1876 from: None,
1877 };
1878 assert_eq!(
1879 format!("{}", AsLemmaSource(&fv)),
1880 "[number -> help \"Enter a quantity\"]"
1881 );
1882 }
1883
1884 #[test]
1885 fn as_lemma_source_text_option_quoted() {
1886 let fv = FactValue::TypeDeclaration {
1887 base: "text".to_string(),
1888 constraints: Some(vec![
1889 (
1890 "option".to_string(),
1891 vec![CommandArg::Text("active".to_string())],
1892 ),
1893 (
1894 "option".to_string(),
1895 vec![CommandArg::Text("inactive".to_string())],
1896 ),
1897 ]),
1898 from: None,
1899 };
1900 assert_eq!(
1901 format!("{}", AsLemmaSource(&fv)),
1902 "[text -> option \"active\" -> option \"inactive\"]"
1903 );
1904 }
1905
1906 #[test]
1907 fn as_lemma_source_scale_unit_not_quoted() {
1908 let fv = FactValue::TypeDeclaration {
1909 base: "scale".to_string(),
1910 constraints: Some(vec![
1911 (
1912 "unit".to_string(),
1913 vec![
1914 CommandArg::Label("eur".to_string()),
1915 CommandArg::Number("1.00".to_string()),
1916 ],
1917 ),
1918 (
1919 "unit".to_string(),
1920 vec![
1921 CommandArg::Label("usd".to_string()),
1922 CommandArg::Number("1.10".to_string()),
1923 ],
1924 ),
1925 ]),
1926 from: None,
1927 };
1928 assert_eq!(
1929 format!("{}", AsLemmaSource(&fv)),
1930 "[scale -> unit eur 1.00 -> unit usd 1.10]"
1931 );
1932 }
1933
1934 #[test]
1935 fn as_lemma_source_scale_minimum_with_unit() {
1936 let fv = FactValue::TypeDeclaration {
1937 base: "scale".to_string(),
1938 constraints: Some(vec![(
1939 "minimum".to_string(),
1940 vec![
1941 CommandArg::Number("0".to_string()),
1942 CommandArg::Label("eur".to_string()),
1943 ],
1944 )]),
1945 from: None,
1946 };
1947 assert_eq!(
1948 format!("{}", AsLemmaSource(&fv)),
1949 "[scale -> minimum 0 eur]"
1950 );
1951 }
1952
1953 #[test]
1954 fn as_lemma_source_boolean_default() {
1955 let fv = FactValue::TypeDeclaration {
1956 base: "boolean".to_string(),
1957 constraints: Some(vec![(
1958 "default".to_string(),
1959 vec![CommandArg::Boolean("true".to_string())],
1960 )]),
1961 from: None,
1962 };
1963 assert_eq!(
1964 format!("{}", AsLemmaSource(&fv)),
1965 "[boolean -> default true]"
1966 );
1967 }
1968
1969 #[test]
1970 fn as_lemma_source_duration_default() {
1971 let fv = FactValue::TypeDeclaration {
1972 base: "duration".to_string(),
1973 constraints: Some(vec![(
1974 "default".to_string(),
1975 vec![
1976 CommandArg::Number("40".to_string()),
1977 CommandArg::Label("hours".to_string()),
1978 ],
1979 )]),
1980 from: None,
1981 };
1982 assert_eq!(
1983 format!("{}", AsLemmaSource(&fv)),
1984 "[duration -> default 40 hours]"
1985 );
1986 }
1987
1988 #[test]
1989 fn as_lemma_source_named_type_default_quoted() {
1990 let fv = FactValue::TypeDeclaration {
1993 base: "filing_status_type".to_string(),
1994 constraints: Some(vec![(
1995 "default".to_string(),
1996 vec![CommandArg::Text("single".to_string())],
1997 )]),
1998 from: None,
1999 };
2000 assert_eq!(
2001 format!("{}", AsLemmaSource(&fv)),
2002 "[filing_status_type -> default \"single\"]"
2003 );
2004 }
2005
2006 #[test]
2007 fn as_lemma_source_help_escapes_quotes() {
2008 let fv = FactValue::TypeDeclaration {
2009 base: "text".to_string(),
2010 constraints: Some(vec![(
2011 "help".to_string(),
2012 vec![CommandArg::Text("say \"hello\"".to_string())],
2013 )]),
2014 from: None,
2015 };
2016 assert_eq!(
2017 format!("{}", AsLemmaSource(&fv)),
2018 "[text -> help \"say \\\"hello\\\"\"]"
2019 );
2020 }
2021
2022 #[test]
2023 fn as_lemma_source_typedef_regular_options_quoted() {
2024 let td = TypeDef::Regular {
2025 source_location: Source::new(
2026 "test",
2027 Span {
2028 start: 0,
2029 end: 0,
2030 line: 1,
2031 col: 0,
2032 },
2033 "test",
2034 std::sync::Arc::from("spec test\nfact x: 1"),
2035 ),
2036 name: "status".to_string(),
2037 parent: "text".to_string(),
2038 constraints: Some(vec![
2039 (
2040 "option".to_string(),
2041 vec![CommandArg::Text("active".to_string())],
2042 ),
2043 (
2044 "option".to_string(),
2045 vec![CommandArg::Text("inactive".to_string())],
2046 ),
2047 ]),
2048 };
2049 let output = format!("{}", AsLemmaSource(&td));
2050 assert!(output.contains("option \"active\""), "got: {}", output);
2051 assert!(output.contains("option \"inactive\""), "got: {}", output);
2052 }
2053
2054 #[test]
2055 fn as_lemma_source_typedef_scale_units_not_quoted() {
2056 let td = TypeDef::Regular {
2057 source_location: Source::new(
2058 "test",
2059 Span {
2060 start: 0,
2061 end: 0,
2062 line: 1,
2063 col: 0,
2064 },
2065 "test",
2066 std::sync::Arc::from("spec test\nfact x: 1"),
2067 ),
2068 name: "money".to_string(),
2069 parent: "scale".to_string(),
2070 constraints: Some(vec![
2071 (
2072 "unit".to_string(),
2073 vec![
2074 CommandArg::Label("eur".to_string()),
2075 CommandArg::Number("1.00".to_string()),
2076 ],
2077 ),
2078 (
2079 "decimals".to_string(),
2080 vec![CommandArg::Number("2".to_string())],
2081 ),
2082 (
2083 "minimum".to_string(),
2084 vec![CommandArg::Number("0".to_string())],
2085 ),
2086 ]),
2087 };
2088 let output = format!("{}", AsLemmaSource(&td));
2089 assert!(output.contains("unit eur 1.00"), "got: {}", output);
2090 assert!(output.contains("decimals 2"), "got: {}", output);
2091 assert!(output.contains("minimum 0"), "got: {}", output);
2092 }
2093}