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, Deserialize)]
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::Is => ComparisonComputation::IsNot,
32 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 collect_fact_paths(&self, facts: &mut std::collections::HashSet<FactPath>) {
1272 self.kind.collect_fact_paths(facts);
1273 }
1274}
1275
1276#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1278#[serde(rename_all = "snake_case")]
1279pub enum ExpressionKind {
1280 Literal(Box<LiteralValue>),
1282 FactPath(FactPath),
1284 RulePath(RulePath),
1286 LogicalAnd(Arc<Expression>, Arc<Expression>),
1287 Arithmetic(Arc<Expression>, ArithmeticComputation, Arc<Expression>),
1288 Comparison(Arc<Expression>, ComparisonComputation, Arc<Expression>),
1289 UnitConversion(Arc<Expression>, SemanticConversionTarget),
1290 LogicalNegation(Arc<Expression>, NegationType),
1291 MathematicalComputation(MathematicalComputation, Arc<Expression>),
1292 Veto(VetoExpression),
1293 Now,
1295 DateRelative(DateRelativeKind, Arc<Expression>, Option<Arc<Expression>>),
1297 DateCalendar(DateCalendarKind, CalendarUnit, Arc<Expression>),
1299}
1300
1301impl ExpressionKind {
1302 fn collect_fact_paths(&self, facts: &mut std::collections::HashSet<FactPath>) {
1304 match self {
1305 ExpressionKind::FactPath(fp) => {
1306 facts.insert(fp.clone());
1307 }
1308 ExpressionKind::LogicalAnd(left, right)
1309 | ExpressionKind::Arithmetic(left, _, right)
1310 | ExpressionKind::Comparison(left, _, right) => {
1311 left.collect_fact_paths(facts);
1312 right.collect_fact_paths(facts);
1313 }
1314 ExpressionKind::UnitConversion(inner, _)
1315 | ExpressionKind::LogicalNegation(inner, _)
1316 | ExpressionKind::MathematicalComputation(_, inner) => {
1317 inner.collect_fact_paths(facts);
1318 }
1319 ExpressionKind::DateRelative(_, date_expr, tolerance) => {
1320 date_expr.collect_fact_paths(facts);
1321 if let Some(tol) = tolerance {
1322 tol.collect_fact_paths(facts);
1323 }
1324 }
1325 ExpressionKind::DateCalendar(_, _, date_expr) => {
1326 date_expr.collect_fact_paths(facts);
1327 }
1328 ExpressionKind::Literal(_)
1329 | ExpressionKind::RulePath(_)
1330 | ExpressionKind::Veto(_)
1331 | ExpressionKind::Now => {}
1332 }
1333 }
1334}
1335
1336#[inline]
1343#[must_use]
1344pub fn is_same_spec(left: &LemmaSpec, right: &LemmaSpec) -> bool {
1345 left == right
1346}
1347
1348#[derive(Clone, Debug, Serialize, Deserialize)]
1350#[serde(tag = "kind", rename_all = "snake_case")]
1351pub enum TypeDefiningSpec {
1352 Local,
1354 Import {
1356 spec: Arc<LemmaSpec>,
1357 resolved_plan_hash: String,
1358 },
1359}
1360
1361#[derive(Clone, Debug, Serialize, Deserialize)]
1363#[serde(rename_all = "snake_case")]
1364pub enum TypeExtends {
1365 Primitive,
1367 Custom {
1370 parent: String,
1371 family: String,
1372 defining_spec: TypeDefiningSpec,
1373 },
1374}
1375
1376impl PartialEq for TypeExtends {
1377 fn eq(&self, other: &Self) -> bool {
1378 match (self, other) {
1379 (TypeExtends::Primitive, TypeExtends::Primitive) => true,
1380 (
1381 TypeExtends::Custom {
1382 parent: lp,
1383 family: lf,
1384 defining_spec: ld,
1385 },
1386 TypeExtends::Custom {
1387 parent: rp,
1388 family: rf,
1389 defining_spec: rd,
1390 },
1391 ) => {
1392 lp == rp
1393 && lf == rf
1394 && match (ld, rd) {
1395 (TypeDefiningSpec::Local, TypeDefiningSpec::Local) => true,
1396 (
1397 TypeDefiningSpec::Import {
1398 spec: left,
1399 resolved_plan_hash: lh,
1400 },
1401 TypeDefiningSpec::Import {
1402 spec: right,
1403 resolved_plan_hash: rh,
1404 },
1405 ) => is_same_spec(left, right) && lh == rh,
1406 _ => false,
1407 }
1408 }
1409 _ => false,
1410 }
1411 }
1412}
1413
1414impl Eq for TypeExtends {}
1415
1416impl TypeExtends {
1417 #[must_use]
1419 pub fn custom_local(parent: String, family: String) -> Self {
1420 TypeExtends::Custom {
1421 parent,
1422 family,
1423 defining_spec: TypeDefiningSpec::Local,
1424 }
1425 }
1426
1427 #[must_use]
1429 pub fn parent_name(&self) -> Option<&str> {
1430 match self {
1431 TypeExtends::Primitive => None,
1432 TypeExtends::Custom { parent, .. } => Some(parent.as_str()),
1433 }
1434 }
1435}
1436
1437#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1442pub struct LemmaType {
1443 pub name: Option<String>,
1445 pub specifications: TypeSpecification,
1447 pub extends: TypeExtends,
1449}
1450
1451impl LemmaType {
1452 pub fn new(name: String, specifications: TypeSpecification, extends: TypeExtends) -> Self {
1454 Self {
1455 name: Some(name),
1456 specifications,
1457 extends,
1458 }
1459 }
1460
1461 pub fn without_name(specifications: TypeSpecification, extends: TypeExtends) -> Self {
1463 Self {
1464 name: None,
1465 specifications,
1466 extends,
1467 }
1468 }
1469
1470 pub fn primitive(specifications: TypeSpecification) -> Self {
1472 Self {
1473 name: None,
1474 specifications,
1475 extends: TypeExtends::Primitive,
1476 }
1477 }
1478
1479 pub fn name(&self) -> String {
1481 self.name.clone().unwrap_or_else(|| {
1482 match &self.specifications {
1483 TypeSpecification::Boolean { .. } => "boolean",
1484 TypeSpecification::Scale { .. } => "scale",
1485 TypeSpecification::Number { .. } => "number",
1486 TypeSpecification::Text { .. } => "text",
1487 TypeSpecification::Date { .. } => "date",
1488 TypeSpecification::Time { .. } => "time",
1489 TypeSpecification::Duration { .. } => "duration",
1490 TypeSpecification::Ratio { .. } => "ratio",
1491 TypeSpecification::Veto { .. } => "veto",
1492 TypeSpecification::Undetermined => "undetermined",
1493 }
1494 .to_string()
1495 })
1496 }
1497
1498 pub fn is_boolean(&self) -> bool {
1500 matches!(&self.specifications, TypeSpecification::Boolean { .. })
1501 }
1502
1503 pub fn is_scale(&self) -> bool {
1505 matches!(&self.specifications, TypeSpecification::Scale { .. })
1506 }
1507
1508 pub fn is_number(&self) -> bool {
1510 matches!(&self.specifications, TypeSpecification::Number { .. })
1511 }
1512
1513 pub fn is_numeric(&self) -> bool {
1515 matches!(
1516 &self.specifications,
1517 TypeSpecification::Scale { .. } | TypeSpecification::Number { .. }
1518 )
1519 }
1520
1521 pub fn is_text(&self) -> bool {
1523 matches!(&self.specifications, TypeSpecification::Text { .. })
1524 }
1525
1526 pub fn is_date(&self) -> bool {
1528 matches!(&self.specifications, TypeSpecification::Date { .. })
1529 }
1530
1531 pub fn is_time(&self) -> bool {
1533 matches!(&self.specifications, TypeSpecification::Time { .. })
1534 }
1535
1536 pub fn is_duration(&self) -> bool {
1538 matches!(&self.specifications, TypeSpecification::Duration { .. })
1539 }
1540
1541 pub fn is_ratio(&self) -> bool {
1543 matches!(&self.specifications, TypeSpecification::Ratio { .. })
1544 }
1545
1546 pub fn vetoed(&self) -> bool {
1548 matches!(&self.specifications, TypeSpecification::Veto { .. })
1549 }
1550
1551 pub fn is_undetermined(&self) -> bool {
1553 matches!(&self.specifications, TypeSpecification::Undetermined)
1554 }
1555
1556 pub fn has_same_base_type(&self, other: &LemmaType) -> bool {
1558 use TypeSpecification::*;
1559 matches!(
1560 (&self.specifications, &other.specifications),
1561 (Boolean { .. }, Boolean { .. })
1562 | (Number { .. }, Number { .. })
1563 | (Scale { .. }, Scale { .. })
1564 | (Text { .. }, Text { .. })
1565 | (Date { .. }, Date { .. })
1566 | (Time { .. }, Time { .. })
1567 | (Duration { .. }, Duration { .. })
1568 | (Ratio { .. }, Ratio { .. })
1569 | (Veto { .. }, Veto { .. })
1570 | (Undetermined, Undetermined)
1571 )
1572 }
1573
1574 #[must_use]
1576 pub fn scale_family_name(&self) -> Option<&str> {
1577 if !self.is_scale() {
1578 return None;
1579 }
1580 match &self.extends {
1581 TypeExtends::Custom { family, .. } => Some(family.as_str()),
1582 TypeExtends::Primitive => self.name.as_deref(),
1583 }
1584 }
1585
1586 #[must_use]
1589 pub fn same_scale_family(&self, other: &LemmaType) -> bool {
1590 if !self.is_scale() || !other.is_scale() {
1591 return false;
1592 }
1593 match (self.scale_family_name(), other.scale_family_name()) {
1594 (Some(self_family), Some(other_family)) => self_family == other_family,
1595 (None, None) => true,
1597 _ => false,
1598 }
1599 }
1600
1601 pub fn create_default_value(&self) -> Option<LiteralValue> {
1603 let value = match &self.specifications {
1604 TypeSpecification::Text { default, .. } => default.clone().map(ValueKind::Text),
1605 TypeSpecification::Number { default, .. } => (*default).map(ValueKind::Number),
1606 TypeSpecification::Scale { default, .. } => {
1607 default.clone().map(|(d, u)| ValueKind::Scale(d, u))
1608 }
1609 TypeSpecification::Boolean { default, .. } => (*default).map(ValueKind::Boolean),
1610 TypeSpecification::Date { default, .. } => default
1611 .clone()
1612 .map(|dt| ValueKind::Date(date_time_to_semantic(&dt))),
1613 TypeSpecification::Time { default, .. } => default
1614 .clone()
1615 .map(|t| ValueKind::Time(time_to_semantic(&t))),
1616 TypeSpecification::Duration { default, .. } => default
1617 .clone()
1618 .map(|(v, u)| ValueKind::Duration(v, duration_unit_to_semantic(&u))),
1619 TypeSpecification::Ratio { .. } => None, TypeSpecification::Veto { .. } => None,
1621 TypeSpecification::Undetermined => None,
1622 };
1623
1624 value.map(|v| LiteralValue {
1625 value: v,
1626 lemma_type: self.clone(),
1627 })
1628 }
1629
1630 pub fn veto_type() -> Self {
1632 Self::primitive(TypeSpecification::veto())
1633 }
1634
1635 pub fn undetermined_type() -> Self {
1638 Self::primitive(TypeSpecification::Undetermined)
1639 }
1640
1641 pub fn decimal_places(&self) -> Option<u8> {
1644 match &self.specifications {
1645 TypeSpecification::Number { decimals, .. } => *decimals,
1646 TypeSpecification::Scale { decimals, .. } => *decimals,
1647 TypeSpecification::Ratio { decimals, .. } => *decimals,
1648 _ => None,
1649 }
1650 }
1651
1652 pub fn example_value(&self) -> &'static str {
1654 match &self.specifications {
1655 TypeSpecification::Text { .. } => "\"hello world\"",
1656 TypeSpecification::Scale { .. } => "12.50 eur",
1657 TypeSpecification::Number { .. } => "3.14",
1658 TypeSpecification::Boolean { .. } => "true",
1659 TypeSpecification::Date { .. } => "2023-12-25T14:30:00Z",
1660 TypeSpecification::Veto { .. } => "veto",
1661 TypeSpecification::Time { .. } => "14:30:00",
1662 TypeSpecification::Duration { .. } => "90 minutes",
1663 TypeSpecification::Ratio { .. } => "50%",
1664 TypeSpecification::Undetermined => unreachable!(
1665 "BUG: example_value called on Undetermined sentinel type; this type must never reach user-facing code"
1666 ),
1667 }
1668 }
1669
1670 #[must_use]
1674 pub fn scale_unit_factor(&self, unit_name: &str) -> Decimal {
1675 let units = match &self.specifications {
1676 TypeSpecification::Scale { units, .. } => units,
1677 _ => unreachable!(
1678 "BUG: scale_unit_factor called with non-scale type {}; only call during evaluation after planning validated scale conversion",
1679 self.name()
1680 ),
1681 };
1682 match units
1683 .iter()
1684 .find(|u| u.name.eq_ignore_ascii_case(unit_name))
1685 {
1686 Some(ScaleUnit { value, .. }) => *value,
1687 None => {
1688 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
1689 unreachable!(
1690 "BUG: unknown unit '{}' for scale type {} (valid: {}); planning must reject invalid conversions with Error",
1691 unit_name,
1692 self.name(),
1693 valid.join(", ")
1694 );
1695 }
1696 }
1697 }
1698}
1699
1700#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
1702pub struct LiteralValue {
1703 pub value: ValueKind,
1704 pub lemma_type: LemmaType,
1705}
1706
1707impl Serialize for LiteralValue {
1708 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1709 where
1710 S: serde::Serializer,
1711 {
1712 use serde::ser::SerializeStruct;
1713 let mut state = serializer.serialize_struct("LiteralValue", 3)?;
1714 state.serialize_field("value", &self.value)?;
1715 state.serialize_field("lemma_type", &self.lemma_type)?;
1716 state.serialize_field("display_value", &self.display_value())?;
1717 state.end()
1718 }
1719}
1720
1721impl LiteralValue {
1722 pub fn text(s: String) -> Self {
1723 Self {
1724 value: ValueKind::Text(s),
1725 lemma_type: primitive_text().clone(),
1726 }
1727 }
1728
1729 pub fn text_with_type(s: String, lemma_type: LemmaType) -> Self {
1730 Self {
1731 value: ValueKind::Text(s),
1732 lemma_type,
1733 }
1734 }
1735
1736 pub fn number(n: Decimal) -> Self {
1737 Self {
1738 value: ValueKind::Number(n),
1739 lemma_type: primitive_number().clone(),
1740 }
1741 }
1742
1743 pub fn number_with_type(n: Decimal, lemma_type: LemmaType) -> Self {
1744 Self {
1745 value: ValueKind::Number(n),
1746 lemma_type,
1747 }
1748 }
1749
1750 pub fn scale_with_type(n: Decimal, unit: String, lemma_type: LemmaType) -> Self {
1751 Self {
1752 value: ValueKind::Scale(n, unit),
1753 lemma_type,
1754 }
1755 }
1756
1757 pub fn number_interpreted_as_scale(value: Decimal, unit_name: String) -> Self {
1760 let lemma_type = LemmaType {
1761 name: None,
1762 specifications: TypeSpecification::Scale {
1763 minimum: None,
1764 maximum: None,
1765 decimals: None,
1766 precision: None,
1767 units: ScaleUnits::from(vec![ScaleUnit {
1768 name: unit_name.clone(),
1769 value: Decimal::from(1),
1770 }]),
1771 help: "Format: {value} {unit} (e.g. 100 kilograms)".to_string(),
1772 default: None,
1773 },
1774 extends: TypeExtends::Primitive,
1775 };
1776 Self {
1777 value: ValueKind::Scale(value, unit_name),
1778 lemma_type,
1779 }
1780 }
1781
1782 pub fn from_bool(b: bool) -> Self {
1783 Self {
1784 value: ValueKind::Boolean(b),
1785 lemma_type: primitive_boolean().clone(),
1786 }
1787 }
1788
1789 pub fn date(dt: SemanticDateTime) -> Self {
1790 Self {
1791 value: ValueKind::Date(dt),
1792 lemma_type: primitive_date().clone(),
1793 }
1794 }
1795
1796 pub fn date_with_type(dt: SemanticDateTime, lemma_type: LemmaType) -> Self {
1797 Self {
1798 value: ValueKind::Date(dt),
1799 lemma_type,
1800 }
1801 }
1802
1803 pub fn time(t: SemanticTime) -> Self {
1804 Self {
1805 value: ValueKind::Time(t),
1806 lemma_type: primitive_time().clone(),
1807 }
1808 }
1809
1810 pub fn time_with_type(t: SemanticTime, lemma_type: LemmaType) -> Self {
1811 Self {
1812 value: ValueKind::Time(t),
1813 lemma_type,
1814 }
1815 }
1816
1817 pub fn duration(value: Decimal, unit: SemanticDurationUnit) -> Self {
1818 Self {
1819 value: ValueKind::Duration(value, unit),
1820 lemma_type: primitive_duration().clone(),
1821 }
1822 }
1823
1824 pub fn duration_with_type(
1825 value: Decimal,
1826 unit: SemanticDurationUnit,
1827 lemma_type: LemmaType,
1828 ) -> Self {
1829 Self {
1830 value: ValueKind::Duration(value, unit),
1831 lemma_type,
1832 }
1833 }
1834
1835 pub fn ratio(r: Decimal, unit: Option<String>) -> Self {
1836 Self {
1837 value: ValueKind::Ratio(r, unit),
1838 lemma_type: primitive_ratio().clone(),
1839 }
1840 }
1841
1842 pub fn ratio_with_type(r: Decimal, unit: Option<String>, lemma_type: LemmaType) -> Self {
1843 Self {
1844 value: ValueKind::Ratio(r, unit),
1845 lemma_type,
1846 }
1847 }
1848
1849 pub fn display_value(&self) -> String {
1851 format!("{}", self)
1852 }
1853
1854 pub fn byte_size(&self) -> usize {
1856 format!("{}", self).len()
1857 }
1858
1859 pub fn get_type(&self) -> &LemmaType {
1861 &self.lemma_type
1862 }
1863}
1864
1865#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1867#[serde(rename_all = "snake_case")]
1868pub enum FactValue {
1869 Literal(LiteralValue),
1870 TypeDeclaration { resolved_type: LemmaType },
1871 SpecReference(String),
1872}
1873
1874#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1876pub struct Fact {
1877 pub path: FactPath,
1878 pub value: FactValue,
1879 pub source: Option<Source>,
1880}
1881
1882#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1884#[serde(rename_all = "snake_case")]
1885pub enum FactData {
1886 Value {
1890 value: LiteralValue,
1891 source: Source,
1892 is_default: bool,
1893 },
1894 TypeDeclaration {
1896 resolved_type: LemmaType,
1897 source: Source,
1898 },
1899 SpecRef {
1901 spec: Arc<crate::parsing::ast::LemmaSpec>,
1902 source: Source,
1903 resolved_plan_hash: String,
1904 },
1905}
1906
1907impl FactData {
1908 pub fn schema_type(&self) -> Option<&LemmaType> {
1910 match self {
1911 FactData::Value { value, .. } => Some(&value.lemma_type),
1912 FactData::TypeDeclaration { resolved_type, .. } => Some(resolved_type),
1913 FactData::SpecRef { .. } => None,
1914 }
1915 }
1916
1917 pub fn value(&self) -> Option<&LiteralValue> {
1919 match self {
1920 FactData::Value { value, .. } => Some(value),
1921 FactData::TypeDeclaration { .. } | FactData::SpecRef { .. } => None,
1922 }
1923 }
1924
1925 pub fn explicit_value(&self) -> Option<&LiteralValue> {
1929 match self {
1930 FactData::Value {
1931 value, is_default, ..
1932 } => {
1933 if *is_default {
1934 None
1935 } else {
1936 Some(value)
1937 }
1938 }
1939 FactData::TypeDeclaration { .. } | FactData::SpecRef { .. } => None,
1940 }
1941 }
1942
1943 pub fn source(&self) -> &Source {
1945 match self {
1946 FactData::Value { source, .. } => source,
1947 FactData::TypeDeclaration { source, .. } => source,
1948 FactData::SpecRef { source, .. } => source,
1949 }
1950 }
1951
1952 pub fn resolved_plan_hash(&self) -> Option<&str> {
1954 match self {
1955 FactData::Value { .. } | FactData::TypeDeclaration { .. } => None,
1956 FactData::SpecRef {
1957 resolved_plan_hash, ..
1958 } => Some(resolved_plan_hash.as_str()),
1959 }
1960 }
1961
1962 pub fn spec_arc(&self) -> Option<&Arc<crate::parsing::ast::LemmaSpec>> {
1964 match self {
1965 FactData::Value { .. } | FactData::TypeDeclaration { .. } => None,
1966 FactData::SpecRef { spec: spec_arc, .. } => Some(spec_arc),
1967 }
1968 }
1969
1970 pub fn spec_ref(&self) -> Option<&str> {
1972 match self {
1973 FactData::Value { .. } | FactData::TypeDeclaration { .. } => None,
1974 FactData::SpecRef { spec, .. } => Some(&spec.name),
1975 }
1976 }
1977}
1978
1979pub fn value_to_semantic(value: &crate::parsing::ast::Value) -> Result<ValueKind, String> {
1981 use crate::parsing::ast::Value;
1982 Ok(match value {
1983 Value::Number(n) => ValueKind::Number(*n),
1984 Value::Text(s) => ValueKind::Text(s.clone()),
1985 Value::Boolean(b) => ValueKind::Boolean(bool::from(*b)),
1986 Value::Date(dt) => ValueKind::Date(date_time_to_semantic(dt)),
1987 Value::Time(t) => ValueKind::Time(time_to_semantic(t)),
1988 Value::Duration(n, u) => ValueKind::Duration(*n, duration_unit_to_semantic(u)),
1989 Value::Scale(n, unit) => ValueKind::Scale(*n, unit.clone()),
1990 Value::Ratio(n, unit) => ValueKind::Ratio(*n, unit.clone()),
1991 })
1992}
1993
1994pub(crate) fn date_time_to_semantic(dt: &crate::parsing::ast::DateTimeValue) -> SemanticDateTime {
1996 SemanticDateTime {
1997 year: dt.year,
1998 month: dt.month,
1999 day: dt.day,
2000 hour: dt.hour,
2001 minute: dt.minute,
2002 second: dt.second,
2003 microsecond: dt.microsecond,
2004 timezone: dt.timezone.as_ref().map(|tz| SemanticTimezone {
2005 offset_hours: tz.offset_hours,
2006 offset_minutes: tz.offset_minutes,
2007 }),
2008 }
2009}
2010
2011pub(crate) fn time_to_semantic(t: &crate::parsing::ast::TimeValue) -> SemanticTime {
2013 SemanticTime {
2014 hour: t.hour.into(),
2015 minute: t.minute.into(),
2016 second: t.second.into(),
2017 timezone: t.timezone.as_ref().map(|tz| SemanticTimezone {
2018 offset_hours: tz.offset_hours,
2019 offset_minutes: tz.offset_minutes,
2020 }),
2021 }
2022}
2023
2024pub(crate) fn duration_unit_to_semantic(
2026 u: &crate::parsing::ast::DurationUnit,
2027) -> SemanticDurationUnit {
2028 use crate::parsing::ast::DurationUnit as DU;
2029 match u {
2030 DU::Year => SemanticDurationUnit::Year,
2031 DU::Month => SemanticDurationUnit::Month,
2032 DU::Week => SemanticDurationUnit::Week,
2033 DU::Day => SemanticDurationUnit::Day,
2034 DU::Hour => SemanticDurationUnit::Hour,
2035 DU::Minute => SemanticDurationUnit::Minute,
2036 DU::Second => SemanticDurationUnit::Second,
2037 DU::Millisecond => SemanticDurationUnit::Millisecond,
2038 DU::Microsecond => SemanticDurationUnit::Microsecond,
2039 }
2040}
2041
2042pub fn conversion_target_to_semantic(
2049 ct: &ConversionTarget,
2050 unit_index: Option<&HashMap<String, (LemmaType, Option<crate::parsing::ast::TypeDef>)>>,
2051) -> Result<SemanticConversionTarget, String> {
2052 match ct {
2053 ConversionTarget::Duration(u) => Ok(SemanticConversionTarget::Duration(
2054 duration_unit_to_semantic(u),
2055 )),
2056 ConversionTarget::Unit(name) => {
2057 let index = unit_index.ok_or_else(|| {
2058 "Unit conversion requires type resolution; unit index not available.".to_string()
2059 })?;
2060 let (lemma_type, _) = index.get(name).ok_or_else(|| {
2061 format!(
2062 "Unknown unit '{}'. Unit must be defined by a scale or ratio type.",
2063 name
2064 )
2065 })?;
2066 if lemma_type.is_ratio() {
2067 Ok(SemanticConversionTarget::RatioUnit(name.clone()))
2068 } else if lemma_type.is_scale() {
2069 Ok(SemanticConversionTarget::ScaleUnit(name.clone()))
2070 } else {
2071 Err(format!(
2072 "Unit '{}' is not a ratio or scale type; cannot use it in conversion.",
2073 name
2074 ))
2075 }
2076 }
2077 }
2078}
2079
2080static PRIMITIVE_BOOLEAN: OnceLock<LemmaType> = OnceLock::new();
2086static PRIMITIVE_SCALE: OnceLock<LemmaType> = OnceLock::new();
2087static PRIMITIVE_NUMBER: OnceLock<LemmaType> = OnceLock::new();
2088static PRIMITIVE_TEXT: OnceLock<LemmaType> = OnceLock::new();
2089static PRIMITIVE_DATE: OnceLock<LemmaType> = OnceLock::new();
2090static PRIMITIVE_TIME: OnceLock<LemmaType> = OnceLock::new();
2091static PRIMITIVE_DURATION: OnceLock<LemmaType> = OnceLock::new();
2092static PRIMITIVE_RATIO: OnceLock<LemmaType> = OnceLock::new();
2093
2094#[must_use]
2096pub fn primitive_boolean() -> &'static LemmaType {
2097 PRIMITIVE_BOOLEAN.get_or_init(|| LemmaType::primitive(TypeSpecification::boolean()))
2098}
2099
2100#[must_use]
2101pub fn primitive_scale() -> &'static LemmaType {
2102 PRIMITIVE_SCALE.get_or_init(|| LemmaType::primitive(TypeSpecification::scale()))
2103}
2104
2105#[must_use]
2106pub fn primitive_number() -> &'static LemmaType {
2107 PRIMITIVE_NUMBER.get_or_init(|| LemmaType::primitive(TypeSpecification::number()))
2108}
2109
2110#[must_use]
2111pub fn primitive_text() -> &'static LemmaType {
2112 PRIMITIVE_TEXT.get_or_init(|| LemmaType::primitive(TypeSpecification::text()))
2113}
2114
2115#[must_use]
2116pub fn primitive_date() -> &'static LemmaType {
2117 PRIMITIVE_DATE.get_or_init(|| LemmaType::primitive(TypeSpecification::date()))
2118}
2119
2120#[must_use]
2121pub fn primitive_time() -> &'static LemmaType {
2122 PRIMITIVE_TIME.get_or_init(|| LemmaType::primitive(TypeSpecification::time()))
2123}
2124
2125#[must_use]
2126pub fn primitive_duration() -> &'static LemmaType {
2127 PRIMITIVE_DURATION.get_or_init(|| LemmaType::primitive(TypeSpecification::duration()))
2128}
2129
2130#[must_use]
2131pub fn primitive_ratio() -> &'static LemmaType {
2132 PRIMITIVE_RATIO.get_or_init(|| LemmaType::primitive(TypeSpecification::ratio()))
2133}
2134
2135#[must_use]
2137pub fn type_spec_for_primitive(kind: PrimitiveKind) -> TypeSpecification {
2138 match kind {
2139 PrimitiveKind::Boolean => TypeSpecification::boolean(),
2140 PrimitiveKind::Scale => TypeSpecification::scale(),
2141 PrimitiveKind::Number => TypeSpecification::number(),
2142 PrimitiveKind::Percent | PrimitiveKind::Ratio => TypeSpecification::ratio(),
2143 PrimitiveKind::Text => TypeSpecification::text(),
2144 PrimitiveKind::Date => TypeSpecification::date(),
2145 PrimitiveKind::Time => TypeSpecification::time(),
2146 PrimitiveKind::Duration => TypeSpecification::duration(),
2147 }
2148}
2149
2150impl fmt::Display for PathSegment {
2155 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2156 write!(f, "{} → {}", self.fact, self.spec)
2157 }
2158}
2159
2160impl fmt::Display for FactPath {
2161 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2162 for segment in &self.segments {
2163 write!(f, "{}.", segment)?;
2164 }
2165 write!(f, "{}", self.fact)
2166 }
2167}
2168
2169impl fmt::Display for RulePath {
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.rule)
2175 }
2176}
2177
2178impl fmt::Display for LemmaType {
2179 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2180 write!(f, "{}", self.name())
2181 }
2182}
2183
2184impl fmt::Display for LiteralValue {
2185 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2186 match &self.value {
2187 ValueKind::Scale(n, u) => {
2188 if let TypeSpecification::Scale { decimals, .. } = &self.lemma_type.specifications {
2189 let s = match decimals {
2190 Some(d) => {
2191 let dp = u32::from(*d);
2192 let rounded = n.round_dp(dp);
2193 format!("{:.prec$}", rounded, prec = *d as usize)
2194 }
2195 None => n.normalize().to_string(),
2196 };
2197 return write!(f, "{} {}", s, u);
2198 }
2199 write!(f, "{}", self.value)
2200 }
2201 ValueKind::Ratio(r, Some(unit_name)) => {
2202 if let TypeSpecification::Ratio { units, .. } = &self.lemma_type.specifications {
2203 if let Ok(unit) = units.get(unit_name) {
2204 let display_value = (*r * unit.value).normalize();
2205 let s = if display_value.fract().is_zero() {
2206 display_value.trunc().to_string()
2207 } else {
2208 display_value.to_string()
2209 };
2210 return match unit_name.as_str() {
2212 "percent" => write!(f, "{}%", s),
2213 "permille" => write!(f, "{}%%", s),
2214 _ => write!(f, "{} {}", s, unit_name),
2215 };
2216 }
2217 }
2218 write!(f, "{}", self.value)
2219 }
2220 _ => write!(f, "{}", self.value),
2221 }
2222 }
2223}
2224
2225#[cfg(test)]
2230mod tests {
2231 use super::*;
2232 use crate::parsing::ast::{BooleanValue, DateTimeValue, DurationUnit, LemmaSpec, TimeValue};
2233 use rust_decimal::Decimal;
2234 use std::str::FromStr;
2235 use std::sync::Arc;
2236
2237 #[test]
2238 fn test_negated_comparison() {
2239 assert_eq!(
2240 negated_comparison(ComparisonComputation::LessThan),
2241 ComparisonComputation::GreaterThanOrEqual
2242 );
2243 assert_eq!(
2244 negated_comparison(ComparisonComputation::GreaterThanOrEqual),
2245 ComparisonComputation::LessThan
2246 );
2247 assert_eq!(
2248 negated_comparison(ComparisonComputation::Is),
2249 ComparisonComputation::IsNot
2250 );
2251 assert_eq!(
2252 negated_comparison(ComparisonComputation::IsNot),
2253 ComparisonComputation::Is
2254 );
2255 }
2256
2257 #[test]
2258 fn test_literal_value_to_primitive_type() {
2259 let one = Decimal::from_str("1").unwrap();
2260
2261 assert_eq!(LiteralValue::text("".to_string()).lemma_type.name(), "text");
2262 assert_eq!(LiteralValue::number(one).lemma_type.name(), "number");
2263 assert_eq!(
2264 LiteralValue::from_bool(bool::from(BooleanValue::True))
2265 .lemma_type
2266 .name(),
2267 "boolean"
2268 );
2269
2270 let dt = DateTimeValue {
2271 year: 2024,
2272 month: 1,
2273 day: 1,
2274 hour: 0,
2275 minute: 0,
2276 second: 0,
2277 microsecond: 0,
2278 timezone: None,
2279 };
2280 assert_eq!(
2281 LiteralValue::date(date_time_to_semantic(&dt))
2282 .lemma_type
2283 .name(),
2284 "date"
2285 );
2286 assert_eq!(
2287 LiteralValue::ratio(one / Decimal::from(100), Some("percent".to_string()))
2288 .lemma_type
2289 .name(),
2290 "ratio"
2291 );
2292 assert_eq!(
2293 LiteralValue::duration(one, duration_unit_to_semantic(&DurationUnit::Second))
2294 .lemma_type
2295 .name(),
2296 "duration"
2297 );
2298 }
2299
2300 #[test]
2301 fn test_spec_type_display() {
2302 assert_eq!(format!("{}", primitive_text()), "text");
2303 assert_eq!(format!("{}", primitive_number()), "number");
2304 assert_eq!(format!("{}", primitive_date()), "date");
2305 assert_eq!(format!("{}", primitive_boolean()), "boolean");
2306 assert_eq!(format!("{}", primitive_duration()), "duration");
2307 }
2308
2309 #[test]
2310 fn test_type_constructor() {
2311 let specs = TypeSpecification::number();
2312 let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
2313 assert_eq!(lemma_type.name(), "dice");
2314 }
2315
2316 #[test]
2317 fn test_type_display() {
2318 let specs = TypeSpecification::text();
2319 let lemma_type = LemmaType::new("name".to_string(), specs, TypeExtends::Primitive);
2320 assert_eq!(format!("{}", lemma_type), "name");
2321 }
2322
2323 #[test]
2324 fn test_type_equality() {
2325 let specs1 = TypeSpecification::number();
2326 let specs2 = TypeSpecification::number();
2327 let lemma_type1 = LemmaType::new("dice".to_string(), specs1, TypeExtends::Primitive);
2328 let lemma_type2 = LemmaType::new("dice".to_string(), specs2, TypeExtends::Primitive);
2329 assert_eq!(lemma_type1, lemma_type2);
2330 }
2331
2332 #[test]
2333 fn test_type_serialization() {
2334 let specs = TypeSpecification::number();
2335 let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
2336 let serialized = serde_json::to_string(&lemma_type).unwrap();
2337 let deserialized: LemmaType = serde_json::from_str(&serialized).unwrap();
2338 assert_eq!(lemma_type, deserialized);
2339 }
2340
2341 #[test]
2342 fn test_literal_value_display_value() {
2343 let ten = Decimal::from_str("10").unwrap();
2344
2345 assert_eq!(
2346 LiteralValue::text("hello".to_string()).display_value(),
2347 "hello"
2348 );
2349 assert_eq!(LiteralValue::number(ten).display_value(), "10");
2350 assert_eq!(LiteralValue::from_bool(true).display_value(), "true");
2351 assert_eq!(LiteralValue::from_bool(false).display_value(), "false");
2352
2353 let ten_percent_ratio = LiteralValue::ratio(
2355 Decimal::from_str("0.10").unwrap(),
2356 Some("percent".to_string()),
2357 );
2358 assert_eq!(ten_percent_ratio.display_value(), "10%");
2359
2360 let time = TimeValue {
2361 hour: 14,
2362 minute: 30,
2363 second: 0,
2364 timezone: None,
2365 };
2366 let time_display = LiteralValue::time(time_to_semantic(&time)).display_value();
2367 assert!(time_display.contains("14"));
2368 assert!(time_display.contains("30"));
2369 }
2370
2371 #[test]
2372 fn test_scale_display_respects_type_decimals() {
2373 let money_type = LemmaType {
2374 name: Some("money".to_string()),
2375 specifications: TypeSpecification::Scale {
2376 minimum: None,
2377 maximum: None,
2378 decimals: Some(2),
2379 precision: None,
2380 units: ScaleUnits::from(vec![ScaleUnit {
2381 name: "eur".to_string(),
2382 value: Decimal::from(1),
2383 }]),
2384 help: String::new(),
2385 default: None,
2386 },
2387 extends: TypeExtends::Primitive,
2388 };
2389 let val = LiteralValue::scale_with_type(
2390 Decimal::from_str("1.8").unwrap(),
2391 "eur".to_string(),
2392 money_type.clone(),
2393 );
2394 assert_eq!(val.display_value(), "1.80 eur");
2395 let more_precision = LiteralValue::scale_with_type(
2396 Decimal::from_str("1.80000").unwrap(),
2397 "eur".to_string(),
2398 money_type,
2399 );
2400 assert_eq!(more_precision.display_value(), "1.80 eur");
2401 let scale_no_decimals = LemmaType {
2402 name: Some("count".to_string()),
2403 specifications: TypeSpecification::Scale {
2404 minimum: None,
2405 maximum: None,
2406 decimals: None,
2407 precision: None,
2408 units: ScaleUnits::from(vec![ScaleUnit {
2409 name: "items".to_string(),
2410 value: Decimal::from(1),
2411 }]),
2412 help: String::new(),
2413 default: None,
2414 },
2415 extends: TypeExtends::Primitive,
2416 };
2417 let val_any = LiteralValue::scale_with_type(
2418 Decimal::from_str("42.50").unwrap(),
2419 "items".to_string(),
2420 scale_no_decimals,
2421 );
2422 assert_eq!(val_any.display_value(), "42.5 items");
2423 }
2424
2425 #[test]
2426 fn test_literal_value_time_type() {
2427 let time = TimeValue {
2428 hour: 14,
2429 minute: 30,
2430 second: 0,
2431 timezone: None,
2432 };
2433 let lit = LiteralValue::time(time_to_semantic(&time));
2434 assert_eq!(lit.lemma_type.name(), "time");
2435 }
2436
2437 #[test]
2438 fn test_scale_family_name_primitive_root() {
2439 let scale_spec = TypeSpecification::scale();
2440 let money_primitive = LemmaType::new(
2441 "money".to_string(),
2442 scale_spec.clone(),
2443 TypeExtends::Primitive,
2444 );
2445 assert_eq!(money_primitive.scale_family_name(), Some("money"));
2446 }
2447
2448 #[test]
2449 fn test_scale_family_name_custom() {
2450 let scale_spec = TypeSpecification::scale();
2451 let money_custom = LemmaType::new(
2452 "money".to_string(),
2453 scale_spec,
2454 TypeExtends::custom_local("money".to_string(), "money".to_string()),
2455 );
2456 assert_eq!(money_custom.scale_family_name(), Some("money"));
2457 }
2458
2459 #[test]
2460 fn test_same_scale_family_same_name_different_extends() {
2461 let scale_spec = TypeSpecification::scale();
2462 let money_primitive = LemmaType::new(
2463 "money".to_string(),
2464 scale_spec.clone(),
2465 TypeExtends::Primitive,
2466 );
2467 let money_custom = LemmaType::new(
2468 "money".to_string(),
2469 scale_spec,
2470 TypeExtends::custom_local("money".to_string(), "money".to_string()),
2471 );
2472 assert!(money_primitive.same_scale_family(&money_custom));
2473 assert!(money_custom.same_scale_family(&money_primitive));
2474 }
2475
2476 #[test]
2477 fn test_same_scale_family_parent_and_child() {
2478 let scale_spec = TypeSpecification::scale();
2479 let type_x = LemmaType::new("x".to_string(), scale_spec.clone(), TypeExtends::Primitive);
2480 let type_x2 = LemmaType::new(
2481 "x2".to_string(),
2482 scale_spec,
2483 TypeExtends::custom_local("x".to_string(), "x".to_string()),
2484 );
2485 assert_eq!(type_x.scale_family_name(), Some("x"));
2486 assert_eq!(type_x2.scale_family_name(), Some("x"));
2487 assert!(type_x.same_scale_family(&type_x2));
2488 assert!(type_x2.same_scale_family(&type_x));
2489 }
2490
2491 #[test]
2492 fn test_same_scale_family_siblings() {
2493 let scale_spec = TypeSpecification::scale();
2494 let type_x2_a = LemmaType::new(
2495 "x2a".to_string(),
2496 scale_spec.clone(),
2497 TypeExtends::custom_local("x".to_string(), "x".to_string()),
2498 );
2499 let type_x2_b = LemmaType::new(
2500 "x2b".to_string(),
2501 scale_spec,
2502 TypeExtends::custom_local("x".to_string(), "x".to_string()),
2503 );
2504 assert!(type_x2_a.same_scale_family(&type_x2_b));
2505 }
2506
2507 #[test]
2508 fn test_same_scale_family_different_families() {
2509 let scale_spec = TypeSpecification::scale();
2510 let money = LemmaType::new(
2511 "money".to_string(),
2512 scale_spec.clone(),
2513 TypeExtends::Primitive,
2514 );
2515 let temperature = LemmaType::new(
2516 "temperature".to_string(),
2517 scale_spec,
2518 TypeExtends::Primitive,
2519 );
2520 assert!(!money.same_scale_family(&temperature));
2521 assert!(!temperature.same_scale_family(&money));
2522 }
2523
2524 #[test]
2525 fn test_same_scale_family_scale_vs_non_scale() {
2526 let scale_spec = TypeSpecification::scale();
2527 let number_spec = TypeSpecification::number();
2528 let scale_type = LemmaType::new("money".to_string(), scale_spec, TypeExtends::Primitive);
2529 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
2530 assert!(!scale_type.same_scale_family(&number_type));
2531 assert!(!number_type.same_scale_family(&scale_type));
2532 }
2533
2534 #[test]
2535 fn test_scale_family_name_non_scale_returns_none() {
2536 let number_spec = TypeSpecification::number();
2537 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
2538 assert_eq!(number_type.scale_family_name(), None);
2539 }
2540
2541 #[test]
2542 fn test_explicit_value_returns_none_for_default() {
2543 let source = crate::Source::new(
2544 "test.lemma",
2545 crate::parsing::ast::Span {
2546 start: 0,
2547 end: 1,
2548 line: 1,
2549 col: 0,
2550 },
2551 );
2552 let fact = FactData::Value {
2553 value: LiteralValue::number(Decimal::from(25)),
2554 source: source.clone(),
2555 is_default: true,
2556 };
2557 assert!(
2558 fact.explicit_value().is_none(),
2559 "is_default=true should yield None from explicit_value()"
2560 );
2561 assert!(
2562 fact.value().is_some(),
2563 "value() should still return the value regardless of is_default"
2564 );
2565 }
2566
2567 #[test]
2568 fn test_explicit_value_returns_some_for_non_default() {
2569 let source = crate::Source::new(
2570 "test.lemma",
2571 crate::parsing::ast::Span {
2572 start: 0,
2573 end: 1,
2574 line: 1,
2575 col: 0,
2576 },
2577 );
2578 let fact = FactData::Value {
2579 value: LiteralValue::number(Decimal::from(42)),
2580 source,
2581 is_default: false,
2582 };
2583 assert!(
2584 fact.explicit_value().is_some(),
2585 "is_default=false should yield Some from explicit_value()"
2586 );
2587 assert_eq!(
2588 fact.explicit_value().unwrap().value,
2589 ValueKind::Number(Decimal::from(42))
2590 );
2591 }
2592
2593 #[test]
2594 fn test_explicit_value_returns_none_for_type_declaration() {
2595 let source = crate::Source::new(
2596 "test.lemma",
2597 crate::parsing::ast::Span {
2598 start: 0,
2599 end: 1,
2600 line: 1,
2601 col: 0,
2602 },
2603 );
2604 let fact = FactData::TypeDeclaration {
2605 resolved_type: primitive_number().clone(),
2606 source,
2607 };
2608 assert!(
2609 fact.explicit_value().is_none(),
2610 "TypeDeclaration should yield None from explicit_value()"
2611 );
2612 }
2613
2614 #[test]
2615 fn test_lemma_type_inequality_local_vs_import_same_shape() {
2616 let dep = Arc::new(LemmaSpec::new("dep".to_string()));
2617 let scale_spec = TypeSpecification::scale();
2618 let local = LemmaType::new(
2619 "t".to_string(),
2620 scale_spec.clone(),
2621 TypeExtends::custom_local("money".to_string(), "money".to_string()),
2622 );
2623 let imported = LemmaType::new(
2624 "t".to_string(),
2625 scale_spec,
2626 TypeExtends::Custom {
2627 parent: "money".to_string(),
2628 family: "money".to_string(),
2629 defining_spec: TypeDefiningSpec::Import {
2630 spec: Arc::clone(&dep),
2631 resolved_plan_hash: "a1b2c3d4".to_string(),
2632 },
2633 },
2634 );
2635 assert_ne!(local, imported);
2636 }
2637
2638 #[test]
2639 fn test_lemma_type_equality_import_same_resolved_spec_semantics() {
2640 let spec_a = Arc::new(LemmaSpec::new("dep".to_string()));
2641 let spec_b = Arc::new(LemmaSpec::new("dep".to_string()));
2642 assert!(is_same_spec(spec_a.as_ref(), spec_b.as_ref()));
2643 let scale_spec = TypeSpecification::scale();
2644 let left = LemmaType::new(
2645 "t".to_string(),
2646 scale_spec.clone(),
2647 TypeExtends::Custom {
2648 parent: "money".to_string(),
2649 family: "money".to_string(),
2650 defining_spec: TypeDefiningSpec::Import {
2651 spec: Arc::clone(&spec_a),
2652 resolved_plan_hash: "a1b2c3d4".to_string(),
2653 },
2654 },
2655 );
2656 let right = LemmaType::new(
2657 "t".to_string(),
2658 scale_spec,
2659 TypeExtends::Custom {
2660 parent: "money".to_string(),
2661 family: "money".to_string(),
2662 defining_spec: TypeDefiningSpec::Import {
2663 spec: spec_b,
2664 resolved_plan_hash: "a1b2c3d4".to_string(),
2665 },
2666 },
2667 );
2668 assert_eq!(left, right);
2669 }
2670}