1use crate::computation::rational::RationalInteger;
4use crate::literals::{
5 BaseMeasureVector, DateTimeValue, MeasureUnit as DomainMeasureUnit, MeasureUnits,
6 RatioUnit as DomainRatioUnit, RatioUnits, TimeValue,
7};
8use crate::planning::semantics::{
9 Dimension, LemmaType as DomainLemmaType, MeasureTrait as DomainMeasureTrait,
10 TypeDefiningSpec as DomainTypeDefiningSpec, TypeExtends as DomainTypeExtends,
11 TypeSpecification as DomainTypeSpecification,
12};
13use rust_decimal::Decimal;
14use serde::{Deserialize, Serialize};
15use std::collections::BTreeMap;
16
17#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
19pub struct RationalFactor {
20 pub numer: String,
21 pub denom: String,
22}
23
24impl RationalFactor {
25 fn from_ratio(value: &RationalInteger) -> Self {
26 let reduced = value
27 .clone()
28 .try_reduce()
29 .expect("BUG: stored measure unit factor must reduce");
30 Self {
31 numer: reduced.numer_to_string(),
32 denom: reduced.denom_to_string(),
33 }
34 }
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39pub struct NamedBound {
40 pub value: Decimal,
41 pub unit: String,
42}
43
44fn planned_bound_decimal(rational: &RationalInteger) -> Decimal {
45 rational
46 .try_to_decimal()
47 .expect("BUG: planned bound must convert to decimal")
48}
49
50fn named_bound_from(bound: &(RationalInteger, String)) -> NamedBound {
51 NamedBound {
52 value: planned_bound_decimal(&bound.0),
53 unit: bound.1.clone(),
54 }
55}
56
57fn optional_named_bound(bound: &Option<(RationalInteger, String)>) -> Option<NamedBound> {
58 bound.as_ref().map(named_bound_from)
59}
60
61fn optional_decimal_bound(bound: &Option<RationalInteger>) -> Option<Decimal> {
62 bound.as_ref().map(planned_bound_decimal)
63}
64
65pub type Decomposition = BTreeMap<String, i32>;
69
70fn decomposition_from(domain: &BaseMeasureVector) -> Decomposition {
71 let mut converted = Decomposition::new();
72 for (dimension, exponent) in domain {
73 let key = match dimension {
74 Dimension::Family(family) => family.to_string(),
75 Dimension::Duration | Dimension::Calendar => dimension.name().to_string(),
76 };
77 if converted.insert(key.clone(), *exponent).is_some() {
78 panic!("BUG: distinct dimensions share API key '{key}'");
79 }
80 }
81 converted
82}
83
84fn optional_decomposition_from(domain: &Option<BaseMeasureVector>) -> Option<Decomposition> {
85 domain.as_ref().map(decomposition_from)
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
89#[serde(rename_all = "snake_case")]
90pub enum MeasureTrait {
91 Duration,
92 Calendar,
93}
94
95impl From<DomainMeasureTrait> for MeasureTrait {
96 fn from(value: DomainMeasureTrait) -> Self {
97 match value {
98 DomainMeasureTrait::Duration => Self::Duration,
99 DomainMeasureTrait::Calendar => Self::Calendar,
100 }
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105pub struct MeasureUnit {
106 pub name: String,
107 #[serde(skip_serializing_if = "Option::is_none", default)]
110 pub factor: Option<RationalFactor>,
111 #[serde(default, skip_serializing_if = "Vec::is_empty")]
115 pub scale_factors: Vec<(String, i32)>,
116 pub derived_measure_factors: Vec<(String, i32)>,
117 pub decomposition: Decomposition,
118 #[serde(skip_serializing_if = "Option::is_none", default)]
119 pub minimum: Option<Decimal>,
120 #[serde(skip_serializing_if = "Option::is_none", default)]
121 pub maximum: Option<Decimal>,
122 #[serde(
123 rename = "suggestion",
124 skip_serializing_if = "Option::is_none",
125 default
126 )]
127 pub suggestion_magnitude: Option<Decimal>,
128}
129
130impl From<&DomainMeasureUnit> for MeasureUnit {
131 fn from(unit: &DomainMeasureUnit) -> Self {
132 let (factor, scale_factors, omit_bounds) = match &unit.scale {
133 crate::literals::UnitScale::Literal(factor) => {
134 (Some(RationalFactor::from_ratio(factor)), Vec::new(), false)
135 }
136 crate::literals::UnitScale::Unresolved { written, .. } => {
137 (None, vec![(written.clone(), 1)], true)
138 }
139 crate::literals::UnitScale::Reference {
140 factors,
141 coefficient,
142 } => {
143 if factors.is_empty() {
144 panic!(
145 "BUG: MeasureUnit wire for '{}' got empty Reference factors",
146 unit.name
147 );
148 }
149 let factor = if *coefficient == crate::computation::rational::rational_one() {
150 None
151 } else {
152 Some(RationalFactor::from_ratio(coefficient))
153 };
154 let scale_factors = factors
155 .iter()
156 .map(|(target, exponent)| (target.to_string(), *exponent))
157 .collect();
158 (factor, scale_factors, true)
159 }
160 };
161 Self {
162 name: unit.name.clone(),
163 factor,
164 scale_factors,
165 derived_measure_factors: unit.derived_measure_factors.clone(),
166 decomposition: decomposition_from(&unit.decomposition),
167 minimum: if omit_bounds {
168 None
169 } else {
170 unit.minimum.as_ref().map(|minimum| {
171 minimum
172 .try_to_decimal()
173 .expect("BUG: planned measure unit minimum must convert to decimal")
174 })
175 },
176 maximum: if omit_bounds {
177 None
178 } else {
179 unit.maximum.as_ref().map(|maximum| {
180 maximum
181 .try_to_decimal()
182 .expect("BUG: planned measure unit maximum must convert to decimal")
183 })
184 },
185 suggestion_magnitude: if omit_bounds {
186 None
187 } else {
188 unit.suggestion_magnitude.as_ref().map(|suggestion| {
189 suggestion
190 .try_to_decimal()
191 .expect("BUG: planned measure unit suggestion must convert to decimal")
192 })
193 },
194 }
195 }
196}
197
198#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
199pub struct RatioUnit {
200 pub name: String,
201 pub value: RationalFactor,
202 #[serde(skip_serializing_if = "Option::is_none", default)]
203 pub minimum: Option<Decimal>,
204 #[serde(skip_serializing_if = "Option::is_none", default)]
205 pub maximum: Option<Decimal>,
206 #[serde(
207 rename = "suggestion",
208 skip_serializing_if = "Option::is_none",
209 default
210 )]
211 pub suggestion_magnitude: Option<Decimal>,
212}
213
214impl From<&DomainRatioUnit> for RatioUnit {
215 fn from(unit: &DomainRatioUnit) -> Self {
216 Self {
217 name: unit.name.clone(),
218 value: RationalFactor::from_ratio(&unit.value),
219 minimum: unit.minimum.as_ref().map(|minimum| {
220 minimum
221 .try_to_decimal()
222 .expect("BUG: planned ratio unit minimum must convert to decimal")
223 }),
224 maximum: unit.maximum.as_ref().map(|maximum| {
225 maximum
226 .try_to_decimal()
227 .expect("BUG: planned ratio unit maximum must convert to decimal")
228 }),
229 suggestion_magnitude: unit.suggestion_magnitude.as_ref().map(|suggestion| {
230 suggestion
231 .try_to_decimal()
232 .expect("BUG: planned ratio unit suggestion must convert to decimal")
233 }),
234 }
235 }
236}
237
238fn measure_units_from(units: &MeasureUnits) -> Vec<MeasureUnit> {
239 units.iter().map(MeasureUnit::from).collect()
240}
241
242fn ratio_units_from(units: &RatioUnits) -> Vec<RatioUnit> {
243 units.iter().map(RatioUnit::from).collect()
244}
245
246#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
247#[serde(tag = "kind", rename_all = "snake_case")]
248pub enum TypeDefiningSpec {
249 Local,
250 Import,
251}
252
253impl From<&DomainTypeDefiningSpec> for TypeDefiningSpec {
254 fn from(value: &DomainTypeDefiningSpec) -> Self {
255 match value {
256 DomainTypeDefiningSpec::Local => Self::Local,
257 DomainTypeDefiningSpec::Import => Self::Import,
258 }
259 }
260}
261
262#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
263#[serde(tag = "kind", rename_all = "snake_case")]
264pub enum TypeExtends {
265 Primitive,
266 Custom {
267 parent: String,
268 family: String,
269 defining_spec: TypeDefiningSpec,
270 },
271}
272
273impl From<&DomainTypeExtends> for TypeExtends {
274 fn from(value: &DomainTypeExtends) -> Self {
275 match value {
276 DomainTypeExtends::Primitive => Self::Primitive,
277 DomainTypeExtends::Custom {
278 parent,
279 family,
280 defining_spec,
281 } => Self::Custom {
282 parent: parent.clone(),
283 family: family.to_string(),
284 defining_spec: TypeDefiningSpec::from(defining_spec),
285 },
286 }
287 }
288}
289
290#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
291#[serde(tag = "kind", rename_all = "snake_case")]
292pub enum TypeSpecification {
293 Boolean {
294 help: String,
295 },
296 Measure {
297 #[serde(default)]
298 minimum: Option<NamedBound>,
299 #[serde(default)]
300 maximum: Option<NamedBound>,
301 units: Vec<MeasureUnit>,
302 #[serde(default)]
303 traits: Vec<MeasureTrait>,
304 #[serde(default)]
305 decomposition: Option<Decomposition>,
306 #[serde(default, skip_serializing_if = "Option::is_none")]
307 unit: Option<String>,
308 help: String,
309 },
310 Number {
311 #[serde(default)]
312 minimum: Option<Decimal>,
313 #[serde(default)]
314 maximum: Option<Decimal>,
315 decimals: Option<u8>,
316 help: String,
317 },
318 NumberRange {
319 #[serde(default)]
320 lower: Option<Decimal>,
321 #[serde(default)]
322 upper: Option<Decimal>,
323 #[serde(default)]
324 minimum: Option<Decimal>,
325 #[serde(default)]
326 maximum: Option<Decimal>,
327 help: String,
328 },
329 Ratio {
330 #[serde(default)]
331 minimum: Option<Decimal>,
332 #[serde(default)]
333 maximum: Option<Decimal>,
334 units: Vec<RatioUnit>,
335 #[serde(default, skip_serializing_if = "Option::is_none")]
336 unit: Option<String>,
337 help: String,
338 },
339 RatioRange {
340 #[serde(default)]
341 lower: Option<Decimal>,
342 #[serde(default)]
343 upper: Option<Decimal>,
344 #[serde(default)]
345 minimum: Option<Decimal>,
346 #[serde(default)]
347 maximum: Option<Decimal>,
348 units: Vec<RatioUnit>,
349 help: String,
350 },
351 Text {
352 length: Option<usize>,
353 options: Vec<String>,
354 help: String,
355 },
356 Date {
357 minimum: Option<DateTimeValue>,
358 maximum: Option<DateTimeValue>,
359 help: String,
360 },
361 DateRange {
362 lower: Option<DateTimeValue>,
363 upper: Option<DateTimeValue>,
364 #[serde(default)]
365 minimum: Option<NamedBound>,
366 #[serde(default)]
367 maximum: Option<NamedBound>,
368 help: String,
369 },
370 Time {
371 minimum: Option<TimeValue>,
372 maximum: Option<TimeValue>,
373 help: String,
374 },
375 TimeRange {
376 lower: Option<TimeValue>,
377 upper: Option<TimeValue>,
378 #[serde(default)]
379 minimum: Option<NamedBound>,
380 #[serde(default)]
381 maximum: Option<NamedBound>,
382 help: String,
383 },
384 MeasureRange {
385 #[serde(default)]
386 lower: Option<NamedBound>,
387 #[serde(default)]
388 upper: Option<NamedBound>,
389 #[serde(default)]
390 minimum: Option<NamedBound>,
391 #[serde(default)]
392 maximum: Option<NamedBound>,
393 units: Vec<MeasureUnit>,
394 #[serde(default)]
395 decomposition: Option<Decomposition>,
396 help: String,
397 },
398 Veto {
399 message: Option<String>,
400 },
401 Undetermined,
402 Spec {
403 #[serde(default, skip_serializing_if = "Option::is_none")]
404 repository: Option<String>,
405 spec: String,
406 effective: String,
407 },
408 List {
409 element: Box<TypeSpecification>,
410 key: ListKey,
411 },
412}
413
414#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
416#[serde(rename_all = "snake_case")]
417pub enum ListKey {
418 Numbered,
419 Fields(Vec<String>),
420}
421
422impl From<&crate::planning::semantics::ListKey> for ListKey {
423 fn from(key: &crate::planning::semantics::ListKey) -> Self {
424 match key {
425 crate::planning::semantics::ListKey::Numbered => Self::Numbered,
426 crate::planning::semantics::ListKey::Fields(fields) => {
427 Self::Fields(fields.iter().map(|field| field.name.clone()).collect())
428 }
429 }
430 }
431}
432
433impl From<&DomainTypeSpecification> for TypeSpecification {
434 fn from(spec: &DomainTypeSpecification) -> Self {
435 let mut list_keys = Vec::new();
436 let mut leaf = spec;
437 while let DomainTypeSpecification::List { element, key, .. } = leaf {
438 list_keys.push(ListKey::from(key));
439 leaf = &element.specifications;
440 }
441 let mut converted = Self::non_list_from(leaf);
442 while let Some(key) = list_keys.pop() {
443 converted = Self::List {
444 element: Box::new(converted),
445 key,
446 };
447 }
448 converted
449 }
450}
451
452impl TypeSpecification {
453 fn non_list_from(spec: &DomainTypeSpecification) -> Self {
454 match spec {
455 DomainTypeSpecification::Boolean { help } => Self::Boolean { help: help.clone() },
456 DomainTypeSpecification::Measure {
457 minimum,
458 maximum,
459 units,
460 traits,
461 decomposition,
462 unit,
463 help,
464 } => Self::Measure {
465 minimum: optional_named_bound(minimum),
466 maximum: optional_named_bound(maximum),
467 units: measure_units_from(units),
468 traits: traits.iter().copied().map(MeasureTrait::from).collect(),
469 decomposition: optional_decomposition_from(decomposition),
470 unit: unit.clone(),
471 help: help.clone(),
472 },
473 DomainTypeSpecification::Number {
474 minimum,
475 maximum,
476 decimals,
477 help,
478 } => Self::Number {
479 minimum: optional_decimal_bound(minimum),
480 maximum: optional_decimal_bound(maximum),
481 decimals: *decimals,
482 help: help.clone(),
483 },
484 DomainTypeSpecification::NumberRange {
485 lower,
486 upper,
487 minimum,
488 maximum,
489 help,
490 } => Self::NumberRange {
491 lower: optional_decimal_bound(lower),
492 upper: optional_decimal_bound(upper),
493 minimum: optional_decimal_bound(minimum),
494 maximum: optional_decimal_bound(maximum),
495 help: help.clone(),
496 },
497 DomainTypeSpecification::Ratio {
498 minimum,
499 maximum,
500 units,
501 unit,
502 help,
503 } => Self::Ratio {
504 minimum: optional_decimal_bound(minimum),
505 maximum: optional_decimal_bound(maximum),
506 units: ratio_units_from(units),
507 unit: unit.clone(),
508 help: help.clone(),
509 },
510 DomainTypeSpecification::RatioRange {
511 lower,
512 upper,
513 minimum,
514 maximum,
515 units,
516 help,
517 } => Self::RatioRange {
518 lower: optional_decimal_bound(lower),
519 upper: optional_decimal_bound(upper),
520 minimum: optional_decimal_bound(minimum),
521 maximum: optional_decimal_bound(maximum),
522 units: ratio_units_from(units),
523 help: help.clone(),
524 },
525 DomainTypeSpecification::Text { length, help } => Self::Text {
526 length: *length,
527 options: Vec::new(),
528 help: help.clone(),
529 },
530 DomainTypeSpecification::Date {
531 minimum,
532 maximum,
533 help,
534 } => Self::Date {
535 minimum: minimum.clone(),
536 maximum: maximum.clone(),
537 help: help.clone(),
538 },
539 DomainTypeSpecification::DateRange {
540 lower,
541 upper,
542 minimum,
543 maximum,
544 help,
545 } => Self::DateRange {
546 lower: lower.clone(),
547 upper: upper.clone(),
548 minimum: optional_named_bound(minimum),
549 maximum: optional_named_bound(maximum),
550 help: help.clone(),
551 },
552 DomainTypeSpecification::Time {
553 minimum,
554 maximum,
555 help,
556 } => Self::Time {
557 minimum: minimum.clone(),
558 maximum: maximum.clone(),
559 help: help.clone(),
560 },
561 DomainTypeSpecification::TimeRange {
562 lower,
563 upper,
564 minimum,
565 maximum,
566 help,
567 } => Self::TimeRange {
568 lower: lower.clone(),
569 upper: upper.clone(),
570 minimum: optional_named_bound(minimum),
571 maximum: optional_named_bound(maximum),
572 help: help.clone(),
573 },
574 DomainTypeSpecification::MeasureRange {
575 lower,
576 upper,
577 minimum,
578 maximum,
579 units,
580 decomposition,
581 help,
582 } => Self::MeasureRange {
583 lower: optional_named_bound(lower),
584 upper: optional_named_bound(upper),
585 minimum: optional_named_bound(minimum),
586 maximum: optional_named_bound(maximum),
587 units: measure_units_from(units),
588 decomposition: optional_decomposition_from(decomposition),
589 help: help.clone(),
590 },
591 DomainTypeSpecification::Veto { message } => Self::Veto {
592 message: message.clone(),
593 },
594 DomainTypeSpecification::Undetermined => Self::Undetermined,
595 DomainTypeSpecification::Spec { spec } => Self::Spec {
596 repository: spec.repository.clone(),
597 spec: spec.spec.clone(),
598 effective: spec.effective.to_string(),
599 },
600 DomainTypeSpecification::List { .. } => {
601 unreachable!("BUG: list specifications are unwound by TypeSpecification::from")
602 }
603 }
604 }
605}
606
607#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
608pub struct LemmaType {
609 pub name: Option<String>,
610 #[serde(flatten)]
611 pub specifications: TypeSpecification,
612 pub extends: TypeExtends,
613}
614
615impl From<&DomainLemmaType> for LemmaType {
616 fn from(lemma_type: &DomainLemmaType) -> Self {
617 let mut specifications = TypeSpecification::from(&lemma_type.specifications);
618 if let TypeSpecification::Text { options, .. } = &mut specifications {
619 *options = lemma_type
620 .options_for_check()
621 .values
622 .iter()
623 .filter_map(|option| match &option.value.value {
624 crate::planning::semantics::ValueKind::Text(text) => Some(text.to_string()),
625 _ => None,
626 })
627 .collect();
628 }
629 Self {
630 name: lemma_type.name.clone(),
631 specifications,
632 extends: TypeExtends::from(&lemma_type.extends),
633 }
634 }
635}
636
637#[cfg(test)]
638mod tests {
639 use super::*;
640 use crate::planning::semantics::TypeFamily;
641 use serde_json::json;
642
643 fn family(repository: Option<&str>, spec: &str, name: &str) -> Dimension {
644 Dimension::Family(TypeFamily {
645 repository: repository.map(str::to_string),
646 spec: spec.to_string(),
647 effective: crate::parsing::ast::EffectiveDate::Origin,
648 name: name.to_string(),
649 })
650 }
651
652 #[test]
653 fn decomposition_keys_families_by_declaring_spec() {
654 let mut domain = BaseMeasureVector::new();
655 domain.insert(family(None, "pricing", "money"), 1);
656 domain.insert(family(Some("lemma"), "units", "mass"), -1);
657 domain.insert(Dimension::Duration, -1);
658 let json = serde_json::to_value(decomposition_from(&domain)).expect("JSON");
659 assert_eq!(
660 json,
661 json!({ "pricing.money": 1, "lemma units.mass": -1, "duration": -1 })
662 );
663 }
664
665 #[test]
666 fn decomposition_keeps_same_named_families_apart() {
667 let mut domain = BaseMeasureVector::new();
668 domain.insert(family(None, "a", "money"), 1);
669 domain.insert(family(None, "b", "money"), -1);
670 let json = serde_json::to_value(decomposition_from(&domain)).expect("JSON");
671 assert_eq!(json, json!({ "a.money": 1, "b.money": -1 }));
672 }
673
674 #[test]
675 fn decomposition_keeps_builtin_apart_from_same_named_family() {
676 let mut domain = BaseMeasureVector::new();
677 domain.insert(family(None, "clock", "duration"), 1);
678 domain.insert(Dimension::Duration, -1);
679 let json = serde_json::to_value(decomposition_from(&domain)).expect("JSON");
680 assert_eq!(json, json!({ "clock.duration": 1, "duration": -1 }));
681 }
682}