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