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