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