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