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