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, Serialize};
7use std::fmt;
8
9// -----------------------------------------------------------------------------
10// Literal value types
11// -----------------------------------------------------------------------------
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum BooleanValue {
16    True,
17    False,
18    Yes,
19    No,
20    Accept,
21    Reject,
22}
23
24impl From<BooleanValue> for bool {
25    fn from(value: BooleanValue) -> bool {
26        matches!(
27            value,
28            BooleanValue::True | BooleanValue::Yes | BooleanValue::Accept
29        )
30    }
31}
32
33impl From<&BooleanValue> for bool {
34    fn from(value: &BooleanValue) -> bool {
35        (*value).into() // Copy makes this ok
36    }
37}
38
39impl From<bool> for BooleanValue {
40    fn from(value: bool) -> BooleanValue {
41        if value {
42            BooleanValue::True
43        } else {
44            BooleanValue::False
45        }
46    }
47}
48
49impl std::ops::Not for BooleanValue {
50    type Output = BooleanValue;
51
52    fn not(self) -> Self::Output {
53        if self.into() {
54            BooleanValue::False
55        } else {
56            BooleanValue::True
57        }
58    }
59}
60
61impl std::ops::Not for &BooleanValue {
62    type Output = BooleanValue;
63
64    fn not(self) -> Self::Output {
65        if (*self).into() {
66            BooleanValue::False
67        } else {
68            BooleanValue::True
69        }
70    }
71}
72
73impl std::str::FromStr for BooleanValue {
74    type Err = String;
75
76    fn from_str(s: &str) -> Result<Self, Self::Err> {
77        match s.trim().to_lowercase().as_str() {
78            "true" => Ok(BooleanValue::True),
79            "false" => Ok(BooleanValue::False),
80            "yes" => Ok(BooleanValue::Yes),
81            "no" => Ok(BooleanValue::No),
82            "accept" => Ok(BooleanValue::Accept),
83            "reject" => Ok(BooleanValue::Reject),
84            _ => Err(format!("Invalid boolean: '{}'", s)),
85        }
86    }
87}
88
89impl BooleanValue {
90    #[must_use]
91    pub fn as_str(&self) -> &'static str {
92        match self {
93            BooleanValue::True => "true",
94            BooleanValue::False => "false",
95            BooleanValue::Yes => "yes",
96            BooleanValue::No => "no",
97            BooleanValue::Accept => "accept",
98            BooleanValue::Reject => "reject",
99        }
100    }
101}
102
103impl fmt::Display for BooleanValue {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        write!(f, "{}", self.as_str())
106    }
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Hash)]
110pub enum DurationUnit {
111    Year,
112    Month,
113    Week,
114    Day,
115    Hour,
116    Minute,
117    Second,
118    Millisecond,
119    Microsecond,
120}
121
122impl Serialize for DurationUnit {
123    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
124    where
125        S: serde::Serializer,
126    {
127        serializer.serialize_str(&self.to_string())
128    }
129}
130
131impl<'de> Deserialize<'de> for DurationUnit {
132    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
133    where
134        D: serde::Deserializer<'de>,
135    {
136        let s = String::deserialize(deserializer)?;
137        s.parse().map_err(serde::de::Error::custom)
138    }
139}
140
141impl fmt::Display for DurationUnit {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        let s = match self {
144            DurationUnit::Year => "years",
145            DurationUnit::Month => "months",
146            DurationUnit::Week => "weeks",
147            DurationUnit::Day => "days",
148            DurationUnit::Hour => "hours",
149            DurationUnit::Minute => "minutes",
150            DurationUnit::Second => "seconds",
151            DurationUnit::Millisecond => "milliseconds",
152            DurationUnit::Microsecond => "microseconds",
153        };
154        write!(f, "{}", s)
155    }
156}
157
158impl std::str::FromStr for DurationUnit {
159    type Err = String;
160
161    fn from_str(s: &str) -> Result<Self, Self::Err> {
162        match s.trim().to_lowercase().as_str() {
163            "year" | "years" => Ok(DurationUnit::Year),
164            "month" | "months" => Ok(DurationUnit::Month),
165            "week" | "weeks" => Ok(DurationUnit::Week),
166            "day" | "days" => Ok(DurationUnit::Day),
167            "hour" | "hours" => Ok(DurationUnit::Hour),
168            "minute" | "minutes" => Ok(DurationUnit::Minute),
169            "second" | "seconds" => Ok(DurationUnit::Second),
170            "millisecond" | "milliseconds" => Ok(DurationUnit::Millisecond),
171            "microsecond" | "microseconds" => Ok(DurationUnit::Microsecond),
172            _ => Err(format!("Unknown duration unit: '{}'", s)),
173        }
174    }
175}
176
177#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
178pub struct TimezoneValue {
179    pub offset_hours: i8,
180    pub offset_minutes: u8,
181}
182
183impl fmt::Display for TimezoneValue {
184    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        if self.offset_hours == 0 && self.offset_minutes == 0 {
186            write!(f, "Z")
187        } else {
188            let sign = if self.offset_hours >= 0 { "+" } else { "-" };
189            let hours = self.offset_hours.abs();
190            write!(f, "{}{:02}:{:02}", sign, hours, self.offset_minutes)
191        }
192    }
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
196pub struct TimeValue {
197    pub hour: u8,
198    pub minute: u8,
199    pub second: u8,
200    pub timezone: Option<TimezoneValue>,
201}
202
203impl fmt::Display for TimeValue {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)
206    }
207}
208
209#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
210pub struct DateTimeValue {
211    pub year: i32,
212    pub month: u32,
213    pub day: u32,
214    pub hour: u32,
215    pub minute: u32,
216    pub second: u32,
217    #[serde(default)]
218    pub microsecond: u32,
219    pub timezone: Option<TimezoneValue>,
220}
221
222impl DateTimeValue {
223    pub fn now() -> Self {
224        let now = chrono::Local::now();
225        let offset_secs = now.offset().local_minus_utc();
226        Self {
227            year: now.year(),
228            month: now.month(),
229            day: now.day(),
230            hour: now.time().hour(),
231            minute: now.time().minute(),
232            second: now.time().second(),
233            microsecond: now.time().nanosecond() / 1000 % 1_000_000,
234            timezone: Some(TimezoneValue {
235                offset_hours: (offset_secs / 3600) as i8,
236                offset_minutes: ((offset_secs.abs() % 3600) / 60) as u8,
237            }),
238        }
239    }
240
241    fn parse_iso_week(s: &str) -> Option<Self> {
242        let parts: Vec<&str> = s.split("-W").collect();
243        if parts.len() != 2 {
244            return None;
245        }
246        let year: i32 = parts[0].parse().ok()?;
247        let week: u32 = parts[1].parse().ok()?;
248        if week == 0 || week > 53 {
249            return None;
250        }
251        let date = chrono::NaiveDate::from_isoywd_opt(year, week, chrono::Weekday::Mon)?;
252        Some(Self {
253            year: date.year(),
254            month: date.month(),
255            day: date.day(),
256            hour: 0,
257            minute: 0,
258            second: 0,
259            microsecond: 0,
260            timezone: None,
261        })
262    }
263}
264
265impl fmt::Display for DateTimeValue {
266    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267        let has_time = self.hour != 0
268            || self.minute != 0
269            || self.second != 0
270            || self.microsecond != 0
271            || self.timezone.is_some();
272        if !has_time {
273            write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
274        } else {
275            write!(
276                f,
277                "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
278                self.year, self.month, self.day, self.hour, self.minute, self.second
279            )?;
280            if self.microsecond != 0 {
281                write!(f, ".{:06}", self.microsecond)?;
282            }
283            if let Some(tz) = &self.timezone {
284                write!(f, "{}", tz)?;
285            }
286            Ok(())
287        }
288    }
289}
290
291/// Literal value data (no type information). Single source of truth in literals.
292#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
293#[serde(rename_all = "snake_case")]
294pub enum Value {
295    Number(Decimal),
296    Scale(Decimal, String),
297    Text(String),
298    Date(DateTimeValue),
299    Time(TimeValue),
300    Boolean(BooleanValue),
301    Duration(Decimal, DurationUnit),
302    Ratio(Decimal, Option<String>),
303}
304
305impl fmt::Display for Value {
306    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
307        match self {
308            Value::Number(n) => write!(f, "{}", n),
309            Value::Text(s) => write!(f, "{}", s),
310            Value::Date(dt) => write!(f, "{}", dt),
311            Value::Boolean(b) => write!(f, "{}", b),
312            Value::Time(time) => write!(f, "{}", time),
313            Value::Scale(n, u) => write!(f, "{} {}", n, u),
314            Value::Duration(n, u) => write!(f, "{} {}", n, u),
315            Value::Ratio(n, u) => match u.as_deref() {
316                Some("percent") => {
317                    let display_value = *n * Decimal::from(100);
318                    let norm = display_value.normalize();
319                    let s = if norm.fract().is_zero() {
320                        norm.trunc().to_string()
321                    } else {
322                        norm.to_string()
323                    };
324                    write!(f, "{}%", s)
325                }
326                Some("permille") => {
327                    let display_value = *n * Decimal::from(1000);
328                    let norm = display_value.normalize();
329                    let s = if norm.fract().is_zero() {
330                        norm.trunc().to_string()
331                    } else {
332                        norm.to_string()
333                    };
334                    write!(f, "{}%%", s)
335                }
336                Some(unit) => {
337                    let norm = n.normalize();
338                    let s = if norm.fract().is_zero() {
339                        norm.trunc().to_string()
340                    } else {
341                        norm.to_string()
342                    };
343                    write!(f, "{} {}", s, unit)
344                }
345                None => {
346                    let norm = n.normalize();
347                    let s = if norm.fract().is_zero() {
348                        norm.trunc().to_string()
349                    } else {
350                        norm.to_string()
351                    };
352                    write!(f, "{}", s)
353                }
354            },
355        }
356    }
357}
358
359// -----------------------------------------------------------------------------
360// FromStr (single source of truth per type)
361// -----------------------------------------------------------------------------
362
363impl std::str::FromStr for DateTimeValue {
364    type Err = String;
365
366    fn from_str(s: &str) -> Result<Self, Self::Err> {
367        if let Ok(dt) = s.parse::<chrono::DateTime<chrono::FixedOffset>>() {
368            let offset = dt.offset().local_minus_utc();
369            let microsecond = dt.nanosecond() / 1000 % 1_000_000;
370            return Ok(DateTimeValue {
371                year: dt.year(),
372                month: dt.month(),
373                day: dt.day(),
374                hour: dt.hour(),
375                minute: dt.minute(),
376                second: dt.second(),
377                microsecond,
378                timezone: Some(TimezoneValue {
379                    offset_hours: (offset / 3600) as i8,
380                    offset_minutes: ((offset.abs() % 3600) / 60) as u8,
381                }),
382            });
383        }
384        if let Ok(dt) = s.parse::<chrono::NaiveDateTime>() {
385            let microsecond = dt.nanosecond() / 1000 % 1_000_000;
386            return Ok(DateTimeValue {
387                year: dt.year(),
388                month: dt.month(),
389                day: dt.day(),
390                hour: dt.hour(),
391                minute: dt.minute(),
392                second: dt.second(),
393                microsecond,
394                timezone: None,
395            });
396        }
397        if let Ok(d) = s.parse::<chrono::NaiveDate>() {
398            return Ok(DateTimeValue {
399                year: d.year(),
400                month: d.month(),
401                day: d.day(),
402                hour: 0,
403                minute: 0,
404                second: 0,
405                microsecond: 0,
406                timezone: None,
407            });
408        }
409        if let Some(week_val) = Self::parse_iso_week(s) {
410            return Ok(week_val);
411        }
412        if let Ok(ym) = chrono::NaiveDate::parse_from_str(&format!("{}-01", s), "%Y-%m-%d") {
413            return Ok(Self {
414                year: ym.year(),
415                month: ym.month(),
416                day: 1,
417                hour: 0,
418                minute: 0,
419                second: 0,
420                microsecond: 0,
421                timezone: None,
422            });
423        }
424        if let Ok(year) = s.parse::<i32>() {
425            if (1..=9999).contains(&year) {
426                return Ok(Self {
427                    year,
428                    month: 1,
429                    day: 1,
430                    hour: 0,
431                    minute: 0,
432                    second: 0,
433                    microsecond: 0,
434                    timezone: None,
435                });
436            }
437        }
438        Err(format!("Invalid date format: '{}'", s))
439    }
440}
441
442impl std::str::FromStr for TimeValue {
443    type Err = String;
444
445    fn from_str(s: &str) -> Result<Self, Self::Err> {
446        if let Ok(t) = s.parse::<chrono::DateTime<chrono::FixedOffset>>() {
447            let offset = t.offset().local_minus_utc();
448            return Ok(TimeValue {
449                hour: t.hour() as u8,
450                minute: t.minute() as u8,
451                second: t.second() as u8,
452                timezone: Some(TimezoneValue {
453                    offset_hours: (offset / 3600) as i8,
454                    offset_minutes: ((offset.abs() % 3600) / 60) as u8,
455                }),
456            });
457        }
458        if let Ok(t) = s.parse::<chrono::NaiveTime>() {
459            return Ok(TimeValue {
460                hour: t.hour() as u8,
461                minute: t.minute() as u8,
462                second: t.second() as u8,
463                timezone: None,
464            });
465        }
466        Err(format!("Invalid time format: '{}'", s))
467    }
468}
469
470/// Number literal with Lemma rules (strip _ and ,; MAX_NUMBER_DIGITS).
471pub(crate) struct NumberLiteral(pub Decimal);
472
473impl std::str::FromStr for NumberLiteral {
474    type Err = String;
475
476    fn from_str(s: &str) -> Result<Self, Self::Err> {
477        let clean = s.trim().replace(['_', ','], "");
478        let digit_count = clean.chars().filter(|c| c.is_ascii_digit()).count();
479        if digit_count > crate::limits::MAX_NUMBER_DIGITS {
480            return Err(format!(
481                "Number has too many digits (max {})",
482                crate::limits::MAX_NUMBER_DIGITS
483            ));
484        }
485        Decimal::from_str(&clean)
486            .map_err(|_| format!("Invalid number: '{}'", s))
487            .map(NumberLiteral)
488    }
489}
490
491/// Text literal with length limit.
492pub(crate) struct TextLiteral(pub String);
493
494impl std::str::FromStr for TextLiteral {
495    type Err = String;
496
497    fn from_str(s: &str) -> Result<Self, Self::Err> {
498        if s.len() > crate::limits::MAX_TEXT_VALUE_LENGTH {
499            return Err(format!(
500                "Text value exceeds maximum length (max {} characters)",
501                crate::limits::MAX_TEXT_VALUE_LENGTH
502            ));
503        }
504        Ok(TextLiteral(s.to_string()))
505    }
506}
507
508/// Duration magnitude: number + unit (e.g. "10 hours").
509pub(crate) struct DurationLiteral(pub Decimal, pub DurationUnit);
510
511impl std::str::FromStr for DurationLiteral {
512    type Err = String;
513
514    fn from_str(s: &str) -> Result<Self, Self::Err> {
515        let trimmed = s.trim();
516        let mut parts: Vec<&str> = trimmed.split_whitespace().collect();
517        if parts.len() < 2 {
518            return Err(format!(
519                "Invalid duration: '{}'. Expected format: <number> <unit> (e.g. 10 hours, 2 weeks)",
520                s
521            ));
522        }
523        let unit_str = parts.pop().unwrap();
524        let number_str = parts.join(" ");
525        let n = number_str
526            .parse::<NumberLiteral>()
527            .map_err(|_| format!("Invalid duration number: '{}'", number_str))?
528            .0;
529        let unit = unit_str.parse()?;
530        Ok(DurationLiteral(n, unit))
531    }
532}
533
534/// Number with unit name (e.g. "1 eur", "50 percent"). Unit not validated against a type.
535pub(crate) struct NumberWithUnit(pub Decimal, pub String);
536
537impl std::str::FromStr for NumberWithUnit {
538    type Err = String;
539
540    fn from_str(s: &str) -> Result<Self, Self::Err> {
541        let trimmed = s.trim();
542        let mut parts = trimmed.split_whitespace();
543        let number_part = parts.next().ok_or_else(|| {
544            if trimmed.is_empty() {
545                "Scale value cannot be empty. Use a number followed by a unit (e.g. '10 eur')."
546                    .to_string()
547            } else {
548                format!(
549                    "Invalid scale value: '{}'. Scale value must be a number followed by a unit (e.g. '10 eur').",
550                    s
551                )
552            }
553        })?;
554        let unit_part = parts.next().ok_or_else(|| {
555            format!(
556                "Scale value must include a unit (e.g. '{} eur').",
557                number_part
558            )
559        })?;
560        let n = number_part
561            .parse::<NumberLiteral>()
562            .map_err(|_| format!("Invalid scale: '{}'", s))?
563            .0;
564        Ok(NumberWithUnit(n, unit_part.to_string()))
565    }
566}