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