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 measure-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 measure (no decomposition) until the decomposition pass runs,
19/// after which every measure carries a non-empty vector.
20pub type BaseMeasureVector = BTreeMap<String, i32>;
21
22// -----------------------------------------------------------------------------
23// Unit tables for Measure and Ratio types
24// -----------------------------------------------------------------------------
25
26pub fn rational_to_serialized_str(rational: &RationalInteger) -> Result<String, String> {
27    rational
28        .try_to_decimal_string()
29        .map_err(|failure| failure.to_string())
30}
31
32pub fn rational_from_parsed_decimal(decimal: Decimal) -> Result<RationalInteger, String> {
33    rational::decimal_to_rational(decimal).map_err(|failure| failure.to_string())
34}
35
36/// Serde for stored rationals: API format is decimal string or JSON number (lifted at boundary).
37pub mod stored_rational_serde {
38    use super::{rational_from_parsed_decimal, rational_to_serialized_str, RationalInteger};
39    use rust_decimal::Decimal;
40    use serde::{Deserialize, Deserializer, Serializer};
41
42    pub fn serialize<S: Serializer>(
43        value: &RationalInteger,
44        serializer: S,
45    ) -> Result<S::Ok, S::Error> {
46        serializer.serialize_str(
47            &rational_to_serialized_str(value)
48                .expect("BUG: planned bound must serialize to decimal string"),
49        )
50    }
51
52    pub mod option {
53        use super::*;
54
55        pub fn serialize<S: Serializer>(
56            value: &Option<RationalInteger>,
57            serializer: S,
58        ) -> Result<S::Ok, S::Error> {
59            match value {
60                Some(rational) => super::serialize(rational, serializer),
61                None => serializer.serialize_none(),
62            }
63        }
64
65        pub fn deserialize<'de, D: Deserializer<'de>>(
66            deserializer: D,
67        ) -> Result<Option<RationalInteger>, D::Error> {
68            Option::<Decimal>::deserialize(deserializer)?
69                .map(rational_from_parsed_decimal)
70                .transpose()
71                .map_err(serde::de::Error::custom)
72        }
73    }
74}
75
76/// A single unit within a Measure type.
77///
78/// `factor` is the conversion factor: 1 of this unit equals `factor` canonical units.
79/// `derived_measure_factors` stores `(measure_ref, exponent)` pairs from compound unit declarations
80/// (e.g., `meter/second` produces `[("meter", 1), ("second", -1)]`). Empty for base units.
81/// `decomposition` is the dimensional decomposition vector, populated during the planning
82/// decomposition pass. It is empty until that pass completes.
83#[derive(Clone, Debug, PartialEq, Eq, Hash)]
84pub struct MeasureUnit {
85    pub name: String,
86    /// Conversion factor: 1 of this unit equals `value` canonical units.
87    pub factor: RationalInteger,
88    pub derived_measure_factors: Vec<(String, i32)>,
89    pub decomposition: BaseMeasureVector,
90    /// Minimum magnitude in this unit (schema/UI); canonical bound is on the type.
91    pub minimum: Option<RationalInteger>,
92    /// Maximum magnitude in this unit (schema/UI).
93    pub maximum: Option<RationalInteger>,
94    /// Default suggestion magnitude in this unit (schema/UI).
95    pub suggestion_magnitude: Option<RationalInteger>,
96}
97
98impl Serialize for MeasureUnit {
99    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
100        use measure_unit_factor_serialization::FactorSerializer;
101        use serde::ser::SerializeStruct;
102        let mut state = serializer.serialize_struct("MeasureUnit", 7)?;
103        state.serialize_field("name", &self.name)?;
104        state.serialize_field("factor", &FactorSerializer::from_ratio(&self.factor))?;
105        state.serialize_field("derived_measure_factors", &self.derived_measure_factors)?;
106        state.serialize_field("decomposition", &self.decomposition)?;
107        if let Some(minimum) = &self.minimum {
108            state.serialize_field(
109                "minimum",
110                &rational_to_serialized_str(minimum)
111                    .expect("BUG: planned measure unit minimum must serialize to decimal string"),
112            )?;
113        }
114        if let Some(maximum) = &self.maximum {
115            state.serialize_field(
116                "maximum",
117                &rational_to_serialized_str(maximum)
118                    .expect("BUG: planned measure unit maximum must serialize to decimal string"),
119            )?;
120        }
121        if let Some(suggestion_magnitude) = &self.suggestion_magnitude {
122            state.serialize_field(
123                "suggestion",
124                &rational_to_serialized_str(suggestion_magnitude).expect(
125                    "BUG: planned measure unit suggestion must serialize to decimal string",
126                ),
127            )?;
128        }
129        state.end()
130    }
131}
132
133impl<'de> Deserialize<'de> for MeasureUnit {
134    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
135        #[derive(Deserialize)]
136        struct MeasureUnitData {
137            name: String,
138            #[serde(with = "measure_unit_factor_serialization")]
139            factor: RationalInteger,
140            #[serde(default)]
141            derived_measure_factors: Vec<(String, i32)>,
142            #[serde(default)]
143            decomposition: BaseMeasureVector,
144            #[serde(default)]
145            minimum: Option<Decimal>,
146            #[serde(default)]
147            maximum: Option<Decimal>,
148            #[serde(default, rename = "suggestion")]
149            suggestion_magnitude: Option<Decimal>,
150        }
151        let data = MeasureUnitData::deserialize(deserializer)?;
152        Ok(Self {
153            name: data.name,
154            factor: data.factor,
155            derived_measure_factors: data.derived_measure_factors,
156            decomposition: data.decomposition,
157            minimum: data
158                .minimum
159                .map(rational_from_parsed_decimal)
160                .transpose()
161                .map_err(serde::de::Error::custom)?,
162            maximum: data
163                .maximum
164                .map(rational_from_parsed_decimal)
165                .transpose()
166                .map_err(serde::de::Error::custom)?,
167            suggestion_magnitude: data
168                .suggestion_magnitude
169                .map(rational_from_parsed_decimal)
170                .transpose()
171                .map_err(serde::de::Error::custom)?,
172        })
173    }
174}
175
176impl MeasureUnit {
177    pub fn from_decimal_factor(
178        name: String,
179        decimal_factor: Decimal,
180        derived_measure_factors: Vec<(String, i32)>,
181    ) -> Result<Self, String> {
182        let factor =
183            rational::decimal_to_rational(decimal_factor).map_err(|failure| failure.to_string())?;
184        Ok(MeasureUnit {
185            name,
186            factor,
187            derived_measure_factors,
188            decomposition: BaseMeasureVector::new(),
189            minimum: None,
190            maximum: None,
191            suggestion_magnitude: None,
192        })
193    }
194
195    pub fn clear_constraint_magnitudes(&mut self) {
196        self.minimum = None;
197        self.maximum = None;
198        self.suggestion_magnitude = None;
199    }
200
201    pub fn is_canonical_factor(&self) -> bool {
202        self.factor == rational::rational_one()
203    }
204
205    pub fn is_positive_factor(&self) -> bool {
206        let numerator = self.factor.numer();
207        let denominator = self.factor.denom();
208        !numerator.is_zero() && numerator.is_positive() == denominator.is_positive()
209    }
210
211    /// Conversion factor as decimal (schema unit factors always commit).
212    pub fn factor_decimal(&self) -> Decimal {
213        rational::RationalInteger::try_to_decimal(&self.factor)
214            .expect("BUG: measure unit factor must materialize to decimal")
215    }
216
217    #[must_use]
218    pub fn minimum_decimal(&self) -> Option<Decimal> {
219        self.minimum.as_ref().map(|bound| {
220            bound
221                .try_to_decimal()
222                .expect("BUG: planned measure unit minimum must materialize to decimal")
223        })
224    }
225
226    #[must_use]
227    pub fn maximum_decimal(&self) -> Option<Decimal> {
228        self.maximum.as_ref().map(|bound| {
229            bound
230                .try_to_decimal()
231                .expect("BUG: planned measure unit maximum must materialize to decimal")
232        })
233    }
234
235    #[must_use]
236    pub fn suggestion_magnitude_decimal(&self) -> Option<Decimal> {
237        self.suggestion_magnitude.as_ref().map(|bound| {
238            bound
239                .try_to_decimal()
240                .expect("BUG: planned measure unit default must materialize to decimal")
241        })
242    }
243
244    /// Maximum bound lifted to canonical units via `maximum * factor`.
245    #[must_use]
246    pub fn maximum_canonical_decimal(&self) -> Option<Decimal> {
247        self.maximum.as_ref().map(|maximum| {
248            let canonical = rational::checked_mul(maximum, &self.factor)
249                .expect("BUG: planned measure unit maximum canonical multiply must succeed");
250            canonical
251                .try_to_decimal()
252                .expect("BUG: planned measure unit maximum canonical must materialize to decimal")
253        })
254    }
255}
256
257mod measure_unit_factor_serialization {
258    use super::RationalInteger;
259    use crate::computation::bigint::BigInt;
260    use crate::computation::rational::try_rational_new;
261    use serde::{Deserialize, Serialize};
262
263    #[derive(Serialize, Deserialize)]
264    pub struct FactorSerializer {
265        numer: String,
266        denom: String,
267    }
268
269    impl FactorSerializer {
270        pub fn from_ratio(value: &RationalInteger) -> Self {
271            let reduced = value
272                .clone()
273                .try_reduce()
274                .expect("BUG: stored measure unit factor must reduce");
275            FactorSerializer {
276                numer: reduced.numer().to_string(),
277                denom: reduced.denom().to_string(),
278            }
279        }
280
281        pub fn into_ratio(self) -> Result<RationalInteger, String> {
282            let numer = BigInt::try_from_str_radix(&self.numer, 10)
283                .map_err(|_| format!("invalid numerator: {}", self.numer))?;
284            let denom = BigInt::try_from_str_radix(&self.denom, 10)
285                .map_err(|_| format!("invalid denominator: {}", self.denom))?;
286            if denom.is_zero() {
287                return Err("MeasureUnit conversion factor denominator cannot be zero".to_string());
288            }
289            try_rational_new(numer, denom).map_err(|e| e.to_string())
290        }
291    }
292
293    pub fn deserialize<'de, D: serde::Deserializer<'de>>(
294        deserializer: D,
295    ) -> Result<RationalInteger, D::Error> {
296        FactorSerializer::deserialize(deserializer)?
297            .into_ratio()
298            .map_err(serde::de::Error::custom)
299    }
300}
301
302#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
303#[serde(transparent)]
304pub struct MeasureUnits(pub Vec<MeasureUnit>);
305
306impl MeasureUnits {
307    pub fn new() -> Self {
308        MeasureUnits(Vec::new())
309    }
310    pub fn get(&self, name: &str) -> Result<&MeasureUnit, String> {
311        self.0.iter().find(|u| u.name == name).ok_or_else(|| {
312            let valid: Vec<&str> = self.0.iter().map(|u| u.name.as_str()).collect();
313            format!(
314                "Unknown unit '{}' for this measure type. Valid units: {}",
315                name,
316                valid.join(", ")
317            )
318        })
319    }
320
321    pub fn iter(&self) -> std::slice::Iter<'_, MeasureUnit> {
322        self.0.iter()
323    }
324    pub fn push(&mut self, u: MeasureUnit) {
325        self.0.push(u);
326    }
327    pub fn is_empty(&self) -> bool {
328        self.0.is_empty()
329    }
330    pub fn len(&self) -> usize {
331        self.0.len()
332    }
333    pub fn map<F: FnMut(MeasureUnit) -> MeasureUnit>(self, f: F) -> Self {
334        MeasureUnits(self.0.into_iter().map(f).collect())
335    }
336}
337
338impl MeasureUnit {
339    pub fn with_decomposition(self, decomposition: BaseMeasureVector) -> Self {
340        Self {
341            decomposition,
342            ..self
343        }
344    }
345    pub fn with_factor(self, factor: RationalInteger) -> Self {
346        Self { factor, ..self }
347    }
348    pub fn with_derived_measure_factors(self, derived_measure_factors: Vec<(String, i32)>) -> Self {
349        Self {
350            derived_measure_factors,
351            ..self
352        }
353    }
354}
355
356impl Default for MeasureUnits {
357    fn default() -> Self {
358        MeasureUnits::new()
359    }
360}
361
362impl From<Vec<MeasureUnit>> for MeasureUnits {
363    fn from(v: Vec<MeasureUnit>) -> Self {
364        MeasureUnits(v)
365    }
366}
367
368impl<'a> IntoIterator for &'a MeasureUnits {
369    type Item = &'a MeasureUnit;
370    type IntoIter = std::slice::Iter<'a, MeasureUnit>;
371    fn into_iter(self) -> Self::IntoIter {
372        self.0.iter()
373    }
374}
375
376#[derive(Clone, Debug, PartialEq, Eq, Hash)]
377pub struct RatioUnit {
378    pub name: String,
379    pub value: RationalInteger,
380    pub minimum: Option<RationalInteger>,
381    pub maximum: Option<RationalInteger>,
382    pub suggestion_magnitude: Option<RationalInteger>,
383}
384
385impl Serialize for RatioUnit {
386    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
387        use measure_unit_factor_serialization::FactorSerializer;
388        use serde::ser::SerializeStruct;
389        let mut state = serializer.serialize_struct("RatioUnit", 5)?;
390        state.serialize_field("name", &self.name)?;
391        state.serialize_field("value", &FactorSerializer::from_ratio(&self.value))?;
392        if let Some(minimum) = &self.minimum {
393            state.serialize_field(
394                "minimum",
395                &rational_to_serialized_str(minimum)
396                    .expect("BUG: planned ratio unit minimum must serialize to decimal string"),
397            )?;
398        }
399        if let Some(maximum) = &self.maximum {
400            state.serialize_field(
401                "maximum",
402                &rational_to_serialized_str(maximum)
403                    .expect("BUG: planned ratio unit maximum must serialize to decimal string"),
404            )?;
405        }
406        if let Some(suggestion_magnitude) = &self.suggestion_magnitude {
407            state.serialize_field(
408                "suggestion",
409                &rational_to_serialized_str(suggestion_magnitude)
410                    .expect("BUG: planned ratio unit suggestion must serialize to decimal string"),
411            )?;
412        }
413        state.end()
414    }
415}
416
417impl<'de> Deserialize<'de> for RatioUnit {
418    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
419        #[derive(Deserialize)]
420        struct RatioUnitData {
421            name: String,
422            #[serde(with = "measure_unit_factor_serialization")]
423            value: RationalInteger,
424            #[serde(default)]
425            minimum: Option<Decimal>,
426            #[serde(default)]
427            maximum: Option<Decimal>,
428            #[serde(default, rename = "suggestion")]
429            suggestion_magnitude: Option<Decimal>,
430        }
431        let data = RatioUnitData::deserialize(deserializer)?;
432        Ok(Self {
433            name: data.name,
434            value: data.value,
435            minimum: data
436                .minimum
437                .map(rational_from_parsed_decimal)
438                .transpose()
439                .map_err(serde::de::Error::custom)?,
440            maximum: data
441                .maximum
442                .map(rational_from_parsed_decimal)
443                .transpose()
444                .map_err(serde::de::Error::custom)?,
445            suggestion_magnitude: data
446                .suggestion_magnitude
447                .map(rational_from_parsed_decimal)
448                .transpose()
449                .map_err(serde::de::Error::custom)?,
450        })
451    }
452}
453
454impl RatioUnit {
455    pub fn clear_constraint_magnitudes(&mut self) {
456        self.minimum = None;
457        self.maximum = None;
458        self.suggestion_magnitude = None;
459    }
460
461    /// Unit scale as decimal (schema ratio unit values always commit).
462    pub fn value_decimal(&self) -> Decimal {
463        self.value
464            .try_to_decimal()
465            .expect("BUG: ratio unit value must materialize to decimal")
466    }
467
468    #[must_use]
469    pub fn minimum_decimal(&self) -> Option<Decimal> {
470        self.minimum.as_ref().map(|bound| {
471            bound
472                .try_to_decimal()
473                .expect("BUG: planned ratio unit minimum must materialize to decimal")
474        })
475    }
476
477    #[must_use]
478    pub fn maximum_decimal(&self) -> Option<Decimal> {
479        self.maximum.as_ref().map(|bound| {
480            bound
481                .try_to_decimal()
482                .expect("BUG: planned ratio unit maximum must materialize to decimal")
483        })
484    }
485
486    #[must_use]
487    pub fn suggestion_magnitude_decimal(&self) -> Option<Decimal> {
488        self.suggestion_magnitude.as_ref().map(|bound| {
489            bound
490                .try_to_decimal()
491                .expect("BUG: planned ratio unit default must materialize to decimal")
492        })
493    }
494
495    /// Maximum bound lifted to canonical ratio space via `maximum * value`.
496    #[must_use]
497    pub fn maximum_canonical_decimal(&self) -> Option<Decimal> {
498        self.maximum.as_ref().map(|maximum| {
499            let canonical = rational::checked_mul(maximum, &self.value)
500                .expect("BUG: planned ratio unit maximum canonical multiply must succeed");
501            canonical
502                .try_to_decimal()
503                .expect("BUG: planned ratio unit maximum canonical must materialize to decimal")
504        })
505    }
506}
507
508#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
509#[serde(transparent)]
510pub struct RatioUnits(pub Vec<RatioUnit>);
511
512impl RatioUnits {
513    pub fn new() -> Self {
514        RatioUnits(Vec::new())
515    }
516    pub fn get(&self, name: &str) -> Result<&RatioUnit, String> {
517        self.0.iter().find(|u| u.name == name).ok_or_else(|| {
518            let valid: Vec<&str> = self.0.iter().map(|u| u.name.as_str()).collect();
519            format!(
520                "Unknown unit '{}' for this ratio type. Valid units: {}",
521                name,
522                valid.join(", ")
523            )
524        })
525    }
526
527    pub fn iter(&self) -> std::slice::Iter<'_, RatioUnit> {
528        self.0.iter()
529    }
530    pub fn push(&mut self, u: RatioUnit) {
531        self.0.push(u);
532    }
533    pub fn is_empty(&self) -> bool {
534        self.0.is_empty()
535    }
536    pub fn len(&self) -> usize {
537        self.0.len()
538    }
539}
540
541impl Default for RatioUnits {
542    fn default() -> Self {
543        RatioUnits::new()
544    }
545}
546
547impl From<Vec<RatioUnit>> for RatioUnits {
548    fn from(v: Vec<RatioUnit>) -> Self {
549        RatioUnits(v)
550    }
551}
552
553impl<'a> IntoIterator for &'a RatioUnits {
554    type Item = &'a RatioUnit;
555    type IntoIter = std::slice::Iter<'a, RatioUnit>;
556    fn into_iter(self) -> Self::IntoIter {
557        self.0.iter()
558    }
559}
560
561// -----------------------------------------------------------------------------
562// Literal value types
563// -----------------------------------------------------------------------------
564
565#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
566#[serde(rename_all = "lowercase")]
567pub enum BooleanValue {
568    True,
569    False,
570    Yes,
571    No,
572}
573
574impl From<BooleanValue> for bool {
575    fn from(value: BooleanValue) -> bool {
576        matches!(value, BooleanValue::True | BooleanValue::Yes)
577    }
578}
579
580impl From<&BooleanValue> for bool {
581    fn from(value: &BooleanValue) -> bool {
582        (*value).into() // Copy makes this ok
583    }
584}
585
586impl From<bool> for BooleanValue {
587    fn from(value: bool) -> BooleanValue {
588        if value {
589            BooleanValue::True
590        } else {
591            BooleanValue::False
592        }
593    }
594}
595
596impl std::ops::Not for BooleanValue {
597    type Output = BooleanValue;
598
599    fn not(self) -> Self::Output {
600        if self.into() {
601            BooleanValue::False
602        } else {
603            BooleanValue::True
604        }
605    }
606}
607
608impl std::ops::Not for &BooleanValue {
609    type Output = BooleanValue;
610
611    fn not(self) -> Self::Output {
612        if (*self).into() {
613            BooleanValue::False
614        } else {
615            BooleanValue::True
616        }
617    }
618}
619
620impl std::str::FromStr for BooleanValue {
621    type Err = String;
622
623    fn from_str(s: &str) -> Result<Self, Self::Err> {
624        match s.trim().to_lowercase().as_str() {
625            "true" => Ok(BooleanValue::True),
626            "false" => Ok(BooleanValue::False),
627            "yes" => Ok(BooleanValue::Yes),
628            "no" => Ok(BooleanValue::No),
629            _ => Err(format!("Invalid boolean: '{}'", s)),
630        }
631    }
632}
633
634impl BooleanValue {
635    #[must_use]
636    pub fn as_str(&self) -> &'static str {
637        match self {
638            BooleanValue::True => "true",
639            BooleanValue::False => "false",
640            BooleanValue::Yes => "yes",
641            BooleanValue::No => "no",
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 hour = self.offset_hours.abs();
665            write!(f, "{}{:02}:{:02}", sign, hour, 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 measure 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 , separators).
1076///
1077/// `Decimal::from_str` rounds excess fractional digits to [`Decimal::MAX_SCALE`]
1078/// significant digits (truncate at input); an integer magnitude that
1079/// cannot be represented is an error.
1080pub(crate) struct NumberLiteral(pub Decimal);
1081
1082impl std::str::FromStr for NumberLiteral {
1083    type Err = String;
1084
1085    fn from_str(s: &str) -> Result<Self, Self::Err> {
1086        let clean = s.trim().replace(['_', ','], "");
1087        Decimal::from_str(&clean)
1088            .map_err(|_| format!("Invalid number: '{}'", s))
1089            .map(NumberLiteral)
1090    }
1091}
1092
1093/// Text literal with length limit.
1094pub(crate) struct TextLiteral(pub String);
1095
1096impl std::str::FromStr for TextLiteral {
1097    type Err = String;
1098
1099    fn from_str(s: &str) -> Result<Self, Self::Err> {
1100        if s.len() > crate::limits::MAX_TEXT_VALUE_LENGTH {
1101            return Err(format!(
1102                "Text value exceeds maximum length (max {} characters)",
1103                crate::limits::MAX_TEXT_VALUE_LENGTH
1104            ));
1105        }
1106        Ok(TextLiteral(s.to_string()))
1107    }
1108}
1109
1110/// Parsed `<number> <unit-name>` for runtime string input (measure and ratio types).
1111pub(crate) struct NumberWithUnit(pub Decimal, pub String);
1112
1113impl std::str::FromStr for NumberWithUnit {
1114    type Err = String;
1115
1116    fn from_str(s: &str) -> Result<Self, Self::Err> {
1117        let trimmed = s.trim();
1118        if trimmed.is_empty() {
1119            return Err(
1120                "Measure value cannot be empty. Use a number followed by a unit (e.g. '10 eur')."
1121                    .to_string(),
1122            );
1123        }
1124
1125        let mut parts = trimmed.split_whitespace();
1126        let number_part = parts
1127            .next()
1128            .expect("split_whitespace yields >=1 token after non-empty guard");
1129        let unit_part = parts.next().ok_or_else(|| {
1130            format!(
1131                "Measure value must include a unit (e.g. '{} eur').",
1132                number_part
1133            )
1134        })?;
1135        if parts.next().is_some() {
1136            return Err(format!(
1137                "Invalid measure value: '{}'. Expected exactly '<number> <unit>', got extra tokens.",
1138                s
1139            ));
1140        }
1141        let n = number_part
1142            .parse::<NumberLiteral>()
1143            .map_err(|_| format!("Invalid measure: '{}'", s))?
1144            .0;
1145        Ok(NumberWithUnit(n, unit_part.to_string()))
1146    }
1147}
1148
1149/// Strict ratio runtime literal.
1150///
1151/// Grammar (all inputs trimmed first):
1152/// - `<number>`                      → `Bare(n)`
1153/// - `<number>%`  (glued, no inner whitespace) → `Percent(n)` raw magnitude
1154/// - `<number>%%` (glued, no inner whitespace) → `Permille(n)` raw magnitude
1155/// - `<number> <unit-name>`          → `Named { value: n, unit: <unit-name> }`
1156///
1157/// `<number>` is parsed by [`NumberLiteral`] (signed, allows `_`/`,` separators).
1158/// Whitespace between the number and a keyword unit may be any non-empty run
1159/// (`"50 percent"`, `"50    percent"`, `"50\tpercent"` are all accepted).
1160///
1161/// The sigils `%` / `%%` are language-level constants meaning "divide by 100 / 1000"
1162/// and unconditionally produce the canonical unit names `"percent"` / `"permille"`.
1163/// They are NOT accepted as standalone unit-position tokens (i.e. `"5 %"` is rejected).
1164///
1165/// Signedness is intentionally not constrained at this layer: bounds are the
1166/// type-system's job (`-> minimum 0%`), and the evaluator can produce signed
1167/// ratios from non-negative inputs (e.g. `this_year - last_year` on `percent`).
1168/// The parser must accept everything the evaluator can emit (round-trip symmetry).
1169///
1170/// `Named` carries the raw unit name; the caller in `parse_number_unit::Ratio`
1171/// resolves it against the type's [`RatioUnits`] table (covering built-in
1172/// `percent`/`permille` and any user-defined units like `basis_points`).
1173#[derive(Debug, Clone, PartialEq, Eq)]
1174pub(crate) enum RatioLiteral {
1175    Bare(Decimal),
1176    Percent(Decimal),
1177    Permille(Decimal),
1178    Named { value: Decimal, unit: String },
1179}
1180
1181impl std::str::FromStr for RatioLiteral {
1182    type Err = String;
1183
1184    fn from_str(s: &str) -> Result<Self, Self::Err> {
1185        let trimmed = s.trim();
1186        if trimmed.is_empty() {
1187            return Err(
1188                "Ratio value cannot be empty. Use a number, optionally followed by '%', '%%', or a unit name (e.g. '0.5', '50%', '25%%', '50 percent')."
1189                    .to_string(),
1190            );
1191        }
1192
1193        let mut parts = trimmed.split_whitespace();
1194        let first = parts
1195            .next()
1196            .expect("split_whitespace yields >=1 token after non-empty guard");
1197        let second = parts.next();
1198        if parts.next().is_some() {
1199            return Err(format!(
1200                "Invalid ratio value: '{}'. Expected '<number>', '<number>%', '<number>%%', or '<number> <unit>'.",
1201                s
1202            ));
1203        }
1204
1205        match second {
1206            // 1-token forms: bare number, or sigil-suffixed number.
1207            None => {
1208                if let Some(rest) = first.strip_suffix("%%") {
1209                    if rest.is_empty() {
1210                        return Err(format!(
1211                            "Invalid ratio value: '{}'. '%%' must follow a number (e.g. '25%%').",
1212                            s
1213                        ));
1214                    }
1215                    let n = rest
1216                        .parse::<NumberLiteral>()
1217                        .map_err(|_| {
1218                            format!(
1219                            "Invalid ratio value: '{}'. '{}' is not a valid number before '%%'.",
1220                            s, rest
1221                        )
1222                        })?
1223                        .0;
1224                    return Ok(RatioLiteral::Permille(n));
1225                }
1226                if let Some(rest) = first.strip_suffix('%') {
1227                    if rest.is_empty() {
1228                        return Err(format!(
1229                            "Invalid ratio value: '{}'. '%' must follow a number (e.g. '50%').",
1230                            s
1231                        ));
1232                    }
1233                    let n = rest
1234                        .parse::<NumberLiteral>()
1235                        .map_err(|_| {
1236                            format!(
1237                                "Invalid ratio value: '{}'. '{}' is not a valid number before '%'.",
1238                                s, rest
1239                            )
1240                        })?
1241                        .0;
1242                    return Ok(RatioLiteral::Percent(n));
1243                }
1244                let n = first.parse::<NumberLiteral>().map_err(|_| {
1245                    format!(
1246                        "Invalid ratio value: '{}'. Must be a number, '<n>%', '<n>%%', '<n> percent', '<n> permille', or '<n> <unit>'.",
1247                        s
1248                    )
1249                })?.0;
1250                Ok(RatioLiteral::Bare(n))
1251            }
1252            // 2-token form: <number> <unit-name>. Sigils are not accepted as unit-position tokens.
1253            Some(unit) => {
1254                if unit == "%" || unit == "%%" {
1255                    return Err(format!(
1256                        "Invalid ratio value: '{}'. '{}' must be glued to the number (e.g. '{}{}'), not separated by whitespace.",
1257                        s, unit, first, unit
1258                    ));
1259                }
1260                let n = first
1261                    .parse::<NumberLiteral>()
1262                    .map_err(|_| {
1263                        format!(
1264                            "Invalid ratio value: '{}'. '{}' is not a valid number.",
1265                            s, first
1266                        )
1267                    })?
1268                    .0;
1269                Ok(RatioLiteral::Named {
1270                    value: n,
1271                    unit: unit.to_string(),
1272                })
1273            }
1274        }
1275    }
1276}
1277
1278#[cfg(test)]
1279mod tests {
1280    use super::BooleanValue;
1281    use std::str::FromStr;
1282
1283    #[test]
1284    fn boolean_value_rejects_accept_and_reject_strings() {
1285        for invalid in ["accept", "reject"] {
1286            assert!(
1287                BooleanValue::from_str(invalid).is_err(),
1288                "'{invalid}' must not parse as boolean"
1289            );
1290        }
1291    }
1292}