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::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 Measure type.
75///
76/// `factor` is the conversion factor: 1 of this unit equals `factor` canonical units.
77/// `derived_measure_factors` stores `(measure_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 MeasureUnit {
83    pub name: String,
84    /// Conversion factor: 1 of this unit equals `value` canonical units.
85    pub factor: RationalInteger,
86    pub derived_measure_factors: Vec<(String, i32)>,
87    pub decomposition: BaseMeasureVector,
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 MeasureUnit {
97    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
98        use measure_unit_factor_serialization::FactorSerializer;
99        use serde::ser::SerializeStruct;
100        let mut state = serializer.serialize_struct("MeasureUnit", 7)?;
101        state.serialize_field("name", &self.name)?;
102        state.serialize_field("factor", &FactorSerializer::from_ratio(&self.factor))?;
103        state.serialize_field("derived_measure_factors", &self.derived_measure_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 measure 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 measure 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 measure unit default must serialize to decimal string"),
124            )?;
125        }
126        state.end()
127    }
128}
129
130impl<'de> Deserialize<'de> for MeasureUnit {
131    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
132        #[derive(Deserialize)]
133        struct MeasureUnitData {
134            name: String,
135            #[serde(with = "measure_unit_factor_serialization")]
136            factor: RationalInteger,
137            #[serde(default)]
138            derived_measure_factors: Vec<(String, i32)>,
139            #[serde(default)]
140            decomposition: BaseMeasureVector,
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 = MeasureUnitData::deserialize(deserializer)?;
149        Ok(Self {
150            name: data.name,
151            factor: data.factor,
152            derived_measure_factors: data.derived_measure_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 MeasureUnit {
174    pub fn from_decimal_factor(
175        name: String,
176        decimal_factor: Decimal,
177        derived_measure_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(MeasureUnit {
182            name,
183            factor,
184            derived_measure_factors,
185            decomposition: BaseMeasureVector::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: measure 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 measure 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 measure 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 measure 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 measure unit maximum canonical multiply must succeed");
244            rational::commit_rational_to_decimal(&canonical)
245                .expect("BUG: planned measure unit maximum canonical must commit to decimal")
246        })
247    }
248}
249
250mod measure_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 measure 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("MeasureUnit 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 MeasureUnits(pub Vec<MeasureUnit>);
298
299impl MeasureUnits {
300    pub fn new() -> Self {
301        MeasureUnits(Vec::new())
302    }
303    pub fn get(&self, name: &str) -> Result<&MeasureUnit, 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 measure type. Valid units: {}",
308                name,
309                valid.join(", ")
310            )
311        })
312    }
313
314    pub fn iter(&self) -> std::slice::Iter<'_, MeasureUnit> {
315        self.0.iter()
316    }
317    pub fn push(&mut self, u: MeasureUnit) {
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(MeasureUnit) -> MeasureUnit>(self, f: F) -> Self {
327        MeasureUnits(self.0.into_iter().map(f).collect())
328    }
329}
330
331impl MeasureUnit {
332    pub fn with_decomposition(self, decomposition: BaseMeasureVector) -> 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_measure_factors(self, derived_measure_factors: Vec<(String, i32)>) -> Self {
342        Self {
343            derived_measure_factors,
344            ..self
345        }
346    }
347}
348
349impl Default for MeasureUnits {
350    fn default() -> Self {
351        MeasureUnits::new()
352    }
353}
354
355impl From<Vec<MeasureUnit>> for MeasureUnits {
356    fn from(v: Vec<MeasureUnit>) -> Self {
357        MeasureUnits(v)
358    }
359}
360
361impl<'a> IntoIterator for &'a MeasureUnits {
362    type Item = &'a MeasureUnit;
363    type IntoIter = std::slice::Iter<'a, MeasureUnit>;
364    fn into_iter(self) -> Self::IntoIter {
365        self.0.iter()
366    }
367}
368
369#[derive(Clone, Debug, PartialEq, Eq, Hash)]
370pub struct RatioUnit {
371    pub name: String,
372    pub value: RationalInteger,
373    pub minimum: Option<RationalInteger>,
374    pub maximum: Option<RationalInteger>,
375    pub default_magnitude: Option<RationalInteger>,
376}
377
378impl Serialize for RatioUnit {
379    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
380        use measure_unit_factor_serialization::FactorSerializer;
381        use serde::ser::SerializeStruct;
382        let mut state = serializer.serialize_struct("RatioUnit", 5)?;
383        state.serialize_field("name", &self.name)?;
384        state.serialize_field("value", &FactorSerializer::from_ratio(&self.value))?;
385        if let Some(minimum) = &self.minimum {
386            state.serialize_field(
387                "minimum",
388                &rational_to_serialized_str(minimum)
389                    .expect("BUG: planned ratio unit minimum must serialize to decimal string"),
390            )?;
391        }
392        if let Some(maximum) = &self.maximum {
393            state.serialize_field(
394                "maximum",
395                &rational_to_serialized_str(maximum)
396                    .expect("BUG: planned ratio unit maximum must serialize to decimal string"),
397            )?;
398        }
399        if let Some(default_magnitude) = &self.default_magnitude {
400            state.serialize_field(
401                "default",
402                &rational_to_serialized_str(default_magnitude)
403                    .expect("BUG: planned ratio unit default must serialize to decimal string"),
404            )?;
405        }
406        state.end()
407    }
408}
409
410impl<'de> Deserialize<'de> for RatioUnit {
411    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
412        #[derive(Deserialize)]
413        struct RatioUnitData {
414            name: String,
415            #[serde(with = "measure_unit_factor_serialization")]
416            value: RationalInteger,
417            #[serde(default)]
418            minimum: Option<Decimal>,
419            #[serde(default)]
420            maximum: Option<Decimal>,
421            #[serde(default, rename = "default")]
422            default_magnitude: Option<Decimal>,
423        }
424        let data = RatioUnitData::deserialize(deserializer)?;
425        Ok(Self {
426            name: data.name,
427            value: data.value,
428            minimum: data
429                .minimum
430                .map(rational_from_parsed_decimal)
431                .transpose()
432                .map_err(serde::de::Error::custom)?,
433            maximum: data
434                .maximum
435                .map(rational_from_parsed_decimal)
436                .transpose()
437                .map_err(serde::de::Error::custom)?,
438            default_magnitude: data
439                .default_magnitude
440                .map(rational_from_parsed_decimal)
441                .transpose()
442                .map_err(serde::de::Error::custom)?,
443        })
444    }
445}
446
447impl RatioUnit {
448    pub fn clear_constraint_magnitudes(&mut self) {
449        self.minimum = None;
450        self.maximum = None;
451        self.default_magnitude = None;
452    }
453
454    /// Unit scale as decimal (schema ratio unit values always commit).
455    pub fn value_decimal(&self) -> Decimal {
456        rational::commit_rational_to_decimal(&self.value)
457            .expect("BUG: ratio unit value must commit to decimal")
458    }
459
460    #[must_use]
461    pub fn minimum_decimal(&self) -> Option<Decimal> {
462        self.minimum.as_ref().map(|bound| {
463            rational::commit_rational_to_decimal(bound)
464                .expect("BUG: planned ratio unit minimum must commit to decimal")
465        })
466    }
467
468    #[must_use]
469    pub fn maximum_decimal(&self) -> Option<Decimal> {
470        self.maximum.as_ref().map(|bound| {
471            rational::commit_rational_to_decimal(bound)
472                .expect("BUG: planned ratio unit maximum must commit to decimal")
473        })
474    }
475
476    #[must_use]
477    pub fn default_magnitude_decimal(&self) -> Option<Decimal> {
478        self.default_magnitude.as_ref().map(|bound| {
479            rational::commit_rational_to_decimal(bound)
480                .expect("BUG: planned ratio unit default must commit to decimal")
481        })
482    }
483
484    /// Maximum bound lifted to canonical ratio space via `maximum * value`.
485    #[must_use]
486    pub fn maximum_canonical_decimal(&self) -> Option<Decimal> {
487        self.maximum.as_ref().map(|maximum| {
488            let canonical = rational::checked_mul(maximum, &self.value)
489                .expect("BUG: planned ratio unit maximum canonical multiply must succeed");
490            rational::commit_rational_to_decimal(&canonical)
491                .expect("BUG: planned ratio unit maximum canonical must commit to decimal")
492        })
493    }
494}
495
496#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
497#[serde(transparent)]
498pub struct RatioUnits(pub Vec<RatioUnit>);
499
500impl RatioUnits {
501    pub fn new() -> Self {
502        RatioUnits(Vec::new())
503    }
504    pub fn get(&self, name: &str) -> Result<&RatioUnit, String> {
505        self.0.iter().find(|u| u.name == name).ok_or_else(|| {
506            let valid: Vec<&str> = self.0.iter().map(|u| u.name.as_str()).collect();
507            format!(
508                "Unknown unit '{}' for this ratio type. Valid units: {}",
509                name,
510                valid.join(", ")
511            )
512        })
513    }
514
515    pub fn iter(&self) -> std::slice::Iter<'_, RatioUnit> {
516        self.0.iter()
517    }
518    pub fn push(&mut self, u: RatioUnit) {
519        self.0.push(u);
520    }
521    pub fn is_empty(&self) -> bool {
522        self.0.is_empty()
523    }
524    pub fn len(&self) -> usize {
525        self.0.len()
526    }
527}
528
529impl Default for RatioUnits {
530    fn default() -> Self {
531        RatioUnits::new()
532    }
533}
534
535impl From<Vec<RatioUnit>> for RatioUnits {
536    fn from(v: Vec<RatioUnit>) -> Self {
537        RatioUnits(v)
538    }
539}
540
541impl<'a> IntoIterator for &'a RatioUnits {
542    type Item = &'a RatioUnit;
543    type IntoIter = std::slice::Iter<'a, RatioUnit>;
544    fn into_iter(self) -> Self::IntoIter {
545        self.0.iter()
546    }
547}
548
549// -----------------------------------------------------------------------------
550// Literal value types
551// -----------------------------------------------------------------------------
552
553#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
554#[serde(rename_all = "lowercase")]
555pub enum BooleanValue {
556    True,
557    False,
558    Yes,
559    No,
560    Accept,
561    Reject,
562}
563
564impl From<BooleanValue> for bool {
565    fn from(value: BooleanValue) -> bool {
566        matches!(
567            value,
568            BooleanValue::True | BooleanValue::Yes | BooleanValue::Accept
569        )
570    }
571}
572
573impl From<&BooleanValue> for bool {
574    fn from(value: &BooleanValue) -> bool {
575        (*value).into() // Copy makes this ok
576    }
577}
578
579impl From<bool> for BooleanValue {
580    fn from(value: bool) -> BooleanValue {
581        if value {
582            BooleanValue::True
583        } else {
584            BooleanValue::False
585        }
586    }
587}
588
589impl std::ops::Not for BooleanValue {
590    type Output = BooleanValue;
591
592    fn not(self) -> Self::Output {
593        if self.into() {
594            BooleanValue::False
595        } else {
596            BooleanValue::True
597        }
598    }
599}
600
601impl std::ops::Not for &BooleanValue {
602    type Output = BooleanValue;
603
604    fn not(self) -> Self::Output {
605        if (*self).into() {
606            BooleanValue::False
607        } else {
608            BooleanValue::True
609        }
610    }
611}
612
613impl std::str::FromStr for BooleanValue {
614    type Err = String;
615
616    fn from_str(s: &str) -> Result<Self, Self::Err> {
617        match s.trim().to_lowercase().as_str() {
618            "true" => Ok(BooleanValue::True),
619            "false" => Ok(BooleanValue::False),
620            "yes" => Ok(BooleanValue::Yes),
621            "no" => Ok(BooleanValue::No),
622            "accept" => Ok(BooleanValue::Accept),
623            "reject" => Ok(BooleanValue::Reject),
624            _ => Err(format!("Invalid boolean: '{}'", s)),
625        }
626    }
627}
628
629impl BooleanValue {
630    #[must_use]
631    pub fn as_str(&self) -> &'static str {
632        match self {
633            BooleanValue::True => "true",
634            BooleanValue::False => "false",
635            BooleanValue::Yes => "yes",
636            BooleanValue::No => "no",
637            BooleanValue::Accept => "accept",
638            BooleanValue::Reject => "reject",
639        }
640    }
641}
642
643impl fmt::Display for BooleanValue {
644    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
645        write!(f, "{}", self.as_str())
646    }
647}
648
649#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
650pub struct TimezoneValue {
651    pub offset_hours: i8,
652    pub offset_minutes: u8,
653}
654
655impl fmt::Display for TimezoneValue {
656    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
657        if self.offset_hours == 0 && self.offset_minutes == 0 {
658            write!(f, "Z")
659        } else {
660            let sign = if self.offset_hours >= 0 { "+" } else { "-" };
661            let hours = self.offset_hours.abs();
662            write!(f, "{}{:02}:{:02}", sign, hours, self.offset_minutes)
663        }
664    }
665}
666
667#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
668pub struct TimeValue {
669    pub hour: u8,
670    pub minute: u8,
671    pub second: u8,
672    #[serde(default)]
673    pub microsecond: u32,
674    pub timezone: Option<TimezoneValue>,
675}
676
677impl fmt::Display for TimeValue {
678    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
679        write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
680        if self.microsecond != 0 {
681            write!(f, ".{:06}", self.microsecond)?;
682        }
683        if let Some(timezone) = &self.timezone {
684            write!(f, "{}", timezone)?;
685        }
686        Ok(())
687    }
688}
689
690#[derive(
691    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize,
692)]
693#[serde(rename_all = "snake_case")]
694pub enum DateGranularity {
695    Year,
696    YearMonth,
697    /// ISO 8601 week date. Stores original (iso_year, week) because the ISO
698    /// week year can differ from the calendar year — e.g. "2026-W01" has
699    /// iso_year=2026 but the stored calendar date year=2025.
700    IsoWeek {
701        iso_year: i32,
702        week: u32,
703    },
704    #[default]
705    Full,
706    DateTime,
707}
708
709#[derive(Debug, Clone, Serialize, Deserialize)]
710pub struct DateTimeValue {
711    pub year: i32,
712    pub month: u32,
713    pub day: u32,
714    pub hour: u32,
715    pub minute: u32,
716    pub second: u32,
717    #[serde(default)]
718    pub microsecond: u32,
719    pub timezone: Option<TimezoneValue>,
720    #[serde(default)]
721    pub granularity: DateGranularity,
722}
723
724impl PartialEq for DateTimeValue {
725    fn eq(&self, other: &Self) -> bool {
726        self.year == other.year
727            && self.month == other.month
728            && self.day == other.day
729            && self.hour == other.hour
730            && self.minute == other.minute
731            && self.second == other.second
732            && self.microsecond == other.microsecond
733            && self.timezone == other.timezone
734    }
735}
736
737impl Eq for DateTimeValue {}
738
739impl PartialOrd for DateTimeValue {
740    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
741        Some(self.cmp(other))
742    }
743}
744
745impl Ord for DateTimeValue {
746    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
747        self.year
748            .cmp(&other.year)
749            .then_with(|| self.month.cmp(&other.month))
750            .then_with(|| self.day.cmp(&other.day))
751            .then_with(|| self.hour.cmp(&other.hour))
752            .then_with(|| self.minute.cmp(&other.minute))
753            .then_with(|| self.second.cmp(&other.second))
754            .then_with(|| self.microsecond.cmp(&other.microsecond))
755            .then_with(|| self.timezone.cmp(&other.timezone))
756    }
757}
758
759impl std::hash::Hash for DateTimeValue {
760    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
761        self.year.hash(state);
762        self.month.hash(state);
763        self.day.hash(state);
764        self.hour.hash(state);
765        self.minute.hash(state);
766        self.second.hash(state);
767        self.microsecond.hash(state);
768        self.timezone.hash(state);
769    }
770}
771
772impl DateTimeValue {
773    pub fn now() -> Self {
774        let now = chrono::Local::now();
775        let offset_secs = now.offset().local_minus_utc();
776        Self {
777            year: now.year(),
778            month: now.month(),
779            day: now.day(),
780            hour: now.time().hour(),
781            minute: now.time().minute(),
782            second: now.time().second(),
783            microsecond: now.time().nanosecond() / 1000 % 1_000_000,
784            timezone: Some(TimezoneValue {
785                offset_hours: (offset_secs / 3600) as i8,
786                offset_minutes: ((offset_secs.abs() % 3600) / 60) as u8,
787            }),
788            granularity: DateGranularity::DateTime,
789        }
790    }
791
792    fn parse_iso_week(s: &str) -> Option<Self> {
793        let parts: Vec<&str> = s.split("-W").collect();
794        if parts.len() != 2 {
795            return None;
796        }
797        let iso_year: i32 = parts[0].parse().ok()?;
798        let week: u32 = parts[1].parse().ok()?;
799        if week == 0 || week > 53 {
800            return None;
801        }
802        let date = chrono::NaiveDate::from_isoywd_opt(iso_year, week, chrono::Weekday::Mon)?;
803        Some(Self {
804            year: date.year(),
805            month: date.month(),
806            day: date.day(),
807            hour: 0,
808            minute: 0,
809            second: 0,
810            microsecond: 0,
811            timezone: None,
812            granularity: DateGranularity::IsoWeek { iso_year, week },
813        })
814    }
815}
816
817impl fmt::Display for DateTimeValue {
818    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
819        match self.granularity {
820            DateGranularity::Year => write!(f, "{:04}", self.year),
821            DateGranularity::YearMonth => write!(f, "{:04}-{:02}", self.year, self.month),
822            DateGranularity::IsoWeek { iso_year, week } => {
823                write!(f, "{:04}-W{:02}", iso_year, week)
824            }
825            DateGranularity::Full => {
826                write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
827            }
828            DateGranularity::DateTime => {
829                write!(
830                    f,
831                    "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
832                    self.year, self.month, self.day, self.hour, self.minute, self.second
833                )?;
834                if self.microsecond != 0 {
835                    write!(f, ".{:06}", self.microsecond)?;
836                }
837                if let Some(tz) = &self.timezone {
838                    write!(f, "{}", tz)?;
839                }
840                Ok(())
841            }
842        }
843    }
844}
845
846/// Literal value data (no type information). Single source of truth in literals.
847///
848/// `NumberWithUnit` is type-agnostic at parse time (`10 eur` and `50%` share this shape).
849/// Planning resolves ratio vs measure via the unit index and target [`TypeSpecification`].
850#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
851#[serde(rename_all = "snake_case")]
852pub enum Value {
853    Number(Decimal),
854    NumberWithUnit(Decimal, String),
855    Text(String),
856    Date(DateTimeValue),
857    Time(TimeValue),
858    Boolean(BooleanValue),
859    Range(Box<Value>, Box<Value>),
860}
861
862impl fmt::Display for Value {
863    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
864        match self {
865            Value::Number(n) => write!(f, "{}", n),
866            Value::Text(s) => write!(f, "{}", s),
867            Value::Date(dt) => write!(f, "{}", dt),
868            Value::Boolean(b) => write!(f, "{}", b),
869            Value::Time(time) => write!(f, "{}", time),
870            Value::NumberWithUnit(n, u) => match u.as_str() {
871                "percent" => {
872                    let norm = n.normalize();
873                    let s = if norm.fract().is_zero() {
874                        norm.trunc().to_string()
875                    } else {
876                        norm.to_string()
877                    };
878                    write!(f, "{}%", s)
879                }
880                "permille" => {
881                    let norm = n.normalize();
882                    let s = if norm.fract().is_zero() {
883                        norm.trunc().to_string()
884                    } else {
885                        norm.to_string()
886                    };
887                    write!(f, "{}%%", s)
888                }
889                unit => {
890                    let norm = n.normalize();
891                    let s = if norm.fract().is_zero() {
892                        norm.trunc().to_string()
893                    } else {
894                        norm.to_string()
895                    };
896                    write!(f, "{} {}", s, unit)
897                }
898            },
899            Value::Range(left, right) => write!(f, "{}...{}", left, right),
900        }
901    }
902}
903
904// -----------------------------------------------------------------------------
905// FromStr (single source of truth per type)
906// -----------------------------------------------------------------------------
907
908impl std::str::FromStr for DateTimeValue {
909    type Err = String;
910
911    fn from_str(s: &str) -> Result<Self, Self::Err> {
912        if let Ok(dt) = s.parse::<chrono::DateTime<chrono::FixedOffset>>() {
913            let offset = dt.offset().local_minus_utc();
914            let microsecond = dt.nanosecond() / 1000 % 1_000_000;
915            return Ok(DateTimeValue {
916                year: dt.year(),
917                month: dt.month(),
918                day: dt.day(),
919                hour: dt.hour(),
920                minute: dt.minute(),
921                second: dt.second(),
922                microsecond,
923                timezone: Some(TimezoneValue {
924                    offset_hours: (offset / 3600) as i8,
925                    offset_minutes: ((offset.abs() % 3600) / 60) as u8,
926                }),
927                granularity: DateGranularity::DateTime,
928            });
929        }
930        if let Ok(dt) = s.parse::<chrono::NaiveDateTime>() {
931            let microsecond = dt.nanosecond() / 1000 % 1_000_000;
932            return Ok(DateTimeValue {
933                year: dt.year(),
934                month: dt.month(),
935                day: dt.day(),
936                hour: dt.hour(),
937                minute: dt.minute(),
938                second: dt.second(),
939                microsecond,
940                timezone: None,
941                granularity: DateGranularity::DateTime,
942            });
943        }
944        if let Ok(d) = s.parse::<chrono::NaiveDate>() {
945            return Ok(DateTimeValue {
946                year: d.year(),
947                month: d.month(),
948                day: d.day(),
949                hour: 0,
950                minute: 0,
951                second: 0,
952                microsecond: 0,
953                timezone: None,
954                granularity: DateGranularity::Full,
955            });
956        }
957        if let Some(week_val) = Self::parse_iso_week(s) {
958            return Ok(week_val);
959        }
960        if let Ok(ym) = chrono::NaiveDate::parse_from_str(&format!("{}-01", s), "%Y-%m-%d") {
961            return Ok(Self {
962                year: ym.year(),
963                month: ym.month(),
964                day: 1,
965                hour: 0,
966                minute: 0,
967                second: 0,
968                microsecond: 0,
969                timezone: None,
970                granularity: DateGranularity::YearMonth,
971            });
972        }
973        if let Ok(year) = s.parse::<i32>() {
974            if (1..=9999).contains(&year) {
975                return Ok(Self {
976                    year,
977                    month: 1,
978                    day: 1,
979                    hour: 0,
980                    minute: 0,
981                    second: 0,
982                    microsecond: 0,
983                    timezone: None,
984                    granularity: DateGranularity::Year,
985                });
986            }
987        }
988        Err(format!("Invalid date format: '{}'", s))
989    }
990}
991
992impl std::str::FromStr for TimeValue {
993    type Err = String;
994
995    fn from_str(s: &str) -> Result<Self, Self::Err> {
996        let trimmed = s.trim();
997
998        let (time_text, timezone) = if trimmed.ends_with('Z') || trimmed.ends_with('z') {
999            (
1000                &trimmed[..trimmed.len() - 1],
1001                Some(TimezoneValue {
1002                    offset_hours: 0,
1003                    offset_minutes: 0,
1004                }),
1005            )
1006        } else if trimmed.len() > 1 {
1007            if let Some(sign_index) = trimmed[1..].rfind(['+', '-']).map(|index| index + 1) {
1008                let timezone_text = &trimmed[sign_index..];
1009                if timezone_text.len() == 6
1010                    && (timezone_text.starts_with('+') || timezone_text.starts_with('-'))
1011                    && timezone_text.as_bytes()[3] == b':'
1012                {
1013                    let offset_hours: i8 = timezone_text[1..3]
1014                        .parse()
1015                        .map_err(|_| format!("Invalid time format: '{}'", s))?;
1016                    let offset_minutes: u8 = timezone_text[4..6]
1017                        .parse()
1018                        .map_err(|_| format!("Invalid time format: '{}'", s))?;
1019                    let signed_hours = if timezone_text.starts_with('-') {
1020                        -offset_hours
1021                    } else {
1022                        offset_hours
1023                    };
1024                    (
1025                        &trimmed[..sign_index],
1026                        Some(TimezoneValue {
1027                            offset_hours: signed_hours,
1028                            offset_minutes,
1029                        }),
1030                    )
1031                } else {
1032                    (trimmed, None)
1033                }
1034            } else {
1035                (trimmed, None)
1036            }
1037        } else {
1038            (trimmed, None)
1039        };
1040
1041        if let Ok(t) = chrono::NaiveTime::parse_from_str(time_text, "%H:%M:%S%.f") {
1042            return Ok(TimeValue {
1043                hour: t.hour() as u8,
1044                minute: t.minute() as u8,
1045                second: t.second() as u8,
1046                microsecond: t.nanosecond() / 1000 % 1_000_000,
1047                timezone,
1048            });
1049        }
1050        if let Ok(t) = chrono::NaiveTime::parse_from_str(time_text, "%H:%M:%S") {
1051            return Ok(TimeValue {
1052                hour: t.hour() as u8,
1053                minute: t.minute() as u8,
1054                second: t.second() as u8,
1055                microsecond: 0,
1056                timezone,
1057            });
1058        }
1059        if let Ok(t) = chrono::NaiveTime::parse_from_str(time_text, "%H:%M") {
1060            return Ok(TimeValue {
1061                hour: t.hour() as u8,
1062                minute: t.minute() as u8,
1063                second: 0,
1064                microsecond: 0,
1065                timezone,
1066            });
1067        }
1068        Err(format!("Invalid time format: '{}'", s))
1069    }
1070}
1071
1072/// Number literal with Lemma rules (strip _ and , separators).
1073///
1074/// `Decimal::from_str` rounds excess fractional digits to the 28 significant
1075/// digits a `Decimal` holds (truncate at input); an integer magnitude that
1076/// cannot be represented is an error.
1077pub(crate) struct NumberLiteral(pub Decimal);
1078
1079impl std::str::FromStr for NumberLiteral {
1080    type Err = String;
1081
1082    fn from_str(s: &str) -> Result<Self, Self::Err> {
1083        let clean = s.trim().replace(['_', ','], "");
1084        Decimal::from_str(&clean)
1085            .map_err(|_| format!("Invalid number: '{}'", s))
1086            .map(NumberLiteral)
1087    }
1088}
1089
1090/// Text literal with length limit.
1091pub(crate) struct TextLiteral(pub String);
1092
1093impl std::str::FromStr for TextLiteral {
1094    type Err = String;
1095
1096    fn from_str(s: &str) -> Result<Self, Self::Err> {
1097        if s.len() > crate::limits::MAX_TEXT_VALUE_LENGTH {
1098            return Err(format!(
1099                "Text value exceeds maximum length (max {} characters)",
1100                crate::limits::MAX_TEXT_VALUE_LENGTH
1101            ));
1102        }
1103        Ok(TextLiteral(s.to_string()))
1104    }
1105}
1106
1107/// Parsed `<number> <unit-name>` for runtime string input (measure and ratio types).
1108pub(crate) struct NumberWithUnit(pub Decimal, pub String);
1109
1110impl std::str::FromStr for NumberWithUnit {
1111    type Err = String;
1112
1113    fn from_str(s: &str) -> Result<Self, Self::Err> {
1114        let trimmed = s.trim();
1115        if trimmed.is_empty() {
1116            return Err(
1117                "Measure value cannot be empty. Use a number followed by a unit (e.g. '10 eur')."
1118                    .to_string(),
1119            );
1120        }
1121
1122        let mut parts = trimmed.split_whitespace();
1123        let number_part = parts
1124            .next()
1125            .expect("split_whitespace yields >=1 token after non-empty guard");
1126        let unit_part = parts.next().ok_or_else(|| {
1127            format!(
1128                "Measure value must include a unit (e.g. '{} eur').",
1129                number_part
1130            )
1131        })?;
1132        if parts.next().is_some() {
1133            return Err(format!(
1134                "Invalid measure value: '{}'. Expected exactly '<number> <unit>', got extra tokens.",
1135                s
1136            ));
1137        }
1138        let n = number_part
1139            .parse::<NumberLiteral>()
1140            .map_err(|_| format!("Invalid measure: '{}'", s))?
1141            .0;
1142        Ok(NumberWithUnit(n, unit_part.to_string()))
1143    }
1144}
1145
1146/// Strict ratio runtime literal.
1147///
1148/// Grammar (all inputs trimmed first):
1149/// - `<number>`                      → `Bare(n)`
1150/// - `<number>%`  (glued, no inner whitespace) → `Percent(n)` raw magnitude
1151/// - `<number>%%` (glued, no inner whitespace) → `Permille(n)` raw magnitude
1152/// - `<number> <unit-name>`          → `Named { value: n, unit: <unit-name> }`
1153///
1154/// `<number>` is parsed by [`NumberLiteral`] (signed, allows `_`/`,` separators).
1155/// Whitespace between the number and a keyword unit may be any non-empty run
1156/// (`"50 percent"`, `"50    percent"`, `"50\tpercent"` are all accepted).
1157///
1158/// The sigils `%` / `%%` are language-level constants meaning "divide by 100 / 1000"
1159/// and unconditionally produce the canonical unit names `"percent"` / `"permille"`.
1160/// They are NOT accepted as standalone unit-position tokens (i.e. `"5 %"` is rejected).
1161///
1162/// Signedness is intentionally not constrained at this layer: bounds are the
1163/// type-system's job (`-> minimum 0%`), and the evaluator can produce signed
1164/// ratios from non-negative inputs (e.g. `this_year - last_year` on `percent`).
1165/// The parser must accept everything the evaluator can emit (round-trip symmetry).
1166///
1167/// `Named` carries the raw unit name; the caller in `parse_number_unit::Ratio`
1168/// resolves it against the type's [`RatioUnits`] table (covering built-in
1169/// `percent`/`permille` and any user-defined units like `basis_points`).
1170#[derive(Debug, Clone, PartialEq, Eq)]
1171pub(crate) enum RatioLiteral {
1172    Bare(Decimal),
1173    Percent(Decimal),
1174    Permille(Decimal),
1175    Named { value: Decimal, unit: String },
1176}
1177
1178impl std::str::FromStr for RatioLiteral {
1179    type Err = String;
1180
1181    fn from_str(s: &str) -> Result<Self, Self::Err> {
1182        let trimmed = s.trim();
1183        if trimmed.is_empty() {
1184            return Err(
1185                "Ratio value cannot be empty. Use a number, optionally followed by '%', '%%', or a unit name (e.g. '0.5', '50%', '25%%', '50 percent')."
1186                    .to_string(),
1187            );
1188        }
1189
1190        let mut parts = trimmed.split_whitespace();
1191        let first = parts
1192            .next()
1193            .expect("split_whitespace yields >=1 token after non-empty guard");
1194        let second = parts.next();
1195        if parts.next().is_some() {
1196            return Err(format!(
1197                "Invalid ratio value: '{}'. Expected '<number>', '<number>%', '<number>%%', or '<number> <unit>'.",
1198                s
1199            ));
1200        }
1201
1202        match second {
1203            // 1-token forms: bare number, or sigil-suffixed number.
1204            None => {
1205                if let Some(rest) = first.strip_suffix("%%") {
1206                    if rest.is_empty() {
1207                        return Err(format!(
1208                            "Invalid ratio value: '{}'. '%%' must follow a number (e.g. '25%%').",
1209                            s
1210                        ));
1211                    }
1212                    let n = rest
1213                        .parse::<NumberLiteral>()
1214                        .map_err(|_| {
1215                            format!(
1216                            "Invalid ratio value: '{}'. '{}' is not a valid number before '%%'.",
1217                            s, rest
1218                        )
1219                        })?
1220                        .0;
1221                    return Ok(RatioLiteral::Permille(n));
1222                }
1223                if let Some(rest) = first.strip_suffix('%') {
1224                    if rest.is_empty() {
1225                        return Err(format!(
1226                            "Invalid ratio value: '{}'. '%' must follow a number (e.g. '50%').",
1227                            s
1228                        ));
1229                    }
1230                    let n = rest
1231                        .parse::<NumberLiteral>()
1232                        .map_err(|_| {
1233                            format!(
1234                                "Invalid ratio value: '{}'. '{}' is not a valid number before '%'.",
1235                                s, rest
1236                            )
1237                        })?
1238                        .0;
1239                    return Ok(RatioLiteral::Percent(n));
1240                }
1241                let n = first.parse::<NumberLiteral>().map_err(|_| {
1242                    format!(
1243                        "Invalid ratio value: '{}'. Must be a number, '<n>%', '<n>%%', '<n> percent', '<n> permille', or '<n> <unit>'.",
1244                        s
1245                    )
1246                })?.0;
1247                Ok(RatioLiteral::Bare(n))
1248            }
1249            // 2-token form: <number> <unit-name>. Sigils are not accepted as unit-position tokens.
1250            Some(unit) => {
1251                if unit == "%" || unit == "%%" {
1252                    return Err(format!(
1253                        "Invalid ratio value: '{}'. '{}' must be glued to the number (e.g. '{}{}'), not separated by whitespace.",
1254                        s, unit, first, unit
1255                    ));
1256                }
1257                let n = first
1258                    .parse::<NumberLiteral>()
1259                    .map_err(|_| {
1260                        format!(
1261                            "Invalid ratio value: '{}'. '{}' is not a valid number.",
1262                            s, first
1263                        )
1264                    })?
1265                    .0;
1266                Ok(RatioLiteral::Named {
1267                    value: n,
1268                    unit: unit.to_string(),
1269                })
1270            }
1271        }
1272    }
1273}