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