Skip to main content

lemma/parsing/
literals.rs

1use crate::error::Error;
2use crate::parsing::ast::*;
3use crate::Source;
4
5use chrono::{Datelike, Timelike};
6use rust_decimal::Decimal;
7use std::str::FromStr;
8
9/// Parse a duration string (e.g. "10 hours", "120 hours") into Value::Duration.
10/// Single implementation for both Lemma source and runtime fact values.
11pub(crate) fn parse_duration_from_string(value_str: &str, source: &Source) -> Result<Value, Error> {
12    let trimmed = value_str.trim();
13    let mut parts: Vec<&str> = trimmed.split_whitespace().collect();
14    if parts.len() < 2 {
15        return Err(Error::validation(
16            format!(
17                "Invalid duration: '{}'. Expected format: <number> <unit> (e.g. 10 hours, 2 weeks)",
18                value_str
19            ),
20            Some(source.clone()),
21            None::<String>,
22        ));
23    }
24    let unit_str = parts.pop().unwrap();
25    let number_str = parts.join(" ").replace(['_', ','], "");
26    let digit_count = number_str.chars().filter(|c| c.is_ascii_digit()).count();
27    if digit_count > crate::limits::MAX_NUMBER_DIGITS {
28        return Err(Error::validation(
29            format!(
30                "Number has too many digits (max {})",
31                crate::limits::MAX_NUMBER_DIGITS
32            ),
33            Some(source.clone()),
34            None::<String>,
35        ));
36    }
37    let n = Decimal::from_str(&number_str).map_err(|_| {
38        Error::validation(
39            format!("Invalid duration number: '{}'", number_str),
40            Some(source.clone()),
41            None::<String>,
42        )
43    })?;
44    let unit_lower = unit_str.to_lowercase();
45    let unit = match unit_lower.as_str() {
46        "year" | "years" => DurationUnit::Year,
47        "month" | "months" => DurationUnit::Month,
48        "week" | "weeks" => DurationUnit::Week,
49        "day" | "days" => DurationUnit::Day,
50        "hour" | "hours" => DurationUnit::Hour,
51        "minute" | "minutes" => DurationUnit::Minute,
52        "second" | "seconds" => DurationUnit::Second,
53        "millisecond" | "milliseconds" => DurationUnit::Millisecond,
54        "microsecond" | "microseconds" => DurationUnit::Microsecond,
55        _ => {
56            return Err(Error::validation(
57                format!(
58                    "Unknown duration unit: '{}'. Expected one of: years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds",
59                    unit_str
60                ),
61                Some(source.clone()),
62                None::<String>,
63            ));
64        }
65    };
66    Ok(Value::Duration(n, unit))
67}
68
69/// Parse a "number unit" string (e.g. "1 eur", "50 percent", "500 permille") into `(number, unit_name)`.
70/// Does not validate the unit against any type; use `ScaleUnits::get()` or `RatioUnits::get()` for that.
71/// Single canonical implementation used by both AST and runtime string parsing for scale and ratio.
72pub(crate) fn parse_number_unit_string(s: &str) -> Result<(Decimal, String), String> {
73    let trimmed = s.trim();
74    let mut parts = trimmed.split_whitespace();
75    let number_part = parts.next().ok_or_else(|| {
76        if trimmed.is_empty() {
77            "Scale value cannot be empty. Use a number followed by a unit (e.g. '10 eur')."
78                .to_string()
79        } else {
80            format!(
81                "Invalid scale value: '{}'. Scale value must be a number followed by a unit (e.g. '10 eur').",
82                s
83            )
84        }
85    })?;
86    let unit_part = parts.next().ok_or_else(|| {
87        format!(
88            "Scale value must include a unit (e.g. '{} eur').",
89            number_part
90        )
91    })?;
92    let clean = number_part.replace(['_', ','], "");
93    let digit_count = clean.chars().filter(|c| c.is_ascii_digit()).count();
94    if digit_count > crate::limits::MAX_NUMBER_DIGITS {
95        return Err(format!(
96            "Number has too many digits (max {})",
97            crate::limits::MAX_NUMBER_DIGITS
98        ));
99    }
100    let n = Decimal::from_str(&clean).map_err(|_| format!("Invalid scale: '{}'", s))?;
101    Ok((n, unit_part.to_string()))
102}
103
104pub(crate) fn parse_datetime_str(s: &str) -> Option<DateTimeValue> {
105    if let Ok(dt) = s.parse::<chrono::DateTime<chrono::FixedOffset>>() {
106        let offset = dt.offset().local_minus_utc();
107        let microsecond = dt.nanosecond() / 1000 % 1_000_000;
108        return Some(DateTimeValue {
109            year: dt.year(),
110            month: dt.month(),
111            day: dt.day(),
112            hour: dt.hour(),
113            minute: dt.minute(),
114            second: dt.second(),
115            microsecond,
116            timezone: Some(TimezoneValue {
117                offset_hours: (offset / 3600) as i8,
118                offset_minutes: ((offset % 3600) / 60) as u8,
119            }),
120        });
121    }
122    if let Ok(dt) = s.parse::<chrono::NaiveDateTime>() {
123        let microsecond = dt.nanosecond() / 1000 % 1_000_000;
124        return Some(DateTimeValue {
125            year: dt.year(),
126            month: dt.month(),
127            day: dt.day(),
128            hour: dt.hour(),
129            minute: dt.minute(),
130            second: dt.second(),
131            microsecond,
132            timezone: None,
133        });
134    }
135    if let Ok(d) = s.parse::<chrono::NaiveDate>() {
136        return Some(DateTimeValue {
137            year: d.year(),
138            month: d.month(),
139            day: d.day(),
140            hour: 0,
141            minute: 0,
142            second: 0,
143            microsecond: 0,
144            timezone: None,
145        });
146    }
147    None
148}
149
150/// Parse a date string into a DateTimeValue (for type constraint parsing)
151pub fn parse_date_string(s: &str) -> Result<DateTimeValue, String> {
152    if let Some(dtv) = parse_datetime_str(s) {
153        return Ok(dtv);
154    }
155    if let Some(dtv) = DateTimeValue::parse(s) {
156        return Ok(dtv);
157    }
158    Err(format!("Invalid date format: '{}'", s))
159}
160
161/// Parse a time string into a TimeValue (for type constraint parsing)
162pub fn parse_time_string(s: &str) -> Result<TimeValue, String> {
163    if let Ok(t) = s.parse::<chrono::DateTime<chrono::FixedOffset>>() {
164        let offset = t.offset().local_minus_utc();
165        return Ok(TimeValue {
166            hour: t.hour() as u8,
167            minute: t.minute() as u8,
168            second: t.second() as u8,
169            timezone: Some(TimezoneValue {
170                offset_hours: (offset / 3600) as i8,
171                offset_minutes: ((offset % 3600) / 60) as u8,
172            }),
173        });
174    }
175
176    if let Ok(t) = s.parse::<chrono::NaiveTime>() {
177        return Ok(TimeValue {
178            hour: t.hour() as u8,
179            minute: t.minute() as u8,
180            second: t.second() as u8,
181            timezone: None,
182        });
183    }
184
185    Err(format!("Invalid time format: '{}'", s))
186}