Skip to main content

lemma/
literals.rs

1//! Literal value types and string parsing. No dependency on parsing/ast.
2//! AST and planning re-export these types where needed.
3
4use chrono::{Datelike, Timelike};
5use rust_decimal::Decimal;
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use std::collections::BTreeMap;
8use std::fmt;
9
10use crate::computation::rational::{self, RationalInteger};
11
12// -----------------------------------------------------------------------------
13// Dimensional decomposition type
14// -----------------------------------------------------------------------------
15
16/// A dimensional decomposition vector. Maps quantity-type names to integer exponents.
17/// For example, velocity `{length: 1, duration: -1}` or acceleration `{length: 1, duration: -2}`.
18/// An empty map indicates a base quantity (no decomposition) until the decomposition pass runs,
19/// after which every quantity carries a non-empty vector.
20pub type BaseQuantityVector = BTreeMap<String, i32>;
21
22// -----------------------------------------------------------------------------
23// Unit tables for Quantity and Ratio types
24// -----------------------------------------------------------------------------
25
26pub fn rational_to_serialized_str(rational: &RationalInteger) -> Result<String, String> {
27    rational::rational_to_decimal_string(rational).map_err(|failure| failure.to_string())
28}
29
30pub fn rational_from_parsed_decimal(decimal: Decimal) -> Result<RationalInteger, String> {
31    rational::decimal_to_rational(decimal).map_err(|failure| failure.to_string())
32}
33
34/// Serde for stored rationals: API format is decimal string or JSON number (lifted at boundary).
35pub mod stored_rational_serde {
36    use super::{rational_from_parsed_decimal, rational_to_serialized_str, RationalInteger};
37    use rust_decimal::Decimal;
38    use serde::{Deserialize, Deserializer, Serializer};
39
40    pub fn serialize<S: Serializer>(
41        value: &RationalInteger,
42        serializer: S,
43    ) -> Result<S::Ok, S::Error> {
44        serializer.serialize_str(
45            &rational_to_serialized_str(value)
46                .expect("BUG: planned bound must serialize to decimal string"),
47        )
48    }
49
50    pub mod option {
51        use super::*;
52
53        pub fn serialize<S: Serializer>(
54            value: &Option<RationalInteger>,
55            serializer: S,
56        ) -> Result<S::Ok, S::Error> {
57            match value {
58                Some(rational) => super::serialize(rational, serializer),
59                None => serializer.serialize_none(),
60            }
61        }
62
63        pub fn deserialize<'de, D: Deserializer<'de>>(
64            deserializer: D,
65        ) -> Result<Option<RationalInteger>, D::Error> {
66            Option::<Decimal>::deserialize(deserializer)?
67                .map(rational_from_parsed_decimal)
68                .transpose()
69                .map_err(serde::de::Error::custom)
70        }
71    }
72}
73
74/// A single unit within a Quantity type.
75///
76/// `factor` is the conversion factor: 1 of this unit equals `factor` canonical units.
77/// `derived_quantity_factors` stores `(quantity_ref, exponent)` pairs from compound unit declarations
78/// (e.g., `meter/second` produces `[("meter", 1), ("second", -1)]`). Empty for base units.
79/// `decomposition` is the dimensional decomposition vector, populated during the planning
80/// decomposition pass. It is empty until that pass completes.
81#[derive(Clone, Debug, PartialEq, Eq, Hash)]
82pub struct QuantityUnit {
83    pub name: String,
84    /// Conversion factor: 1 of this unit equals `value` canonical units.
85    pub factor: RationalInteger,
86    pub derived_quantity_factors: Vec<(String, i32)>,
87    pub decomposition: BaseQuantityVector,
88    /// Minimum magnitude in this unit (schema/UI); canonical bound is on the type.
89    pub minimum: Option<RationalInteger>,
90    /// Maximum magnitude in this unit (schema/UI).
91    pub maximum: Option<RationalInteger>,
92    /// Default suggestion magnitude in this unit (schema/UI).
93    pub default_magnitude: Option<RationalInteger>,
94}
95
96impl Serialize for QuantityUnit {
97    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
98        use quantity_unit_factor_serialization::FactorSerializer;
99        use serde::ser::SerializeStruct;
100        let mut state = serializer.serialize_struct("QuantityUnit", 7)?;
101        state.serialize_field("name", &self.name)?;
102        state.serialize_field("factor", &FactorSerializer::from_ratio(&self.factor))?;
103        state.serialize_field("derived_quantity_factors", &self.derived_quantity_factors)?;
104        state.serialize_field("decomposition", &self.decomposition)?;
105        if let Some(minimum) = &self.minimum {
106            state.serialize_field(
107                "minimum",
108                &rational_to_serialized_str(minimum)
109                    .expect("BUG: planned quantity unit minimum must serialize to decimal string"),
110            )?;
111        }
112        if let Some(maximum) = &self.maximum {
113            state.serialize_field(
114                "maximum",
115                &rational_to_serialized_str(maximum)
116                    .expect("BUG: planned quantity unit maximum must serialize to decimal string"),
117            )?;
118        }
119        if let Some(default_magnitude) = &self.default_magnitude {
120            state.serialize_field(
121                "default",
122                &rational_to_serialized_str(default_magnitude)
123                    .expect("BUG: planned quantity unit default must serialize to decimal string"),
124            )?;
125        }
126        state.end()
127    }
128}
129
130impl<'de> Deserialize<'de> for QuantityUnit {
131    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
132        #[derive(Deserialize)]
133        struct QuantityUnitData {
134            name: String,
135            #[serde(with = "quantity_unit_factor_serialization")]
136            factor: RationalInteger,
137            #[serde(default)]
138            derived_quantity_factors: Vec<(String, i32)>,
139            #[serde(default)]
140            decomposition: BaseQuantityVector,
141            #[serde(default)]
142            minimum: Option<Decimal>,
143            #[serde(default)]
144            maximum: Option<Decimal>,
145            #[serde(default, rename = "default")]
146            default_magnitude: Option<Decimal>,
147        }
148        let data = QuantityUnitData::deserialize(deserializer)?;
149        Ok(Self {
150            name: data.name,
151            factor: data.factor,
152            derived_quantity_factors: data.derived_quantity_factors,
153            decomposition: data.decomposition,
154            minimum: data
155                .minimum
156                .map(rational_from_parsed_decimal)
157                .transpose()
158                .map_err(serde::de::Error::custom)?,
159            maximum: data
160                .maximum
161                .map(rational_from_parsed_decimal)
162                .transpose()
163                .map_err(serde::de::Error::custom)?,
164            default_magnitude: data
165                .default_magnitude
166                .map(rational_from_parsed_decimal)
167                .transpose()
168                .map_err(serde::de::Error::custom)?,
169        })
170    }
171}
172
173impl QuantityUnit {
174    pub fn from_decimal_factor(
175        name: String,
176        decimal_factor: Decimal,
177        derived_quantity_factors: Vec<(String, i32)>,
178    ) -> Result<Self, String> {
179        let factor =
180            rational::decimal_to_rational(decimal_factor).map_err(|failure| failure.to_string())?;
181        Ok(QuantityUnit {
182            name,
183            factor,
184            derived_quantity_factors,
185            decomposition: BaseQuantityVector::new(),
186            minimum: None,
187            maximum: None,
188            default_magnitude: None,
189        })
190    }
191
192    pub fn clear_constraint_magnitudes(&mut self) {
193        self.minimum = None;
194        self.maximum = None;
195        self.default_magnitude = None;
196    }
197
198    pub fn is_canonical_factor(&self) -> bool {
199        self.factor == rational::rational_one()
200    }
201
202    pub fn is_positive_factor(&self) -> bool {
203        let numerator = self.factor.numer();
204        let denominator = self.factor.denom();
205        !numerator.is_zero() && numerator.is_positive() == denominator.is_positive()
206    }
207
208    /// Conversion factor as decimal (schema unit factors always commit).
209    pub fn factor_decimal(&self) -> Decimal {
210        rational::commit_rational_to_decimal(&self.factor)
211            .expect("BUG: quantity unit factor must commit to decimal")
212    }
213
214    #[must_use]
215    pub fn minimum_decimal(&self) -> Option<Decimal> {
216        self.minimum.as_ref().map(|bound| {
217            rational::commit_rational_to_decimal(bound)
218                .expect("BUG: planned quantity unit minimum must commit to decimal")
219        })
220    }
221
222    #[must_use]
223    pub fn maximum_decimal(&self) -> Option<Decimal> {
224        self.maximum.as_ref().map(|bound| {
225            rational::commit_rational_to_decimal(bound)
226                .expect("BUG: planned quantity unit maximum must commit to decimal")
227        })
228    }
229
230    #[must_use]
231    pub fn default_magnitude_decimal(&self) -> Option<Decimal> {
232        self.default_magnitude.as_ref().map(|bound| {
233            rational::commit_rational_to_decimal(bound)
234                .expect("BUG: planned quantity unit default must commit to decimal")
235        })
236    }
237
238    /// Maximum bound lifted to canonical units via `maximum * factor`.
239    #[must_use]
240    pub fn maximum_canonical_decimal(&self) -> Option<Decimal> {
241        self.maximum.as_ref().map(|maximum| {
242            let canonical = rational::checked_mul(maximum, &self.factor)
243                .expect("BUG: planned quantity unit maximum canonical multiply must succeed");
244            rational::commit_rational_to_decimal(&canonical)
245                .expect("BUG: planned quantity unit maximum canonical must commit to decimal")
246        })
247    }
248}
249
250mod quantity_unit_factor_serialization {
251    use super::RationalInteger;
252    use crate::computation::bigint::BigInt;
253    use crate::computation::rational::try_rational_new;
254    use serde::{Deserialize, Serialize};
255
256    #[derive(Serialize, Deserialize)]
257    pub struct FactorSerializer {
258        numer: String,
259        denom: String,
260    }
261
262    impl FactorSerializer {
263        pub fn from_ratio(value: &RationalInteger) -> Self {
264            let reduced = value
265                .clone()
266                .try_reduce()
267                .expect("BUG: stored quantity unit factor must reduce");
268            FactorSerializer {
269                numer: reduced.numer().to_string(),
270                denom: reduced.denom().to_string(),
271            }
272        }
273
274        pub fn into_ratio(self) -> Result<RationalInteger, String> {
275            let numer = BigInt::try_from_str_radix(&self.numer, 10)
276                .map_err(|_| format!("invalid numerator: {}", self.numer))?;
277            let denom = BigInt::try_from_str_radix(&self.denom, 10)
278                .map_err(|_| format!("invalid denominator: {}", self.denom))?;
279            if denom.is_zero() {
280                return Err("QuantityUnit conversion factor denominator cannot be zero".to_string());
281            }
282            try_rational_new(numer, denom).map_err(|e| e.to_string())
283        }
284    }
285
286    pub fn deserialize<'de, D: serde::Deserializer<'de>>(
287        deserializer: D,
288    ) -> Result<RationalInteger, D::Error> {
289        FactorSerializer::deserialize(deserializer)?
290            .into_ratio()
291            .map_err(serde::de::Error::custom)
292    }
293}
294
295#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
296#[serde(transparent)]
297pub struct QuantityUnits(pub Vec<QuantityUnit>);
298
299impl QuantityUnits {
300    pub fn new() -> Self {
301        QuantityUnits(Vec::new())
302    }
303    pub fn get(&self, name: &str) -> Result<&QuantityUnit, String> {
304        self.0.iter().find(|u| u.name == name).ok_or_else(|| {
305            let valid: Vec<&str> = self.0.iter().map(|u| u.name.as_str()).collect();
306            format!(
307                "Unknown unit '{}' for this quantity type. Valid units: {}",
308                name,
309                valid.join(", ")
310            )
311        })
312    }
313
314    pub fn iter(&self) -> std::slice::Iter<'_, QuantityUnit> {
315        self.0.iter()
316    }
317    pub fn push(&mut self, u: QuantityUnit) {
318        self.0.push(u);
319    }
320    pub fn is_empty(&self) -> bool {
321        self.0.is_empty()
322    }
323    pub fn len(&self) -> usize {
324        self.0.len()
325    }
326    pub fn map<F: FnMut(QuantityUnit) -> QuantityUnit>(self, f: F) -> Self {
327        QuantityUnits(self.0.into_iter().map(f).collect())
328    }
329}
330
331impl QuantityUnit {
332    pub fn with_decomposition(self, decomposition: BaseQuantityVector) -> Self {
333        Self {
334            decomposition,
335            ..self
336        }
337    }
338    pub fn with_factor(self, factor: RationalInteger) -> Self {
339        Self { factor, ..self }
340    }
341    pub fn with_derived_quantity_factors(
342        self,
343        derived_quantity_factors: Vec<(String, i32)>,
344    ) -> Self {
345        Self {
346            derived_quantity_factors,
347            ..self
348        }
349    }
350}
351
352impl Default for QuantityUnits {
353    fn default() -> Self {
354        QuantityUnits::new()
355    }
356}
357
358impl From<Vec<QuantityUnit>> for QuantityUnits {
359    fn from(v: Vec<QuantityUnit>) -> Self {
360        QuantityUnits(v)
361    }
362}
363
364impl<'a> IntoIterator for &'a QuantityUnits {
365    type Item = &'a QuantityUnit;
366    type IntoIter = std::slice::Iter<'a, QuantityUnit>;
367    fn into_iter(self) -> Self::IntoIter {
368        self.0.iter()
369    }
370}
371
372#[derive(Clone, Debug, PartialEq, Eq, Hash)]
373pub struct RatioUnit {
374    pub name: String,
375    pub value: RationalInteger,
376    pub minimum: Option<RationalInteger>,
377    pub maximum: Option<RationalInteger>,
378    pub default_magnitude: Option<RationalInteger>,
379}
380
381impl Serialize for RatioUnit {
382    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
383        use quantity_unit_factor_serialization::FactorSerializer;
384        use serde::ser::SerializeStruct;
385        let mut state = serializer.serialize_struct("RatioUnit", 5)?;
386        state.serialize_field("name", &self.name)?;
387        state.serialize_field("value", &FactorSerializer::from_ratio(&self.value))?;
388        if let Some(minimum) = &self.minimum {
389            state.serialize_field(
390                "minimum",
391                &rational_to_serialized_str(minimum)
392                    .expect("BUG: planned ratio unit minimum must serialize to decimal string"),
393            )?;
394        }
395        if let Some(maximum) = &self.maximum {
396            state.serialize_field(
397                "maximum",
398                &rational_to_serialized_str(maximum)
399                    .expect("BUG: planned ratio unit maximum must serialize to decimal string"),
400            )?;
401        }
402        if let Some(default_magnitude) = &self.default_magnitude {
403            state.serialize_field(
404                "default",
405                &rational_to_serialized_str(default_magnitude)
406                    .expect("BUG: planned ratio unit default must serialize to decimal string"),
407            )?;
408        }
409        state.end()
410    }
411}
412
413impl<'de> Deserialize<'de> for RatioUnit {
414    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
415        #[derive(Deserialize)]
416        struct RatioUnitData {
417            name: String,
418            #[serde(with = "quantity_unit_factor_serialization")]
419            value: RationalInteger,
420            #[serde(default)]
421            minimum: Option<Decimal>,
422            #[serde(default)]
423            maximum: Option<Decimal>,
424            #[serde(default, rename = "default")]
425            default_magnitude: Option<Decimal>,
426        }
427        let data = RatioUnitData::deserialize(deserializer)?;
428        Ok(Self {
429            name: data.name,
430            value: data.value,
431            minimum: data
432                .minimum
433                .map(rational_from_parsed_decimal)
434                .transpose()
435                .map_err(serde::de::Error::custom)?,
436            maximum: data
437                .maximum
438                .map(rational_from_parsed_decimal)
439                .transpose()
440                .map_err(serde::de::Error::custom)?,
441            default_magnitude: data
442                .default_magnitude
443                .map(rational_from_parsed_decimal)
444                .transpose()
445                .map_err(serde::de::Error::custom)?,
446        })
447    }
448}
449
450impl RatioUnit {
451    pub fn clear_constraint_magnitudes(&mut self) {
452        self.minimum = None;
453        self.maximum = None;
454        self.default_magnitude = None;
455    }
456
457    /// Unit scale as decimal (schema ratio unit values always commit).
458    pub fn value_decimal(&self) -> Decimal {
459        rational::commit_rational_to_decimal(&self.value)
460            .expect("BUG: ratio unit value must commit to decimal")
461    }
462
463    #[must_use]
464    pub fn minimum_decimal(&self) -> Option<Decimal> {
465        self.minimum.as_ref().map(|bound| {
466            rational::commit_rational_to_decimal(bound)
467                .expect("BUG: planned ratio unit minimum must commit to decimal")
468        })
469    }
470
471    #[must_use]
472    pub fn maximum_decimal(&self) -> Option<Decimal> {
473        self.maximum.as_ref().map(|bound| {
474            rational::commit_rational_to_decimal(bound)
475                .expect("BUG: planned ratio unit maximum must commit to decimal")
476        })
477    }
478
479    #[must_use]
480    pub fn default_magnitude_decimal(&self) -> Option<Decimal> {
481        self.default_magnitude.as_ref().map(|bound| {
482            rational::commit_rational_to_decimal(bound)
483                .expect("BUG: planned ratio unit default must commit to decimal")
484        })
485    }
486
487    /// Maximum bound lifted to canonical ratio space via `maximum * value`.
488    #[must_use]
489    pub fn maximum_canonical_decimal(&self) -> Option<Decimal> {
490        self.maximum.as_ref().map(|maximum| {
491            let canonical = rational::checked_mul(maximum, &self.value)
492                .expect("BUG: planned ratio unit maximum canonical multiply must succeed");
493            rational::commit_rational_to_decimal(&canonical)
494                .expect("BUG: planned ratio unit maximum canonical must commit to decimal")
495        })
496    }
497}
498
499#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
500#[serde(transparent)]
501pub struct RatioUnits(pub Vec<RatioUnit>);
502
503impl RatioUnits {
504    pub fn new() -> Self {
505        RatioUnits(Vec::new())
506    }
507    pub fn get(&self, name: &str) -> Result<&RatioUnit, String> {
508        self.0.iter().find(|u| u.name == name).ok_or_else(|| {
509            let valid: Vec<&str> = self.0.iter().map(|u| u.name.as_str()).collect();
510            format!(
511                "Unknown unit '{}' for this ratio type. Valid units: {}",
512                name,
513                valid.join(", ")
514            )
515        })
516    }
517
518    pub fn iter(&self) -> std::slice::Iter<'_, RatioUnit> {
519        self.0.iter()
520    }
521    pub fn push(&mut self, u: RatioUnit) {
522        self.0.push(u);
523    }
524    pub fn is_empty(&self) -> bool {
525        self.0.is_empty()
526    }
527    pub fn len(&self) -> usize {
528        self.0.len()
529    }
530}
531
532impl Default for RatioUnits {
533    fn default() -> Self {
534        RatioUnits::new()
535    }
536}
537
538impl From<Vec<RatioUnit>> for RatioUnits {
539    fn from(v: Vec<RatioUnit>) -> Self {
540        RatioUnits(v)
541    }
542}
543
544impl<'a> IntoIterator for &'a RatioUnits {
545    type Item = &'a RatioUnit;
546    type IntoIter = std::slice::Iter<'a, RatioUnit>;
547    fn into_iter(self) -> Self::IntoIter {
548        self.0.iter()
549    }
550}
551
552// -----------------------------------------------------------------------------
553// Literal value types
554// -----------------------------------------------------------------------------
555
556#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
557#[serde(rename_all = "lowercase")]
558pub enum BooleanValue {
559    True,
560    False,
561    Yes,
562    No,
563    Accept,
564    Reject,
565}
566
567impl From<BooleanValue> for bool {
568    fn from(value: BooleanValue) -> bool {
569        matches!(
570            value,
571            BooleanValue::True | BooleanValue::Yes | BooleanValue::Accept
572        )
573    }
574}
575
576impl From<&BooleanValue> for bool {
577    fn from(value: &BooleanValue) -> bool {
578        (*value).into() // Copy makes this ok
579    }
580}
581
582impl From<bool> for BooleanValue {
583    fn from(value: bool) -> BooleanValue {
584        if value {
585            BooleanValue::True
586        } else {
587            BooleanValue::False
588        }
589    }
590}
591
592impl std::ops::Not for BooleanValue {
593    type Output = BooleanValue;
594
595    fn not(self) -> Self::Output {
596        if self.into() {
597            BooleanValue::False
598        } else {
599            BooleanValue::True
600        }
601    }
602}
603
604impl std::ops::Not for &BooleanValue {
605    type Output = BooleanValue;
606
607    fn not(self) -> Self::Output {
608        if (*self).into() {
609            BooleanValue::False
610        } else {
611            BooleanValue::True
612        }
613    }
614}
615
616impl std::str::FromStr for BooleanValue {
617    type Err = String;
618
619    fn from_str(s: &str) -> Result<Self, Self::Err> {
620        match s.trim().to_lowercase().as_str() {
621            "true" => Ok(BooleanValue::True),
622            "false" => Ok(BooleanValue::False),
623            "yes" => Ok(BooleanValue::Yes),
624            "no" => Ok(BooleanValue::No),
625            "accept" => Ok(BooleanValue::Accept),
626            "reject" => Ok(BooleanValue::Reject),
627            _ => Err(format!("Invalid boolean: '{}'", s)),
628        }
629    }
630}
631
632impl BooleanValue {
633    #[must_use]
634    pub fn as_str(&self) -> &'static str {
635        match self {
636            BooleanValue::True => "true",
637            BooleanValue::False => "false",
638            BooleanValue::Yes => "yes",
639            BooleanValue::No => "no",
640            BooleanValue::Accept => "accept",
641            BooleanValue::Reject => "reject",
642        }
643    }
644}
645
646impl fmt::Display for BooleanValue {
647    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
648        write!(f, "{}", self.as_str())
649    }
650}
651
652#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
653pub struct TimezoneValue {
654    pub offset_hours: i8,
655    pub offset_minutes: u8,
656}
657
658impl fmt::Display for TimezoneValue {
659    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
660        if self.offset_hours == 0 && self.offset_minutes == 0 {
661            write!(f, "Z")
662        } else {
663            let sign = if self.offset_hours >= 0 { "+" } else { "-" };
664            let hours = self.offset_hours.abs();
665            write!(f, "{}{:02}:{:02}", sign, hours, self.offset_minutes)
666        }
667    }
668}
669
670#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
671pub struct TimeValue {
672    pub hour: u8,
673    pub minute: u8,
674    pub second: u8,
675    #[serde(default)]
676    pub microsecond: u32,
677    pub timezone: Option<TimezoneValue>,
678}
679
680impl fmt::Display for TimeValue {
681    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
682        write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
683        if self.microsecond != 0 {
684            write!(f, ".{:06}", self.microsecond)?;
685        }
686        if let Some(timezone) = &self.timezone {
687            write!(f, "{}", timezone)?;
688        }
689        Ok(())
690    }
691}
692
693#[derive(
694    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
695)]
696#[serde(rename_all = "snake_case")]
697pub enum DateGranularity {
698    Year,
699    YearMonth,
700    /// ISO 8601 week date. Stores original (iso_year, week) because the ISO
701    /// week year can differ from the calendar year — e.g. "2026-W01" has
702    /// iso_year=2026 but the stored calendar date year=2025.
703    IsoWeek {
704        iso_year: i32,
705        week: u32,
706    },
707    #[default]
708    Full,
709    DateTime,
710}
711
712#[derive(Debug, Clone, Serialize, Deserialize)]
713pub struct DateTimeValue {
714    pub year: i32,
715    pub month: u32,
716    pub day: u32,
717    pub hour: u32,
718    pub minute: u32,
719    pub second: u32,
720    #[serde(default)]
721    pub microsecond: u32,
722    pub timezone: Option<TimezoneValue>,
723    #[serde(default)]
724    pub granularity: DateGranularity,
725}
726
727impl PartialEq for DateTimeValue {
728    fn eq(&self, other: &Self) -> bool {
729        self.year == other.year
730            && self.month == other.month
731            && self.day == other.day
732            && self.hour == other.hour
733            && self.minute == other.minute
734            && self.second == other.second
735            && self.microsecond == other.microsecond
736            && self.timezone == other.timezone
737    }
738}
739
740impl Eq for DateTimeValue {}
741
742impl PartialOrd for DateTimeValue {
743    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
744        Some(self.cmp(other))
745    }
746}
747
748impl Ord for DateTimeValue {
749    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
750        self.year
751            .cmp(&other.year)
752            .then_with(|| self.month.cmp(&other.month))
753            .then_with(|| self.day.cmp(&other.day))
754            .then_with(|| self.hour.cmp(&other.hour))
755            .then_with(|| self.minute.cmp(&other.minute))
756            .then_with(|| self.second.cmp(&other.second))
757            .then_with(|| self.microsecond.cmp(&other.microsecond))
758            .then_with(|| self.timezone.cmp(&other.timezone))
759    }
760}
761
762impl std::hash::Hash for DateTimeValue {
763    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
764        self.year.hash(state);
765        self.month.hash(state);
766        self.day.hash(state);
767        self.hour.hash(state);
768        self.minute.hash(state);
769        self.second.hash(state);
770        self.microsecond.hash(state);
771        self.timezone.hash(state);
772    }
773}
774
775impl DateTimeValue {
776    pub fn now() -> Self {
777        let now = chrono::Local::now();
778        let offset_secs = now.offset().local_minus_utc();
779        Self {
780            year: now.year(),
781            month: now.month(),
782            day: now.day(),
783            hour: now.time().hour(),
784            minute: now.time().minute(),
785            second: now.time().second(),
786            microsecond: now.time().nanosecond() / 1000 % 1_000_000,
787            timezone: Some(TimezoneValue {
788                offset_hours: (offset_secs / 3600) as i8,
789                offset_minutes: ((offset_secs.abs() % 3600) / 60) as u8,
790            }),
791            granularity: DateGranularity::DateTime,
792        }
793    }
794
795    fn parse_iso_week(s: &str) -> Option<Self> {
796        let parts: Vec<&str> = s.split("-W").collect();
797        if parts.len() != 2 {
798            return None;
799        }
800        let iso_year: i32 = parts[0].parse().ok()?;
801        let week: u32 = parts[1].parse().ok()?;
802        if week == 0 || week > 53 {
803            return None;
804        }
805        let date = chrono::NaiveDate::from_isoywd_opt(iso_year, week, chrono::Weekday::Mon)?;
806        Some(Self {
807            year: date.year(),
808            month: date.month(),
809            day: date.day(),
810            hour: 0,
811            minute: 0,
812            second: 0,
813            microsecond: 0,
814            timezone: None,
815            granularity: DateGranularity::IsoWeek { iso_year, week },
816        })
817    }
818}
819
820impl fmt::Display for DateTimeValue {
821    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
822        match self.granularity {
823            DateGranularity::Year => write!(f, "{:04}", self.year),
824            DateGranularity::YearMonth => write!(f, "{:04}-{:02}", self.year, self.month),
825            DateGranularity::IsoWeek { iso_year, week } => {
826                write!(f, "{:04}-W{:02}", iso_year, week)
827            }
828            DateGranularity::Full => {
829                write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
830            }
831            DateGranularity::DateTime => {
832                write!(
833                    f,
834                    "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
835                    self.year, self.month, self.day, self.hour, self.minute, self.second
836                )?;
837                if self.microsecond != 0 {
838                    write!(f, ".{:06}", self.microsecond)?;
839                }
840                if let Some(tz) = &self.timezone {
841                    write!(f, "{}", tz)?;
842                }
843                Ok(())
844            }
845        }
846    }
847}
848
849/// Literal value data (no type information). Single source of truth in literals.
850///
851/// `NumberWithUnit` is type-agnostic at parse time (`10 eur` and `50%` share this shape).
852/// Planning resolves ratio vs quantity via the unit index and target [`TypeSpecification`].
853#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
854#[serde(rename_all = "snake_case")]
855pub enum Value {
856    Number(Decimal),
857    NumberWithUnit(Decimal, String),
858    Text(String),
859    Date(DateTimeValue),
860    Time(TimeValue),
861    Boolean(BooleanValue),
862    Range(Box<Value>, Box<Value>),
863}
864
865impl fmt::Display for Value {
866    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
867        match self {
868            Value::Number(n) => write!(f, "{}", n),
869            Value::Text(s) => write!(f, "{}", s),
870            Value::Date(dt) => write!(f, "{}", dt),
871            Value::Boolean(b) => write!(f, "{}", b),
872            Value::Time(time) => write!(f, "{}", time),
873            Value::NumberWithUnit(n, u) => match u.as_str() {
874                "percent" => {
875                    let norm = n.normalize();
876                    let s = if norm.fract().is_zero() {
877                        norm.trunc().to_string()
878                    } else {
879                        norm.to_string()
880                    };
881                    write!(f, "{}%", s)
882                }
883                "permille" => {
884                    let norm = n.normalize();
885                    let s = if norm.fract().is_zero() {
886                        norm.trunc().to_string()
887                    } else {
888                        norm.to_string()
889                    };
890                    write!(f, "{}%%", s)
891                }
892                unit => {
893                    let norm = n.normalize();
894                    let s = if norm.fract().is_zero() {
895                        norm.trunc().to_string()
896                    } else {
897                        norm.to_string()
898                    };
899                    write!(f, "{} {}", s, unit)
900                }
901            },
902            Value::Range(left, right) => write!(f, "{}...{}", left, right),
903        }
904    }
905}
906
907// -----------------------------------------------------------------------------
908// FromStr (single source of truth per type)
909// -----------------------------------------------------------------------------
910
911impl std::str::FromStr for DateTimeValue {
912    type Err = String;
913
914    fn from_str(s: &str) -> Result<Self, Self::Err> {
915        if let Ok(dt) = s.parse::<chrono::DateTime<chrono::FixedOffset>>() {
916            let offset = dt.offset().local_minus_utc();
917            let microsecond = dt.nanosecond() / 1000 % 1_000_000;
918            return Ok(DateTimeValue {
919                year: dt.year(),
920                month: dt.month(),
921                day: dt.day(),
922                hour: dt.hour(),
923                minute: dt.minute(),
924                second: dt.second(),
925                microsecond,
926                timezone: Some(TimezoneValue {
927                    offset_hours: (offset / 3600) as i8,
928                    offset_minutes: ((offset.abs() % 3600) / 60) as u8,
929                }),
930                granularity: DateGranularity::DateTime,
931            });
932        }
933        if let Ok(dt) = s.parse::<chrono::NaiveDateTime>() {
934            let microsecond = dt.nanosecond() / 1000 % 1_000_000;
935            return Ok(DateTimeValue {
936                year: dt.year(),
937                month: dt.month(),
938                day: dt.day(),
939                hour: dt.hour(),
940                minute: dt.minute(),
941                second: dt.second(),
942                microsecond,
943                timezone: None,
944                granularity: DateGranularity::DateTime,
945            });
946        }
947        if let Ok(d) = s.parse::<chrono::NaiveDate>() {
948            return Ok(DateTimeValue {
949                year: d.year(),
950                month: d.month(),
951                day: d.day(),
952                hour: 0,
953                minute: 0,
954                second: 0,
955                microsecond: 0,
956                timezone: None,
957                granularity: DateGranularity::Full,
958            });
959        }
960        if let Some(week_val) = Self::parse_iso_week(s) {
961            return Ok(week_val);
962        }
963        if let Ok(ym) = chrono::NaiveDate::parse_from_str(&format!("{}-01", s), "%Y-%m-%d") {
964            return Ok(Self {
965                year: ym.year(),
966                month: ym.month(),
967                day: 1,
968                hour: 0,
969                minute: 0,
970                second: 0,
971                microsecond: 0,
972                timezone: None,
973                granularity: DateGranularity::YearMonth,
974            });
975        }
976        if let Ok(year) = s.parse::<i32>() {
977            if (1..=9999).contains(&year) {
978                return Ok(Self {
979                    year,
980                    month: 1,
981                    day: 1,
982                    hour: 0,
983                    minute: 0,
984                    second: 0,
985                    microsecond: 0,
986                    timezone: None,
987                    granularity: DateGranularity::Year,
988                });
989            }
990        }
991        Err(format!("Invalid date format: '{}'", s))
992    }
993}
994
995impl std::str::FromStr for TimeValue {
996    type Err = String;
997
998    fn from_str(s: &str) -> Result<Self, Self::Err> {
999        let trimmed = s.trim();
1000
1001        let (time_text, timezone) = if trimmed.ends_with('Z') || trimmed.ends_with('z') {
1002            (
1003                &trimmed[..trimmed.len() - 1],
1004                Some(TimezoneValue {
1005                    offset_hours: 0,
1006                    offset_minutes: 0,
1007                }),
1008            )
1009        } else if trimmed.len() > 1 {
1010            if let Some(sign_index) = trimmed[1..].rfind(['+', '-']).map(|index| index + 1) {
1011                let timezone_text = &trimmed[sign_index..];
1012                if timezone_text.len() == 6
1013                    && (timezone_text.starts_with('+') || timezone_text.starts_with('-'))
1014                    && timezone_text.as_bytes()[3] == b':'
1015                {
1016                    let offset_hours: i8 = timezone_text[1..3]
1017                        .parse()
1018                        .map_err(|_| format!("Invalid time format: '{}'", s))?;
1019                    let offset_minutes: u8 = timezone_text[4..6]
1020                        .parse()
1021                        .map_err(|_| format!("Invalid time format: '{}'", s))?;
1022                    let signed_hours = if timezone_text.starts_with('-') {
1023                        -offset_hours
1024                    } else {
1025                        offset_hours
1026                    };
1027                    (
1028                        &trimmed[..sign_index],
1029                        Some(TimezoneValue {
1030                            offset_hours: signed_hours,
1031                            offset_minutes,
1032                        }),
1033                    )
1034                } else {
1035                    (trimmed, None)
1036                }
1037            } else {
1038                (trimmed, None)
1039            }
1040        } else {
1041            (trimmed, None)
1042        };
1043
1044        if let Ok(t) = chrono::NaiveTime::parse_from_str(time_text, "%H:%M:%S%.f") {
1045            return Ok(TimeValue {
1046                hour: t.hour() as u8,
1047                minute: t.minute() as u8,
1048                second: t.second() as u8,
1049                microsecond: t.nanosecond() / 1000 % 1_000_000,
1050                timezone,
1051            });
1052        }
1053        if let Ok(t) = chrono::NaiveTime::parse_from_str(time_text, "%H:%M:%S") {
1054            return Ok(TimeValue {
1055                hour: t.hour() as u8,
1056                minute: t.minute() as u8,
1057                second: t.second() as u8,
1058                microsecond: 0,
1059                timezone,
1060            });
1061        }
1062        if let Ok(t) = chrono::NaiveTime::parse_from_str(time_text, "%H:%M") {
1063            return Ok(TimeValue {
1064                hour: t.hour() as u8,
1065                minute: t.minute() as u8,
1066                second: 0,
1067                microsecond: 0,
1068                timezone,
1069            });
1070        }
1071        Err(format!("Invalid time format: '{}'", s))
1072    }
1073}
1074
1075/// Number literal with Lemma rules (strip _ and ,; MAX_NUMBER_DIGITS).
1076pub(crate) struct NumberLiteral(pub Decimal);
1077
1078impl std::str::FromStr for NumberLiteral {
1079    type Err = String;
1080
1081    fn from_str(s: &str) -> Result<Self, Self::Err> {
1082        let clean = s.trim().replace(['_', ','], "");
1083        let digit_count = clean.chars().filter(|c| c.is_ascii_digit()).count();
1084        if digit_count > crate::limits::MAX_NUMBER_DIGITS {
1085            return Err(format!(
1086                "Number has too many digits (max {})",
1087                crate::limits::MAX_NUMBER_DIGITS
1088            ));
1089        }
1090        Decimal::from_str(&clean)
1091            .map_err(|_| format!("Invalid number: '{}'", s))
1092            .map(NumberLiteral)
1093    }
1094}
1095
1096/// Text literal with length limit.
1097pub(crate) struct TextLiteral(pub String);
1098
1099impl std::str::FromStr for TextLiteral {
1100    type Err = String;
1101
1102    fn from_str(s: &str) -> Result<Self, Self::Err> {
1103        if s.len() > crate::limits::MAX_TEXT_VALUE_LENGTH {
1104            return Err(format!(
1105                "Text value exceeds maximum length (max {} characters)",
1106                crate::limits::MAX_TEXT_VALUE_LENGTH
1107            ));
1108        }
1109        Ok(TextLiteral(s.to_string()))
1110    }
1111}
1112
1113/// Parsed `<number> <unit-name>` for runtime string input (quantity and ratio types).
1114pub(crate) struct NumberWithUnit(pub Decimal, pub String);
1115
1116impl std::str::FromStr for NumberWithUnit {
1117    type Err = String;
1118
1119    fn from_str(s: &str) -> Result<Self, Self::Err> {
1120        let trimmed = s.trim();
1121        if trimmed.is_empty() {
1122            return Err(
1123                "Quantity value cannot be empty. Use a number followed by a unit (e.g. '10 eur')."
1124                    .to_string(),
1125            );
1126        }
1127
1128        let mut parts = trimmed.split_whitespace();
1129        let number_part = parts
1130            .next()
1131            .expect("split_whitespace yields >=1 token after non-empty guard");
1132        let unit_part = parts.next().ok_or_else(|| {
1133            format!(
1134                "Quantity value must include a unit (e.g. '{} eur').",
1135                number_part
1136            )
1137        })?;
1138        if parts.next().is_some() {
1139            return Err(format!(
1140                "Invalid quantity value: '{}'. Expected exactly '<number> <unit>', got extra tokens.",
1141                s
1142            ));
1143        }
1144        let n = number_part
1145            .parse::<NumberLiteral>()
1146            .map_err(|_| format!("Invalid quantity: '{}'", s))?
1147            .0;
1148        Ok(NumberWithUnit(n, unit_part.to_string()))
1149    }
1150}
1151
1152/// Strict ratio runtime literal.
1153///
1154/// Grammar (all inputs trimmed first):
1155/// - `<number>`                      → `Bare(n)`
1156/// - `<number>%`  (glued, no inner whitespace) → `Percent(n)` raw magnitude
1157/// - `<number>%%` (glued, no inner whitespace) → `Permille(n)` raw magnitude
1158/// - `<number> <unit-name>`          → `Named { value: n, unit: <unit-name> }`
1159///
1160/// `<number>` is parsed by [`NumberLiteral`] (signed, allows `_`/`,` separators).
1161/// Whitespace between the number and a keyword unit may be any non-empty run
1162/// (`"50 percent"`, `"50    percent"`, `"50\tpercent"` are all accepted).
1163///
1164/// The sigils `%` / `%%` are language-level constants meaning "divide by 100 / 1000"
1165/// and unconditionally produce the canonical unit names `"percent"` / `"permille"`.
1166/// They are NOT accepted as standalone unit-position tokens (i.e. `"5 %"` is rejected).
1167///
1168/// Signedness is intentionally not constrained at this layer: bounds are the
1169/// type-system's job (`-> minimum 0%`), and the evaluator can produce signed
1170/// ratios from non-negative inputs (e.g. `this_year - last_year` on `percent`).
1171/// The parser must accept everything the evaluator can emit (round-trip symmetry).
1172///
1173/// `Named` carries the raw unit name; the caller in `parse_number_unit::Ratio`
1174/// resolves it against the type's [`RatioUnits`] table (covering built-in
1175/// `percent`/`permille` and any user-defined units like `basis_points`).
1176#[derive(Debug, Clone, PartialEq, Eq)]
1177pub(crate) enum RatioLiteral {
1178    Bare(Decimal),
1179    Percent(Decimal),
1180    Permille(Decimal),
1181    Named { value: Decimal, unit: String },
1182}
1183
1184impl std::str::FromStr for RatioLiteral {
1185    type Err = String;
1186
1187    fn from_str(s: &str) -> Result<Self, Self::Err> {
1188        let trimmed = s.trim();
1189        if trimmed.is_empty() {
1190            return Err(
1191                "Ratio value cannot be empty. Use a number, optionally followed by '%', '%%', or a unit name (e.g. '0.5', '50%', '25%%', '50 percent')."
1192                    .to_string(),
1193            );
1194        }
1195
1196        let mut parts = trimmed.split_whitespace();
1197        let first = parts
1198            .next()
1199            .expect("split_whitespace yields >=1 token after non-empty guard");
1200        let second = parts.next();
1201        if parts.next().is_some() {
1202            return Err(format!(
1203                "Invalid ratio value: '{}'. Expected '<number>', '<number>%', '<number>%%', or '<number> <unit>'.",
1204                s
1205            ));
1206        }
1207
1208        match second {
1209            // 1-token forms: bare number, or sigil-suffixed number.
1210            None => {
1211                if let Some(rest) = first.strip_suffix("%%") {
1212                    if rest.is_empty() {
1213                        return Err(format!(
1214                            "Invalid ratio value: '{}'. '%%' must follow a number (e.g. '25%%').",
1215                            s
1216                        ));
1217                    }
1218                    let n = rest
1219                        .parse::<NumberLiteral>()
1220                        .map_err(|_| {
1221                            format!(
1222                            "Invalid ratio value: '{}'. '{}' is not a valid number before '%%'.",
1223                            s, rest
1224                        )
1225                        })?
1226                        .0;
1227                    return Ok(RatioLiteral::Permille(n));
1228                }
1229                if let Some(rest) = first.strip_suffix('%') {
1230                    if rest.is_empty() {
1231                        return Err(format!(
1232                            "Invalid ratio value: '{}'. '%' must follow a number (e.g. '50%').",
1233                            s
1234                        ));
1235                    }
1236                    let n = rest
1237                        .parse::<NumberLiteral>()
1238                        .map_err(|_| {
1239                            format!(
1240                                "Invalid ratio value: '{}'. '{}' is not a valid number before '%'.",
1241                                s, rest
1242                            )
1243                        })?
1244                        .0;
1245                    return Ok(RatioLiteral::Percent(n));
1246                }
1247                let n = first.parse::<NumberLiteral>().map_err(|_| {
1248                    format!(
1249                        "Invalid ratio value: '{}'. Must be a number, '<n>%', '<n>%%', '<n> percent', '<n> permille', or '<n> <unit>'.",
1250                        s
1251                    )
1252                })?.0;
1253                Ok(RatioLiteral::Bare(n))
1254            }
1255            // 2-token form: <number> <unit-name>. Sigils are not accepted as unit-position tokens.
1256            Some(unit) => {
1257                if unit == "%" || unit == "%%" {
1258                    return Err(format!(
1259                        "Invalid ratio value: '{}'. '{}' must be glued to the number (e.g. '{}{}'), not separated by whitespace.",
1260                        s, unit, first, unit
1261                    ));
1262                }
1263                let n = first
1264                    .parse::<NumberLiteral>()
1265                    .map_err(|_| {
1266                        format!(
1267                            "Invalid ratio value: '{}'. '{}' is not a valid number.",
1268                            s, first
1269                        )
1270                    })?
1271                    .0;
1272                Ok(RatioLiteral::Named {
1273                    value: n,
1274                    unit: unit.to_string(),
1275                })
1276            }
1277        }
1278    }
1279}