1pub use crate::parsing::ast::{
9 ArithmeticComputation, ComparisonComputation, MathematicalComputation, NegationType, Span,
10 VetoExpression,
11};
12pub use crate::parsing::source::Source;
13
14#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
16#[serde(rename_all = "snake_case")]
17pub enum LogicalComputation {
18 And,
19 Or,
20 Not,
21}
22
23#[must_use]
25pub fn negated_comparison(op: ComparisonComputation) -> ComparisonComputation {
26 match op {
27 ComparisonComputation::LessThan => ComparisonComputation::GreaterThanOrEqual,
28 ComparisonComputation::LessThanOrEqual => ComparisonComputation::GreaterThan,
29 ComparisonComputation::GreaterThan => ComparisonComputation::LessThanOrEqual,
30 ComparisonComputation::GreaterThanOrEqual => ComparisonComputation::LessThan,
31 ComparisonComputation::Equal | ComparisonComputation::Is => ComparisonComputation::IsNot,
32 ComparisonComputation::NotEqual | ComparisonComputation::IsNot => ComparisonComputation::Is,
33 }
34}
35
36use crate::parsing::ast::{
38 BooleanValue, CalendarUnit, CommandArg, ConversionTarget, DateCalendarKind, DateRelativeKind,
39 DateTimeValue, DurationUnit, TimeValue,
40};
41use crate::parsing::literals::{parse_date_string, parse_duration_from_string, parse_time_string};
42use crate::Error;
43use rust_decimal::Decimal;
44use serde::{Deserialize, Serialize};
45use std::collections::HashMap;
46use std::fmt;
47use std::hash::Hash;
48use std::sync::{Arc, OnceLock};
49
50#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
55pub struct ScaleUnit {
56 pub name: String,
57 pub value: Decimal,
58}
59
60#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
61#[serde(transparent)]
62pub struct ScaleUnits(pub Vec<ScaleUnit>);
63
64impl ScaleUnits {
65 pub fn new() -> Self {
66 ScaleUnits(Vec::new())
67 }
68 pub fn get(&self, name: &str) -> Result<&ScaleUnit, String> {
69 self.0.iter().find(|u| u.name == name).ok_or_else(|| {
70 let valid: Vec<&str> = self.0.iter().map(|u| u.name.as_str()).collect();
71 format!(
72 "Unknown unit '{}' for this scale type. Valid units: {}",
73 name,
74 valid.join(", ")
75 )
76 })
77 }
78 pub fn iter(&self) -> std::slice::Iter<'_, ScaleUnit> {
79 self.0.iter()
80 }
81 pub fn push(&mut self, u: ScaleUnit) {
82 self.0.push(u);
83 }
84 pub fn is_empty(&self) -> bool {
85 self.0.is_empty()
86 }
87 pub fn len(&self) -> usize {
88 self.0.len()
89 }
90}
91
92impl Default for ScaleUnits {
93 fn default() -> Self {
94 ScaleUnits::new()
95 }
96}
97
98impl From<Vec<ScaleUnit>> for ScaleUnits {
99 fn from(v: Vec<ScaleUnit>) -> Self {
100 ScaleUnits(v)
101 }
102}
103
104impl<'a> IntoIterator for &'a ScaleUnits {
105 type Item = &'a ScaleUnit;
106 type IntoIter = std::slice::Iter<'a, ScaleUnit>;
107 fn into_iter(self) -> Self::IntoIter {
108 self.0.iter()
109 }
110}
111
112#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
113pub struct RatioUnit {
114 pub name: String,
115 pub value: Decimal,
116}
117
118#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
119#[serde(transparent)]
120pub struct RatioUnits(pub Vec<RatioUnit>);
121
122impl RatioUnits {
123 pub fn new() -> Self {
124 RatioUnits(Vec::new())
125 }
126 pub fn get(&self, name: &str) -> Result<&RatioUnit, String> {
127 self.0.iter().find(|u| u.name == name).ok_or_else(|| {
128 let valid: Vec<&str> = self.0.iter().map(|u| u.name.as_str()).collect();
129 format!(
130 "Unknown unit '{}' for this ratio type. Valid units: {}",
131 name,
132 valid.join(", ")
133 )
134 })
135 }
136 pub fn iter(&self) -> std::slice::Iter<'_, RatioUnit> {
137 self.0.iter()
138 }
139 pub fn push(&mut self, u: RatioUnit) {
140 self.0.push(u);
141 }
142 pub fn is_empty(&self) -> bool {
143 self.0.is_empty()
144 }
145 pub fn len(&self) -> usize {
146 self.0.len()
147 }
148}
149
150impl Default for RatioUnits {
151 fn default() -> Self {
152 RatioUnits::new()
153 }
154}
155
156impl From<Vec<RatioUnit>> for RatioUnits {
157 fn from(v: Vec<RatioUnit>) -> Self {
158 RatioUnits(v)
159 }
160}
161
162impl<'a> IntoIterator for &'a RatioUnits {
163 type Item = &'a RatioUnit;
164 type IntoIter = std::slice::Iter<'a, RatioUnit>;
165 fn into_iter(self) -> Self::IntoIter {
166 self.0.iter()
167 }
168}
169
170#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
171pub enum TypeSpecification {
172 Boolean {
173 help: String,
174 default: Option<bool>,
175 },
176 Scale {
177 minimum: Option<Decimal>,
178 maximum: Option<Decimal>,
179 decimals: Option<u8>,
180 precision: Option<Decimal>,
181 units: ScaleUnits,
182 help: String,
183 default: Option<(Decimal, String)>,
184 },
185 Number {
186 minimum: Option<Decimal>,
187 maximum: Option<Decimal>,
188 decimals: Option<u8>,
189 precision: Option<Decimal>,
190 help: String,
191 default: Option<Decimal>,
192 },
193 Ratio {
194 minimum: Option<Decimal>,
195 maximum: Option<Decimal>,
196 decimals: Option<u8>,
197 units: RatioUnits,
198 help: String,
199 default: Option<Decimal>,
200 },
201 Text {
202 minimum: Option<usize>,
203 maximum: Option<usize>,
204 length: Option<usize>,
205 options: Vec<String>,
206 help: String,
207 default: Option<String>,
208 },
209 Date {
210 minimum: Option<DateTimeValue>,
211 maximum: Option<DateTimeValue>,
212 help: String,
213 default: Option<DateTimeValue>,
214 },
215 Time {
216 minimum: Option<TimeValue>,
217 maximum: Option<TimeValue>,
218 help: String,
219 default: Option<TimeValue>,
220 },
221 Duration {
222 help: String,
223 default: Option<(Decimal, DurationUnit)>,
224 },
225 Veto {
226 message: Option<String>,
227 },
228 Undetermined,
232}
233
234impl TypeSpecification {
235 pub fn boolean() -> Self {
236 TypeSpecification::Boolean {
237 help: "Values: true, false".to_string(),
238 default: None,
239 }
240 }
241 pub fn scale() -> Self {
242 TypeSpecification::Scale {
243 minimum: None,
244 maximum: None,
245 decimals: None,
246 precision: None,
247 units: ScaleUnits::new(),
248 help: "Format: value+unit (e.g. 100+unit)".to_string(),
249 default: None,
250 }
251 }
252 pub fn number() -> Self {
253 TypeSpecification::Number {
254 minimum: None,
255 maximum: None,
256 decimals: None,
257 precision: None,
258 help: "Numeric value".to_string(),
259 default: None,
260 }
261 }
262 pub fn ratio() -> Self {
263 TypeSpecification::Ratio {
264 minimum: None,
265 maximum: None,
266 decimals: None,
267 units: RatioUnits(vec![
268 RatioUnit {
269 name: "percent".to_string(),
270 value: Decimal::from(100),
271 },
272 RatioUnit {
273 name: "permille".to_string(),
274 value: Decimal::from(1000),
275 },
276 ]),
277 help: "Format: value+unit (e.g. 21+percent)".to_string(),
278 default: None,
279 }
280 }
281 pub fn text() -> Self {
282 TypeSpecification::Text {
283 minimum: None,
284 maximum: None,
285 length: None,
286 options: vec![],
287 help: "Text value".to_string(),
288 default: None,
289 }
290 }
291 pub fn date() -> Self {
292 TypeSpecification::Date {
293 minimum: None,
294 maximum: None,
295 help: "Format: YYYY-MM-DD (e.g. 2024-01-15)".to_string(),
296 default: None,
297 }
298 }
299 pub fn time() -> Self {
300 TypeSpecification::Time {
301 minimum: None,
302 maximum: None,
303 help: "Format: HH:MM:SS (e.g. 14:30:00)".to_string(),
304 default: None,
305 }
306 }
307 pub fn duration() -> Self {
308 TypeSpecification::Duration {
309 help: "Format: value+unit (e.g. 40+hours). Units: years, months, weeks, days, hours, minutes, seconds".to_string(),
310 default: None,
311 }
312 }
313 pub fn veto() -> Self {
314 TypeSpecification::Veto { message: None }
315 }
316
317 pub fn apply_constraint(mut self, command: &str, args: &[CommandArg]) -> Result<Self, String> {
318 match &mut self {
319 TypeSpecification::Boolean { help, default } => match command {
320 "help" => {
321 *help = args
322 .first()
323 .map(|a| a.value().to_string())
324 .unwrap_or_default();
325 }
326 "default" => {
327 let arg = args
328 .first()
329 .ok_or_else(|| "default requires an argument".to_string())?;
330 match arg {
331 CommandArg::Boolean(s) | CommandArg::Label(s) => {
332 let d = s
333 .parse::<BooleanValue>()
334 .map_err(|_| format!("invalid default value: {:?}", s))?;
335 *default = Some(d.into());
336 }
337 other => {
338 return Err(format!(
339 "default for boolean type requires a boolean literal (true/false/yes/no/accept/reject), got {:?}",
340 other.value()
341 ));
342 }
343 }
344 }
345 _ => {
346 return Err(format!(
347 "Invalid command '{}' for boolean type. Valid commands: help, default",
348 command
349 ));
350 }
351 },
352 TypeSpecification::Scale {
353 decimals,
354 minimum,
355 maximum,
356 precision,
357 units,
358 help,
359 default,
360 } => match command {
361 "decimals" => {
362 let d = args
363 .first()
364 .ok_or_else(|| "decimals requires an argument".to_string())?
365 .value()
366 .parse::<u8>()
367 .map_err(|_| {
368 format!(
369 "invalid decimals value: {:?}",
370 args.first().map(|a| a.value())
371 )
372 })?;
373 *decimals = Some(d);
374 }
375 "unit" if args.len() >= 2 => {
376 let unit_name = args[0].value().to_string();
377 if units.iter().any(|u| u.name == unit_name) {
378 return Err(format!(
379 "Unit '{}' is already defined in this scale type.",
380 unit_name
381 ));
382 }
383 let value = args[1]
384 .value()
385 .parse::<Decimal>()
386 .map_err(|_| format!("invalid unit value: {}", args[1].value()))?;
387 units.0.push(ScaleUnit {
388 name: unit_name,
389 value,
390 });
391 }
392 "minimum" => {
393 let m = args
394 .first()
395 .ok_or_else(|| "minimum requires an argument".to_string())?
396 .value()
397 .parse::<Decimal>()
398 .map_err(|_| {
399 format!(
400 "invalid minimum value: {:?}",
401 args.first().map(|a| a.value())
402 )
403 })?;
404 *minimum = Some(m);
405 }
406 "maximum" => {
407 let m = args
408 .first()
409 .ok_or_else(|| "maximum requires an argument".to_string())?
410 .value()
411 .parse::<Decimal>()
412 .map_err(|_| {
413 format!(
414 "invalid maximum value: {:?}",
415 args.first().map(|a| a.value())
416 )
417 })?;
418 *maximum = Some(m);
419 }
420 "precision" => {
421 let p = args
422 .first()
423 .ok_or_else(|| "precision requires an argument".to_string())?
424 .value()
425 .parse::<Decimal>()
426 .map_err(|_| {
427 format!(
428 "invalid precision value: {:?}",
429 args.first().map(|a| a.value())
430 )
431 })?;
432 *precision = Some(p);
433 }
434 "help" => {
435 *help = args
436 .first()
437 .map(|a| a.value().to_string())
438 .unwrap_or_default();
439 }
440 "default" => {
441 if args.len() < 2 {
442 return Err(
443 "default requires a value and unit (e.g., 'default 1 kilogram')"
444 .to_string(),
445 );
446 }
447 match &args[0] {
448 CommandArg::Number(s) => {
449 let value = s
450 .parse::<Decimal>()
451 .map_err(|_| format!("invalid default value: {:?}", s))?;
452 let unit_name = args[1].value().to_string();
453 *default = Some((value, unit_name));
454 }
455 other => {
456 return Err(format!(
457 "default for scale type requires a number literal as value, got {:?}",
458 other.value()
459 ));
460 }
461 }
462 }
463 _ => {
464 return Err(format!(
465 "Invalid command '{}' for scale type. Valid commands: unit, minimum, maximum, decimals, precision, help, default",
466 command
467 ));
468 }
469 },
470 TypeSpecification::Number {
471 decimals,
472 minimum,
473 maximum,
474 precision,
475 help,
476 default,
477 } => match command {
478 "decimals" => {
479 let d = args
480 .first()
481 .ok_or_else(|| "decimals requires an argument".to_string())?
482 .value()
483 .parse::<u8>()
484 .map_err(|_| {
485 format!(
486 "invalid decimals value: {:?}",
487 args.first().map(|a| a.value())
488 )
489 })?;
490 *decimals = Some(d);
491 }
492 "unit" => {
493 return Err(
494 "Invalid command 'unit' for number type. Number types are dimensionless and cannot have units. Use 'scale' type instead.".to_string()
495 );
496 }
497 "minimum" => {
498 let m = args
499 .first()
500 .ok_or_else(|| "minimum requires an argument".to_string())?
501 .value()
502 .parse::<Decimal>()
503 .map_err(|_| {
504 format!(
505 "invalid minimum value: {:?}",
506 args.first().map(|a| a.value())
507 )
508 })?;
509 *minimum = Some(m);
510 }
511 "maximum" => {
512 let m = args
513 .first()
514 .ok_or_else(|| "maximum requires an argument".to_string())?
515 .value()
516 .parse::<Decimal>()
517 .map_err(|_| {
518 format!(
519 "invalid maximum value: {:?}",
520 args.first().map(|a| a.value())
521 )
522 })?;
523 *maximum = Some(m);
524 }
525 "precision" => {
526 let p = args
527 .first()
528 .ok_or_else(|| "precision requires an argument".to_string())?
529 .value()
530 .parse::<Decimal>()
531 .map_err(|_| {
532 format!(
533 "invalid precision value: {:?}",
534 args.first().map(|a| a.value())
535 )
536 })?;
537 *precision = Some(p);
538 }
539 "help" => {
540 *help = args
541 .first()
542 .map(|a| a.value().to_string())
543 .unwrap_or_default();
544 }
545 "default" => {
546 let arg = args
547 .first()
548 .ok_or_else(|| "default requires an argument".to_string())?;
549 match arg {
550 CommandArg::Number(s) => {
551 let d = s
552 .parse::<Decimal>()
553 .map_err(|_| format!("invalid default value: {:?}", s))?;
554 *default = Some(d);
555 }
556 other => {
557 return Err(format!(
558 "default for number type requires a number literal, got {:?}",
559 other.value()
560 ));
561 }
562 }
563 }
564 _ => {
565 return Err(format!(
566 "Invalid command '{}' for number type. Valid commands: minimum, maximum, decimals, precision, help, default",
567 command
568 ));
569 }
570 },
571 TypeSpecification::Ratio {
572 decimals,
573 minimum,
574 maximum,
575 units,
576 help,
577 default,
578 } => match command {
579 "decimals" => {
580 let d = args
581 .first()
582 .ok_or_else(|| "decimals requires an argument".to_string())?
583 .value()
584 .parse::<u8>()
585 .map_err(|_| {
586 format!(
587 "invalid decimals value: {:?}",
588 args.first().map(|a| a.value())
589 )
590 })?;
591 *decimals = Some(d);
592 }
593 "unit" if args.len() >= 2 => {
594 let unit_name = args[0].value().to_string();
595 if units.iter().any(|u| u.name == unit_name) {
596 return Err(format!(
597 "Unit '{}' is already defined in this ratio type.",
598 unit_name
599 ));
600 }
601 let value = args[1]
602 .value()
603 .parse::<Decimal>()
604 .map_err(|_| format!("invalid unit value: {}", args[1].value()))?;
605 units.0.push(RatioUnit {
606 name: unit_name,
607 value,
608 });
609 }
610 "minimum" => {
611 let m = args
612 .first()
613 .ok_or_else(|| "minimum requires an argument".to_string())?
614 .value()
615 .parse::<Decimal>()
616 .map_err(|_| {
617 format!(
618 "invalid minimum value: {:?}",
619 args.first().map(|a| a.value())
620 )
621 })?;
622 *minimum = Some(m);
623 }
624 "maximum" => {
625 let m = args
626 .first()
627 .ok_or_else(|| "maximum requires an argument".to_string())?
628 .value()
629 .parse::<Decimal>()
630 .map_err(|_| {
631 format!(
632 "invalid maximum value: {:?}",
633 args.first().map(|a| a.value())
634 )
635 })?;
636 *maximum = Some(m);
637 }
638 "help" => {
639 *help = args
640 .first()
641 .map(|a| a.value().to_string())
642 .unwrap_or_default();
643 }
644 "default" => {
645 let arg = args
646 .first()
647 .ok_or_else(|| "default requires an argument".to_string())?;
648 match arg {
649 CommandArg::Number(s) => {
650 let d = s
651 .parse::<Decimal>()
652 .map_err(|_| format!("invalid default value: {:?}", s))?;
653 *default = Some(d);
654 }
655 other => {
656 return Err(format!(
657 "default for ratio type requires a number literal, got {:?}",
658 other.value()
659 ));
660 }
661 }
662 }
663 _ => {
664 return Err(format!(
665 "Invalid command '{}' for ratio type. Valid commands: unit, minimum, maximum, decimals, help, default",
666 command
667 ));
668 }
669 },
670 TypeSpecification::Text {
671 minimum,
672 maximum,
673 length,
674 options,
675 help,
676 default,
677 } => match command {
678 "option" if args.len() == 1 => {
679 options.push(args[0].value().to_string());
680 }
681 "options" => {
682 *options = args.iter().map(|a| a.value().to_string()).collect();
683 }
684 "minimum" => {
685 let m = args
686 .first()
687 .ok_or_else(|| "minimum requires an argument".to_string())?
688 .value()
689 .parse::<usize>()
690 .map_err(|_| {
691 format!(
692 "invalid minimum value: {:?}",
693 args.first().map(|a| a.value())
694 )
695 })?;
696 *minimum = Some(m);
697 }
698 "maximum" => {
699 let m = args
700 .first()
701 .ok_or_else(|| "maximum requires an argument".to_string())?
702 .value()
703 .parse::<usize>()
704 .map_err(|_| {
705 format!(
706 "invalid maximum value: {:?}",
707 args.first().map(|a| a.value())
708 )
709 })?;
710 *maximum = Some(m);
711 }
712 "length" => {
713 let l = args
714 .first()
715 .ok_or_else(|| "length requires an argument".to_string())?
716 .value()
717 .parse::<usize>()
718 .map_err(|_| {
719 format!(
720 "invalid length value: {:?}",
721 args.first().map(|a| a.value())
722 )
723 })?;
724 *length = Some(l);
725 }
726 "help" => {
727 *help = args
728 .first()
729 .map(|a| a.value().to_string())
730 .unwrap_or_default();
731 }
732 "default" => {
733 let arg = args
734 .first()
735 .ok_or_else(|| "default requires an argument".to_string())?;
736 match arg {
737 CommandArg::Text(s) => {
738 *default = Some(s.clone());
739 }
740 other => {
741 return Err(format!(
742 "default for text type requires a text literal (quoted string), got {:?}",
743 other.value()
744 ));
745 }
746 }
747 }
748 _ => {
749 return Err(format!(
750 "Invalid command '{}' for text type. Valid commands: options, minimum, maximum, length, help, default",
751 command
752 ));
753 }
754 },
755 TypeSpecification::Date {
756 minimum,
757 maximum,
758 help,
759 default,
760 } => match command {
761 "minimum" => {
762 let arg = args
763 .first()
764 .ok_or_else(|| "minimum requires an argument".to_string())?;
765 *minimum = Some(parse_date_string(arg.value())?);
766 }
767 "maximum" => {
768 let arg = args
769 .first()
770 .ok_or_else(|| "maximum requires an argument".to_string())?;
771 *maximum = Some(parse_date_string(arg.value())?);
772 }
773 "help" => {
774 *help = args
775 .first()
776 .map(|a| a.value().to_string())
777 .unwrap_or_default();
778 }
779 "default" => {
780 let arg = args
781 .first()
782 .ok_or_else(|| "default requires an argument".to_string())?;
783 *default = Some(parse_date_string(arg.value())?);
784 }
785 _ => {
786 return Err(format!(
787 "Invalid command '{}' for date type. Valid commands: minimum, maximum, help, default",
788 command
789 ));
790 }
791 },
792 TypeSpecification::Time {
793 minimum,
794 maximum,
795 help,
796 default,
797 } => match command {
798 "minimum" => {
799 let arg = args
800 .first()
801 .ok_or_else(|| "minimum requires an argument".to_string())?;
802 *minimum = Some(parse_time_string(arg.value())?);
803 }
804 "maximum" => {
805 let arg = args
806 .first()
807 .ok_or_else(|| "maximum requires an argument".to_string())?;
808 *maximum = Some(parse_time_string(arg.value())?);
809 }
810 "help" => {
811 *help = args
812 .first()
813 .map(|a| a.value().to_string())
814 .unwrap_or_default();
815 }
816 "default" => {
817 let arg = args
818 .first()
819 .ok_or_else(|| "default requires an argument".to_string())?;
820 *default = Some(parse_time_string(arg.value())?);
821 }
822 _ => {
823 return Err(format!(
824 "Invalid command '{}' for time type. Valid commands: minimum, maximum, help, default",
825 command
826 ));
827 }
828 },
829 TypeSpecification::Duration { help, default } => match command {
830 "help" => {
831 *help = args
832 .first()
833 .map(|a| a.value().to_string())
834 .unwrap_or_default();
835 }
836 "default" if args.len() >= 2 => {
837 let value = args[0]
838 .value()
839 .parse::<Decimal>()
840 .map_err(|_| format!("invalid duration value: {}", args[0].value()))?;
841 let unit = args[1]
842 .value()
843 .parse::<DurationUnit>()
844 .map_err(|_| format!("invalid duration unit: {}", args[1].value()))?;
845 *default = Some((value, unit));
846 }
847 _ => {
848 return Err(format!(
849 "Invalid command '{}' for duration type. Valid commands: help, default",
850 command
851 ));
852 }
853 },
854 TypeSpecification::Veto { .. } => {
855 return Err(format!(
856 "Invalid command '{}' for veto type. Veto is not a user-declarable type and cannot have constraints",
857 command
858 ));
859 }
860 TypeSpecification::Undetermined => {
861 return Err(format!(
862 "Invalid command '{}' for undetermined sentinel type. Undetermined is an internal type used during type inference and cannot have constraints",
863 command
864 ));
865 }
866 }
867 Ok(self)
868 }
869}
870
871pub fn parse_number_unit(
874 value_str: &str,
875 type_spec: &TypeSpecification,
876) -> Result<crate::parsing::ast::Value, String> {
877 use crate::parsing::ast::Value;
878 use crate::parsing::literals::parse_number_unit_string;
879 use std::str::FromStr;
880
881 let trimmed = value_str.trim();
882 match type_spec {
883 TypeSpecification::Scale { units, .. } => {
884 if units.is_empty() {
885 unreachable!(
886 "BUG: Scale type has no units; should have been validated during planning"
887 );
888 }
889 match parse_number_unit_string(trimmed) {
890 Ok((n, unit_name)) => {
891 let unit = units.get(&unit_name).map_err(|e| e.to_string())?;
892 Ok(Value::Scale(n, unit.name.clone()))
893 }
894 Err(e) => {
895 if trimmed.split_whitespace().count() == 1 && !trimmed.is_empty() {
896 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
897 let example_unit = units.iter().next().unwrap().name.as_str();
898 Err(format!(
899 "Scale value must include a unit, for example: '{} {}'. Valid units: {}.",
900 trimmed,
901 example_unit,
902 valid.join(", ")
903 ))
904 } else {
905 Err(e)
906 }
907 }
908 }
909 }
910 TypeSpecification::Ratio { units, .. } => {
911 if units.is_empty() {
912 unreachable!(
913 "BUG: Ratio type has no units; should have been validated during planning"
914 );
915 }
916 match parse_number_unit_string(trimmed) {
917 Ok((n, unit_name)) => {
918 let unit = units.get(&unit_name).map_err(|e| e.to_string())?;
919 Ok(Value::Ratio(n / unit.value, Some(unit.name.clone())))
920 }
921 Err(_) => {
922 if trimmed.split_whitespace().count() == 1 && !trimmed.is_empty() {
923 let clean = trimmed.replace(['_', ','], "");
924 let digit_count = clean.chars().filter(|c| c.is_ascii_digit()).count();
925 if digit_count > crate::limits::MAX_NUMBER_DIGITS {
926 return Err(format!(
927 "Number has too many digits (max {})",
928 crate::limits::MAX_NUMBER_DIGITS
929 ));
930 }
931 let n = Decimal::from_str(&clean)
932 .map_err(|_| format!("Invalid ratio: '{}'", trimmed))?;
933 Ok(Value::Ratio(n, None))
934 } else {
935 Err("Ratio value must be a number, optionally followed by a unit (e.g. '0.5' or '50 percent').".to_string())
936 }
937 }
938 }
939 }
940 _ => Err("parse_number_unit only accepts Scale or Ratio type".to_string()),
941 }
942}
943
944pub fn parse_value_from_string(
947 value_str: &str,
948 type_spec: &TypeSpecification,
949 source: &Source,
950) -> Result<crate::parsing::ast::Value, Error> {
951 use crate::parsing::ast::{BooleanValue, Value};
952 use std::str::FromStr;
953
954 let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
955
956 match type_spec {
957 TypeSpecification::Text { .. } => Ok(Value::Text(value_str.to_string())),
958 TypeSpecification::Number { .. } => {
959 let clean = value_str.replace(['_', ','], "");
960 let digit_count = clean.chars().filter(|c| c.is_ascii_digit()).count();
961 if digit_count > crate::limits::MAX_NUMBER_DIGITS {
962 return Err(to_err(format!(
963 "Number has too many digits (max {})",
964 crate::limits::MAX_NUMBER_DIGITS
965 )));
966 }
967 let n = Decimal::from_str(&clean).map_err(|_| to_err(format!("Invalid number: '{}'", value_str)))?;
968 Ok(Value::Number(n))
969 }
970 TypeSpecification::Scale { .. } => {
971 parse_number_unit(value_str, type_spec).map_err(to_err)
972 }
973 TypeSpecification::Boolean { .. } => {
974 let bv = match value_str.to_lowercase().as_str() {
975 "true" => BooleanValue::True,
976 "false" => BooleanValue::False,
977 "yes" => BooleanValue::Yes,
978 "no" => BooleanValue::No,
979 "accept" => BooleanValue::Accept,
980 "reject" => BooleanValue::Reject,
981 _ => return Err(to_err(format!("Invalid boolean: '{}'", value_str))),
982 };
983 Ok(Value::Boolean(bv))
984 }
985 TypeSpecification::Date { .. } => {
986 let date = parse_date_string(value_str).map_err(to_err)?;
987 Ok(Value::Date(date))
988 }
989 TypeSpecification::Time { .. } => {
990 let time = parse_time_string(value_str).map_err(to_err)?;
991 Ok(Value::Time(time))
992 }
993 TypeSpecification::Duration { .. } => {
994 parse_duration_from_string(value_str, source)
995 }
996 TypeSpecification::Ratio { .. } => {
997 parse_number_unit(value_str, type_spec).map_err(to_err)
998 }
999 TypeSpecification::Veto { .. } => Err(to_err(
1000 "Veto type cannot be parsed from string".to_string(),
1001 )),
1002 TypeSpecification::Undetermined => unreachable!(
1003 "BUG: parse_value_from_string called with Undetermined sentinel type; this type exists only during type inference"
1004 ),
1005 }
1006}
1007
1008#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1014#[serde(rename_all = "snake_case")]
1015pub enum SemanticDurationUnit {
1016 Year,
1017 Month,
1018 Week,
1019 Day,
1020 Hour,
1021 Minute,
1022 Second,
1023 Millisecond,
1024 Microsecond,
1025}
1026
1027impl fmt::Display for SemanticDurationUnit {
1028 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1029 let s = match self {
1030 SemanticDurationUnit::Year => "years",
1031 SemanticDurationUnit::Month => "months",
1032 SemanticDurationUnit::Week => "weeks",
1033 SemanticDurationUnit::Day => "days",
1034 SemanticDurationUnit::Hour => "hours",
1035 SemanticDurationUnit::Minute => "minutes",
1036 SemanticDurationUnit::Second => "seconds",
1037 SemanticDurationUnit::Millisecond => "milliseconds",
1038 SemanticDurationUnit::Microsecond => "microseconds",
1039 };
1040 write!(f, "{}", s)
1041 }
1042}
1043
1044#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1048#[serde(rename_all = "snake_case")]
1049pub enum SemanticConversionTarget {
1050 Duration(SemanticDurationUnit),
1051 ScaleUnit(String),
1052 RatioUnit(String),
1053}
1054
1055impl fmt::Display for SemanticConversionTarget {
1056 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1057 match self {
1058 SemanticConversionTarget::Duration(u) => write!(f, "{}", u),
1059 SemanticConversionTarget::ScaleUnit(s) => write!(f, "{}", s),
1060 SemanticConversionTarget::RatioUnit(s) => write!(f, "{}", s),
1061 }
1062 }
1063}
1064
1065#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1067pub struct SemanticTimezone {
1068 pub offset_hours: i8,
1069 pub offset_minutes: u8,
1070}
1071
1072impl fmt::Display for SemanticTimezone {
1073 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1074 if self.offset_hours == 0 && self.offset_minutes == 0 {
1075 write!(f, "Z")
1076 } else {
1077 let sign = if self.offset_hours >= 0 { "+" } else { "-" };
1078 let hours = self.offset_hours.abs();
1079 write!(f, "{}{:02}:{:02}", sign, hours, self.offset_minutes)
1080 }
1081 }
1082}
1083
1084#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1086pub struct SemanticTime {
1087 pub hour: u32,
1088 pub minute: u32,
1089 pub second: u32,
1090 pub timezone: Option<SemanticTimezone>,
1091}
1092
1093impl fmt::Display for SemanticTime {
1094 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1095 write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)
1096 }
1097}
1098
1099#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1101pub struct SemanticDateTime {
1102 pub year: i32,
1103 pub month: u32,
1104 pub day: u32,
1105 pub hour: u32,
1106 pub minute: u32,
1107 pub second: u32,
1108 #[serde(default)]
1109 pub microsecond: u32,
1110 pub timezone: Option<SemanticTimezone>,
1111}
1112
1113impl fmt::Display for SemanticDateTime {
1114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1115 let has_time = self.hour != 0
1116 || self.minute != 0
1117 || self.second != 0
1118 || self.microsecond != 0
1119 || self.timezone.is_some();
1120 if !has_time {
1121 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
1122 } else {
1123 write!(
1124 f,
1125 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
1126 self.year, self.month, self.day, self.hour, self.minute, self.second
1127 )?;
1128 if self.microsecond != 0 {
1129 write!(f, ".{:06}", self.microsecond)?;
1130 }
1131 if let Some(tz) = &self.timezone {
1132 write!(f, "{}", tz)?;
1133 }
1134 Ok(())
1135 }
1136 }
1137}
1138
1139#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1142#[serde(rename_all = "snake_case")]
1143pub enum ValueKind {
1144 Number(Decimal),
1145 Scale(Decimal, String),
1147 Text(String),
1148 Date(SemanticDateTime),
1149 Time(SemanticTime),
1150 Boolean(bool),
1151 Duration(Decimal, SemanticDurationUnit),
1153 Ratio(Decimal, Option<String>),
1155}
1156
1157impl fmt::Display for ValueKind {
1158 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1159 use crate::parsing::ast::Value;
1160 match self {
1161 ValueKind::Number(n) => {
1162 let norm = n.normalize();
1163 let s = if norm.fract().is_zero() {
1164 norm.trunc().to_string()
1165 } else {
1166 norm.to_string()
1167 };
1168 write!(f, "{}", s)
1169 }
1170 ValueKind::Scale(n, u) => write!(f, "{}", Value::Scale(*n, u.clone())),
1171 ValueKind::Text(s) => write!(f, "{}", Value::Text(s.clone())),
1172 ValueKind::Ratio(r, u) => write!(f, "{}", Value::Ratio(*r, u.clone())),
1173 ValueKind::Date(dt) => write!(f, "{}", dt),
1174 ValueKind::Time(t) => write!(
1175 f,
1176 "{}",
1177 Value::Time(crate::parsing::ast::TimeValue {
1178 hour: t.hour as u8,
1179 minute: t.minute as u8,
1180 second: t.second as u8,
1181 timezone: t
1182 .timezone
1183 .as_ref()
1184 .map(|tz| crate::parsing::ast::TimezoneValue {
1185 offset_hours: tz.offset_hours,
1186 offset_minutes: tz.offset_minutes,
1187 }),
1188 })
1189 ),
1190 ValueKind::Boolean(b) => write!(f, "{}", b),
1191 ValueKind::Duration(v, u) => write!(f, "{} {}", v, u),
1192 }
1193 }
1194}
1195
1196#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1205pub struct PathSegment {
1206 pub fact: String,
1208 pub spec: String,
1210}
1211
1212#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1217pub struct FactPath {
1218 pub segments: Vec<PathSegment>,
1220 pub fact: String,
1222}
1223
1224impl FactPath {
1225 pub fn new(segments: Vec<PathSegment>, fact: String) -> Self {
1227 Self { segments, fact }
1228 }
1229
1230 pub fn local(fact: String) -> Self {
1232 Self {
1233 segments: vec![],
1234 fact,
1235 }
1236 }
1237
1238 pub fn input_key(&self) -> String {
1241 let mut s = String::new();
1242 for segment in &self.segments {
1243 s.push_str(&segment.fact);
1244 s.push('.');
1245 }
1246 s.push_str(&self.fact);
1247 s
1248 }
1249}
1250
1251#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1255pub struct RulePath {
1256 pub segments: Vec<PathSegment>,
1258 pub rule: String,
1260}
1261
1262impl RulePath {
1263 pub fn new(segments: Vec<PathSegment>, rule: String) -> Self {
1265 Self { segments, rule }
1266 }
1267}
1268
1269#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1278pub struct Expression {
1279 pub kind: ExpressionKind,
1280 pub source_location: Option<Source>,
1281}
1282
1283impl Expression {
1284 pub fn new(kind: ExpressionKind, source_location: Source) -> Self {
1285 Self {
1286 kind,
1287 source_location: Some(source_location),
1288 }
1289 }
1290
1291 pub fn with_source(kind: ExpressionKind, source_location: Option<Source>) -> Self {
1293 Self {
1294 kind,
1295 source_location,
1296 }
1297 }
1298
1299 pub fn get_source_text(&self, sources: &HashMap<String, String>) -> Option<String> {
1301 let source = self.source_location.as_ref()?;
1302 let file_source = sources.get(&source.attribute)?;
1303 let span = &source.span;
1304 Some(file_source.get(span.start..span.end)?.to_string())
1305 }
1306
1307 pub fn collect_fact_paths(&self, facts: &mut std::collections::HashSet<FactPath>) {
1309 self.kind.collect_fact_paths(facts);
1310 }
1311}
1312
1313#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1315#[serde(rename_all = "snake_case")]
1316pub enum ExpressionKind {
1317 Literal(Box<LiteralValue>),
1319 FactPath(FactPath),
1321 RulePath(RulePath),
1323 LogicalAnd(Arc<Expression>, Arc<Expression>),
1324 Arithmetic(Arc<Expression>, ArithmeticComputation, Arc<Expression>),
1325 Comparison(Arc<Expression>, ComparisonComputation, Arc<Expression>),
1326 UnitConversion(Arc<Expression>, SemanticConversionTarget),
1327 LogicalNegation(Arc<Expression>, NegationType),
1328 MathematicalComputation(MathematicalComputation, Arc<Expression>),
1329 Veto(VetoExpression),
1330 Now,
1332 DateRelative(DateRelativeKind, Arc<Expression>, Option<Arc<Expression>>),
1334 DateCalendar(DateCalendarKind, CalendarUnit, Arc<Expression>),
1336}
1337
1338impl ExpressionKind {
1339 fn collect_fact_paths(&self, facts: &mut std::collections::HashSet<FactPath>) {
1341 match self {
1342 ExpressionKind::FactPath(fp) => {
1343 facts.insert(fp.clone());
1344 }
1345 ExpressionKind::LogicalAnd(left, right)
1346 | ExpressionKind::Arithmetic(left, _, right)
1347 | ExpressionKind::Comparison(left, _, right) => {
1348 left.collect_fact_paths(facts);
1349 right.collect_fact_paths(facts);
1350 }
1351 ExpressionKind::UnitConversion(inner, _)
1352 | ExpressionKind::LogicalNegation(inner, _)
1353 | ExpressionKind::MathematicalComputation(_, inner) => {
1354 inner.collect_fact_paths(facts);
1355 }
1356 ExpressionKind::DateRelative(_, date_expr, tolerance) => {
1357 date_expr.collect_fact_paths(facts);
1358 if let Some(tol) = tolerance {
1359 tol.collect_fact_paths(facts);
1360 }
1361 }
1362 ExpressionKind::DateCalendar(_, _, date_expr) => {
1363 date_expr.collect_fact_paths(facts);
1364 }
1365 ExpressionKind::Literal(_)
1366 | ExpressionKind::RulePath(_)
1367 | ExpressionKind::Veto(_)
1368 | ExpressionKind::Now => {}
1369 }
1370 }
1371}
1372
1373#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
1379pub enum TypeExtends {
1380 Primitive,
1382 Custom { parent: String, family: String },
1384}
1385
1386impl TypeExtends {
1387 #[must_use]
1389 pub fn parent_name(&self) -> Option<&str> {
1390 match self {
1391 TypeExtends::Primitive => None,
1392 TypeExtends::Custom { parent, .. } => Some(parent.as_str()),
1393 }
1394 }
1395}
1396
1397#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
1402pub struct LemmaType {
1403 pub name: Option<String>,
1405 pub specifications: TypeSpecification,
1407 pub extends: TypeExtends,
1409}
1410
1411impl LemmaType {
1412 pub fn new(name: String, specifications: TypeSpecification, extends: TypeExtends) -> Self {
1414 Self {
1415 name: Some(name),
1416 specifications,
1417 extends,
1418 }
1419 }
1420
1421 pub fn without_name(specifications: TypeSpecification, extends: TypeExtends) -> Self {
1423 Self {
1424 name: None,
1425 specifications,
1426 extends,
1427 }
1428 }
1429
1430 pub fn primitive(specifications: TypeSpecification) -> Self {
1432 Self {
1433 name: None,
1434 specifications,
1435 extends: TypeExtends::Primitive,
1436 }
1437 }
1438
1439 pub fn name(&self) -> String {
1441 self.name.clone().unwrap_or_else(|| {
1442 match &self.specifications {
1443 TypeSpecification::Boolean { .. } => "boolean",
1444 TypeSpecification::Scale { .. } => "scale",
1445 TypeSpecification::Number { .. } => "number",
1446 TypeSpecification::Text { .. } => "text",
1447 TypeSpecification::Date { .. } => "date",
1448 TypeSpecification::Time { .. } => "time",
1449 TypeSpecification::Duration { .. } => "duration",
1450 TypeSpecification::Ratio { .. } => "ratio",
1451 TypeSpecification::Veto { .. } => "veto",
1452 TypeSpecification::Undetermined => "undetermined",
1453 }
1454 .to_string()
1455 })
1456 }
1457
1458 pub fn is_boolean(&self) -> bool {
1460 matches!(&self.specifications, TypeSpecification::Boolean { .. })
1461 }
1462
1463 pub fn is_scale(&self) -> bool {
1465 matches!(&self.specifications, TypeSpecification::Scale { .. })
1466 }
1467
1468 pub fn is_number(&self) -> bool {
1470 matches!(&self.specifications, TypeSpecification::Number { .. })
1471 }
1472
1473 pub fn is_numeric(&self) -> bool {
1475 matches!(
1476 &self.specifications,
1477 TypeSpecification::Scale { .. } | TypeSpecification::Number { .. }
1478 )
1479 }
1480
1481 pub fn is_text(&self) -> bool {
1483 matches!(&self.specifications, TypeSpecification::Text { .. })
1484 }
1485
1486 pub fn is_date(&self) -> bool {
1488 matches!(&self.specifications, TypeSpecification::Date { .. })
1489 }
1490
1491 pub fn is_time(&self) -> bool {
1493 matches!(&self.specifications, TypeSpecification::Time { .. })
1494 }
1495
1496 pub fn is_duration(&self) -> bool {
1498 matches!(&self.specifications, TypeSpecification::Duration { .. })
1499 }
1500
1501 pub fn is_ratio(&self) -> bool {
1503 matches!(&self.specifications, TypeSpecification::Ratio { .. })
1504 }
1505
1506 pub fn vetoed(&self) -> bool {
1508 matches!(&self.specifications, TypeSpecification::Veto { .. })
1509 }
1510
1511 pub fn is_undetermined(&self) -> bool {
1513 matches!(&self.specifications, TypeSpecification::Undetermined)
1514 }
1515
1516 pub fn has_same_base_type(&self, other: &LemmaType) -> bool {
1518 use TypeSpecification::*;
1519 matches!(
1520 (&self.specifications, &other.specifications),
1521 (Boolean { .. }, Boolean { .. })
1522 | (Number { .. }, Number { .. })
1523 | (Scale { .. }, Scale { .. })
1524 | (Text { .. }, Text { .. })
1525 | (Date { .. }, Date { .. })
1526 | (Time { .. }, Time { .. })
1527 | (Duration { .. }, Duration { .. })
1528 | (Ratio { .. }, Ratio { .. })
1529 | (Veto { .. }, Veto { .. })
1530 | (Undetermined, Undetermined)
1531 )
1532 }
1533
1534 #[must_use]
1536 pub fn scale_family_name(&self) -> Option<&str> {
1537 if !self.is_scale() {
1538 return None;
1539 }
1540 match &self.extends {
1541 TypeExtends::Custom { family, .. } => Some(family.as_str()),
1542 TypeExtends::Primitive => self.name.as_deref(),
1543 }
1544 }
1545
1546 #[must_use]
1549 pub fn same_scale_family(&self, other: &LemmaType) -> bool {
1550 if !self.is_scale() || !other.is_scale() {
1551 return false;
1552 }
1553 match (self.scale_family_name(), other.scale_family_name()) {
1554 (Some(self_family), Some(other_family)) => self_family == other_family,
1555 (None, None) => true,
1557 _ => false,
1558 }
1559 }
1560
1561 pub fn create_default_value(&self) -> Option<LiteralValue> {
1563 let value = match &self.specifications {
1564 TypeSpecification::Text { default, .. } => default.clone().map(ValueKind::Text),
1565 TypeSpecification::Number { default, .. } => (*default).map(ValueKind::Number),
1566 TypeSpecification::Scale { default, .. } => {
1567 default.clone().map(|(d, u)| ValueKind::Scale(d, u))
1568 }
1569 TypeSpecification::Boolean { default, .. } => (*default).map(ValueKind::Boolean),
1570 TypeSpecification::Date { default, .. } => default
1571 .clone()
1572 .map(|dt| ValueKind::Date(date_time_to_semantic(&dt))),
1573 TypeSpecification::Time { default, .. } => default
1574 .clone()
1575 .map(|t| ValueKind::Time(time_to_semantic(&t))),
1576 TypeSpecification::Duration { default, .. } => default
1577 .clone()
1578 .map(|(v, u)| ValueKind::Duration(v, duration_unit_to_semantic(&u))),
1579 TypeSpecification::Ratio { .. } => None, TypeSpecification::Veto { .. } => None,
1581 TypeSpecification::Undetermined => None,
1582 };
1583
1584 value.map(|v| LiteralValue {
1585 value: v,
1586 lemma_type: self.clone(),
1587 })
1588 }
1589
1590 pub fn veto_type() -> Self {
1592 Self::primitive(TypeSpecification::veto())
1593 }
1594
1595 pub fn undetermined_type() -> Self {
1598 Self::primitive(TypeSpecification::Undetermined)
1599 }
1600
1601 pub fn decimal_places(&self) -> Option<u8> {
1604 match &self.specifications {
1605 TypeSpecification::Number { decimals, .. } => *decimals,
1606 TypeSpecification::Scale { decimals, .. } => *decimals,
1607 TypeSpecification::Ratio { decimals, .. } => *decimals,
1608 _ => None,
1609 }
1610 }
1611
1612 pub fn example_value(&self) -> &'static str {
1614 match &self.specifications {
1615 TypeSpecification::Text { .. } => "\"hello world\"",
1616 TypeSpecification::Scale { .. } => "12.50 eur",
1617 TypeSpecification::Number { .. } => "3.14",
1618 TypeSpecification::Boolean { .. } => "true",
1619 TypeSpecification::Date { .. } => "2023-12-25T14:30:00Z",
1620 TypeSpecification::Veto { .. } => "veto",
1621 TypeSpecification::Time { .. } => "14:30:00",
1622 TypeSpecification::Duration { .. } => "90 minutes",
1623 TypeSpecification::Ratio { .. } => "50%",
1624 TypeSpecification::Undetermined => unreachable!(
1625 "BUG: example_value called on Undetermined sentinel type; this type must never reach user-facing code"
1626 ),
1627 }
1628 }
1629
1630 #[must_use]
1634 pub fn scale_unit_factor(&self, unit_name: &str) -> Decimal {
1635 let units = match &self.specifications {
1636 TypeSpecification::Scale { units, .. } => units,
1637 _ => unreachable!(
1638 "BUG: scale_unit_factor called with non-scale type {}; only call during evaluation after planning validated scale conversion",
1639 self.name()
1640 ),
1641 };
1642 match units
1643 .iter()
1644 .find(|u| u.name.eq_ignore_ascii_case(unit_name))
1645 {
1646 Some(ScaleUnit { value, .. }) => *value,
1647 None => {
1648 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
1649 unreachable!(
1650 "BUG: unknown unit '{}' for scale type {} (valid: {}); planning must reject invalid conversions with Error",
1651 unit_name,
1652 self.name(),
1653 valid.join(", ")
1654 );
1655 }
1656 }
1657 }
1658}
1659
1660#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize)]
1662pub struct LiteralValue {
1663 pub value: ValueKind,
1664 pub lemma_type: LemmaType,
1665}
1666
1667impl Serialize for LiteralValue {
1668 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1669 where
1670 S: serde::Serializer,
1671 {
1672 use serde::ser::SerializeStruct;
1673 let mut state = serializer.serialize_struct("LiteralValue", 3)?;
1674 state.serialize_field("value", &self.value)?;
1675 state.serialize_field("lemma_type", &self.lemma_type)?;
1676 state.serialize_field("display_value", &self.display_value())?;
1677 state.end()
1678 }
1679}
1680
1681impl LiteralValue {
1682 pub fn text(s: String) -> Self {
1683 Self {
1684 value: ValueKind::Text(s),
1685 lemma_type: primitive_text().clone(),
1686 }
1687 }
1688
1689 pub fn text_with_type(s: String, lemma_type: LemmaType) -> Self {
1690 Self {
1691 value: ValueKind::Text(s),
1692 lemma_type,
1693 }
1694 }
1695
1696 pub fn number(n: Decimal) -> Self {
1697 Self {
1698 value: ValueKind::Number(n),
1699 lemma_type: primitive_number().clone(),
1700 }
1701 }
1702
1703 pub fn number_with_type(n: Decimal, lemma_type: LemmaType) -> Self {
1704 Self {
1705 value: ValueKind::Number(n),
1706 lemma_type,
1707 }
1708 }
1709
1710 pub fn scale_with_type(n: Decimal, unit: String, lemma_type: LemmaType) -> Self {
1711 Self {
1712 value: ValueKind::Scale(n, unit),
1713 lemma_type,
1714 }
1715 }
1716
1717 pub fn number_interpreted_as_scale(value: Decimal, unit_name: String) -> Self {
1720 let lemma_type = LemmaType {
1721 name: None,
1722 specifications: TypeSpecification::Scale {
1723 minimum: None,
1724 maximum: None,
1725 decimals: None,
1726 precision: None,
1727 units: ScaleUnits::from(vec![ScaleUnit {
1728 name: unit_name.clone(),
1729 value: Decimal::from(1),
1730 }]),
1731 help: "Format: value+unit (e.g. 100+unit)".to_string(),
1732 default: None,
1733 },
1734 extends: TypeExtends::Primitive,
1735 };
1736 Self {
1737 value: ValueKind::Scale(value, unit_name),
1738 lemma_type,
1739 }
1740 }
1741
1742 pub fn from_bool(b: bool) -> Self {
1743 Self {
1744 value: ValueKind::Boolean(b),
1745 lemma_type: primitive_boolean().clone(),
1746 }
1747 }
1748
1749 pub fn date(dt: SemanticDateTime) -> Self {
1750 Self {
1751 value: ValueKind::Date(dt),
1752 lemma_type: primitive_date().clone(),
1753 }
1754 }
1755
1756 pub fn date_with_type(dt: SemanticDateTime, lemma_type: LemmaType) -> Self {
1757 Self {
1758 value: ValueKind::Date(dt),
1759 lemma_type,
1760 }
1761 }
1762
1763 pub fn time(t: SemanticTime) -> Self {
1764 Self {
1765 value: ValueKind::Time(t),
1766 lemma_type: primitive_time().clone(),
1767 }
1768 }
1769
1770 pub fn time_with_type(t: SemanticTime, lemma_type: LemmaType) -> Self {
1771 Self {
1772 value: ValueKind::Time(t),
1773 lemma_type,
1774 }
1775 }
1776
1777 pub fn duration(value: Decimal, unit: SemanticDurationUnit) -> Self {
1778 Self {
1779 value: ValueKind::Duration(value, unit),
1780 lemma_type: primitive_duration().clone(),
1781 }
1782 }
1783
1784 pub fn duration_with_type(
1785 value: Decimal,
1786 unit: SemanticDurationUnit,
1787 lemma_type: LemmaType,
1788 ) -> Self {
1789 Self {
1790 value: ValueKind::Duration(value, unit),
1791 lemma_type,
1792 }
1793 }
1794
1795 pub fn ratio(r: Decimal, unit: Option<String>) -> Self {
1796 Self {
1797 value: ValueKind::Ratio(r, unit),
1798 lemma_type: primitive_ratio().clone(),
1799 }
1800 }
1801
1802 pub fn ratio_with_type(r: Decimal, unit: Option<String>, lemma_type: LemmaType) -> Self {
1803 Self {
1804 value: ValueKind::Ratio(r, unit),
1805 lemma_type,
1806 }
1807 }
1808
1809 pub fn display_value(&self) -> String {
1811 format!("{}", self)
1812 }
1813
1814 pub fn byte_size(&self) -> usize {
1816 format!("{}", self).len()
1817 }
1818
1819 pub fn get_type(&self) -> &LemmaType {
1821 &self.lemma_type
1822 }
1823}
1824
1825#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
1827pub enum FactValue {
1828 Literal(LiteralValue),
1829 TypeDeclaration { resolved_type: LemmaType },
1830 SpecReference(String),
1831}
1832
1833#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
1835pub struct Fact {
1836 pub path: FactPath,
1837 pub value: FactValue,
1838 pub source: Option<Source>,
1839}
1840
1841#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
1843pub enum FactData {
1844 Value {
1848 value: LiteralValue,
1849 source: Source,
1850 is_default: bool,
1851 },
1852 TypeDeclaration {
1854 resolved_type: LemmaType,
1855 source: Source,
1856 },
1857 SpecRef {
1860 spec: Arc<crate::parsing::ast::LemmaSpec>,
1861 source: Source,
1862 expected_hash_pin: Option<String>,
1863 },
1864}
1865
1866impl FactData {
1867 pub fn schema_type(&self) -> Option<&LemmaType> {
1869 match self {
1870 FactData::Value { value, .. } => Some(&value.lemma_type),
1871 FactData::TypeDeclaration { resolved_type, .. } => Some(resolved_type),
1872 FactData::SpecRef { .. } => None,
1873 }
1874 }
1875
1876 pub fn value(&self) -> Option<&LiteralValue> {
1878 match self {
1879 FactData::Value { value, .. } => Some(value),
1880 FactData::TypeDeclaration { .. } | FactData::SpecRef { .. } => None,
1881 }
1882 }
1883
1884 pub fn explicit_value(&self) -> Option<&LiteralValue> {
1888 match self {
1889 FactData::Value {
1890 value, is_default, ..
1891 } => {
1892 if *is_default {
1893 None
1894 } else {
1895 Some(value)
1896 }
1897 }
1898 FactData::TypeDeclaration { .. } | FactData::SpecRef { .. } => None,
1899 }
1900 }
1901
1902 pub fn source(&self) -> &Source {
1904 match self {
1905 FactData::Value { source, .. } => source,
1906 FactData::TypeDeclaration { source, .. } => source,
1907 FactData::SpecRef { source, .. } => source,
1908 }
1909 }
1910
1911 pub fn expected_hash_pin(&self) -> Option<&str> {
1913 match self {
1914 FactData::Value { .. } | FactData::TypeDeclaration { .. } => None,
1915 FactData::SpecRef {
1916 expected_hash_pin, ..
1917 } => expected_hash_pin.as_deref(),
1918 }
1919 }
1920
1921 pub fn spec_arc(&self) -> Option<&Arc<crate::parsing::ast::LemmaSpec>> {
1923 match self {
1924 FactData::Value { .. } | FactData::TypeDeclaration { .. } => None,
1925 FactData::SpecRef { spec: spec_arc, .. } => Some(spec_arc),
1926 }
1927 }
1928
1929 pub fn spec_ref(&self) -> Option<&str> {
1931 match self {
1932 FactData::Value { .. } | FactData::TypeDeclaration { .. } => None,
1933 FactData::SpecRef { spec, .. } => Some(&spec.name),
1934 }
1935 }
1936}
1937
1938pub fn value_to_semantic(value: &crate::parsing::ast::Value) -> Result<ValueKind, String> {
1940 use crate::parsing::ast::Value;
1941 Ok(match value {
1942 Value::Number(n) => ValueKind::Number(*n),
1943 Value::Text(s) => ValueKind::Text(s.clone()),
1944 Value::Boolean(b) => ValueKind::Boolean(bool::from(b.clone())),
1945 Value::Date(dt) => ValueKind::Date(date_time_to_semantic(dt)),
1946 Value::Time(t) => ValueKind::Time(time_to_semantic(t)),
1947 Value::Duration(n, u) => ValueKind::Duration(*n, duration_unit_to_semantic(u)),
1948 Value::Scale(n, unit) => ValueKind::Scale(*n, unit.clone()),
1949 Value::Ratio(n, unit) => ValueKind::Ratio(*n, unit.clone()),
1950 })
1951}
1952
1953pub(crate) fn date_time_to_semantic(dt: &crate::parsing::ast::DateTimeValue) -> SemanticDateTime {
1955 SemanticDateTime {
1956 year: dt.year,
1957 month: dt.month,
1958 day: dt.day,
1959 hour: dt.hour,
1960 minute: dt.minute,
1961 second: dt.second,
1962 microsecond: dt.microsecond,
1963 timezone: dt.timezone.as_ref().map(|tz| SemanticTimezone {
1964 offset_hours: tz.offset_hours,
1965 offset_minutes: tz.offset_minutes,
1966 }),
1967 }
1968}
1969
1970pub(crate) fn time_to_semantic(t: &crate::parsing::ast::TimeValue) -> SemanticTime {
1972 SemanticTime {
1973 hour: t.hour.into(),
1974 minute: t.minute.into(),
1975 second: t.second.into(),
1976 timezone: t.timezone.as_ref().map(|tz| SemanticTimezone {
1977 offset_hours: tz.offset_hours,
1978 offset_minutes: tz.offset_minutes,
1979 }),
1980 }
1981}
1982
1983pub(crate) fn duration_unit_to_semantic(
1985 u: &crate::parsing::ast::DurationUnit,
1986) -> SemanticDurationUnit {
1987 use crate::parsing::ast::DurationUnit as DU;
1988 match u {
1989 DU::Year => SemanticDurationUnit::Year,
1990 DU::Month => SemanticDurationUnit::Month,
1991 DU::Week => SemanticDurationUnit::Week,
1992 DU::Day => SemanticDurationUnit::Day,
1993 DU::Hour => SemanticDurationUnit::Hour,
1994 DU::Minute => SemanticDurationUnit::Minute,
1995 DU::Second => SemanticDurationUnit::Second,
1996 DU::Millisecond => SemanticDurationUnit::Millisecond,
1997 DU::Microsecond => SemanticDurationUnit::Microsecond,
1998 }
1999}
2000
2001pub fn conversion_target_to_semantic(
2008 ct: &ConversionTarget,
2009 unit_index: Option<&HashMap<String, (LemmaType, Option<crate::parsing::ast::TypeDef>)>>,
2010) -> Result<SemanticConversionTarget, String> {
2011 match ct {
2012 ConversionTarget::Duration(u) => Ok(SemanticConversionTarget::Duration(
2013 duration_unit_to_semantic(u),
2014 )),
2015 ConversionTarget::Unit(name) => {
2016 let index = unit_index.ok_or_else(|| {
2017 "Unit conversion requires type resolution; unit index not available.".to_string()
2018 })?;
2019 let (lemma_type, _) = index.get(name).ok_or_else(|| {
2020 format!(
2021 "Unknown unit '{}'. Unit must be defined by a scale or ratio type.",
2022 name
2023 )
2024 })?;
2025 if lemma_type.is_ratio() {
2026 Ok(SemanticConversionTarget::RatioUnit(name.clone()))
2027 } else if lemma_type.is_scale() {
2028 Ok(SemanticConversionTarget::ScaleUnit(name.clone()))
2029 } else {
2030 Err(format!(
2031 "Unit '{}' is not a ratio or scale type; cannot use it in conversion.",
2032 name
2033 ))
2034 }
2035 }
2036 }
2037}
2038
2039static PRIMITIVE_BOOLEAN: OnceLock<LemmaType> = OnceLock::new();
2045static PRIMITIVE_SCALE: OnceLock<LemmaType> = OnceLock::new();
2046static PRIMITIVE_NUMBER: OnceLock<LemmaType> = OnceLock::new();
2047static PRIMITIVE_TEXT: OnceLock<LemmaType> = OnceLock::new();
2048static PRIMITIVE_DATE: OnceLock<LemmaType> = OnceLock::new();
2049static PRIMITIVE_TIME: OnceLock<LemmaType> = OnceLock::new();
2050static PRIMITIVE_DURATION: OnceLock<LemmaType> = OnceLock::new();
2051static PRIMITIVE_RATIO: OnceLock<LemmaType> = OnceLock::new();
2052
2053#[must_use]
2055pub fn primitive_boolean() -> &'static LemmaType {
2056 PRIMITIVE_BOOLEAN.get_or_init(|| LemmaType::primitive(TypeSpecification::boolean()))
2057}
2058
2059#[must_use]
2060pub fn primitive_scale() -> &'static LemmaType {
2061 PRIMITIVE_SCALE.get_or_init(|| LemmaType::primitive(TypeSpecification::scale()))
2062}
2063
2064#[must_use]
2065pub fn primitive_number() -> &'static LemmaType {
2066 PRIMITIVE_NUMBER.get_or_init(|| LemmaType::primitive(TypeSpecification::number()))
2067}
2068
2069#[must_use]
2070pub fn primitive_text() -> &'static LemmaType {
2071 PRIMITIVE_TEXT.get_or_init(|| LemmaType::primitive(TypeSpecification::text()))
2072}
2073
2074#[must_use]
2075pub fn primitive_date() -> &'static LemmaType {
2076 PRIMITIVE_DATE.get_or_init(|| LemmaType::primitive(TypeSpecification::date()))
2077}
2078
2079#[must_use]
2080pub fn primitive_time() -> &'static LemmaType {
2081 PRIMITIVE_TIME.get_or_init(|| LemmaType::primitive(TypeSpecification::time()))
2082}
2083
2084#[must_use]
2085pub fn primitive_duration() -> &'static LemmaType {
2086 PRIMITIVE_DURATION.get_or_init(|| LemmaType::primitive(TypeSpecification::duration()))
2087}
2088
2089#[must_use]
2090pub fn primitive_ratio() -> &'static LemmaType {
2091 PRIMITIVE_RATIO.get_or_init(|| LemmaType::primitive(TypeSpecification::ratio()))
2092}
2093
2094impl fmt::Display for PathSegment {
2099 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2100 write!(f, "{} → {}", self.fact, self.spec)
2101 }
2102}
2103
2104impl fmt::Display for FactPath {
2105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2106 for segment in &self.segments {
2107 write!(f, "{}.", segment)?;
2108 }
2109 write!(f, "{}", self.fact)
2110 }
2111}
2112
2113impl fmt::Display for RulePath {
2114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2115 for segment in &self.segments {
2116 write!(f, "{}.", segment)?;
2117 }
2118 write!(f, "{}", self.rule)
2119 }
2120}
2121
2122impl fmt::Display for LemmaType {
2123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2124 write!(f, "{}", self.name())
2125 }
2126}
2127
2128impl fmt::Display for LiteralValue {
2129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2130 match &self.value {
2131 ValueKind::Scale(n, u) => {
2132 if let TypeSpecification::Scale { decimals, .. } = &self.lemma_type.specifications {
2133 let s = match decimals {
2134 Some(d) => {
2135 let dp = u32::from(*d);
2136 let rounded = n.round_dp(dp);
2137 format!("{:.prec$}", rounded, prec = *d as usize)
2138 }
2139 None => n.normalize().to_string(),
2140 };
2141 return write!(f, "{} {}", s, u);
2142 }
2143 write!(f, "{}", self.value)
2144 }
2145 ValueKind::Ratio(r, Some(unit_name)) => {
2146 if let TypeSpecification::Ratio { units, .. } = &self.lemma_type.specifications {
2147 if let Ok(unit) = units.get(unit_name) {
2148 let display_value = (*r * unit.value).normalize();
2149 let s = if display_value.fract().is_zero() {
2150 display_value.trunc().to_string()
2151 } else {
2152 display_value.to_string()
2153 };
2154 return match unit_name.as_str() {
2156 "percent" => write!(f, "{}%", s),
2157 "permille" => write!(f, "{}%%", s),
2158 _ => write!(f, "{} {}", s, unit_name),
2159 };
2160 }
2161 }
2162 write!(f, "{}", self.value)
2163 }
2164 _ => write!(f, "{}", self.value),
2165 }
2166 }
2167}
2168
2169#[cfg(test)]
2174mod tests {
2175 use super::*;
2176 use crate::parsing::ast::{BooleanValue, DateTimeValue, DurationUnit, TimeValue};
2177 use rust_decimal::Decimal;
2178 use std::str::FromStr;
2179
2180 #[test]
2181 fn test_negated_comparison() {
2182 assert_eq!(
2183 negated_comparison(ComparisonComputation::LessThan),
2184 ComparisonComputation::GreaterThanOrEqual
2185 );
2186 assert_eq!(
2187 negated_comparison(ComparisonComputation::GreaterThanOrEqual),
2188 ComparisonComputation::LessThan
2189 );
2190 assert_eq!(
2191 negated_comparison(ComparisonComputation::Equal),
2192 ComparisonComputation::IsNot,
2193 "== negates to 'is not'"
2194 );
2195 assert_eq!(
2196 negated_comparison(ComparisonComputation::NotEqual),
2197 ComparisonComputation::Is,
2198 "!= negates to 'is'"
2199 );
2200 assert_eq!(
2201 negated_comparison(ComparisonComputation::Is),
2202 ComparisonComputation::IsNot
2203 );
2204 assert_eq!(
2205 negated_comparison(ComparisonComputation::IsNot),
2206 ComparisonComputation::Is
2207 );
2208 }
2209
2210 #[test]
2211 fn test_literal_value_to_primitive_type() {
2212 let one = Decimal::from_str("1").unwrap();
2213
2214 assert_eq!(LiteralValue::text("".to_string()).lemma_type.name(), "text");
2215 assert_eq!(LiteralValue::number(one).lemma_type.name(), "number");
2216 assert_eq!(
2217 LiteralValue::from_bool(bool::from(BooleanValue::True))
2218 .lemma_type
2219 .name(),
2220 "boolean"
2221 );
2222
2223 let dt = DateTimeValue {
2224 year: 2024,
2225 month: 1,
2226 day: 1,
2227 hour: 0,
2228 minute: 0,
2229 second: 0,
2230 microsecond: 0,
2231 timezone: None,
2232 };
2233 assert_eq!(
2234 LiteralValue::date(date_time_to_semantic(&dt))
2235 .lemma_type
2236 .name(),
2237 "date"
2238 );
2239 assert_eq!(
2240 LiteralValue::ratio(one / Decimal::from(100), Some("percent".to_string()))
2241 .lemma_type
2242 .name(),
2243 "ratio"
2244 );
2245 assert_eq!(
2246 LiteralValue::duration(one, duration_unit_to_semantic(&DurationUnit::Second))
2247 .lemma_type
2248 .name(),
2249 "duration"
2250 );
2251 }
2252
2253 #[test]
2254 fn test_spec_type_display() {
2255 assert_eq!(format!("{}", primitive_text()), "text");
2256 assert_eq!(format!("{}", primitive_number()), "number");
2257 assert_eq!(format!("{}", primitive_date()), "date");
2258 assert_eq!(format!("{}", primitive_boolean()), "boolean");
2259 assert_eq!(format!("{}", primitive_duration()), "duration");
2260 }
2261
2262 #[test]
2263 fn test_type_constructor() {
2264 let specs = TypeSpecification::number();
2265 let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
2266 assert_eq!(lemma_type.name(), "dice");
2267 }
2268
2269 #[test]
2270 fn test_type_display() {
2271 let specs = TypeSpecification::text();
2272 let lemma_type = LemmaType::new("name".to_string(), specs, TypeExtends::Primitive);
2273 assert_eq!(format!("{}", lemma_type), "name");
2274 }
2275
2276 #[test]
2277 fn test_type_equality() {
2278 let specs1 = TypeSpecification::number();
2279 let specs2 = TypeSpecification::number();
2280 let lemma_type1 = LemmaType::new("dice".to_string(), specs1, TypeExtends::Primitive);
2281 let lemma_type2 = LemmaType::new("dice".to_string(), specs2, TypeExtends::Primitive);
2282 assert_eq!(lemma_type1, lemma_type2);
2283 }
2284
2285 #[test]
2286 fn test_type_serialization() {
2287 let specs = TypeSpecification::number();
2288 let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
2289 let serialized = serde_json::to_string(&lemma_type).unwrap();
2290 let deserialized: LemmaType = serde_json::from_str(&serialized).unwrap();
2291 assert_eq!(lemma_type, deserialized);
2292 }
2293
2294 #[test]
2295 fn test_literal_value_display_value() {
2296 let ten = Decimal::from_str("10").unwrap();
2297
2298 assert_eq!(
2299 LiteralValue::text("hello".to_string()).display_value(),
2300 "hello"
2301 );
2302 assert_eq!(LiteralValue::number(ten).display_value(), "10");
2303 assert_eq!(LiteralValue::from_bool(true).display_value(), "true");
2304 assert_eq!(LiteralValue::from_bool(false).display_value(), "false");
2305
2306 let ten_percent_ratio = LiteralValue::ratio(
2308 Decimal::from_str("0.10").unwrap(),
2309 Some("percent".to_string()),
2310 );
2311 assert_eq!(ten_percent_ratio.display_value(), "10%");
2312
2313 let time = TimeValue {
2314 hour: 14,
2315 minute: 30,
2316 second: 0,
2317 timezone: None,
2318 };
2319 let time_display = LiteralValue::time(time_to_semantic(&time)).display_value();
2320 assert!(time_display.contains("14"));
2321 assert!(time_display.contains("30"));
2322 }
2323
2324 #[test]
2325 fn test_scale_display_respects_type_decimals() {
2326 let money_type = LemmaType {
2327 name: Some("money".to_string()),
2328 specifications: TypeSpecification::Scale {
2329 minimum: None,
2330 maximum: None,
2331 decimals: Some(2),
2332 precision: None,
2333 units: ScaleUnits::from(vec![ScaleUnit {
2334 name: "eur".to_string(),
2335 value: Decimal::from(1),
2336 }]),
2337 help: String::new(),
2338 default: None,
2339 },
2340 extends: TypeExtends::Primitive,
2341 };
2342 let val = LiteralValue::scale_with_type(
2343 Decimal::from_str("1.8").unwrap(),
2344 "eur".to_string(),
2345 money_type.clone(),
2346 );
2347 assert_eq!(val.display_value(), "1.80 eur");
2348 let more_precision = LiteralValue::scale_with_type(
2349 Decimal::from_str("1.80000").unwrap(),
2350 "eur".to_string(),
2351 money_type,
2352 );
2353 assert_eq!(more_precision.display_value(), "1.80 eur");
2354 let scale_no_decimals = LemmaType {
2355 name: Some("count".to_string()),
2356 specifications: TypeSpecification::Scale {
2357 minimum: None,
2358 maximum: None,
2359 decimals: None,
2360 precision: None,
2361 units: ScaleUnits::from(vec![ScaleUnit {
2362 name: "items".to_string(),
2363 value: Decimal::from(1),
2364 }]),
2365 help: String::new(),
2366 default: None,
2367 },
2368 extends: TypeExtends::Primitive,
2369 };
2370 let val_any = LiteralValue::scale_with_type(
2371 Decimal::from_str("42.50").unwrap(),
2372 "items".to_string(),
2373 scale_no_decimals,
2374 );
2375 assert_eq!(val_any.display_value(), "42.5 items");
2376 }
2377
2378 #[test]
2379 fn test_literal_value_time_type() {
2380 let time = TimeValue {
2381 hour: 14,
2382 minute: 30,
2383 second: 0,
2384 timezone: None,
2385 };
2386 let lit = LiteralValue::time(time_to_semantic(&time));
2387 assert_eq!(lit.lemma_type.name(), "time");
2388 }
2389
2390 #[test]
2391 fn test_scale_family_name_primitive_root() {
2392 let scale_spec = TypeSpecification::scale();
2393 let money_primitive = LemmaType::new(
2394 "money".to_string(),
2395 scale_spec.clone(),
2396 TypeExtends::Primitive,
2397 );
2398 assert_eq!(money_primitive.scale_family_name(), Some("money"));
2399 }
2400
2401 #[test]
2402 fn test_scale_family_name_custom() {
2403 let scale_spec = TypeSpecification::scale();
2404 let money_custom = LemmaType::new(
2405 "money".to_string(),
2406 scale_spec,
2407 TypeExtends::Custom {
2408 parent: "money".to_string(),
2409 family: "money".to_string(),
2410 },
2411 );
2412 assert_eq!(money_custom.scale_family_name(), Some("money"));
2413 }
2414
2415 #[test]
2416 fn test_same_scale_family_same_name_different_extends() {
2417 let scale_spec = TypeSpecification::scale();
2418 let money_primitive = LemmaType::new(
2419 "money".to_string(),
2420 scale_spec.clone(),
2421 TypeExtends::Primitive,
2422 );
2423 let money_custom = LemmaType::new(
2424 "money".to_string(),
2425 scale_spec,
2426 TypeExtends::Custom {
2427 parent: "money".to_string(),
2428 family: "money".to_string(),
2429 },
2430 );
2431 assert!(money_primitive.same_scale_family(&money_custom));
2432 assert!(money_custom.same_scale_family(&money_primitive));
2433 }
2434
2435 #[test]
2436 fn test_same_scale_family_parent_and_child() {
2437 let scale_spec = TypeSpecification::scale();
2438 let type_x = LemmaType::new("x".to_string(), scale_spec.clone(), TypeExtends::Primitive);
2439 let type_x2 = LemmaType::new(
2440 "x2".to_string(),
2441 scale_spec,
2442 TypeExtends::Custom {
2443 parent: "x".to_string(),
2444 family: "x".to_string(),
2445 },
2446 );
2447 assert_eq!(type_x.scale_family_name(), Some("x"));
2448 assert_eq!(type_x2.scale_family_name(), Some("x"));
2449 assert!(type_x.same_scale_family(&type_x2));
2450 assert!(type_x2.same_scale_family(&type_x));
2451 }
2452
2453 #[test]
2454 fn test_same_scale_family_siblings() {
2455 let scale_spec = TypeSpecification::scale();
2456 let type_x2_a = LemmaType::new(
2457 "x2a".to_string(),
2458 scale_spec.clone(),
2459 TypeExtends::Custom {
2460 parent: "x".to_string(),
2461 family: "x".to_string(),
2462 },
2463 );
2464 let type_x2_b = LemmaType::new(
2465 "x2b".to_string(),
2466 scale_spec,
2467 TypeExtends::Custom {
2468 parent: "x".to_string(),
2469 family: "x".to_string(),
2470 },
2471 );
2472 assert!(type_x2_a.same_scale_family(&type_x2_b));
2473 }
2474
2475 #[test]
2476 fn test_same_scale_family_different_families() {
2477 let scale_spec = TypeSpecification::scale();
2478 let money = LemmaType::new(
2479 "money".to_string(),
2480 scale_spec.clone(),
2481 TypeExtends::Primitive,
2482 );
2483 let temperature = LemmaType::new(
2484 "temperature".to_string(),
2485 scale_spec,
2486 TypeExtends::Primitive,
2487 );
2488 assert!(!money.same_scale_family(&temperature));
2489 assert!(!temperature.same_scale_family(&money));
2490 }
2491
2492 #[test]
2493 fn test_same_scale_family_scale_vs_non_scale() {
2494 let scale_spec = TypeSpecification::scale();
2495 let number_spec = TypeSpecification::number();
2496 let scale_type = LemmaType::new("money".to_string(), scale_spec, TypeExtends::Primitive);
2497 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
2498 assert!(!scale_type.same_scale_family(&number_type));
2499 assert!(!number_type.same_scale_family(&scale_type));
2500 }
2501
2502 #[test]
2503 fn test_scale_family_name_non_scale_returns_none() {
2504 let number_spec = TypeSpecification::number();
2505 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
2506 assert_eq!(number_type.scale_family_name(), None);
2507 }
2508
2509 #[test]
2510 fn test_explicit_value_returns_none_for_default() {
2511 let source = crate::Source::new(
2512 "test.lemma",
2513 crate::parsing::ast::Span {
2514 start: 0,
2515 end: 1,
2516 line: 1,
2517 col: 0,
2518 },
2519 std::sync::Arc::from("x"),
2520 );
2521 let fact = FactData::Value {
2522 value: LiteralValue::number(Decimal::from(25)),
2523 source: source.clone(),
2524 is_default: true,
2525 };
2526 assert!(
2527 fact.explicit_value().is_none(),
2528 "is_default=true should yield None from explicit_value()"
2529 );
2530 assert!(
2531 fact.value().is_some(),
2532 "value() should still return the value regardless of is_default"
2533 );
2534 }
2535
2536 #[test]
2537 fn test_explicit_value_returns_some_for_non_default() {
2538 let source = crate::Source::new(
2539 "test.lemma",
2540 crate::parsing::ast::Span {
2541 start: 0,
2542 end: 1,
2543 line: 1,
2544 col: 0,
2545 },
2546 std::sync::Arc::from("x"),
2547 );
2548 let fact = FactData::Value {
2549 value: LiteralValue::number(Decimal::from(42)),
2550 source,
2551 is_default: false,
2552 };
2553 assert!(
2554 fact.explicit_value().is_some(),
2555 "is_default=false should yield Some from explicit_value()"
2556 );
2557 assert_eq!(
2558 fact.explicit_value().unwrap().value,
2559 ValueKind::Number(Decimal::from(42))
2560 );
2561 }
2562
2563 #[test]
2564 fn test_explicit_value_returns_none_for_type_declaration() {
2565 let source = crate::Source::new(
2566 "test.lemma",
2567 crate::parsing::ast::Span {
2568 start: 0,
2569 end: 1,
2570 line: 1,
2571 col: 0,
2572 },
2573 std::sync::Arc::from("x"),
2574 );
2575 let fact = FactData::TypeDeclaration {
2576 resolved_type: primitive_number().clone(),
2577 source,
2578 };
2579 assert!(
2580 fact.explicit_value().is_none(),
2581 "TypeDeclaration should yield None from explicit_value()"
2582 );
2583 }
2584}