Skip to main content

lemma/
literals.rs

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