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