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::Constraint;
38use crate::parsing::ast::{
39 BooleanValue, CalendarUnit, CommandArg, ConversionTarget, DateCalendarKind, DateRelativeKind,
40 DateTimeValue, DurationUnit, LemmaSpec, PrimitiveKind, TimeValue, TypeConstraintCommand,
41};
42use crate::Error;
43use rust_decimal::Decimal;
44use serde::{Deserialize, Serialize};
45use std::collections::HashMap;
46use std::fmt;
47use std::hash::Hash;
48use std::sync::{Arc, OnceLock};
49
50pub use crate::literals::{RatioUnit, RatioUnits, ScaleUnit, ScaleUnits};
57
58#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
59#[serde(tag = "kind", rename_all = "lowercase")]
60pub enum TypeSpecification {
61 Boolean {
62 help: String,
63 },
64 Scale {
65 minimum: Option<Decimal>,
66 maximum: Option<Decimal>,
67 decimals: Option<u8>,
68 precision: Option<Decimal>,
69 units: ScaleUnits,
70 help: String,
71 },
72 Number {
73 minimum: Option<Decimal>,
74 maximum: Option<Decimal>,
75 decimals: Option<u8>,
76 precision: Option<Decimal>,
77 help: String,
78 },
79 Ratio {
80 minimum: Option<Decimal>,
81 maximum: Option<Decimal>,
82 decimals: Option<u8>,
83 units: RatioUnits,
84 help: String,
85 },
86 Text {
87 length: Option<usize>,
88 options: Vec<String>,
89 help: String,
90 },
91 Date {
92 minimum: Option<DateTimeValue>,
93 maximum: Option<DateTimeValue>,
94 help: String,
95 },
96 Time {
97 minimum: Option<TimeValue>,
98 maximum: Option<TimeValue>,
99 help: String,
100 },
101 Duration {
102 minimum: Option<(Decimal, SemanticDurationUnit)>,
103 maximum: Option<(Decimal, SemanticDurationUnit)>,
104 help: String,
105 },
106 Veto {
107 message: Option<String>,
108 },
109 Undetermined,
113}
114
115fn require_literal<'a>(
121 args: &'a [CommandArg],
122 cmd: &str,
123) -> Result<&'a crate::literals::Value, String> {
124 let arg = args
125 .first()
126 .ok_or_else(|| format!("{} requires an argument", cmd))?;
127 match arg {
128 CommandArg::Literal(v) => Ok(v),
129 CommandArg::Label(name) => Err(format!(
130 "{} requires a literal value, got identifier '{}'",
131 cmd, name
132 )),
133 }
134}
135
136fn apply_type_help_command(help: &mut String, args: &[CommandArg]) -> Result<(), String> {
137 match require_literal(args, "help")? {
138 crate::literals::Value::Text(s) => {
139 *help = s.clone();
140 Ok(())
141 }
142 other => Err(format!(
143 "help requires a text literal (quoted string), got {}",
144 value_kind_name(other)
145 )),
146 }
147}
148
149fn value_kind_name(v: &crate::literals::Value) -> &'static str {
151 use crate::literals::Value;
152 match v {
153 Value::Number(_) => "number",
154 Value::Scale(_, _) => "scale",
155 Value::Text(_) => "text",
156 Value::Date(_) => "date",
157 Value::Time(_) => "time",
158 Value::Boolean(_) => "boolean",
159 Value::Duration(_, _) => "duration",
160 Value::Ratio(_, _) => "ratio",
161 }
162}
163
164fn decimal_to_u8(d: Decimal, ctx: &str) -> Result<u8, String> {
166 use rust_decimal::prelude::ToPrimitive;
167 if !d.fract().is_zero() {
168 return Err(format!(
169 "{} requires a whole number, got fractional value {}",
170 ctx, d
171 ));
172 }
173 d.to_u8()
174 .ok_or_else(|| format!("{} value out of range for u8: {}", ctx, d))
175}
176
177fn decimal_to_usize(d: Decimal, ctx: &str) -> Result<usize, String> {
179 use rust_decimal::prelude::ToPrimitive;
180 if !d.fract().is_zero() {
181 return Err(format!(
182 "{} requires a whole number, got fractional value {}",
183 ctx, d
184 ));
185 }
186 d.to_usize()
187 .ok_or_else(|| format!("{} value out of range for usize: {}", ctx, d))
188}
189
190fn require_decimal_literal(args: &[CommandArg], cmd: &str) -> Result<Decimal, String> {
196 match require_literal(args, cmd)? {
197 crate::literals::Value::Number(d) => Ok(*d),
198 other => Err(format!(
199 "{} requires a number literal, got {}",
200 cmd,
201 value_kind_name(other)
202 )),
203 }
204}
205
206fn require_scale_literal(
215 args: &[CommandArg],
216 units: &ScaleUnits,
217 cmd: &str,
218) -> Result<Decimal, String> {
219 use crate::literals::Value;
220 match require_literal(args, cmd)? {
221 Value::Scale(d, unit_name) => {
222 let unit = units.get(unit_name)?;
223 Ok(*d * unit.value)
224 }
225 Value::Number(d) => Ok(*d),
226 other => Err(format!(
227 "{} requires a scale or number literal, got {}",
228 cmd,
229 value_kind_name(other)
230 )),
231 }
232}
233
234fn require_ratio_literal(args: &[CommandArg], cmd: &str) -> Result<Decimal, String> {
244 use crate::literals::Value;
245 match require_literal(args, cmd)? {
246 Value::Ratio(d, _) => Ok(*d),
247 Value::Number(d) => Ok(*d),
248 other => Err(format!(
249 "{} requires a ratio or number literal, got {}",
250 cmd,
251 value_kind_name(other)
252 )),
253 }
254}
255
256fn option_name(arg: &CommandArg, cmd: &str) -> Result<String, String> {
262 match arg {
263 CommandArg::Literal(crate::literals::Value::Text(s)) => Ok(s.clone()),
264 CommandArg::Label(name) => Ok(name.clone()),
265 CommandArg::Literal(other) => Err(format!(
266 "{} requires a text literal or identifier, got {}",
267 cmd,
268 value_kind_name(other)
269 )),
270 }
271}
272
273fn require_date_literal(args: &[CommandArg], cmd: &str) -> Result<DateTimeValue, String> {
275 match require_literal(args, cmd)? {
276 crate::literals::Value::Date(dt) => Ok(dt.clone()),
277 other => Err(format!(
278 "{} requires a date literal (e.g. 2024-01-01), got {}",
279 cmd,
280 value_kind_name(other)
281 )),
282 }
283}
284
285fn require_time_literal(args: &[CommandArg], cmd: &str) -> Result<TimeValue, String> {
287 match require_literal(args, cmd)? {
288 crate::literals::Value::Time(t) => Ok(t.clone()),
289 other => Err(format!(
290 "{} requires a time literal (e.g. 12:30:00), got {}",
291 cmd,
292 value_kind_name(other)
293 )),
294 }
295}
296
297fn require_duration_literal(
299 args: &[CommandArg],
300 cmd: &str,
301) -> Result<(Decimal, DurationUnit), String> {
302 match require_literal(args, cmd)? {
303 crate::literals::Value::Duration(d, unit) => Ok((*d, unit.clone())),
304 other => Err(format!(
305 "{} requires a duration literal (e.g. 1 day), got {}",
306 cmd,
307 value_kind_name(other)
308 )),
309 }
310}
311
312impl TypeSpecification {
313 pub fn boolean() -> Self {
314 TypeSpecification::Boolean {
315 help: "Values: true, false".to_string(),
316 }
317 }
318 pub fn scale() -> Self {
319 TypeSpecification::Scale {
320 minimum: None,
321 maximum: None,
322 decimals: None,
323 precision: None,
324 units: ScaleUnits::new(),
325 help: "Format: {value} {unit} (e.g. 100 kilograms)".to_string(),
326 }
327 }
328 pub fn number() -> Self {
329 TypeSpecification::Number {
330 minimum: None,
331 maximum: None,
332 decimals: None,
333 precision: None,
334 help: "Numeric value".to_string(),
335 }
336 }
337 pub fn ratio() -> Self {
338 TypeSpecification::Ratio {
339 minimum: None,
340 maximum: None,
341 decimals: None,
342 units: RatioUnits(vec![
343 RatioUnit {
344 name: "percent".to_string(),
345 value: Decimal::from(100),
346 },
347 RatioUnit {
348 name: "permille".to_string(),
349 value: Decimal::from(1000),
350 },
351 ]),
352 help: "Format: {value} {unit} (e.g. 21 percent)".to_string(),
353 }
354 }
355 pub fn text() -> Self {
356 TypeSpecification::Text {
357 length: None,
358 options: vec![],
359 help: "Text value".to_string(),
360 }
361 }
362 pub fn date() -> Self {
363 TypeSpecification::Date {
364 minimum: None,
365 maximum: None,
366 help: "Format: YYYY-MM-DD (e.g. 2024-01-15)".to_string(),
367 }
368 }
369 pub fn time() -> Self {
370 TypeSpecification::Time {
371 minimum: None,
372 maximum: None,
373 help: "Format: HH:MM:SS (e.g. 14:30:00)".to_string(),
374 }
375 }
376 pub fn duration() -> Self {
377 TypeSpecification::Duration {
378 minimum: None,
379 maximum: None,
380 help: "Format: {value} {unit} (e.g. 40 hours). Units: years, months, weeks, days, hours, minutes, seconds".to_string(),
381 }
382 }
383 pub fn veto() -> Self {
384 TypeSpecification::Veto { message: None }
385 }
386
387 pub fn apply_constraint(
395 mut self,
396 command: TypeConstraintCommand,
397 args: &[CommandArg],
398 declared_default: &mut Option<ValueKind>,
399 ) -> Result<Self, String> {
400 match &mut self {
401 TypeSpecification::Boolean { help } => match command {
402 TypeConstraintCommand::Help => {
403 apply_type_help_command(help, args)?;
404 }
405 TypeConstraintCommand::Default => match require_literal(args, "default")? {
406 crate::literals::Value::Boolean(bv) => {
407 *declared_default = Some(ValueKind::Boolean(bool::from(*bv)));
408 }
409 other => {
410 return Err(format!(
411 "default for boolean type requires a boolean literal (true/false/yes/no/accept/reject), got {}",
412 value_kind_name(other)
413 ));
414 }
415 },
416 other => {
417 return Err(format!(
418 "Invalid command '{}' for boolean type. Valid commands: help, default",
419 other
420 ));
421 }
422 },
423 TypeSpecification::Scale {
424 decimals,
425 minimum,
426 maximum,
427 precision,
428 units,
429 help,
430 } => match command {
431 TypeConstraintCommand::Decimals => {
432 let d = require_decimal_literal(args, "decimals")?;
433 *decimals = Some(decimal_to_u8(d, "decimals")?);
434 }
435 TypeConstraintCommand::Unit => {
436 let (unit_name, value) = match args {
437 [CommandArg::Label(name), CommandArg::Literal(crate::literals::Value::Number(v))] => {
438 (name.clone(), *v)
439 }
440 _ => {
441 return Err(
442 "unit requires a unit name followed by a numeric conversion factor (e.g., 'unit eur 1.00')"
443 .to_string(),
444 );
445 }
446 };
447 if units.iter().any(|u| u.name == unit_name) {
448 return Err(format!(
449 "Unit '{}' is already defined in this scale type.",
450 unit_name
451 ));
452 }
453 units.0.push(ScaleUnit {
454 name: unit_name,
455 value,
456 });
457 }
458 TypeConstraintCommand::Minimum => {
459 *minimum = Some(require_scale_literal(args, units, "minimum")?);
460 }
461 TypeConstraintCommand::Maximum => {
462 *maximum = Some(require_scale_literal(args, units, "maximum")?);
463 }
464 TypeConstraintCommand::Precision => {
465 *precision = Some(require_scale_literal(args, units, "precision")?);
466 }
467 TypeConstraintCommand::Help => {
468 apply_type_help_command(help, args)?;
469 }
470 TypeConstraintCommand::Default => match require_literal(args, "default")? {
471 crate::literals::Value::Scale(value, unit_name) => {
472 *declared_default = Some(ValueKind::Scale(*value, unit_name.clone()));
473 }
474 other => {
475 return Err(format!(
476 "default for scale type requires a scale literal '{{value}} {{unit}}' (e.g. '1 kilogram'), got {}",
477 value_kind_name(other)
478 ));
479 }
480 },
481 _ => {
482 return Err(format!(
483 "Invalid command '{}' for scale type. Valid commands: unit, minimum, maximum, decimals, precision, help, default",
484 command
485 ));
486 }
487 },
488 TypeSpecification::Number {
489 decimals,
490 minimum,
491 maximum,
492 precision,
493 help,
494 } => match command {
495 TypeConstraintCommand::Decimals => {
496 let d = require_decimal_literal(args, "decimals")?;
497 *decimals = Some(decimal_to_u8(d, "decimals")?);
498 }
499 TypeConstraintCommand::Unit => {
500 return Err(
501 "Invalid command 'unit' for number type. Number types are dimensionless and cannot have units. Use 'scale' type instead.".to_string()
502 );
503 }
504 TypeConstraintCommand::Minimum => {
505 *minimum = Some(require_decimal_literal(args, "minimum")?);
506 }
507 TypeConstraintCommand::Maximum => {
508 *maximum = Some(require_decimal_literal(args, "maximum")?);
509 }
510 TypeConstraintCommand::Precision => {
511 *precision = Some(require_decimal_literal(args, "precision")?);
512 }
513 TypeConstraintCommand::Help => {
514 apply_type_help_command(help, args)?;
515 }
516 TypeConstraintCommand::Default => match require_literal(args, "default")? {
517 crate::literals::Value::Number(d) => {
518 *declared_default = Some(ValueKind::Number(*d));
519 }
520 other => {
521 return Err(format!(
522 "default for number type requires a number literal, got {}",
523 value_kind_name(other)
524 ));
525 }
526 },
527 _ => {
528 return Err(format!(
529 "Invalid command '{}' for number type. Valid commands: minimum, maximum, decimals, precision, help, default",
530 command
531 ));
532 }
533 },
534 TypeSpecification::Ratio {
535 decimals,
536 minimum,
537 maximum,
538 units,
539 help,
540 } => match command {
541 TypeConstraintCommand::Decimals => {
542 let d = require_decimal_literal(args, "decimals")?;
543 *decimals = Some(decimal_to_u8(d, "decimals")?);
544 }
545 TypeConstraintCommand::Unit => {
546 let (unit_name, value) = match args {
547 [CommandArg::Label(name), CommandArg::Literal(crate::literals::Value::Number(v))] => {
548 (name.clone(), *v)
549 }
550 _ => {
551 return Err(
552 "unit requires a unit name followed by a numeric conversion factor (e.g., 'unit percent 100')"
553 .to_string(),
554 );
555 }
556 };
557 if units.iter().any(|u| u.name == unit_name) {
558 return Err(format!(
559 "Unit '{}' is already defined in this ratio type.",
560 unit_name
561 ));
562 }
563 units.0.push(RatioUnit {
564 name: unit_name,
565 value,
566 });
567 }
568 TypeConstraintCommand::Minimum => {
569 *minimum = Some(require_ratio_literal(args, "minimum")?);
570 }
571 TypeConstraintCommand::Maximum => {
572 *maximum = Some(require_ratio_literal(args, "maximum")?);
573 }
574 TypeConstraintCommand::Help => {
575 apply_type_help_command(help, args)?;
576 }
577 TypeConstraintCommand::Default => {
578 let d = require_ratio_literal(args, "default")?;
579 *declared_default = Some(ValueKind::Ratio(d, None));
580 }
581 _ => {
582 return Err(format!(
583 "Invalid command '{}' for ratio type. Valid commands: unit, minimum, maximum, decimals, help, default",
584 command
585 ));
586 }
587 },
588 TypeSpecification::Text {
589 length,
590 options,
591 help,
592 } => match command {
593 TypeConstraintCommand::Option => {
594 if args.len() != 1 {
595 return Err("option takes exactly one argument".to_string());
596 }
597 options.push(option_name(&args[0], "option")?);
598 }
599 TypeConstraintCommand::Options => {
600 let mut collected = Vec::with_capacity(args.len());
601 for arg in args {
602 collected.push(option_name(arg, "options")?);
603 }
604 *options = collected;
605 }
606 TypeConstraintCommand::Length => {
607 let d = require_decimal_literal(args, "length")?;
608 *length = Some(decimal_to_usize(d, "length")?);
609 }
610 TypeConstraintCommand::Help => {
611 apply_type_help_command(help, args)?;
612 }
613 TypeConstraintCommand::Default => match require_literal(args, "default")? {
614 crate::literals::Value::Text(s) => {
615 *declared_default = Some(ValueKind::Text(s.clone()));
616 }
617 other => {
618 return Err(format!(
619 "default for text type requires a text literal (quoted string), got {}",
620 value_kind_name(other)
621 ));
622 }
623 },
624 _ => {
625 return Err(format!(
626 "Invalid command '{}' for text type. Valid commands: options, length, help, default",
627 command
628 ));
629 }
630 },
631 TypeSpecification::Date {
632 minimum,
633 maximum,
634 help,
635 } => match command {
636 TypeConstraintCommand::Minimum => {
637 let dt = require_date_literal(args, "minimum")?;
638 *minimum = Some(dt);
639 }
640 TypeConstraintCommand::Maximum => {
641 let dt = require_date_literal(args, "maximum")?;
642 *maximum = Some(dt);
643 }
644 TypeConstraintCommand::Help => {
645 apply_type_help_command(help, args)?;
646 }
647 TypeConstraintCommand::Default => {
648 let dt = require_date_literal(args, "default")?;
649 *declared_default = Some(ValueKind::Date(date_time_to_semantic(&dt)));
650 }
651 _ => {
652 return Err(format!(
653 "Invalid command '{}' for date type. Valid commands: minimum, maximum, help, default",
654 command
655 ));
656 }
657 },
658 TypeSpecification::Time {
659 minimum,
660 maximum,
661 help,
662 } => match command {
663 TypeConstraintCommand::Minimum => {
664 let t = require_time_literal(args, "minimum")?;
665 *minimum = Some(t);
666 }
667 TypeConstraintCommand::Maximum => {
668 let t = require_time_literal(args, "maximum")?;
669 *maximum = Some(t);
670 }
671 TypeConstraintCommand::Help => {
672 apply_type_help_command(help, args)?;
673 }
674 TypeConstraintCommand::Default => {
675 let t = require_time_literal(args, "default")?;
676 *declared_default = Some(ValueKind::Time(time_to_semantic(&t)));
677 }
678 _ => {
679 return Err(format!(
680 "Invalid command '{}' for time type. Valid commands: minimum, maximum, help, default",
681 command
682 ));
683 }
684 },
685 TypeSpecification::Duration {
686 minimum,
687 maximum,
688 help,
689 } => match command {
690 TypeConstraintCommand::Help => {
691 apply_type_help_command(help, args)?;
692 }
693 TypeConstraintCommand::Minimum => {
694 let (value, unit) = require_duration_literal(args, "minimum")?;
695 *minimum = Some((value, duration_unit_to_semantic(&unit)));
696 }
697 TypeConstraintCommand::Maximum => {
698 let (value, unit) = require_duration_literal(args, "maximum")?;
699 *maximum = Some((value, duration_unit_to_semantic(&unit)));
700 }
701 TypeConstraintCommand::Default => {
702 let (value, unit) = require_duration_literal(args, "default")?;
703 *declared_default =
704 Some(ValueKind::Duration(value, duration_unit_to_semantic(&unit)));
705 }
706 _ => {
707 return Err(format!(
708 "Invalid command '{}' for duration type. Valid commands: minimum, maximum, help, default",
709 command
710 ));
711 }
712 },
713 TypeSpecification::Veto { .. } => {
714 return Err(format!(
715 "Invalid command '{}' for veto type. Veto is not a user-declarable type and cannot have constraints",
716 command
717 ));
718 }
719 TypeSpecification::Undetermined => {
720 return Err(format!(
721 "Invalid command '{}' for undetermined sentinel type. Undetermined is an internal type used during type inference and cannot have constraints",
722 command
723 ));
724 }
725 }
726 Ok(self)
727 }
728}
729
730pub fn parse_number_unit(
733 value_str: &str,
734 type_spec: &TypeSpecification,
735) -> Result<crate::parsing::ast::Value, String> {
736 use crate::literals::{NumberWithUnit, RatioLiteral};
737 use crate::parsing::ast::Value;
738
739 let trimmed = value_str.trim();
740 match type_spec {
741 TypeSpecification::Scale { units, .. } => {
742 if units.is_empty() {
743 unreachable!(
744 "BUG: Scale type has no units; should have been validated during planning"
745 );
746 }
747 match trimmed.parse::<NumberWithUnit>() {
748 Ok(n) => {
749 let unit = units.get(&n.1).map_err(|e| e.to_string())?;
750 Ok(Value::Scale(n.0, unit.name.clone()))
751 }
752 Err(e) => {
753 if trimmed.split_whitespace().count() == 1 && !trimmed.is_empty() {
754 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
755 let example_unit = units
756 .iter()
757 .next()
758 .expect("BUG: units non-empty after guard")
759 .name
760 .as_str();
761 Err(format!(
762 "Scale value must include a unit, for example: '{} {}'. Valid units: {}.",
763 trimmed,
764 example_unit,
765 valid.join(", ")
766 ))
767 } else {
768 Err(e)
769 }
770 }
771 }
772 }
773 TypeSpecification::Ratio { units, .. } => {
774 if units.is_empty() {
775 unreachable!(
776 "BUG: Ratio type has no units; should have been validated during planning"
777 );
778 }
779 match trimmed.parse::<RatioLiteral>()? {
780 RatioLiteral::Bare(n) => Ok(Value::Ratio(n, None)),
781 RatioLiteral::Percent(n) => {
786 let unit = units.get("percent").map_err(|e| e.to_string())?;
787 Ok(Value::Ratio(n, Some(unit.name.clone())))
788 }
789 RatioLiteral::Permille(n) => {
790 let unit = units.get("permille").map_err(|e| e.to_string())?;
791 Ok(Value::Ratio(n, Some(unit.name.clone())))
792 }
793 RatioLiteral::Named { value, unit } => {
794 let resolved = units.get(&unit).map_err(|e| e.to_string())?;
795 Ok(Value::Ratio(
796 value / resolved.value,
797 Some(resolved.name.clone()),
798 ))
799 }
800 }
801 }
802 _ => Err("parse_number_unit only accepts Scale or Ratio type".to_string()),
803 }
804}
805
806pub fn parse_value_from_string(
809 value_str: &str,
810 type_spec: &TypeSpecification,
811 source: &Source,
812) -> Result<crate::parsing::ast::Value, Error> {
813 use crate::parsing::ast::Value;
814
815 let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
816
817 match type_spec {
818 TypeSpecification::Text { .. } => value_str
819 .parse::<crate::literals::TextLiteral>()
820 .map(|t| Value::Text(t.0))
821 .map_err(to_err),
822 TypeSpecification::Number { .. } => value_str
823 .parse::<crate::literals::NumberLiteral>()
824 .map(|n| Value::Number(n.0))
825 .map_err(to_err),
826 TypeSpecification::Scale { .. } => {
827 parse_number_unit(value_str, type_spec).map_err(to_err)
828 }
829 TypeSpecification::Boolean { .. } => value_str
830 .parse::<BooleanValue>()
831 .map(Value::Boolean)
832 .map_err(to_err),
833 TypeSpecification::Date { .. } => {
834 let date = value_str.parse::<DateTimeValue>().map_err(to_err)?;
835 Ok(Value::Date(date))
836 }
837 TypeSpecification::Time { .. } => {
838 let time = value_str.parse::<TimeValue>().map_err(to_err)?;
839 Ok(Value::Time(time))
840 }
841 TypeSpecification::Duration { .. } => value_str
842 .parse::<crate::literals::DurationLiteral>()
843 .map(|d| Value::Duration(d.0, d.1))
844 .map_err(to_err),
845 TypeSpecification::Ratio { .. } => {
846 parse_number_unit(value_str, type_spec).map_err(to_err)
847 }
848 TypeSpecification::Veto { .. } => Err(to_err(
849 "Veto type cannot be parsed from string".to_string(),
850 )),
851 TypeSpecification::Undetermined => unreachable!(
852 "BUG: parse_value_from_string called with Undetermined sentinel type; this type exists only during type inference"
853 ),
854 }
855}
856
857#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
863#[serde(rename_all = "snake_case")]
864pub enum SemanticDurationUnit {
865 Year,
866 Month,
867 Week,
868 Day,
869 Hour,
870 Minute,
871 Second,
872 Millisecond,
873 Microsecond,
874}
875
876impl fmt::Display for SemanticDurationUnit {
877 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
878 let s = match self {
879 SemanticDurationUnit::Year => "years",
880 SemanticDurationUnit::Month => "months",
881 SemanticDurationUnit::Week => "weeks",
882 SemanticDurationUnit::Day => "days",
883 SemanticDurationUnit::Hour => "hours",
884 SemanticDurationUnit::Minute => "minutes",
885 SemanticDurationUnit::Second => "seconds",
886 SemanticDurationUnit::Millisecond => "milliseconds",
887 SemanticDurationUnit::Microsecond => "microseconds",
888 };
889 write!(f, "{}", s)
890 }
891}
892
893#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
897#[serde(rename_all = "snake_case")]
898pub enum SemanticConversionTarget {
899 Duration(SemanticDurationUnit),
900 ScaleUnit(String),
901 RatioUnit(String),
902}
903
904impl fmt::Display for SemanticConversionTarget {
905 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
906 match self {
907 SemanticConversionTarget::Duration(u) => write!(f, "{}", u),
908 SemanticConversionTarget::ScaleUnit(s) => write!(f, "{}", s),
909 SemanticConversionTarget::RatioUnit(s) => write!(f, "{}", s),
910 }
911 }
912}
913
914#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
916pub struct SemanticTimezone {
917 pub offset_hours: i8,
918 pub offset_minutes: u8,
919}
920
921impl fmt::Display for SemanticTimezone {
922 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
923 if self.offset_hours == 0 && self.offset_minutes == 0 {
924 write!(f, "Z")
925 } else {
926 let sign = if self.offset_hours >= 0 { "+" } else { "-" };
927 let hours = self.offset_hours.abs();
928 write!(f, "{}{:02}:{:02}", sign, hours, self.offset_minutes)
929 }
930 }
931}
932
933#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
935pub struct SemanticTime {
936 pub hour: u32,
937 pub minute: u32,
938 pub second: u32,
939 pub timezone: Option<SemanticTimezone>,
940}
941
942impl fmt::Display for SemanticTime {
943 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
944 write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)
945 }
946}
947
948#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
950pub struct SemanticDateTime {
951 pub year: i32,
952 pub month: u32,
953 pub day: u32,
954 pub hour: u32,
955 pub minute: u32,
956 pub second: u32,
957 #[serde(default)]
958 pub microsecond: u32,
959 pub timezone: Option<SemanticTimezone>,
960}
961
962impl fmt::Display for SemanticDateTime {
963 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
964 let has_time = self.hour != 0
965 || self.minute != 0
966 || self.second != 0
967 || self.microsecond != 0
968 || self.timezone.is_some();
969 if !has_time {
970 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
971 } else {
972 write!(
973 f,
974 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
975 self.year, self.month, self.day, self.hour, self.minute, self.second
976 )?;
977 if self.microsecond != 0 {
978 write!(f, ".{:06}", self.microsecond)?;
979 }
980 if let Some(tz) = &self.timezone {
981 write!(f, "{}", tz)?;
982 }
983 Ok(())
984 }
985 }
986}
987
988#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
991#[serde(rename_all = "snake_case")]
992pub enum ValueKind {
993 Number(Decimal),
994 Scale(Decimal, String),
996 Text(String),
997 Date(SemanticDateTime),
998 Time(SemanticTime),
999 Boolean(bool),
1000 Duration(Decimal, SemanticDurationUnit),
1002 Ratio(Decimal, Option<String>),
1004}
1005
1006impl fmt::Display for ValueKind {
1007 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1008 use crate::parsing::ast::Value;
1009 match self {
1010 ValueKind::Number(n) => {
1011 let norm = n.normalize();
1012 let s = if norm.fract().is_zero() {
1013 norm.trunc().to_string()
1014 } else {
1015 norm.to_string()
1016 };
1017 write!(f, "{}", s)
1018 }
1019 ValueKind::Scale(n, u) => write!(f, "{}", Value::Scale(*n, u.clone())),
1020 ValueKind::Text(s) => write!(f, "{}", Value::Text(s.clone())),
1021 ValueKind::Ratio(r, u) => write!(f, "{}", Value::Ratio(*r, u.clone())),
1022 ValueKind::Date(dt) => write!(f, "{}", dt),
1023 ValueKind::Time(t) => write!(
1024 f,
1025 "{}",
1026 Value::Time(crate::parsing::ast::TimeValue {
1027 hour: t.hour as u8,
1028 minute: t.minute as u8,
1029 second: t.second as u8,
1030 timezone: t
1031 .timezone
1032 .as_ref()
1033 .map(|tz| crate::parsing::ast::TimezoneValue {
1034 offset_hours: tz.offset_hours,
1035 offset_minutes: tz.offset_minutes,
1036 }),
1037 })
1038 ),
1039 ValueKind::Boolean(b) => write!(f, "{}", b),
1040 ValueKind::Duration(v, u) => write!(f, "{} {}", v, u),
1041 }
1042 }
1043}
1044
1045#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1054pub struct PathSegment {
1055 pub data: String,
1057 pub spec: String,
1059}
1060
1061#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1066pub struct DataPath {
1067 pub segments: Vec<PathSegment>,
1069 pub data: String,
1071}
1072
1073impl DataPath {
1074 pub fn new(segments: Vec<PathSegment>, data: String) -> Self {
1076 Self { segments, data }
1077 }
1078
1079 pub fn local(data: String) -> Self {
1081 Self {
1082 segments: vec![],
1083 data,
1084 }
1085 }
1086
1087 pub fn input_key(&self) -> String {
1090 let mut s = String::new();
1091 for segment in &self.segments {
1092 s.push_str(&segment.data);
1093 s.push('.');
1094 }
1095 s.push_str(&self.data);
1096 s
1097 }
1098}
1099
1100#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1104pub struct RulePath {
1105 pub segments: Vec<PathSegment>,
1107 pub rule: String,
1109}
1110
1111impl RulePath {
1112 pub fn new(segments: Vec<PathSegment>, rule: String) -> Self {
1114 Self { segments, rule }
1115 }
1116}
1117
1118#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1127pub struct Expression {
1128 pub kind: ExpressionKind,
1129 pub source_location: Option<Source>,
1130}
1131
1132impl Expression {
1133 pub fn new(kind: ExpressionKind, source_location: Source) -> Self {
1134 Self {
1135 kind,
1136 source_location: Some(source_location),
1137 }
1138 }
1139
1140 pub fn with_source(kind: ExpressionKind, source_location: Option<Source>) -> Self {
1142 Self {
1143 kind,
1144 source_location,
1145 }
1146 }
1147
1148 pub fn collect_data_paths(&self, data: &mut std::collections::HashSet<DataPath>) {
1150 self.kind.collect_data_paths(data);
1151 }
1152}
1153
1154#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1156#[serde(rename_all = "snake_case")]
1157pub enum ExpressionKind {
1158 Literal(Box<LiteralValue>),
1160 DataPath(DataPath),
1162 RulePath(RulePath),
1164 LogicalAnd(Arc<Expression>, Arc<Expression>),
1165 Arithmetic(Arc<Expression>, ArithmeticComputation, Arc<Expression>),
1166 Comparison(Arc<Expression>, ComparisonComputation, Arc<Expression>),
1167 UnitConversion(Arc<Expression>, SemanticConversionTarget),
1168 LogicalNegation(Arc<Expression>, NegationType),
1169 MathematicalComputation(MathematicalComputation, Arc<Expression>),
1170 Veto(VetoExpression),
1171 Now,
1173 DateRelative(DateRelativeKind, Arc<Expression>, Option<Arc<Expression>>),
1175 DateCalendar(DateCalendarKind, CalendarUnit, Arc<Expression>),
1177}
1178
1179impl ExpressionKind {
1180 pub(crate) fn collect_data_paths(&self, data: &mut std::collections::HashSet<DataPath>) {
1182 match self {
1183 ExpressionKind::DataPath(fp) => {
1184 data.insert(fp.clone());
1185 }
1186 ExpressionKind::LogicalAnd(left, right)
1187 | ExpressionKind::Arithmetic(left, _, right)
1188 | ExpressionKind::Comparison(left, _, right) => {
1189 left.collect_data_paths(data);
1190 right.collect_data_paths(data);
1191 }
1192 ExpressionKind::UnitConversion(inner, _)
1193 | ExpressionKind::LogicalNegation(inner, _)
1194 | ExpressionKind::MathematicalComputation(_, inner) => {
1195 inner.collect_data_paths(data);
1196 }
1197 ExpressionKind::DateRelative(_, date_expr, tolerance) => {
1198 date_expr.collect_data_paths(data);
1199 if let Some(tol) = tolerance {
1200 tol.collect_data_paths(data);
1201 }
1202 }
1203 ExpressionKind::DateCalendar(_, _, date_expr) => {
1204 date_expr.collect_data_paths(data);
1205 }
1206 ExpressionKind::Literal(_)
1207 | ExpressionKind::RulePath(_)
1208 | ExpressionKind::Veto(_)
1209 | ExpressionKind::Now => {}
1210 }
1211 }
1212}
1213
1214#[inline]
1221#[must_use]
1222pub fn is_same_spec(left: &LemmaSpec, right: &LemmaSpec) -> bool {
1223 left == right
1224}
1225
1226#[derive(Clone, Debug, Serialize, Deserialize)]
1228#[serde(tag = "kind", rename_all = "snake_case")]
1229pub enum TypeDefiningSpec {
1230 Local,
1232 Import { spec: Arc<LemmaSpec> },
1234}
1235
1236#[derive(Clone, Debug, Serialize, Deserialize)]
1238#[serde(rename_all = "snake_case")]
1239pub enum TypeExtends {
1240 Primitive,
1242 Custom {
1245 parent: String,
1246 family: String,
1247 defining_spec: TypeDefiningSpec,
1248 },
1249}
1250
1251impl PartialEq for TypeExtends {
1252 fn eq(&self, other: &Self) -> bool {
1253 match (self, other) {
1254 (TypeExtends::Primitive, TypeExtends::Primitive) => true,
1255 (
1256 TypeExtends::Custom {
1257 parent: lp,
1258 family: lf,
1259 defining_spec: ld,
1260 },
1261 TypeExtends::Custom {
1262 parent: rp,
1263 family: rf,
1264 defining_spec: rd,
1265 },
1266 ) => {
1267 lp == rp
1268 && lf == rf
1269 && match (ld, rd) {
1270 (TypeDefiningSpec::Local, TypeDefiningSpec::Local) => true,
1271 (
1272 TypeDefiningSpec::Import { spec: left },
1273 TypeDefiningSpec::Import { spec: right },
1274 ) => is_same_spec(left, right),
1275 _ => false,
1276 }
1277 }
1278 _ => false,
1279 }
1280 }
1281}
1282
1283impl Eq for TypeExtends {}
1284
1285impl TypeExtends {
1286 #[must_use]
1288 pub fn custom_local(parent: String, family: String) -> Self {
1289 TypeExtends::Custom {
1290 parent,
1291 family,
1292 defining_spec: TypeDefiningSpec::Local,
1293 }
1294 }
1295
1296 #[must_use]
1298 pub fn parent_name(&self) -> Option<&str> {
1299 match self {
1300 TypeExtends::Primitive => None,
1301 TypeExtends::Custom { parent, .. } => Some(parent.as_str()),
1302 }
1303 }
1304}
1305
1306#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1311pub struct LemmaType {
1312 pub name: Option<String>,
1314 #[serde(flatten)]
1319 pub specifications: TypeSpecification,
1320 pub extends: TypeExtends,
1322}
1323
1324impl LemmaType {
1325 pub fn new(name: String, specifications: TypeSpecification, extends: TypeExtends) -> Self {
1327 Self {
1328 name: Some(name),
1329 specifications,
1330 extends,
1331 }
1332 }
1333
1334 pub fn without_name(specifications: TypeSpecification, extends: TypeExtends) -> Self {
1336 Self {
1337 name: None,
1338 specifications,
1339 extends,
1340 }
1341 }
1342
1343 pub fn primitive(specifications: TypeSpecification) -> Self {
1345 Self {
1346 name: None,
1347 specifications,
1348 extends: TypeExtends::Primitive,
1349 }
1350 }
1351
1352 pub fn name(&self) -> String {
1354 self.name.clone().unwrap_or_else(|| {
1355 match &self.specifications {
1356 TypeSpecification::Boolean { .. } => "boolean",
1357 TypeSpecification::Scale { .. } => "scale",
1358 TypeSpecification::Number { .. } => "number",
1359 TypeSpecification::Text { .. } => "text",
1360 TypeSpecification::Date { .. } => "date",
1361 TypeSpecification::Time { .. } => "time",
1362 TypeSpecification::Duration { .. } => "duration",
1363 TypeSpecification::Ratio { .. } => "ratio",
1364 TypeSpecification::Veto { .. } => "veto",
1365 TypeSpecification::Undetermined => "undetermined",
1366 }
1367 .to_string()
1368 })
1369 }
1370
1371 pub fn is_boolean(&self) -> bool {
1373 matches!(&self.specifications, TypeSpecification::Boolean { .. })
1374 }
1375
1376 pub fn is_scale(&self) -> bool {
1378 matches!(&self.specifications, TypeSpecification::Scale { .. })
1379 }
1380
1381 pub fn is_number(&self) -> bool {
1383 matches!(&self.specifications, TypeSpecification::Number { .. })
1384 }
1385
1386 pub fn is_numeric(&self) -> bool {
1388 matches!(
1389 &self.specifications,
1390 TypeSpecification::Scale { .. } | TypeSpecification::Number { .. }
1391 )
1392 }
1393
1394 pub fn is_text(&self) -> bool {
1396 matches!(&self.specifications, TypeSpecification::Text { .. })
1397 }
1398
1399 pub fn is_date(&self) -> bool {
1401 matches!(&self.specifications, TypeSpecification::Date { .. })
1402 }
1403
1404 pub fn is_time(&self) -> bool {
1406 matches!(&self.specifications, TypeSpecification::Time { .. })
1407 }
1408
1409 pub fn is_duration(&self) -> bool {
1411 matches!(&self.specifications, TypeSpecification::Duration { .. })
1412 }
1413
1414 pub fn is_ratio(&self) -> bool {
1416 matches!(&self.specifications, TypeSpecification::Ratio { .. })
1417 }
1418
1419 pub fn vetoed(&self) -> bool {
1421 matches!(&self.specifications, TypeSpecification::Veto { .. })
1422 }
1423
1424 pub fn is_undetermined(&self) -> bool {
1426 matches!(&self.specifications, TypeSpecification::Undetermined)
1427 }
1428
1429 pub fn has_same_base_type(&self, other: &LemmaType) -> bool {
1431 use TypeSpecification::*;
1432 matches!(
1433 (&self.specifications, &other.specifications),
1434 (Boolean { .. }, Boolean { .. })
1435 | (Number { .. }, Number { .. })
1436 | (Scale { .. }, Scale { .. })
1437 | (Text { .. }, Text { .. })
1438 | (Date { .. }, Date { .. })
1439 | (Time { .. }, Time { .. })
1440 | (Duration { .. }, Duration { .. })
1441 | (Ratio { .. }, Ratio { .. })
1442 | (Veto { .. }, Veto { .. })
1443 | (Undetermined, Undetermined)
1444 )
1445 }
1446
1447 #[must_use]
1449 pub fn scale_family_name(&self) -> Option<&str> {
1450 if !self.is_scale() {
1451 return None;
1452 }
1453 match &self.extends {
1454 TypeExtends::Custom { family, .. } => Some(family.as_str()),
1455 TypeExtends::Primitive => self.name.as_deref(),
1456 }
1457 }
1458
1459 #[must_use]
1462 pub fn same_scale_family(&self, other: &LemmaType) -> bool {
1463 if !self.is_scale() || !other.is_scale() {
1464 return false;
1465 }
1466 match (self.scale_family_name(), other.scale_family_name()) {
1467 (Some(self_family), Some(other_family)) => self_family == other_family,
1468 (None, None) => true,
1470 _ => false,
1471 }
1472 }
1473
1474 pub fn veto_type() -> Self {
1476 Self::primitive(TypeSpecification::veto())
1477 }
1478
1479 pub fn undetermined_type() -> Self {
1482 Self::primitive(TypeSpecification::Undetermined)
1483 }
1484
1485 pub fn decimal_places(&self) -> Option<u8> {
1488 match &self.specifications {
1489 TypeSpecification::Number { decimals, .. } => *decimals,
1490 TypeSpecification::Scale { decimals, .. } => *decimals,
1491 TypeSpecification::Ratio { decimals, .. } => *decimals,
1492 _ => None,
1493 }
1494 }
1495
1496 pub fn example_value(&self) -> &'static str {
1498 match &self.specifications {
1499 TypeSpecification::Text { .. } => "\"hello world\"",
1500 TypeSpecification::Scale { .. } => "12.50 eur",
1501 TypeSpecification::Number { .. } => "3.14",
1502 TypeSpecification::Boolean { .. } => "true",
1503 TypeSpecification::Date { .. } => "2023-12-25T14:30:00Z",
1504 TypeSpecification::Veto { .. } => "veto",
1505 TypeSpecification::Time { .. } => "14:30:00",
1506 TypeSpecification::Duration { .. } => "90 minutes",
1507 TypeSpecification::Ratio { .. } => "50%",
1508 TypeSpecification::Undetermined => unreachable!(
1509 "BUG: example_value called on Undetermined sentinel type; this type must never reach user-facing code"
1510 ),
1511 }
1512 }
1513
1514 #[must_use]
1518 pub fn scale_unit_factor(&self, unit_name: &str) -> Decimal {
1519 let units = match &self.specifications {
1520 TypeSpecification::Scale { units, .. } => units,
1521 _ => unreachable!(
1522 "BUG: scale_unit_factor called with non-scale type {}; only call during evaluation after planning validated scale conversion",
1523 self.name()
1524 ),
1525 };
1526 match units
1527 .iter()
1528 .find(|u| u.name.eq_ignore_ascii_case(unit_name))
1529 {
1530 Some(ScaleUnit { value, .. }) => *value,
1531 None => {
1532 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
1533 unreachable!(
1534 "BUG: unknown unit '{}' for scale type {} (valid: {}); planning must reject invalid conversions with Error",
1535 unit_name,
1536 self.name(),
1537 valid.join(", ")
1538 );
1539 }
1540 }
1541 }
1542}
1543
1544#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
1546pub struct LiteralValue {
1547 pub value: ValueKind,
1548 pub lemma_type: LemmaType,
1549}
1550
1551impl Serialize for LiteralValue {
1552 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1553 where
1554 S: serde::Serializer,
1555 {
1556 use serde::ser::SerializeStruct;
1557 let mut state = serializer.serialize_struct("LiteralValue", 3)?;
1558 state.serialize_field("value", &self.value)?;
1559 state.serialize_field("lemma_type", &self.lemma_type)?;
1560 state.serialize_field("display_value", &self.display_value())?;
1561 state.end()
1562 }
1563}
1564
1565impl LiteralValue {
1566 pub fn text(s: String) -> Self {
1567 Self {
1568 value: ValueKind::Text(s),
1569 lemma_type: primitive_text().clone(),
1570 }
1571 }
1572
1573 pub fn text_with_type(s: String, lemma_type: LemmaType) -> Self {
1574 Self {
1575 value: ValueKind::Text(s),
1576 lemma_type,
1577 }
1578 }
1579
1580 pub fn number(n: Decimal) -> Self {
1581 Self {
1582 value: ValueKind::Number(n),
1583 lemma_type: primitive_number().clone(),
1584 }
1585 }
1586
1587 pub fn number_with_type(n: Decimal, lemma_type: LemmaType) -> Self {
1588 Self {
1589 value: ValueKind::Number(n),
1590 lemma_type,
1591 }
1592 }
1593
1594 pub fn scale_with_type(n: Decimal, unit: String, lemma_type: LemmaType) -> Self {
1595 Self {
1596 value: ValueKind::Scale(n, unit),
1597 lemma_type,
1598 }
1599 }
1600
1601 pub fn number_interpreted_as_scale(value: Decimal, unit_name: String) -> Self {
1604 let lemma_type = LemmaType {
1605 name: None,
1606 specifications: TypeSpecification::Scale {
1607 minimum: None,
1608 maximum: None,
1609 decimals: None,
1610 precision: None,
1611 units: ScaleUnits::from(vec![ScaleUnit {
1612 name: unit_name.clone(),
1613 value: Decimal::from(1),
1614 }]),
1615 help: "Format: {value} {unit} (e.g. 100 kilograms)".to_string(),
1616 },
1617 extends: TypeExtends::Primitive,
1618 };
1619 Self {
1620 value: ValueKind::Scale(value, unit_name),
1621 lemma_type,
1622 }
1623 }
1624
1625 pub fn from_bool(b: bool) -> Self {
1626 Self {
1627 value: ValueKind::Boolean(b),
1628 lemma_type: primitive_boolean().clone(),
1629 }
1630 }
1631
1632 pub fn date(dt: SemanticDateTime) -> Self {
1633 Self {
1634 value: ValueKind::Date(dt),
1635 lemma_type: primitive_date().clone(),
1636 }
1637 }
1638
1639 pub fn date_with_type(dt: SemanticDateTime, lemma_type: LemmaType) -> Self {
1640 Self {
1641 value: ValueKind::Date(dt),
1642 lemma_type,
1643 }
1644 }
1645
1646 pub fn time(t: SemanticTime) -> Self {
1647 Self {
1648 value: ValueKind::Time(t),
1649 lemma_type: primitive_time().clone(),
1650 }
1651 }
1652
1653 pub fn time_with_type(t: SemanticTime, lemma_type: LemmaType) -> Self {
1654 Self {
1655 value: ValueKind::Time(t),
1656 lemma_type,
1657 }
1658 }
1659
1660 pub fn duration(value: Decimal, unit: SemanticDurationUnit) -> Self {
1661 Self {
1662 value: ValueKind::Duration(value, unit),
1663 lemma_type: primitive_duration().clone(),
1664 }
1665 }
1666
1667 pub fn duration_with_type(
1668 value: Decimal,
1669 unit: SemanticDurationUnit,
1670 lemma_type: LemmaType,
1671 ) -> Self {
1672 Self {
1673 value: ValueKind::Duration(value, unit),
1674 lemma_type,
1675 }
1676 }
1677
1678 pub fn ratio(r: Decimal, unit: Option<String>) -> Self {
1679 Self {
1680 value: ValueKind::Ratio(r, unit),
1681 lemma_type: primitive_ratio().clone(),
1682 }
1683 }
1684
1685 pub fn ratio_with_type(r: Decimal, unit: Option<String>, lemma_type: LemmaType) -> Self {
1686 Self {
1687 value: ValueKind::Ratio(r, unit),
1688 lemma_type,
1689 }
1690 }
1691
1692 pub fn display_value(&self) -> String {
1694 format!("{}", self)
1695 }
1696
1697 pub fn byte_size(&self) -> usize {
1699 format!("{}", self).len()
1700 }
1701
1702 pub fn get_type(&self) -> &LemmaType {
1704 &self.lemma_type
1705 }
1706}
1707
1708#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1710#[serde(rename_all = "snake_case")]
1711pub enum DataValue {
1712 Literal(LiteralValue),
1713 TypeDeclaration { resolved_type: LemmaType },
1714 SpecReference(String),
1715}
1716
1717#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1719pub struct Data {
1720 pub path: DataPath,
1721 pub value: DataValue,
1722 pub source: Option<Source>,
1723}
1724
1725#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1728#[serde(rename_all = "snake_case", tag = "kind")]
1729pub enum ReferenceTarget {
1730 Data(DataPath),
1731 Rule(RulePath),
1732}
1733
1734#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1736#[serde(rename_all = "snake_case")]
1737pub enum DataDefinition {
1738 Value { value: LiteralValue, source: Source },
1740 TypeDeclaration {
1745 resolved_type: LemmaType,
1746 declared_default: Option<ValueKind>,
1747 source: Source,
1748 },
1749 SpecRef {
1751 spec: Arc<crate::parsing::ast::LemmaSpec>,
1752 source: Source,
1753 },
1754 Reference {
1779 target: ReferenceTarget,
1780 resolved_type: LemmaType,
1781 local_constraints: Option<Vec<Constraint>>,
1782 local_default: Option<ValueKind>,
1783 source: Source,
1784 },
1785}
1786
1787impl DataDefinition {
1788 pub fn schema_type(&self) -> Option<&LemmaType> {
1790 match self {
1791 DataDefinition::Value { value, .. } => Some(&value.lemma_type),
1792 DataDefinition::TypeDeclaration { resolved_type, .. } => Some(resolved_type),
1793 DataDefinition::Reference { resolved_type, .. } => Some(resolved_type),
1794 DataDefinition::SpecRef { .. } => None,
1795 }
1796 }
1797
1798 pub fn value(&self) -> Option<&LiteralValue> {
1802 match self {
1803 DataDefinition::Value { value, .. } => Some(value),
1804 DataDefinition::TypeDeclaration { .. }
1805 | DataDefinition::SpecRef { .. }
1806 | DataDefinition::Reference { .. } => None,
1807 }
1808 }
1809
1810 pub fn schema_default(&self) -> Option<LiteralValue> {
1822 match self {
1823 DataDefinition::Value { value, .. } => Some(value.clone()),
1824 DataDefinition::TypeDeclaration {
1825 resolved_type,
1826 declared_default: Some(dv),
1827 ..
1828 } => Some(LiteralValue {
1829 value: dv.clone(),
1830 lemma_type: resolved_type.clone(),
1831 }),
1832 DataDefinition::Reference {
1833 resolved_type,
1834 local_default: Some(dv),
1835 ..
1836 } => Some(LiteralValue {
1837 value: dv.clone(),
1838 lemma_type: resolved_type.clone(),
1839 }),
1840 DataDefinition::TypeDeclaration { .. }
1841 | DataDefinition::Reference { .. }
1842 | DataDefinition::SpecRef { .. } => None,
1843 }
1844 }
1845
1846 pub fn source(&self) -> &Source {
1848 match self {
1849 DataDefinition::Value { source, .. } => source,
1850 DataDefinition::TypeDeclaration { source, .. } => source,
1851 DataDefinition::SpecRef { source, .. } => source,
1852 DataDefinition::Reference { source, .. } => source,
1853 }
1854 }
1855
1856 pub fn spec_arc(&self) -> Option<&Arc<crate::parsing::ast::LemmaSpec>> {
1858 match self {
1859 DataDefinition::Value { .. }
1860 | DataDefinition::TypeDeclaration { .. }
1861 | DataDefinition::Reference { .. } => None,
1862 DataDefinition::SpecRef { spec: spec_arc, .. } => Some(spec_arc),
1863 }
1864 }
1865
1866 pub fn spec_ref(&self) -> Option<&str> {
1868 match self {
1869 DataDefinition::Value { .. }
1870 | DataDefinition::TypeDeclaration { .. }
1871 | DataDefinition::Reference { .. } => None,
1872 DataDefinition::SpecRef { spec, .. } => Some(&spec.name),
1873 }
1874 }
1875
1876 pub fn reference_target(&self) -> Option<&ReferenceTarget> {
1879 match self {
1880 DataDefinition::Reference { target, .. } => Some(target),
1881 _ => None,
1882 }
1883 }
1884}
1885
1886pub fn value_to_semantic(value: &crate::parsing::ast::Value) -> Result<ValueKind, String> {
1888 use crate::parsing::ast::Value;
1889 Ok(match value {
1890 Value::Number(n) => ValueKind::Number(*n),
1891 Value::Text(s) => ValueKind::Text(s.clone()),
1892 Value::Boolean(b) => ValueKind::Boolean(bool::from(*b)),
1893 Value::Date(dt) => ValueKind::Date(date_time_to_semantic(dt)),
1894 Value::Time(t) => ValueKind::Time(time_to_semantic(t)),
1895 Value::Duration(n, u) => ValueKind::Duration(*n, duration_unit_to_semantic(u)),
1896 Value::Scale(n, unit) => ValueKind::Scale(*n, unit.clone()),
1897 Value::Ratio(n, unit) => ValueKind::Ratio(*n, unit.clone()),
1898 })
1899}
1900
1901pub(crate) fn date_time_to_semantic(dt: &crate::parsing::ast::DateTimeValue) -> SemanticDateTime {
1903 SemanticDateTime {
1904 year: dt.year,
1905 month: dt.month,
1906 day: dt.day,
1907 hour: dt.hour,
1908 minute: dt.minute,
1909 second: dt.second,
1910 microsecond: dt.microsecond,
1911 timezone: dt.timezone.as_ref().map(|tz| SemanticTimezone {
1912 offset_hours: tz.offset_hours,
1913 offset_minutes: tz.offset_minutes,
1914 }),
1915 }
1916}
1917
1918pub(crate) fn time_to_semantic(t: &crate::parsing::ast::TimeValue) -> SemanticTime {
1920 SemanticTime {
1921 hour: t.hour.into(),
1922 minute: t.minute.into(),
1923 second: t.second.into(),
1924 timezone: t.timezone.as_ref().map(|tz| SemanticTimezone {
1925 offset_hours: tz.offset_hours,
1926 offset_minutes: tz.offset_minutes,
1927 }),
1928 }
1929}
1930
1931pub(crate) fn compare_semantic_dates(
1938 left: &SemanticDateTime,
1939 right: &SemanticDateTime,
1940) -> std::cmp::Ordering {
1941 left.year
1942 .cmp(&right.year)
1943 .then_with(|| left.month.cmp(&right.month))
1944 .then_with(|| left.day.cmp(&right.day))
1945 .then_with(|| left.hour.cmp(&right.hour))
1946 .then_with(|| left.minute.cmp(&right.minute))
1947 .then_with(|| left.second.cmp(&right.second))
1948}
1949
1950pub(crate) fn compare_semantic_times(
1954 left: &SemanticTime,
1955 right: &SemanticTime,
1956) -> std::cmp::Ordering {
1957 left.hour
1958 .cmp(&right.hour)
1959 .then_with(|| left.minute.cmp(&right.minute))
1960 .then_with(|| left.second.cmp(&right.second))
1961}
1962
1963pub(crate) fn duration_unit_to_semantic(
1965 u: &crate::parsing::ast::DurationUnit,
1966) -> SemanticDurationUnit {
1967 use crate::parsing::ast::DurationUnit as DU;
1968 match u {
1969 DU::Year => SemanticDurationUnit::Year,
1970 DU::Month => SemanticDurationUnit::Month,
1971 DU::Week => SemanticDurationUnit::Week,
1972 DU::Day => SemanticDurationUnit::Day,
1973 DU::Hour => SemanticDurationUnit::Hour,
1974 DU::Minute => SemanticDurationUnit::Minute,
1975 DU::Second => SemanticDurationUnit::Second,
1976 DU::Millisecond => SemanticDurationUnit::Millisecond,
1977 DU::Microsecond => SemanticDurationUnit::Microsecond,
1978 }
1979}
1980
1981pub fn conversion_target_to_semantic(
1988 ct: &ConversionTarget,
1989 unit_index: Option<&HashMap<String, LemmaType>>,
1990) -> Result<SemanticConversionTarget, String> {
1991 match ct {
1992 ConversionTarget::Duration(u) => Ok(SemanticConversionTarget::Duration(
1993 duration_unit_to_semantic(u),
1994 )),
1995 ConversionTarget::Unit(name) => {
1996 let index = unit_index.ok_or_else(|| {
1997 "Unit conversion requires type resolution; unit index not available.".to_string()
1998 })?;
1999 let lemma_type = index.get(name).ok_or_else(|| {
2000 format!(
2001 "Unknown unit '{}'. Unit must be defined by a scale or ratio type.",
2002 name
2003 )
2004 })?;
2005 if lemma_type.is_ratio() {
2006 Ok(SemanticConversionTarget::RatioUnit(name.clone()))
2007 } else if lemma_type.is_scale() {
2008 Ok(SemanticConversionTarget::ScaleUnit(name.clone()))
2009 } else {
2010 Err(format!(
2011 "Unit '{}' is not a ratio or scale type; cannot use it in conversion.",
2012 name
2013 ))
2014 }
2015 }
2016 }
2017}
2018
2019static PRIMITIVE_BOOLEAN: OnceLock<LemmaType> = OnceLock::new();
2025static PRIMITIVE_SCALE: OnceLock<LemmaType> = OnceLock::new();
2026static PRIMITIVE_NUMBER: OnceLock<LemmaType> = OnceLock::new();
2027static PRIMITIVE_TEXT: OnceLock<LemmaType> = OnceLock::new();
2028static PRIMITIVE_DATE: OnceLock<LemmaType> = OnceLock::new();
2029static PRIMITIVE_TIME: OnceLock<LemmaType> = OnceLock::new();
2030static PRIMITIVE_DURATION: OnceLock<LemmaType> = OnceLock::new();
2031static PRIMITIVE_RATIO: OnceLock<LemmaType> = OnceLock::new();
2032
2033#[must_use]
2035pub fn primitive_boolean() -> &'static LemmaType {
2036 PRIMITIVE_BOOLEAN.get_or_init(|| LemmaType::primitive(TypeSpecification::boolean()))
2037}
2038
2039#[must_use]
2040pub fn primitive_scale() -> &'static LemmaType {
2041 PRIMITIVE_SCALE.get_or_init(|| LemmaType::primitive(TypeSpecification::scale()))
2042}
2043
2044#[must_use]
2045pub fn primitive_number() -> &'static LemmaType {
2046 PRIMITIVE_NUMBER.get_or_init(|| LemmaType::primitive(TypeSpecification::number()))
2047}
2048
2049#[must_use]
2050pub fn primitive_text() -> &'static LemmaType {
2051 PRIMITIVE_TEXT.get_or_init(|| LemmaType::primitive(TypeSpecification::text()))
2052}
2053
2054#[must_use]
2055pub fn primitive_date() -> &'static LemmaType {
2056 PRIMITIVE_DATE.get_or_init(|| LemmaType::primitive(TypeSpecification::date()))
2057}
2058
2059#[must_use]
2060pub fn primitive_time() -> &'static LemmaType {
2061 PRIMITIVE_TIME.get_or_init(|| LemmaType::primitive(TypeSpecification::time()))
2062}
2063
2064#[must_use]
2065pub fn primitive_duration() -> &'static LemmaType {
2066 PRIMITIVE_DURATION.get_or_init(|| LemmaType::primitive(TypeSpecification::duration()))
2067}
2068
2069#[must_use]
2070pub fn primitive_ratio() -> &'static LemmaType {
2071 PRIMITIVE_RATIO.get_or_init(|| LemmaType::primitive(TypeSpecification::ratio()))
2072}
2073
2074#[must_use]
2076pub fn type_spec_for_primitive(kind: PrimitiveKind) -> TypeSpecification {
2077 match kind {
2078 PrimitiveKind::Boolean => TypeSpecification::boolean(),
2079 PrimitiveKind::Scale => TypeSpecification::scale(),
2080 PrimitiveKind::Number => TypeSpecification::number(),
2081 PrimitiveKind::Percent | PrimitiveKind::Ratio => TypeSpecification::ratio(),
2082 PrimitiveKind::Text => TypeSpecification::text(),
2083 PrimitiveKind::Date => TypeSpecification::date(),
2084 PrimitiveKind::Time => TypeSpecification::time(),
2085 PrimitiveKind::Duration => TypeSpecification::duration(),
2086 }
2087}
2088
2089impl fmt::Display for PathSegment {
2094 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2095 write!(f, "{} → {}", self.data, self.spec)
2096 }
2097}
2098
2099impl fmt::Display for DataPath {
2100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2101 for segment in &self.segments {
2102 write!(f, "{}.", segment)?;
2103 }
2104 write!(f, "{}", self.data)
2105 }
2106}
2107
2108impl fmt::Display for RulePath {
2109 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2110 for segment in &self.segments {
2111 write!(f, "{}.", segment)?;
2112 }
2113 write!(f, "{}", self.rule)
2114 }
2115}
2116
2117impl fmt::Display for LemmaType {
2118 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2119 write!(f, "{}", self.name())
2120 }
2121}
2122
2123impl fmt::Display for LiteralValue {
2124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2125 match &self.value {
2126 ValueKind::Scale(n, u) => {
2127 if let TypeSpecification::Scale { decimals, .. } = &self.lemma_type.specifications {
2128 let s = match decimals {
2129 Some(d) => {
2130 let dp = u32::from(*d);
2131 let rounded = n.round_dp(dp);
2132 format!("{:.prec$}", rounded, prec = *d as usize)
2133 }
2134 None => n.normalize().to_string(),
2135 };
2136 return write!(f, "{} {}", s, u);
2137 }
2138 write!(f, "{}", self.value)
2139 }
2140 ValueKind::Ratio(r, Some(unit_name)) => {
2141 if let TypeSpecification::Ratio { units, .. } = &self.lemma_type.specifications {
2142 if let Ok(unit) = units.get(unit_name) {
2143 let display_value = (*r * unit.value).normalize();
2144 let s = if display_value.fract().is_zero() {
2145 display_value.trunc().to_string()
2146 } else {
2147 display_value.to_string()
2148 };
2149 return match unit_name.as_str() {
2151 "percent" => write!(f, "{}%", s),
2152 "permille" => write!(f, "{}%%", s),
2153 _ => write!(f, "{} {}", s, unit_name),
2154 };
2155 }
2156 }
2157 write!(f, "{}", self.value)
2158 }
2159 _ => write!(f, "{}", self.value),
2160 }
2161 }
2162}
2163
2164#[cfg(test)]
2169mod tests {
2170 use super::*;
2171 use crate::parsing::ast::{BooleanValue, DateTimeValue, DurationUnit, LemmaSpec, TimeValue};
2172 use rust_decimal::Decimal;
2173 use std::str::FromStr;
2174 use std::sync::Arc;
2175
2176 #[test]
2177 fn test_negated_comparison() {
2178 assert_eq!(
2179 negated_comparison(ComparisonComputation::LessThan),
2180 ComparisonComputation::GreaterThanOrEqual
2181 );
2182 assert_eq!(
2183 negated_comparison(ComparisonComputation::GreaterThanOrEqual),
2184 ComparisonComputation::LessThan
2185 );
2186 assert_eq!(
2187 negated_comparison(ComparisonComputation::Is),
2188 ComparisonComputation::IsNot
2189 );
2190 assert_eq!(
2191 negated_comparison(ComparisonComputation::IsNot),
2192 ComparisonComputation::Is
2193 );
2194 }
2195
2196 #[test]
2197 fn test_literal_value_to_primitive_type() {
2198 let one = Decimal::from_str("1").unwrap();
2199
2200 assert_eq!(LiteralValue::text("".to_string()).lemma_type.name(), "text");
2201 assert_eq!(LiteralValue::number(one).lemma_type.name(), "number");
2202 assert_eq!(
2203 LiteralValue::from_bool(bool::from(BooleanValue::True))
2204 .lemma_type
2205 .name(),
2206 "boolean"
2207 );
2208
2209 let dt = DateTimeValue {
2210 year: 2024,
2211 month: 1,
2212 day: 1,
2213 hour: 0,
2214 minute: 0,
2215 second: 0,
2216 microsecond: 0,
2217 timezone: None,
2218 };
2219 assert_eq!(
2220 LiteralValue::date(date_time_to_semantic(&dt))
2221 .lemma_type
2222 .name(),
2223 "date"
2224 );
2225 assert_eq!(
2226 LiteralValue::ratio(one / Decimal::from(100), Some("percent".to_string()))
2227 .lemma_type
2228 .name(),
2229 "ratio"
2230 );
2231 assert_eq!(
2232 LiteralValue::duration(one, duration_unit_to_semantic(&DurationUnit::Second))
2233 .lemma_type
2234 .name(),
2235 "duration"
2236 );
2237 }
2238
2239 #[test]
2240 fn test_type_display() {
2241 let specs = TypeSpecification::text();
2242 let lemma_type = LemmaType::new("name".to_string(), specs, TypeExtends::Primitive);
2243 assert_eq!(format!("{}", lemma_type), "name");
2244 }
2245
2246 #[test]
2247 fn test_type_serialization() {
2248 let specs = TypeSpecification::number();
2249 let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
2250 let serialized = serde_json::to_string(&lemma_type).unwrap();
2251 let deserialized: LemmaType = serde_json::from_str(&serialized).unwrap();
2252 assert_eq!(lemma_type, deserialized);
2253 }
2254
2255 #[test]
2256 fn test_literal_value_display_value() {
2257 let ten = Decimal::from_str("10").unwrap();
2258
2259 assert_eq!(
2260 LiteralValue::text("hello".to_string()).display_value(),
2261 "hello"
2262 );
2263 assert_eq!(LiteralValue::number(ten).display_value(), "10");
2264 assert_eq!(LiteralValue::from_bool(true).display_value(), "true");
2265 assert_eq!(LiteralValue::from_bool(false).display_value(), "false");
2266
2267 let ten_percent_ratio = LiteralValue::ratio(
2269 Decimal::from_str("0.10").unwrap(),
2270 Some("percent".to_string()),
2271 );
2272 assert_eq!(ten_percent_ratio.display_value(), "10%");
2273
2274 let time = TimeValue {
2275 hour: 14,
2276 minute: 30,
2277 second: 0,
2278 timezone: None,
2279 };
2280 let time_display = LiteralValue::time(time_to_semantic(&time)).display_value();
2281 assert!(time_display.contains("14"));
2282 assert!(time_display.contains("30"));
2283 }
2284
2285 #[test]
2286 fn test_scale_display_respects_type_decimals() {
2287 let money_type = LemmaType {
2288 name: Some("money".to_string()),
2289 specifications: TypeSpecification::Scale {
2290 minimum: None,
2291 maximum: None,
2292 decimals: Some(2),
2293 precision: None,
2294 units: ScaleUnits::from(vec![ScaleUnit {
2295 name: "eur".to_string(),
2296 value: Decimal::from(1),
2297 }]),
2298 help: String::new(),
2299 },
2300 extends: TypeExtends::Primitive,
2301 };
2302 let val = LiteralValue::scale_with_type(
2303 Decimal::from_str("1.8").unwrap(),
2304 "eur".to_string(),
2305 money_type.clone(),
2306 );
2307 assert_eq!(val.display_value(), "1.80 eur");
2308 let more_precision = LiteralValue::scale_with_type(
2309 Decimal::from_str("1.80000").unwrap(),
2310 "eur".to_string(),
2311 money_type,
2312 );
2313 assert_eq!(more_precision.display_value(), "1.80 eur");
2314 let scale_no_decimals = LemmaType {
2315 name: Some("count".to_string()),
2316 specifications: TypeSpecification::Scale {
2317 minimum: None,
2318 maximum: None,
2319 decimals: None,
2320 precision: None,
2321 units: ScaleUnits::from(vec![ScaleUnit {
2322 name: "items".to_string(),
2323 value: Decimal::from(1),
2324 }]),
2325 help: String::new(),
2326 },
2327 extends: TypeExtends::Primitive,
2328 };
2329 let val_any = LiteralValue::scale_with_type(
2330 Decimal::from_str("42.50").unwrap(),
2331 "items".to_string(),
2332 scale_no_decimals,
2333 );
2334 assert_eq!(val_any.display_value(), "42.5 items");
2335 }
2336
2337 #[test]
2338 fn test_literal_value_time_type() {
2339 let time = TimeValue {
2340 hour: 14,
2341 minute: 30,
2342 second: 0,
2343 timezone: None,
2344 };
2345 let lit = LiteralValue::time(time_to_semantic(&time));
2346 assert_eq!(lit.lemma_type.name(), "time");
2347 }
2348
2349 #[test]
2350 fn test_scale_family_name_primitive_root() {
2351 let scale_spec = TypeSpecification::scale();
2352 let money_primitive = LemmaType::new(
2353 "money".to_string(),
2354 scale_spec.clone(),
2355 TypeExtends::Primitive,
2356 );
2357 assert_eq!(money_primitive.scale_family_name(), Some("money"));
2358 }
2359
2360 #[test]
2361 fn test_scale_family_name_custom() {
2362 let scale_spec = TypeSpecification::scale();
2363 let money_custom = LemmaType::new(
2364 "money".to_string(),
2365 scale_spec,
2366 TypeExtends::custom_local("money".to_string(), "money".to_string()),
2367 );
2368 assert_eq!(money_custom.scale_family_name(), Some("money"));
2369 }
2370
2371 #[test]
2372 fn test_same_scale_family_same_name_different_extends() {
2373 let scale_spec = TypeSpecification::scale();
2374 let money_primitive = LemmaType::new(
2375 "money".to_string(),
2376 scale_spec.clone(),
2377 TypeExtends::Primitive,
2378 );
2379 let money_custom = LemmaType::new(
2380 "money".to_string(),
2381 scale_spec,
2382 TypeExtends::custom_local("money".to_string(), "money".to_string()),
2383 );
2384 assert!(money_primitive.same_scale_family(&money_custom));
2385 assert!(money_custom.same_scale_family(&money_primitive));
2386 }
2387
2388 #[test]
2389 fn test_same_scale_family_parent_and_child() {
2390 let scale_spec = TypeSpecification::scale();
2391 let type_x = LemmaType::new("x".to_string(), scale_spec.clone(), TypeExtends::Primitive);
2392 let type_x2 = LemmaType::new(
2393 "x2".to_string(),
2394 scale_spec,
2395 TypeExtends::custom_local("x".to_string(), "x".to_string()),
2396 );
2397 assert_eq!(type_x.scale_family_name(), Some("x"));
2398 assert_eq!(type_x2.scale_family_name(), Some("x"));
2399 assert!(type_x.same_scale_family(&type_x2));
2400 assert!(type_x2.same_scale_family(&type_x));
2401 }
2402
2403 #[test]
2404 fn test_same_scale_family_siblings() {
2405 let scale_spec = TypeSpecification::scale();
2406 let type_x2_a = LemmaType::new(
2407 "x2a".to_string(),
2408 scale_spec.clone(),
2409 TypeExtends::custom_local("x".to_string(), "x".to_string()),
2410 );
2411 let type_x2_b = LemmaType::new(
2412 "x2b".to_string(),
2413 scale_spec,
2414 TypeExtends::custom_local("x".to_string(), "x".to_string()),
2415 );
2416 assert!(type_x2_a.same_scale_family(&type_x2_b));
2417 }
2418
2419 #[test]
2420 fn test_same_scale_family_different_families() {
2421 let scale_spec = TypeSpecification::scale();
2422 let money = LemmaType::new(
2423 "money".to_string(),
2424 scale_spec.clone(),
2425 TypeExtends::Primitive,
2426 );
2427 let temperature = LemmaType::new(
2428 "temperature".to_string(),
2429 scale_spec,
2430 TypeExtends::Primitive,
2431 );
2432 assert!(!money.same_scale_family(&temperature));
2433 assert!(!temperature.same_scale_family(&money));
2434 }
2435
2436 #[test]
2437 fn test_same_scale_family_scale_vs_non_scale() {
2438 let scale_spec = TypeSpecification::scale();
2439 let number_spec = TypeSpecification::number();
2440 let scale_type = LemmaType::new("money".to_string(), scale_spec, TypeExtends::Primitive);
2441 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
2442 assert!(!scale_type.same_scale_family(&number_type));
2443 assert!(!number_type.same_scale_family(&scale_type));
2444 }
2445
2446 #[test]
2447 fn test_scale_family_name_non_scale_returns_none() {
2448 let number_spec = TypeSpecification::number();
2449 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
2450 assert_eq!(number_type.scale_family_name(), None);
2451 }
2452
2453 #[test]
2454 fn test_lemma_type_inequality_local_vs_import_same_shape() {
2455 let dep = Arc::new(LemmaSpec::new("dep".to_string()));
2456 let scale_spec = TypeSpecification::scale();
2457 let local = LemmaType::new(
2458 "t".to_string(),
2459 scale_spec.clone(),
2460 TypeExtends::custom_local("money".to_string(), "money".to_string()),
2461 );
2462 let imported = LemmaType::new(
2463 "t".to_string(),
2464 scale_spec,
2465 TypeExtends::Custom {
2466 parent: "money".to_string(),
2467 family: "money".to_string(),
2468 defining_spec: TypeDefiningSpec::Import {
2469 spec: Arc::clone(&dep),
2470 },
2471 },
2472 );
2473 assert_ne!(local, imported);
2474 }
2475
2476 #[test]
2477 fn test_lemma_type_equality_import_same_resolved_spec_semantics() {
2478 let spec_a = Arc::new(LemmaSpec::new("dep".to_string()));
2479 let spec_b = Arc::new(LemmaSpec::new("dep".to_string()));
2480 assert!(is_same_spec(spec_a.as_ref(), spec_b.as_ref()));
2481 let scale_spec = TypeSpecification::scale();
2482 let left = LemmaType::new(
2483 "t".to_string(),
2484 scale_spec.clone(),
2485 TypeExtends::Custom {
2486 parent: "money".to_string(),
2487 family: "money".to_string(),
2488 defining_spec: TypeDefiningSpec::Import {
2489 spec: Arc::clone(&spec_a),
2490 },
2491 },
2492 );
2493 let right = LemmaType::new(
2494 "t".to_string(),
2495 scale_spec,
2496 TypeExtends::Custom {
2497 parent: "money".to_string(),
2498 family: "money".to_string(),
2499 defining_spec: TypeDefiningSpec::Import { spec: spec_b },
2500 },
2501 );
2502 assert_eq!(left, right);
2503 }
2504}