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 let Some(u) = units
448 .0
449 .iter_mut()
450 .find(|u| u.name.eq_ignore_ascii_case(&unit_name))
451 {
452 u.value = value;
453 } else {
454 units.0.push(ScaleUnit {
455 name: unit_name,
456 value,
457 });
458 }
459 }
460 TypeConstraintCommand::Minimum => {
461 *minimum = Some(require_scale_literal(args, units, "minimum")?);
462 }
463 TypeConstraintCommand::Maximum => {
464 *maximum = Some(require_scale_literal(args, units, "maximum")?);
465 }
466 TypeConstraintCommand::Precision => {
467 *precision = Some(require_scale_literal(args, units, "precision")?);
468 }
469 TypeConstraintCommand::Help => {
470 apply_type_help_command(help, args)?;
471 }
472 TypeConstraintCommand::Default => match require_literal(args, "default")? {
473 crate::literals::Value::Scale(value, unit_name) => {
474 *declared_default = Some(ValueKind::Scale(*value, unit_name.clone()));
475 }
476 other => {
477 return Err(format!(
478 "default for scale type requires a scale literal '{{value}} {{unit}}' (e.g. '1 kilogram'), got {}",
479 value_kind_name(other)
480 ));
481 }
482 },
483 _ => {
484 return Err(format!(
485 "Invalid command '{}' for scale type. Valid commands: unit, minimum, maximum, decimals, precision, help, default",
486 command
487 ));
488 }
489 },
490 TypeSpecification::Number {
491 decimals,
492 minimum,
493 maximum,
494 precision,
495 help,
496 } => match command {
497 TypeConstraintCommand::Decimals => {
498 let d = require_decimal_literal(args, "decimals")?;
499 *decimals = Some(decimal_to_u8(d, "decimals")?);
500 }
501 TypeConstraintCommand::Unit => {
502 return Err(
503 "Invalid command 'unit' for number type. Number types are dimensionless and cannot have units. Use 'scale' type instead.".to_string()
504 );
505 }
506 TypeConstraintCommand::Minimum => {
507 *minimum = Some(require_decimal_literal(args, "minimum")?);
508 }
509 TypeConstraintCommand::Maximum => {
510 *maximum = Some(require_decimal_literal(args, "maximum")?);
511 }
512 TypeConstraintCommand::Precision => {
513 *precision = Some(require_decimal_literal(args, "precision")?);
514 }
515 TypeConstraintCommand::Help => {
516 apply_type_help_command(help, args)?;
517 }
518 TypeConstraintCommand::Default => match require_literal(args, "default")? {
519 crate::literals::Value::Number(d) => {
520 *declared_default = Some(ValueKind::Number(*d));
521 }
522 other => {
523 return Err(format!(
524 "default for number type requires a number literal, got {}",
525 value_kind_name(other)
526 ));
527 }
528 },
529 _ => {
530 return Err(format!(
531 "Invalid command '{}' for number type. Valid commands: minimum, maximum, decimals, precision, help, default",
532 command
533 ));
534 }
535 },
536 TypeSpecification::Ratio {
537 decimals,
538 minimum,
539 maximum,
540 units,
541 help,
542 } => match command {
543 TypeConstraintCommand::Decimals => {
544 let d = require_decimal_literal(args, "decimals")?;
545 *decimals = Some(decimal_to_u8(d, "decimals")?);
546 }
547 TypeConstraintCommand::Unit => {
548 let (unit_name, value) = match args {
549 [CommandArg::Label(name), CommandArg::Literal(crate::literals::Value::Number(v))] => {
550 (name.clone(), *v)
551 }
552 _ => {
553 return Err(
554 "unit requires a unit name followed by a numeric conversion factor (e.g., 'unit percent 100')"
555 .to_string(),
556 );
557 }
558 };
559 if let Some(u) = units
560 .0
561 .iter_mut()
562 .find(|u| u.name.eq_ignore_ascii_case(&unit_name))
563 {
564 u.value = value;
565 } else {
566 units.0.push(RatioUnit {
567 name: unit_name,
568 value,
569 });
570 }
571 }
572 TypeConstraintCommand::Minimum => {
573 *minimum = Some(require_ratio_literal(args, "minimum")?);
574 }
575 TypeConstraintCommand::Maximum => {
576 *maximum = Some(require_ratio_literal(args, "maximum")?);
577 }
578 TypeConstraintCommand::Help => {
579 apply_type_help_command(help, args)?;
580 }
581 TypeConstraintCommand::Default => {
582 let d = require_ratio_literal(args, "default")?;
583 *declared_default = Some(ValueKind::Ratio(d, None));
584 }
585 _ => {
586 return Err(format!(
587 "Invalid command '{}' for ratio type. Valid commands: unit, minimum, maximum, decimals, help, default",
588 command
589 ));
590 }
591 },
592 TypeSpecification::Text {
593 length,
594 options,
595 help,
596 } => match command {
597 TypeConstraintCommand::Option => {
598 if args.len() != 1 {
599 return Err("option takes exactly one argument".to_string());
600 }
601 options.push(option_name(&args[0], "option")?);
602 }
603 TypeConstraintCommand::Options => {
604 let mut collected = Vec::with_capacity(args.len());
605 for arg in args {
606 collected.push(option_name(arg, "options")?);
607 }
608 *options = collected;
609 }
610 TypeConstraintCommand::Length => {
611 let d = require_decimal_literal(args, "length")?;
612 *length = Some(decimal_to_usize(d, "length")?);
613 }
614 TypeConstraintCommand::Help => {
615 apply_type_help_command(help, args)?;
616 }
617 TypeConstraintCommand::Default => match require_literal(args, "default")? {
618 crate::literals::Value::Text(s) => {
619 *declared_default = Some(ValueKind::Text(s.clone()));
620 }
621 other => {
622 return Err(format!(
623 "default for text type requires a text literal (quoted string), got {}",
624 value_kind_name(other)
625 ));
626 }
627 },
628 _ => {
629 return Err(format!(
630 "Invalid command '{}' for text type. Valid commands: options, length, help, default",
631 command
632 ));
633 }
634 },
635 TypeSpecification::Date {
636 minimum,
637 maximum,
638 help,
639 } => match command {
640 TypeConstraintCommand::Minimum => {
641 let dt = require_date_literal(args, "minimum")?;
642 *minimum = Some(dt);
643 }
644 TypeConstraintCommand::Maximum => {
645 let dt = require_date_literal(args, "maximum")?;
646 *maximum = Some(dt);
647 }
648 TypeConstraintCommand::Help => {
649 apply_type_help_command(help, args)?;
650 }
651 TypeConstraintCommand::Default => {
652 let dt = require_date_literal(args, "default")?;
653 *declared_default = Some(ValueKind::Date(date_time_to_semantic(&dt)));
654 }
655 _ => {
656 return Err(format!(
657 "Invalid command '{}' for date type. Valid commands: minimum, maximum, help, default",
658 command
659 ));
660 }
661 },
662 TypeSpecification::Time {
663 minimum,
664 maximum,
665 help,
666 } => match command {
667 TypeConstraintCommand::Minimum => {
668 let t = require_time_literal(args, "minimum")?;
669 *minimum = Some(t);
670 }
671 TypeConstraintCommand::Maximum => {
672 let t = require_time_literal(args, "maximum")?;
673 *maximum = Some(t);
674 }
675 TypeConstraintCommand::Help => {
676 apply_type_help_command(help, args)?;
677 }
678 TypeConstraintCommand::Default => {
679 let t = require_time_literal(args, "default")?;
680 *declared_default = Some(ValueKind::Time(time_to_semantic(&t)));
681 }
682 _ => {
683 return Err(format!(
684 "Invalid command '{}' for time type. Valid commands: minimum, maximum, help, default",
685 command
686 ));
687 }
688 },
689 TypeSpecification::Duration {
690 minimum,
691 maximum,
692 help,
693 } => match command {
694 TypeConstraintCommand::Help => {
695 apply_type_help_command(help, args)?;
696 }
697 TypeConstraintCommand::Minimum => {
698 let (value, unit) = require_duration_literal(args, "minimum")?;
699 *minimum = Some((value, duration_unit_to_semantic(&unit)));
700 }
701 TypeConstraintCommand::Maximum => {
702 let (value, unit) = require_duration_literal(args, "maximum")?;
703 *maximum = Some((value, duration_unit_to_semantic(&unit)));
704 }
705 TypeConstraintCommand::Default => {
706 let (value, unit) = require_duration_literal(args, "default")?;
707 *declared_default =
708 Some(ValueKind::Duration(value, duration_unit_to_semantic(&unit)));
709 }
710 _ => {
711 return Err(format!(
712 "Invalid command '{}' for duration type. Valid commands: minimum, maximum, help, default",
713 command
714 ));
715 }
716 },
717 TypeSpecification::Veto { .. } => {
718 return Err(format!(
719 "Invalid command '{}' for veto type. Veto is not a user-declarable type and cannot have constraints",
720 command
721 ));
722 }
723 TypeSpecification::Undetermined => {
724 return Err(format!(
725 "Invalid command '{}' for undetermined sentinel type. Undetermined is an internal type used during type inference and cannot have constraints",
726 command
727 ));
728 }
729 }
730 Ok(self)
731 }
732}
733
734pub fn parse_number_unit(
737 value_str: &str,
738 type_spec: &TypeSpecification,
739) -> Result<crate::parsing::ast::Value, String> {
740 use crate::literals::{NumberWithUnit, RatioLiteral};
741 use crate::parsing::ast::Value;
742
743 let trimmed = value_str.trim();
744 match type_spec {
745 TypeSpecification::Scale { units, .. } => {
746 if units.is_empty() {
747 unreachable!(
748 "BUG: Scale type has no units; should have been validated during planning"
749 );
750 }
751 match trimmed.parse::<NumberWithUnit>() {
752 Ok(n) => {
753 let unit = units.get(&n.1).map_err(|e| e.to_string())?;
754 Ok(Value::Scale(n.0, unit.name.clone()))
755 }
756 Err(e) => {
757 if trimmed.split_whitespace().count() == 1 && !trimmed.is_empty() {
758 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
759 let example_unit = units
760 .iter()
761 .next()
762 .expect("BUG: units non-empty after guard")
763 .name
764 .as_str();
765 Err(format!(
766 "Scale value must include a unit, for example: '{} {}'. Valid units: {}.",
767 trimmed,
768 example_unit,
769 valid.join(", ")
770 ))
771 } else {
772 Err(e)
773 }
774 }
775 }
776 }
777 TypeSpecification::Ratio { units, .. } => {
778 if units.is_empty() {
779 unreachable!(
780 "BUG: Ratio type has no units; should have been validated during planning"
781 );
782 }
783 match trimmed.parse::<RatioLiteral>()? {
784 RatioLiteral::Bare(n) => Ok(Value::Ratio(n, None)),
785 RatioLiteral::Percent(n) => {
790 let unit = units.get("percent").map_err(|e| e.to_string())?;
791 Ok(Value::Ratio(n, Some(unit.name.clone())))
792 }
793 RatioLiteral::Permille(n) => {
794 let unit = units.get("permille").map_err(|e| e.to_string())?;
795 Ok(Value::Ratio(n, Some(unit.name.clone())))
796 }
797 RatioLiteral::Named { value, unit } => {
798 let resolved = units.get(&unit).map_err(|e| e.to_string())?;
799 Ok(Value::Ratio(
800 value / resolved.value,
801 Some(resolved.name.clone()),
802 ))
803 }
804 }
805 }
806 _ => Err("parse_number_unit only accepts Scale or Ratio type".to_string()),
807 }
808}
809
810pub fn parse_value_from_string(
813 value_str: &str,
814 type_spec: &TypeSpecification,
815 source: &Source,
816) -> Result<crate::parsing::ast::Value, Error> {
817 use crate::parsing::ast::Value;
818
819 let to_err = |msg: String| Error::validation(msg, Some(source.clone()), None::<String>);
820
821 match type_spec {
822 TypeSpecification::Text { .. } => value_str
823 .parse::<crate::literals::TextLiteral>()
824 .map(|t| Value::Text(t.0))
825 .map_err(to_err),
826 TypeSpecification::Number { .. } => value_str
827 .parse::<crate::literals::NumberLiteral>()
828 .map(|n| Value::Number(n.0))
829 .map_err(to_err),
830 TypeSpecification::Scale { .. } => {
831 parse_number_unit(value_str, type_spec).map_err(to_err)
832 }
833 TypeSpecification::Boolean { .. } => value_str
834 .parse::<BooleanValue>()
835 .map(Value::Boolean)
836 .map_err(to_err),
837 TypeSpecification::Date { .. } => {
838 let date = value_str.parse::<DateTimeValue>().map_err(to_err)?;
839 Ok(Value::Date(date))
840 }
841 TypeSpecification::Time { .. } => {
842 let time = value_str.parse::<TimeValue>().map_err(to_err)?;
843 Ok(Value::Time(time))
844 }
845 TypeSpecification::Duration { .. } => value_str
846 .parse::<crate::literals::DurationLiteral>()
847 .map(|d| Value::Duration(d.0, d.1))
848 .map_err(to_err),
849 TypeSpecification::Ratio { .. } => {
850 parse_number_unit(value_str, type_spec).map_err(to_err)
851 }
852 TypeSpecification::Veto { .. } => Err(to_err(
853 "Veto type cannot be parsed from string".to_string(),
854 )),
855 TypeSpecification::Undetermined => unreachable!(
856 "BUG: parse_value_from_string called with Undetermined sentinel type; this type exists only during type inference"
857 ),
858 }
859}
860
861#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
867#[serde(rename_all = "snake_case")]
868pub enum SemanticDurationUnit {
869 Year,
870 Month,
871 Week,
872 Day,
873 Hour,
874 Minute,
875 Second,
876 Millisecond,
877 Microsecond,
878}
879
880impl fmt::Display for SemanticDurationUnit {
881 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
882 let s = match self {
883 SemanticDurationUnit::Year => "years",
884 SemanticDurationUnit::Month => "months",
885 SemanticDurationUnit::Week => "weeks",
886 SemanticDurationUnit::Day => "days",
887 SemanticDurationUnit::Hour => "hours",
888 SemanticDurationUnit::Minute => "minutes",
889 SemanticDurationUnit::Second => "seconds",
890 SemanticDurationUnit::Millisecond => "milliseconds",
891 SemanticDurationUnit::Microsecond => "microseconds",
892 };
893 write!(f, "{}", s)
894 }
895}
896
897#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
901#[serde(rename_all = "snake_case")]
902pub enum SemanticConversionTarget {
903 Duration(SemanticDurationUnit),
904 ScaleUnit(String),
905 RatioUnit(String),
906}
907
908impl fmt::Display for SemanticConversionTarget {
909 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
910 match self {
911 SemanticConversionTarget::Duration(u) => write!(f, "{}", u),
912 SemanticConversionTarget::ScaleUnit(s) => write!(f, "{}", s),
913 SemanticConversionTarget::RatioUnit(s) => write!(f, "{}", s),
914 }
915 }
916}
917
918#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
920pub struct SemanticTimezone {
921 pub offset_hours: i8,
922 pub offset_minutes: u8,
923}
924
925impl fmt::Display for SemanticTimezone {
926 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
927 if self.offset_hours == 0 && self.offset_minutes == 0 {
928 write!(f, "Z")
929 } else {
930 let sign = if self.offset_hours >= 0 { "+" } else { "-" };
931 let hours = self.offset_hours.abs();
932 write!(f, "{}{:02}:{:02}", sign, hours, self.offset_minutes)
933 }
934 }
935}
936
937#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
939pub struct SemanticTime {
940 pub hour: u32,
941 pub minute: u32,
942 pub second: u32,
943 pub timezone: Option<SemanticTimezone>,
944}
945
946impl fmt::Display for SemanticTime {
947 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
948 write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)
949 }
950}
951
952#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
954pub struct SemanticDateTime {
955 pub year: i32,
956 pub month: u32,
957 pub day: u32,
958 pub hour: u32,
959 pub minute: u32,
960 pub second: u32,
961 #[serde(default)]
962 pub microsecond: u32,
963 pub timezone: Option<SemanticTimezone>,
964}
965
966impl fmt::Display for SemanticDateTime {
967 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
968 let has_time = self.hour != 0
969 || self.minute != 0
970 || self.second != 0
971 || self.microsecond != 0
972 || self.timezone.is_some();
973 if !has_time {
974 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
975 } else {
976 write!(
977 f,
978 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
979 self.year, self.month, self.day, self.hour, self.minute, self.second
980 )?;
981 if self.microsecond != 0 {
982 write!(f, ".{:06}", self.microsecond)?;
983 }
984 if let Some(tz) = &self.timezone {
985 write!(f, "{}", tz)?;
986 }
987 Ok(())
988 }
989 }
990}
991
992#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
995#[serde(rename_all = "snake_case")]
996pub enum ValueKind {
997 Number(Decimal),
998 Scale(Decimal, String),
1000 Text(String),
1001 Date(SemanticDateTime),
1002 Time(SemanticTime),
1003 Boolean(bool),
1004 Duration(Decimal, SemanticDurationUnit),
1006 Ratio(Decimal, Option<String>),
1008}
1009
1010impl fmt::Display for ValueKind {
1011 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1012 use crate::parsing::ast::Value;
1013 match self {
1014 ValueKind::Number(n) => {
1015 let norm = n.normalize();
1016 let s = if norm.fract().is_zero() {
1017 norm.trunc().to_string()
1018 } else {
1019 norm.to_string()
1020 };
1021 write!(f, "{}", s)
1022 }
1023 ValueKind::Scale(n, u) => write!(f, "{}", Value::Scale(*n, u.clone())),
1024 ValueKind::Text(s) => write!(f, "{}", Value::Text(s.clone())),
1025 ValueKind::Ratio(r, u) => write!(f, "{}", Value::Ratio(*r, u.clone())),
1026 ValueKind::Date(dt) => write!(f, "{}", dt),
1027 ValueKind::Time(t) => write!(
1028 f,
1029 "{}",
1030 Value::Time(crate::parsing::ast::TimeValue {
1031 hour: t.hour as u8,
1032 minute: t.minute as u8,
1033 second: t.second as u8,
1034 timezone: t
1035 .timezone
1036 .as_ref()
1037 .map(|tz| crate::parsing::ast::TimezoneValue {
1038 offset_hours: tz.offset_hours,
1039 offset_minutes: tz.offset_minutes,
1040 }),
1041 })
1042 ),
1043 ValueKind::Boolean(b) => write!(f, "{}", b),
1044 ValueKind::Duration(v, u) => write!(f, "{} {}", v, u),
1045 }
1046 }
1047}
1048
1049#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1058pub struct PathSegment {
1059 pub data: String,
1061 pub spec: String,
1063}
1064
1065#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1069pub struct DataPath {
1070 pub segments: Vec<PathSegment>,
1072 pub data: String,
1074}
1075
1076impl DataPath {
1077 pub fn new(segments: Vec<PathSegment>, data: String) -> Self {
1079 Self { segments, data }
1080 }
1081
1082 pub fn local(data: String) -> Self {
1084 Self {
1085 segments: vec![],
1086 data,
1087 }
1088 }
1089
1090 pub fn input_key(&self) -> String {
1093 let mut s = String::new();
1094 for segment in &self.segments {
1095 s.push_str(&segment.data);
1096 s.push('.');
1097 }
1098 s.push_str(&self.data);
1099 s
1100 }
1101}
1102
1103#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1107pub struct RulePath {
1108 pub segments: Vec<PathSegment>,
1110 pub rule: String,
1112}
1113
1114impl RulePath {
1115 pub fn new(segments: Vec<PathSegment>, rule: String) -> Self {
1117 Self { segments, rule }
1118 }
1119}
1120
1121#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1130pub struct Expression {
1131 pub kind: ExpressionKind,
1132 pub source_location: Option<Source>,
1133}
1134
1135impl Expression {
1136 pub fn new(kind: ExpressionKind, source_location: Source) -> Self {
1137 Self {
1138 kind,
1139 source_location: Some(source_location),
1140 }
1141 }
1142
1143 pub fn with_source(kind: ExpressionKind, source_location: Option<Source>) -> Self {
1145 Self {
1146 kind,
1147 source_location,
1148 }
1149 }
1150
1151 pub fn collect_data_paths(&self, data: &mut std::collections::HashSet<DataPath>) {
1153 self.kind.collect_data_paths(data);
1154 }
1155}
1156
1157#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1159#[serde(rename_all = "snake_case")]
1160pub enum ExpressionKind {
1161 Literal(Box<LiteralValue>),
1163 DataPath(DataPath),
1165 RulePath(RulePath),
1167 LogicalAnd(Arc<Expression>, Arc<Expression>),
1168 Arithmetic(Arc<Expression>, ArithmeticComputation, Arc<Expression>),
1169 Comparison(Arc<Expression>, ComparisonComputation, Arc<Expression>),
1170 UnitConversion(Arc<Expression>, SemanticConversionTarget),
1171 LogicalNegation(Arc<Expression>, NegationType),
1172 MathematicalComputation(MathematicalComputation, Arc<Expression>),
1173 Veto(VetoExpression),
1174 Now,
1176 DateRelative(DateRelativeKind, Arc<Expression>, Option<Arc<Expression>>),
1178 DateCalendar(DateCalendarKind, CalendarUnit, Arc<Expression>),
1180}
1181
1182impl ExpressionKind {
1183 pub(crate) fn collect_data_paths(&self, data: &mut std::collections::HashSet<DataPath>) {
1185 match self {
1186 ExpressionKind::DataPath(fp) => {
1187 data.insert(fp.clone());
1188 }
1189 ExpressionKind::LogicalAnd(left, right)
1190 | ExpressionKind::Arithmetic(left, _, right)
1191 | ExpressionKind::Comparison(left, _, right) => {
1192 left.collect_data_paths(data);
1193 right.collect_data_paths(data);
1194 }
1195 ExpressionKind::UnitConversion(inner, _)
1196 | ExpressionKind::LogicalNegation(inner, _)
1197 | ExpressionKind::MathematicalComputation(_, inner) => {
1198 inner.collect_data_paths(data);
1199 }
1200 ExpressionKind::DateRelative(_, date_expr, tolerance) => {
1201 date_expr.collect_data_paths(data);
1202 if let Some(tol) = tolerance {
1203 tol.collect_data_paths(data);
1204 }
1205 }
1206 ExpressionKind::DateCalendar(_, _, date_expr) => {
1207 date_expr.collect_data_paths(data);
1208 }
1209 ExpressionKind::Literal(_)
1210 | ExpressionKind::RulePath(_)
1211 | ExpressionKind::Veto(_)
1212 | ExpressionKind::Now => {}
1213 }
1214 }
1215}
1216
1217#[derive(Clone, Debug, Serialize, Deserialize)]
1223#[serde(tag = "kind", rename_all = "snake_case")]
1224pub enum TypeDefiningSpec {
1225 Local,
1227 Import { spec: Arc<LemmaSpec> },
1229}
1230
1231#[derive(Clone, Debug, Serialize, Deserialize)]
1233#[serde(rename_all = "snake_case")]
1234pub enum TypeExtends {
1235 Primitive,
1237 Custom {
1240 parent: String,
1241 family: String,
1242 defining_spec: TypeDefiningSpec,
1243 },
1244}
1245
1246impl PartialEq for TypeExtends {
1247 fn eq(&self, other: &Self) -> bool {
1248 match (self, other) {
1249 (TypeExtends::Primitive, TypeExtends::Primitive) => true,
1250 (
1251 TypeExtends::Custom {
1252 parent: lp,
1253 family: lf,
1254 defining_spec: ld,
1255 },
1256 TypeExtends::Custom {
1257 parent: rp,
1258 family: rf,
1259 defining_spec: rd,
1260 },
1261 ) => {
1262 lp == rp
1263 && lf == rf
1264 && match (ld, rd) {
1265 (TypeDefiningSpec::Local, TypeDefiningSpec::Local) => true,
1266 (
1267 TypeDefiningSpec::Import { spec: left },
1268 TypeDefiningSpec::Import { spec: right },
1269 ) => Arc::ptr_eq(left, right),
1270 _ => false,
1271 }
1272 }
1273 _ => false,
1274 }
1275 }
1276}
1277
1278impl Eq for TypeExtends {}
1279
1280impl TypeExtends {
1281 #[must_use]
1283 pub fn custom_local(parent: String, family: String) -> Self {
1284 TypeExtends::Custom {
1285 parent,
1286 family,
1287 defining_spec: TypeDefiningSpec::Local,
1288 }
1289 }
1290
1291 #[must_use]
1293 pub fn parent_name(&self) -> Option<&str> {
1294 match self {
1295 TypeExtends::Primitive => None,
1296 TypeExtends::Custom { parent, .. } => Some(parent.as_str()),
1297 }
1298 }
1299}
1300
1301#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1306pub struct LemmaType {
1307 pub name: Option<String>,
1309 #[serde(flatten)]
1314 pub specifications: TypeSpecification,
1315 pub extends: TypeExtends,
1317}
1318
1319impl LemmaType {
1320 pub fn new(name: String, specifications: TypeSpecification, extends: TypeExtends) -> Self {
1322 Self {
1323 name: Some(name),
1324 specifications,
1325 extends,
1326 }
1327 }
1328
1329 pub fn without_name(specifications: TypeSpecification, extends: TypeExtends) -> Self {
1331 Self {
1332 name: None,
1333 specifications,
1334 extends,
1335 }
1336 }
1337
1338 pub fn primitive(specifications: TypeSpecification) -> Self {
1340 Self {
1341 name: None,
1342 specifications,
1343 extends: TypeExtends::Primitive,
1344 }
1345 }
1346
1347 pub fn name(&self) -> String {
1349 self.name.clone().unwrap_or_else(|| {
1350 match &self.specifications {
1351 TypeSpecification::Boolean { .. } => "boolean",
1352 TypeSpecification::Scale { .. } => "scale",
1353 TypeSpecification::Number { .. } => "number",
1354 TypeSpecification::Text { .. } => "text",
1355 TypeSpecification::Date { .. } => "date",
1356 TypeSpecification::Time { .. } => "time",
1357 TypeSpecification::Duration { .. } => "duration",
1358 TypeSpecification::Ratio { .. } => "ratio",
1359 TypeSpecification::Veto { .. } => "veto",
1360 TypeSpecification::Undetermined => "undetermined",
1361 }
1362 .to_string()
1363 })
1364 }
1365
1366 pub fn is_boolean(&self) -> bool {
1368 matches!(&self.specifications, TypeSpecification::Boolean { .. })
1369 }
1370
1371 pub fn is_scale(&self) -> bool {
1373 matches!(&self.specifications, TypeSpecification::Scale { .. })
1374 }
1375
1376 pub fn is_number(&self) -> bool {
1378 matches!(&self.specifications, TypeSpecification::Number { .. })
1379 }
1380
1381 pub fn is_numeric(&self) -> bool {
1383 matches!(
1384 &self.specifications,
1385 TypeSpecification::Scale { .. } | TypeSpecification::Number { .. }
1386 )
1387 }
1388
1389 pub fn is_text(&self) -> bool {
1391 matches!(&self.specifications, TypeSpecification::Text { .. })
1392 }
1393
1394 pub fn is_date(&self) -> bool {
1396 matches!(&self.specifications, TypeSpecification::Date { .. })
1397 }
1398
1399 pub fn is_time(&self) -> bool {
1401 matches!(&self.specifications, TypeSpecification::Time { .. })
1402 }
1403
1404 pub fn is_duration(&self) -> bool {
1406 matches!(&self.specifications, TypeSpecification::Duration { .. })
1407 }
1408
1409 pub fn is_ratio(&self) -> bool {
1411 matches!(&self.specifications, TypeSpecification::Ratio { .. })
1412 }
1413
1414 pub fn vetoed(&self) -> bool {
1416 matches!(&self.specifications, TypeSpecification::Veto { .. })
1417 }
1418
1419 pub fn is_undetermined(&self) -> bool {
1421 matches!(&self.specifications, TypeSpecification::Undetermined)
1422 }
1423
1424 pub fn has_same_base_type(&self, other: &LemmaType) -> bool {
1426 use TypeSpecification::*;
1427 matches!(
1428 (&self.specifications, &other.specifications),
1429 (Boolean { .. }, Boolean { .. })
1430 | (Number { .. }, Number { .. })
1431 | (Scale { .. }, Scale { .. })
1432 | (Text { .. }, Text { .. })
1433 | (Date { .. }, Date { .. })
1434 | (Time { .. }, Time { .. })
1435 | (Duration { .. }, Duration { .. })
1436 | (Ratio { .. }, Ratio { .. })
1437 | (Veto { .. }, Veto { .. })
1438 | (Undetermined, Undetermined)
1439 )
1440 }
1441
1442 #[must_use]
1444 pub fn scale_family_name(&self) -> Option<&str> {
1445 if !self.is_scale() {
1446 return None;
1447 }
1448 match &self.extends {
1449 TypeExtends::Custom { family, .. } => Some(family.as_str()),
1450 TypeExtends::Primitive => self.name.as_deref(),
1451 }
1452 }
1453
1454 #[must_use]
1457 pub fn same_scale_family(&self, other: &LemmaType) -> bool {
1458 if !self.is_scale() || !other.is_scale() {
1459 return false;
1460 }
1461 match (self.scale_family_name(), other.scale_family_name()) {
1462 (Some(self_family), Some(other_family)) => self_family == other_family,
1463 (None, None) => true,
1465 _ => false,
1466 }
1467 }
1468
1469 pub fn veto_type() -> Self {
1471 Self::primitive(TypeSpecification::veto())
1472 }
1473
1474 pub fn undetermined_type() -> Self {
1477 Self::primitive(TypeSpecification::Undetermined)
1478 }
1479
1480 pub fn decimal_places(&self) -> Option<u8> {
1483 match &self.specifications {
1484 TypeSpecification::Number { decimals, .. } => *decimals,
1485 TypeSpecification::Scale { decimals, .. } => *decimals,
1486 TypeSpecification::Ratio { decimals, .. } => *decimals,
1487 _ => None,
1488 }
1489 }
1490
1491 pub fn example_value(&self) -> &'static str {
1493 match &self.specifications {
1494 TypeSpecification::Text { .. } => "\"hello world\"",
1495 TypeSpecification::Scale { .. } => "12.50 eur",
1496 TypeSpecification::Number { .. } => "3.14",
1497 TypeSpecification::Boolean { .. } => "true",
1498 TypeSpecification::Date { .. } => "2023-12-25T14:30:00Z",
1499 TypeSpecification::Veto { .. } => "veto",
1500 TypeSpecification::Time { .. } => "14:30:00",
1501 TypeSpecification::Duration { .. } => "90 minutes",
1502 TypeSpecification::Ratio { .. } => "50%",
1503 TypeSpecification::Undetermined => unreachable!(
1504 "BUG: example_value called on Undetermined sentinel type; this type must never reach user-facing code"
1505 ),
1506 }
1507 }
1508
1509 #[must_use]
1513 pub fn scale_unit_factor(&self, unit_name: &str) -> Decimal {
1514 let units = match &self.specifications {
1515 TypeSpecification::Scale { units, .. } => units,
1516 _ => unreachable!(
1517 "BUG: scale_unit_factor called with non-scale type {}; only call during evaluation after planning validated scale conversion",
1518 self.name()
1519 ),
1520 };
1521 match units
1522 .iter()
1523 .find(|u| u.name.eq_ignore_ascii_case(unit_name))
1524 {
1525 Some(ScaleUnit { value, .. }) => *value,
1526 None => {
1527 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
1528 unreachable!(
1529 "BUG: unknown unit '{}' for scale type {} (valid: {}); planning must reject invalid conversions with Error",
1530 unit_name,
1531 self.name(),
1532 valid.join(", ")
1533 );
1534 }
1535 }
1536 }
1537}
1538
1539#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
1541pub struct LiteralValue {
1542 pub value: ValueKind,
1543 pub lemma_type: LemmaType,
1544}
1545
1546impl Serialize for LiteralValue {
1547 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1548 where
1549 S: serde::Serializer,
1550 {
1551 use serde::ser::SerializeStruct;
1552 let mut state = serializer.serialize_struct("LiteralValue", 3)?;
1553 state.serialize_field("value", &self.value)?;
1554 state.serialize_field("lemma_type", &self.lemma_type)?;
1555 state.serialize_field("display_value", &self.display_value())?;
1556 state.end()
1557 }
1558}
1559
1560impl LiteralValue {
1561 pub fn text(s: String) -> Self {
1562 Self {
1563 value: ValueKind::Text(s),
1564 lemma_type: primitive_text().clone(),
1565 }
1566 }
1567
1568 pub fn text_with_type(s: String, lemma_type: LemmaType) -> Self {
1569 Self {
1570 value: ValueKind::Text(s),
1571 lemma_type,
1572 }
1573 }
1574
1575 pub fn number(n: Decimal) -> Self {
1576 Self {
1577 value: ValueKind::Number(n),
1578 lemma_type: primitive_number().clone(),
1579 }
1580 }
1581
1582 pub fn number_with_type(n: Decimal, lemma_type: LemmaType) -> Self {
1583 Self {
1584 value: ValueKind::Number(n),
1585 lemma_type,
1586 }
1587 }
1588
1589 pub fn scale_with_type(n: Decimal, unit: String, lemma_type: LemmaType) -> Self {
1590 Self {
1591 value: ValueKind::Scale(n, unit),
1592 lemma_type,
1593 }
1594 }
1595
1596 pub fn number_interpreted_as_scale(value: Decimal, unit_name: String) -> Self {
1599 let lemma_type = LemmaType {
1600 name: None,
1601 specifications: TypeSpecification::Scale {
1602 minimum: None,
1603 maximum: None,
1604 decimals: None,
1605 precision: None,
1606 units: ScaleUnits::from(vec![ScaleUnit {
1607 name: unit_name.clone(),
1608 value: Decimal::from(1),
1609 }]),
1610 help: "Format: {value} {unit} (e.g. 100 kilograms)".to_string(),
1611 },
1612 extends: TypeExtends::Primitive,
1613 };
1614 Self {
1615 value: ValueKind::Scale(value, unit_name),
1616 lemma_type,
1617 }
1618 }
1619
1620 pub fn from_bool(b: bool) -> Self {
1621 Self {
1622 value: ValueKind::Boolean(b),
1623 lemma_type: primitive_boolean().clone(),
1624 }
1625 }
1626
1627 pub fn date(dt: SemanticDateTime) -> Self {
1628 Self {
1629 value: ValueKind::Date(dt),
1630 lemma_type: primitive_date().clone(),
1631 }
1632 }
1633
1634 pub fn date_with_type(dt: SemanticDateTime, lemma_type: LemmaType) -> Self {
1635 Self {
1636 value: ValueKind::Date(dt),
1637 lemma_type,
1638 }
1639 }
1640
1641 pub fn time(t: SemanticTime) -> Self {
1642 Self {
1643 value: ValueKind::Time(t),
1644 lemma_type: primitive_time().clone(),
1645 }
1646 }
1647
1648 pub fn time_with_type(t: SemanticTime, lemma_type: LemmaType) -> Self {
1649 Self {
1650 value: ValueKind::Time(t),
1651 lemma_type,
1652 }
1653 }
1654
1655 pub fn duration(value: Decimal, unit: SemanticDurationUnit) -> Self {
1656 Self {
1657 value: ValueKind::Duration(value, unit),
1658 lemma_type: primitive_duration().clone(),
1659 }
1660 }
1661
1662 pub fn duration_with_type(
1663 value: Decimal,
1664 unit: SemanticDurationUnit,
1665 lemma_type: LemmaType,
1666 ) -> Self {
1667 Self {
1668 value: ValueKind::Duration(value, unit),
1669 lemma_type,
1670 }
1671 }
1672
1673 pub fn ratio(r: Decimal, unit: Option<String>) -> Self {
1674 Self {
1675 value: ValueKind::Ratio(r, unit),
1676 lemma_type: primitive_ratio().clone(),
1677 }
1678 }
1679
1680 pub fn ratio_with_type(r: Decimal, unit: Option<String>, lemma_type: LemmaType) -> Self {
1681 Self {
1682 value: ValueKind::Ratio(r, unit),
1683 lemma_type,
1684 }
1685 }
1686
1687 pub fn display_value(&self) -> String {
1689 format!("{}", self)
1690 }
1691
1692 pub fn byte_size(&self) -> usize {
1694 format!("{}", self).len()
1695 }
1696
1697 pub fn get_type(&self) -> &LemmaType {
1699 &self.lemma_type
1700 }
1701}
1702
1703#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1705#[serde(rename_all = "snake_case")]
1706pub enum DataValue {
1707 Definition {
1708 schema_type: LemmaType,
1709 #[serde(default, skip_serializing_if = "Option::is_none")]
1710 bound_value: Option<LiteralValue>,
1711 },
1712}
1713
1714impl DataValue {
1715 #[must_use]
1716 pub fn from_bound_literal(value: LiteralValue) -> Self {
1717 let schema_type = value.get_type().clone();
1718 Self::Definition {
1719 schema_type,
1720 bound_value: Some(value),
1721 }
1722 }
1723}
1724
1725#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1727pub struct Data {
1728 pub path: DataPath,
1729 pub value: DataValue,
1730 pub source: Option<Source>,
1731}
1732
1733#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1736#[serde(rename_all = "snake_case", tag = "kind")]
1737pub enum ReferenceTarget {
1738 Data(DataPath),
1739 Rule(RulePath),
1740}
1741
1742#[derive(Clone, Debug, Serialize, Deserialize)]
1744#[serde(rename_all = "snake_case")]
1745pub enum DataDefinition {
1746 Value { value: LiteralValue, source: Source },
1748 TypeDeclaration {
1753 resolved_type: LemmaType,
1754 declared_default: Option<ValueKind>,
1755 source: Source,
1756 },
1757 Import {
1759 spec: Arc<crate::parsing::ast::LemmaSpec>,
1760 source: Source,
1761 },
1762 Reference {
1788 target: ReferenceTarget,
1789 resolved_type: LemmaType,
1790 local_constraints: Option<Vec<Constraint>>,
1791 local_default: Option<ValueKind>,
1792 source: Source,
1793 },
1794}
1795
1796impl DataDefinition {
1797 pub fn schema_type(&self) -> Option<&LemmaType> {
1799 match self {
1800 DataDefinition::Value { value, .. } => Some(&value.lemma_type),
1801 DataDefinition::TypeDeclaration { resolved_type, .. } => Some(resolved_type),
1802 DataDefinition::Reference { resolved_type, .. } => Some(resolved_type),
1803 DataDefinition::Import { .. } => None,
1804 }
1805 }
1806
1807 pub fn value(&self) -> Option<&LiteralValue> {
1811 match self {
1812 DataDefinition::Value { value, .. } => Some(value),
1813 DataDefinition::TypeDeclaration { .. }
1814 | DataDefinition::Import { .. }
1815 | DataDefinition::Reference { .. } => None,
1816 }
1817 }
1818
1819 #[inline]
1823 pub fn bound_value(&self) -> Option<&LiteralValue> {
1824 self.value()
1825 }
1826
1827 pub fn default_suggestion(&self) -> Option<LiteralValue> {
1832 match self {
1833 DataDefinition::TypeDeclaration {
1834 resolved_type,
1835 declared_default: Some(dv),
1836 ..
1837 } => Some(LiteralValue {
1838 value: dv.clone(),
1839 lemma_type: resolved_type.clone(),
1840 }),
1841 DataDefinition::Reference {
1842 resolved_type,
1843 local_default: Some(dv),
1844 ..
1845 } => Some(LiteralValue {
1846 value: dv.clone(),
1847 lemma_type: resolved_type.clone(),
1848 }),
1849 DataDefinition::Value { .. }
1850 | DataDefinition::TypeDeclaration { .. }
1851 | DataDefinition::Reference { .. }
1852 | DataDefinition::Import { .. } => None,
1853 }
1854 }
1855
1856 pub fn source(&self) -> &Source {
1858 match self {
1859 DataDefinition::Value { source, .. } => source,
1860 DataDefinition::TypeDeclaration { source, .. } => source,
1861 DataDefinition::Import { source, .. } => source,
1862 DataDefinition::Reference { source, .. } => source,
1863 }
1864 }
1865
1866 pub fn reference_target(&self) -> Option<&ReferenceTarget> {
1869 match self {
1870 DataDefinition::Reference { target, .. } => Some(target),
1871 _ => None,
1872 }
1873 }
1874}
1875
1876pub fn value_to_semantic(value: &crate::parsing::ast::Value) -> Result<ValueKind, String> {
1878 use crate::parsing::ast::Value;
1879 Ok(match value {
1880 Value::Number(n) => ValueKind::Number(*n),
1881 Value::Text(s) => ValueKind::Text(s.clone()),
1882 Value::Boolean(b) => ValueKind::Boolean(bool::from(*b)),
1883 Value::Date(dt) => ValueKind::Date(date_time_to_semantic(dt)),
1884 Value::Time(t) => ValueKind::Time(time_to_semantic(t)),
1885 Value::Duration(n, u) => ValueKind::Duration(*n, duration_unit_to_semantic(u)),
1886 Value::Scale(n, unit) => ValueKind::Scale(*n, unit.clone()),
1887 Value::Ratio(n, unit) => ValueKind::Ratio(*n, unit.clone()),
1888 })
1889}
1890
1891pub(crate) fn date_time_to_semantic(dt: &crate::parsing::ast::DateTimeValue) -> SemanticDateTime {
1893 SemanticDateTime {
1894 year: dt.year,
1895 month: dt.month,
1896 day: dt.day,
1897 hour: dt.hour,
1898 minute: dt.minute,
1899 second: dt.second,
1900 microsecond: dt.microsecond,
1901 timezone: dt.timezone.as_ref().map(|tz| SemanticTimezone {
1902 offset_hours: tz.offset_hours,
1903 offset_minutes: tz.offset_minutes,
1904 }),
1905 }
1906}
1907
1908pub(crate) fn time_to_semantic(t: &crate::parsing::ast::TimeValue) -> SemanticTime {
1910 SemanticTime {
1911 hour: t.hour.into(),
1912 minute: t.minute.into(),
1913 second: t.second.into(),
1914 timezone: t.timezone.as_ref().map(|tz| SemanticTimezone {
1915 offset_hours: tz.offset_hours,
1916 offset_minutes: tz.offset_minutes,
1917 }),
1918 }
1919}
1920
1921pub(crate) fn compare_semantic_dates(
1928 left: &SemanticDateTime,
1929 right: &SemanticDateTime,
1930) -> std::cmp::Ordering {
1931 left.year
1932 .cmp(&right.year)
1933 .then_with(|| left.month.cmp(&right.month))
1934 .then_with(|| left.day.cmp(&right.day))
1935 .then_with(|| left.hour.cmp(&right.hour))
1936 .then_with(|| left.minute.cmp(&right.minute))
1937 .then_with(|| left.second.cmp(&right.second))
1938}
1939
1940pub(crate) fn compare_semantic_times(
1944 left: &SemanticTime,
1945 right: &SemanticTime,
1946) -> std::cmp::Ordering {
1947 left.hour
1948 .cmp(&right.hour)
1949 .then_with(|| left.minute.cmp(&right.minute))
1950 .then_with(|| left.second.cmp(&right.second))
1951}
1952
1953pub(crate) fn duration_unit_to_semantic(
1955 u: &crate::parsing::ast::DurationUnit,
1956) -> SemanticDurationUnit {
1957 use crate::parsing::ast::DurationUnit as DU;
1958 match u {
1959 DU::Year => SemanticDurationUnit::Year,
1960 DU::Month => SemanticDurationUnit::Month,
1961 DU::Week => SemanticDurationUnit::Week,
1962 DU::Day => SemanticDurationUnit::Day,
1963 DU::Hour => SemanticDurationUnit::Hour,
1964 DU::Minute => SemanticDurationUnit::Minute,
1965 DU::Second => SemanticDurationUnit::Second,
1966 DU::Millisecond => SemanticDurationUnit::Millisecond,
1967 DU::Microsecond => SemanticDurationUnit::Microsecond,
1968 }
1969}
1970
1971pub fn conversion_target_to_semantic(
1978 ct: &ConversionTarget,
1979 unit_index: Option<&HashMap<String, LemmaType>>,
1980) -> Result<SemanticConversionTarget, String> {
1981 match ct {
1982 ConversionTarget::Duration(u) => Ok(SemanticConversionTarget::Duration(
1983 duration_unit_to_semantic(u),
1984 )),
1985 ConversionTarget::Unit(name) => {
1986 let index = unit_index.ok_or_else(|| {
1987 "Unit conversion requires type resolution; unit index not available.".to_string()
1988 })?;
1989 let lemma_type = index.get(name).ok_or_else(|| {
1990 format!(
1991 "Unknown unit '{}'. Unit must be defined by a scale or ratio type.",
1992 name
1993 )
1994 })?;
1995 if lemma_type.is_ratio() {
1996 Ok(SemanticConversionTarget::RatioUnit(name.clone()))
1997 } else if lemma_type.is_scale() {
1998 Ok(SemanticConversionTarget::ScaleUnit(name.clone()))
1999 } else {
2000 Err(format!(
2001 "Unit '{}' is not a ratio or scale type; cannot use it in conversion.",
2002 name
2003 ))
2004 }
2005 }
2006 }
2007}
2008
2009static PRIMITIVE_BOOLEAN: OnceLock<LemmaType> = OnceLock::new();
2015static PRIMITIVE_SCALE: OnceLock<LemmaType> = OnceLock::new();
2016static PRIMITIVE_NUMBER: OnceLock<LemmaType> = OnceLock::new();
2017static PRIMITIVE_TEXT: OnceLock<LemmaType> = OnceLock::new();
2018static PRIMITIVE_DATE: OnceLock<LemmaType> = OnceLock::new();
2019static PRIMITIVE_TIME: OnceLock<LemmaType> = OnceLock::new();
2020static PRIMITIVE_DURATION: OnceLock<LemmaType> = OnceLock::new();
2021static PRIMITIVE_RATIO: OnceLock<LemmaType> = OnceLock::new();
2022
2023#[must_use]
2025pub fn primitive_boolean() -> &'static LemmaType {
2026 PRIMITIVE_BOOLEAN.get_or_init(|| LemmaType::primitive(TypeSpecification::boolean()))
2027}
2028
2029#[must_use]
2030pub fn primitive_scale() -> &'static LemmaType {
2031 PRIMITIVE_SCALE.get_or_init(|| LemmaType::primitive(TypeSpecification::scale()))
2032}
2033
2034#[must_use]
2035pub fn primitive_number() -> &'static LemmaType {
2036 PRIMITIVE_NUMBER.get_or_init(|| LemmaType::primitive(TypeSpecification::number()))
2037}
2038
2039#[must_use]
2040pub fn primitive_text() -> &'static LemmaType {
2041 PRIMITIVE_TEXT.get_or_init(|| LemmaType::primitive(TypeSpecification::text()))
2042}
2043
2044#[must_use]
2045pub fn primitive_date() -> &'static LemmaType {
2046 PRIMITIVE_DATE.get_or_init(|| LemmaType::primitive(TypeSpecification::date()))
2047}
2048
2049#[must_use]
2050pub fn primitive_time() -> &'static LemmaType {
2051 PRIMITIVE_TIME.get_or_init(|| LemmaType::primitive(TypeSpecification::time()))
2052}
2053
2054#[must_use]
2055pub fn primitive_duration() -> &'static LemmaType {
2056 PRIMITIVE_DURATION.get_or_init(|| LemmaType::primitive(TypeSpecification::duration()))
2057}
2058
2059#[must_use]
2060pub fn primitive_ratio() -> &'static LemmaType {
2061 PRIMITIVE_RATIO.get_or_init(|| LemmaType::primitive(TypeSpecification::ratio()))
2062}
2063
2064#[must_use]
2066pub fn type_spec_for_primitive(kind: PrimitiveKind) -> TypeSpecification {
2067 match kind {
2068 PrimitiveKind::Boolean => TypeSpecification::boolean(),
2069 PrimitiveKind::Scale => TypeSpecification::scale(),
2070 PrimitiveKind::Number => TypeSpecification::number(),
2071 PrimitiveKind::Percent | PrimitiveKind::Ratio => TypeSpecification::ratio(),
2072 PrimitiveKind::Text => TypeSpecification::text(),
2073 PrimitiveKind::Date => TypeSpecification::date(),
2074 PrimitiveKind::Time => TypeSpecification::time(),
2075 PrimitiveKind::Duration => TypeSpecification::duration(),
2076 }
2077}
2078
2079impl fmt::Display for PathSegment {
2084 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2085 write!(f, "{} → {}", self.data, self.spec)
2086 }
2087}
2088
2089impl fmt::Display for DataPath {
2090 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2091 for segment in &self.segments {
2092 write!(f, "{}.", segment)?;
2093 }
2094 write!(f, "{}", self.data)
2095 }
2096}
2097
2098impl fmt::Display for RulePath {
2099 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2100 for segment in &self.segments {
2101 write!(f, "{}.", segment)?;
2102 }
2103 write!(f, "{}", self.rule)
2104 }
2105}
2106
2107impl fmt::Display for LemmaType {
2108 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2109 write!(f, "{}", self.name())
2110 }
2111}
2112
2113impl fmt::Display for LiteralValue {
2114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2115 match &self.value {
2116 ValueKind::Scale(n, u) => {
2117 if let TypeSpecification::Scale { decimals, .. } = &self.lemma_type.specifications {
2118 let s = match decimals {
2119 Some(d) => {
2120 let dp = u32::from(*d);
2121 let rounded = n.round_dp(dp);
2122 format!("{:.prec$}", rounded, prec = *d as usize)
2123 }
2124 None => n.normalize().to_string(),
2125 };
2126 return write!(f, "{} {}", s, u);
2127 }
2128 write!(f, "{}", self.value)
2129 }
2130 ValueKind::Ratio(r, Some(unit_name)) => {
2131 if let TypeSpecification::Ratio { units, .. } = &self.lemma_type.specifications {
2132 if let Ok(unit) = units.get(unit_name) {
2133 let display_value = (*r * unit.value).normalize();
2134 let s = if display_value.fract().is_zero() {
2135 display_value.trunc().to_string()
2136 } else {
2137 display_value.to_string()
2138 };
2139 return match unit_name.as_str() {
2141 "percent" => write!(f, "{}%", s),
2142 "permille" => write!(f, "{}%%", s),
2143 _ => write!(f, "{} {}", s, unit_name),
2144 };
2145 }
2146 }
2147 write!(f, "{}", self.value)
2148 }
2149 _ => write!(f, "{}", self.value),
2150 }
2151 }
2152}
2153
2154#[cfg(test)]
2159mod tests {
2160 use super::*;
2161 use crate::parsing::ast::{BooleanValue, DateTimeValue, DurationUnit, LemmaSpec, TimeValue};
2162 use rust_decimal::Decimal;
2163 use std::str::FromStr;
2164 use std::sync::Arc;
2165
2166 #[test]
2167 fn test_negated_comparison() {
2168 assert_eq!(
2169 negated_comparison(ComparisonComputation::LessThan),
2170 ComparisonComputation::GreaterThanOrEqual
2171 );
2172 assert_eq!(
2173 negated_comparison(ComparisonComputation::GreaterThanOrEqual),
2174 ComparisonComputation::LessThan
2175 );
2176 assert_eq!(
2177 negated_comparison(ComparisonComputation::Is),
2178 ComparisonComputation::IsNot
2179 );
2180 assert_eq!(
2181 negated_comparison(ComparisonComputation::IsNot),
2182 ComparisonComputation::Is
2183 );
2184 }
2185
2186 #[test]
2187 fn test_literal_value_to_primitive_type() {
2188 let one = Decimal::from_str("1").unwrap();
2189
2190 assert_eq!(LiteralValue::text("".to_string()).lemma_type.name(), "text");
2191 assert_eq!(LiteralValue::number(one).lemma_type.name(), "number");
2192 assert_eq!(
2193 LiteralValue::from_bool(bool::from(BooleanValue::True))
2194 .lemma_type
2195 .name(),
2196 "boolean"
2197 );
2198
2199 let dt = DateTimeValue {
2200 year: 2024,
2201 month: 1,
2202 day: 1,
2203 hour: 0,
2204 minute: 0,
2205 second: 0,
2206 microsecond: 0,
2207 timezone: None,
2208 };
2209 assert_eq!(
2210 LiteralValue::date(date_time_to_semantic(&dt))
2211 .lemma_type
2212 .name(),
2213 "date"
2214 );
2215 assert_eq!(
2216 LiteralValue::ratio(one / Decimal::from(100), Some("percent".to_string()))
2217 .lemma_type
2218 .name(),
2219 "ratio"
2220 );
2221 assert_eq!(
2222 LiteralValue::duration(one, duration_unit_to_semantic(&DurationUnit::Second))
2223 .lemma_type
2224 .name(),
2225 "duration"
2226 );
2227 }
2228
2229 #[test]
2230 fn test_type_display() {
2231 let specs = TypeSpecification::text();
2232 let lemma_type = LemmaType::new("name".to_string(), specs, TypeExtends::Primitive);
2233 assert_eq!(format!("{}", lemma_type), "name");
2234 }
2235
2236 #[test]
2237 fn test_type_serialization() {
2238 let specs = TypeSpecification::number();
2239 let lemma_type = LemmaType::new("dice".to_string(), specs, TypeExtends::Primitive);
2240 let serialized = serde_json::to_string(&lemma_type).unwrap();
2241 let deserialized: LemmaType = serde_json::from_str(&serialized).unwrap();
2242 assert_eq!(lemma_type, deserialized);
2243 }
2244
2245 #[test]
2246 fn test_literal_value_display_value() {
2247 let ten = Decimal::from_str("10").unwrap();
2248
2249 assert_eq!(
2250 LiteralValue::text("hello".to_string()).display_value(),
2251 "hello"
2252 );
2253 assert_eq!(LiteralValue::number(ten).display_value(), "10");
2254 assert_eq!(LiteralValue::from_bool(true).display_value(), "true");
2255 assert_eq!(LiteralValue::from_bool(false).display_value(), "false");
2256
2257 let ten_percent_ratio = LiteralValue::ratio(
2259 Decimal::from_str("0.10").unwrap(),
2260 Some("percent".to_string()),
2261 );
2262 assert_eq!(ten_percent_ratio.display_value(), "10%");
2263
2264 let time = TimeValue {
2265 hour: 14,
2266 minute: 30,
2267 second: 0,
2268 timezone: None,
2269 };
2270 let time_display = LiteralValue::time(time_to_semantic(&time)).display_value();
2271 assert!(time_display.contains("14"));
2272 assert!(time_display.contains("30"));
2273 }
2274
2275 #[test]
2276 fn test_scale_display_respects_type_decimals() {
2277 let money_type = LemmaType {
2278 name: Some("money".to_string()),
2279 specifications: TypeSpecification::Scale {
2280 minimum: None,
2281 maximum: None,
2282 decimals: Some(2),
2283 precision: None,
2284 units: ScaleUnits::from(vec![ScaleUnit {
2285 name: "eur".to_string(),
2286 value: Decimal::from(1),
2287 }]),
2288 help: String::new(),
2289 },
2290 extends: TypeExtends::Primitive,
2291 };
2292 let val = LiteralValue::scale_with_type(
2293 Decimal::from_str("1.8").unwrap(),
2294 "eur".to_string(),
2295 money_type.clone(),
2296 );
2297 assert_eq!(val.display_value(), "1.80 eur");
2298 let more_precision = LiteralValue::scale_with_type(
2299 Decimal::from_str("1.80000").unwrap(),
2300 "eur".to_string(),
2301 money_type,
2302 );
2303 assert_eq!(more_precision.display_value(), "1.80 eur");
2304 let scale_no_decimals = LemmaType {
2305 name: Some("count".to_string()),
2306 specifications: TypeSpecification::Scale {
2307 minimum: None,
2308 maximum: None,
2309 decimals: None,
2310 precision: None,
2311 units: ScaleUnits::from(vec![ScaleUnit {
2312 name: "items".to_string(),
2313 value: Decimal::from(1),
2314 }]),
2315 help: String::new(),
2316 },
2317 extends: TypeExtends::Primitive,
2318 };
2319 let val_any = LiteralValue::scale_with_type(
2320 Decimal::from_str("42.50").unwrap(),
2321 "items".to_string(),
2322 scale_no_decimals,
2323 );
2324 assert_eq!(val_any.display_value(), "42.5 items");
2325 }
2326
2327 #[test]
2328 fn test_literal_value_time_type() {
2329 let time = TimeValue {
2330 hour: 14,
2331 minute: 30,
2332 second: 0,
2333 timezone: None,
2334 };
2335 let lit = LiteralValue::time(time_to_semantic(&time));
2336 assert_eq!(lit.lemma_type.name(), "time");
2337 }
2338
2339 #[test]
2340 fn test_scale_family_name_primitive_root() {
2341 let scale_spec = TypeSpecification::scale();
2342 let money_primitive = LemmaType::new(
2343 "money".to_string(),
2344 scale_spec.clone(),
2345 TypeExtends::Primitive,
2346 );
2347 assert_eq!(money_primitive.scale_family_name(), Some("money"));
2348 }
2349
2350 #[test]
2351 fn test_scale_family_name_custom() {
2352 let scale_spec = TypeSpecification::scale();
2353 let money_custom = LemmaType::new(
2354 "money".to_string(),
2355 scale_spec,
2356 TypeExtends::custom_local("money".to_string(), "money".to_string()),
2357 );
2358 assert_eq!(money_custom.scale_family_name(), Some("money"));
2359 }
2360
2361 #[test]
2362 fn test_same_scale_family_same_name_different_extends() {
2363 let scale_spec = TypeSpecification::scale();
2364 let money_primitive = LemmaType::new(
2365 "money".to_string(),
2366 scale_spec.clone(),
2367 TypeExtends::Primitive,
2368 );
2369 let money_custom = LemmaType::new(
2370 "money".to_string(),
2371 scale_spec,
2372 TypeExtends::custom_local("money".to_string(), "money".to_string()),
2373 );
2374 assert!(money_primitive.same_scale_family(&money_custom));
2375 assert!(money_custom.same_scale_family(&money_primitive));
2376 }
2377
2378 #[test]
2379 fn test_same_scale_family_parent_and_child() {
2380 let scale_spec = TypeSpecification::scale();
2381 let type_x = LemmaType::new("x".to_string(), scale_spec.clone(), TypeExtends::Primitive);
2382 let type_x2 = LemmaType::new(
2383 "x2".to_string(),
2384 scale_spec,
2385 TypeExtends::custom_local("x".to_string(), "x".to_string()),
2386 );
2387 assert_eq!(type_x.scale_family_name(), Some("x"));
2388 assert_eq!(type_x2.scale_family_name(), Some("x"));
2389 assert!(type_x.same_scale_family(&type_x2));
2390 assert!(type_x2.same_scale_family(&type_x));
2391 }
2392
2393 #[test]
2394 fn test_same_scale_family_siblings() {
2395 let scale_spec = TypeSpecification::scale();
2396 let type_x2_a = LemmaType::new(
2397 "x2a".to_string(),
2398 scale_spec.clone(),
2399 TypeExtends::custom_local("x".to_string(), "x".to_string()),
2400 );
2401 let type_x2_b = LemmaType::new(
2402 "x2b".to_string(),
2403 scale_spec,
2404 TypeExtends::custom_local("x".to_string(), "x".to_string()),
2405 );
2406 assert!(type_x2_a.same_scale_family(&type_x2_b));
2407 }
2408
2409 #[test]
2410 fn test_same_scale_family_different_families() {
2411 let scale_spec = TypeSpecification::scale();
2412 let money = LemmaType::new(
2413 "money".to_string(),
2414 scale_spec.clone(),
2415 TypeExtends::Primitive,
2416 );
2417 let temperature = LemmaType::new(
2418 "temperature".to_string(),
2419 scale_spec,
2420 TypeExtends::Primitive,
2421 );
2422 assert!(!money.same_scale_family(&temperature));
2423 assert!(!temperature.same_scale_family(&money));
2424 }
2425
2426 #[test]
2427 fn test_same_scale_family_scale_vs_non_scale() {
2428 let scale_spec = TypeSpecification::scale();
2429 let number_spec = TypeSpecification::number();
2430 let scale_type = LemmaType::new("money".to_string(), scale_spec, TypeExtends::Primitive);
2431 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
2432 assert!(!scale_type.same_scale_family(&number_type));
2433 assert!(!number_type.same_scale_family(&scale_type));
2434 }
2435
2436 #[test]
2437 fn test_scale_family_name_non_scale_returns_none() {
2438 let number_spec = TypeSpecification::number();
2439 let number_type = LemmaType::new("amount".to_string(), number_spec, TypeExtends::Primitive);
2440 assert_eq!(number_type.scale_family_name(), None);
2441 }
2442
2443 #[test]
2444 fn test_lemma_type_inequality_local_vs_import_same_shape() {
2445 let dep = Arc::new(LemmaSpec::new("dep".to_string()));
2446 let scale_spec = TypeSpecification::scale();
2447 let local = LemmaType::new(
2448 "t".to_string(),
2449 scale_spec.clone(),
2450 TypeExtends::custom_local("money".to_string(), "money".to_string()),
2451 );
2452 let imported = LemmaType::new(
2453 "t".to_string(),
2454 scale_spec,
2455 TypeExtends::Custom {
2456 parent: "money".to_string(),
2457 family: "money".to_string(),
2458 defining_spec: TypeDefiningSpec::Import {
2459 spec: Arc::clone(&dep),
2460 },
2461 },
2462 );
2463 assert_ne!(local, imported);
2464 }
2465
2466 #[test]
2467 fn test_lemma_type_equality_import_same_arc_pointer_identity() {
2468 let shared_spec = Arc::new(LemmaSpec::new("dep".to_string()));
2472 let scale_spec = TypeSpecification::scale();
2473 let left = LemmaType::new(
2474 "t".to_string(),
2475 scale_spec.clone(),
2476 TypeExtends::Custom {
2477 parent: "money".to_string(),
2478 family: "money".to_string(),
2479 defining_spec: TypeDefiningSpec::Import {
2480 spec: Arc::clone(&shared_spec),
2481 },
2482 },
2483 );
2484 let right = LemmaType::new(
2485 "t".to_string(),
2486 scale_spec,
2487 TypeExtends::Custom {
2488 parent: "money".to_string(),
2489 family: "money".to_string(),
2490 defining_spec: TypeDefiningSpec::Import {
2491 spec: Arc::clone(&shared_spec),
2492 },
2493 },
2494 );
2495 assert_eq!(left, right);
2496 }
2497
2498 #[test]
2499 fn test_lemma_type_inequality_import_different_arc_pointer() {
2500 let spec_a = Arc::new(LemmaSpec::new("dep".to_string()));
2502 let spec_b = Arc::new(LemmaSpec::new("dep".to_string()));
2503 let scale_spec = TypeSpecification::scale();
2504 let left = LemmaType::new(
2505 "t".to_string(),
2506 scale_spec.clone(),
2507 TypeExtends::Custom {
2508 parent: "money".to_string(),
2509 family: "money".to_string(),
2510 defining_spec: TypeDefiningSpec::Import {
2511 spec: Arc::clone(&spec_a),
2512 },
2513 },
2514 );
2515 let right = LemmaType::new(
2516 "t".to_string(),
2517 scale_spec,
2518 TypeExtends::Custom {
2519 parent: "money".to_string(),
2520 family: "money".to_string(),
2521 defining_spec: TypeDefiningSpec::Import { spec: spec_b },
2522 },
2523 );
2524 assert_ne!(left, right);
2525 }
2526}