aimx/literals/
literal.rs

1//! Literal value parsing and conversion for AIMX expressions.
2//!
3//! Provides the [`Literal`] enum and helpers to parse and convert booleans,
4//! numbers, text, dates, and tasks used by the expression engine.
5//! Key entry points: [`parse_literal`], [`parse_bool`], [`parse_task`].
6
7use crate::{
8    aim::{Typedef, WriterLike, Writer},
9    literals::{parse_date, parse_number, parse_text},
10};
11use anyhow::{Result, anyhow};
12use jiff::civil::DateTime;
13use nom::{
14    IResult, Parser,
15    branch::alt,
16    bytes::complete::tag,
17    character::complete::{char, multispace0},
18    combinator::{map, opt, value}, sequence::delimited,
19};
20use std::cmp::Ordering;
21use std::{
22    fmt,
23    sync::Arc,
24};
25
26/// Literal values supported by AIMX expressions.
27///
28/// Provides the core literal variants and helpers used by the expression
29/// engine for parsing, type checking, comparison, and conversion.
30#[derive(Debug, Clone, PartialEq)]
31pub enum Literal {
32    /// Empty or untyped literal.
33    Empty,
34    /// Boolean value.
35    Bool(bool),
36    /// Date/time value.
37    Date(DateTime),
38    /// 64-bit floating point number.
39    Number(f64),
40    /// Task with optional status and description.
41    /// Status semantics: Some(true)=completed, Some(false)=failed, None=pending.
42    Task(Option<bool>, Arc<str>),
43    /// UTF-8 string value.
44    Text(Arc<str>),
45}
46
47impl Literal {
48    /// Check if this literal matches the specified type.
49    ///
50    /// # Arguments
51    ///
52    /// * `typedef` - The type definition to check against
53    ///
54    /// # Returns
55    ///
56    /// Returns `true` if this literal matches the specified type, `false` otherwise.
57    pub fn is_type(&self, typedef: &Typedef) -> bool {
58        match self {
59            Literal::Empty => false,
60            Literal::Bool(_) => typedef.is_bool() | typedef.is_any(),
61            Literal::Date(_) => typedef.is_date() | typedef.is_any(),
62            Literal::Number(_) => typedef.is_number() | typedef.is_any(),
63            Literal::Task(..) => typedef.is_task() | typedef.is_any(),
64            Literal::Text(_) => typedef.is_text() | typedef.is_any(),
65        }
66    }
67
68    /// Get the type of this literal.
69    ///
70    /// # Returns
71    ///
72    /// Returns a `Result<Typedef>` containing the type of this literal,
73    /// or an error if this is an Empty literal.
74    pub fn get_type(&self) -> Result<Typedef> {
75        match self {
76            Literal::Empty => Err(anyhow!("Expecting type, found Empty")),
77            Literal::Bool(_) => Ok(Typedef::Bool),
78            Literal::Date(_) => Ok(Typedef::Date),
79            Literal::Number(_) => Ok(Typedef::Number),
80            Literal::Task(..) => Ok(Typedef::Task),
81            Literal::Text(_) => Ok(Typedef::Text),
82        }
83    }
84
85    /// Convert this literal to match the type of another literal.
86    ///
87    /// This method performs type conversion according to the grammar's
88    /// type promotion rules, converting this literal to match the type
89    /// of the provided reference literal.
90    ///
91    /// # Arguments
92    ///
93    /// * `literal` - The reference literal whose type determines the conversion
94    ///
95    /// # Returns
96    ///
97    /// Returns a `Result<Literal>` containing the converted literal or an error
98    /// if conversion is not possible.
99    pub fn as_type(self, literal: &Literal) -> Result<Literal> {
100        match literal {
101            Literal::Empty => Err(anyhow!(
102                "Expecting type as {}, found Empty",
103                literal.type_as_string()
104            )),
105            Literal::Bool(_) => Ok(self.as_bool()),
106            Literal::Date(_) => self.as_date(),
107            Literal::Number(_) => self.as_number(),
108            Literal::Task(..) => self.as_task(),
109            Literal::Text(_) => self.as_text(),
110        }
111    }
112
113    pub fn to_type(self, typedef: &Typedef) -> Result<Literal> {
114        match typedef {
115            Typedef::Any => Ok(self),
116            Typedef::Bool => Ok(self.as_bool()),
117            Typedef::Date => self.as_date(),
118            Typedef::Number => self.as_number(),
119            Typedef::Task => self.as_task(),
120            Typedef::Text => self.as_text(),
121            _ => Err(anyhow!("Expecting literal type, found {}", typedef)),
122        }
123    }
124
125    /// Check if this literal is empty.
126    pub fn is_empty(&self) -> bool {
127        matches!(self, Literal::Empty)
128    }
129
130    /// Check if this literal represents a boolean value.
131    pub fn is_bool(&self) -> bool {
132        matches!(self, Literal::Bool(_))
133    }
134
135    /// Create a boolean literal from a boolean value.
136    pub fn from_bool(b: bool) -> Self {
137        Literal::Bool(b)
138    }
139
140    /// Convert this literal to a boolean representation.
141    ///
142    /// Performs type conversion to boolean according to the grammar's
143    /// truthiness rules:
144    /// - Numbers: 0 is false, non-zero is true
145    /// - Text: Empty string is false, non-empty is true
146    /// - Dates: Always true (they exist)
147    /// - Tasks: Status determines value, no status is false
148    ///
149    /// This function provides an implicit error free conversion from any
150    /// literal to bool specifically for the conditional ternary operator
151    /// making a type safe error free check possible.
152    ///
153    /// e.g. `user.birthday ? user.birthday : _`
154    pub fn as_bool(self) -> Literal {
155        match self {
156            Literal::Empty => Literal::Bool(false),
157            Literal::Bool(_) => self,
158            Literal::Date(_) => {
159                // Check for non-zero days
160                if let Ok(number) = self.as_number() {
161                    number.as_bool()
162                } else {
163                    Literal::Bool(false)
164                }
165            }
166            Literal::Number(number) => Literal::Bool(number != 0.0),
167            Literal::Task(status, _) => match status {
168                Some(state) => Literal::Bool(state),
169                None => Literal::Bool(false),
170            },
171            Literal::Text(text) => Literal::Bool(!text.is_empty()),
172        }
173    }
174
175    /// Extract a boolean value from this literal.
176    ///
177    /// This is a convenience method for when you specifically need a `bool` value.
178    pub fn to_bool(&self) -> bool {
179        match self {
180            Literal::Empty => false,
181            Literal::Bool(b) => *b,
182            Literal::Date(_) => {
183                // Check for non-zero days
184                if let Ok(number) = self.to_number() {
185                    number != 0.0
186                } else {
187                    false
188                }
189            }
190            Literal::Number(number) => *number != 0.0,
191            Literal::Task(status, _) => (*status).unwrap_or_default(),
192            Literal::Text(text) => !text.is_empty(),
193        }
194    }
195
196    /// Check if this literal represents a date value.
197    pub fn is_date(&self) -> bool {
198        matches!(self, Literal::Date(_))
199    }
200
201    /// Create a date literal from a DateTime value.
202    pub fn from_date(d: DateTime) -> Self {
203        Literal::Date(d)
204    }
205
206    /// Convert this literal to a date representation.
207    ///
208    /// Attempts to convert the literal to a date according to the grammar's
209    /// conversion rules:
210    /// - Boolean: true becomes Unix epoch + 1 second, false becomes Unix epoch
211    /// - Number: Interpreted as Unix timestamp
212    /// - Text: Parsed as date if possible
213    /// - Task: Text component parsed as date if possible
214    pub fn as_date(self) -> Result<Literal> {
215        match self {
216            Literal::Empty => Err(anyhow!("Expecting type as Date, found Empty")),
217            Literal::Bool(state) => {
218                // Convert boolean to timestamp: true = 1, false = 0
219                let timestamp = if state { 1i64 } else { 0i64 };
220                match DateTime::new(1970, 1, 1, 0, 0, 0, 0) {
221                    Ok(epoch) => {
222                        // Add the timestamp seconds to the epoch
223                        match jiff::Span::new().try_seconds(timestamp) {
224                            Ok(duration) => match epoch.checked_add(duration) {
225                                Ok(new_dt) => Ok(Literal::Date(new_dt)),
226                                Err(_) => Ok(Literal::Date(epoch)),
227                            },
228                            Err(_) => Ok(Literal::Date(epoch)),
229                        }
230                    }
231                    Err(_) => Err(anyhow!("Failed to create Date from Bool")),
232                }
233            }
234            Literal::Date(_) => Ok(self),
235            Literal::Number(number) => {
236                // Convert number to timestamp (assuming it's a Unix timestamp)
237                let timestamp = number as i64;
238                match DateTime::new(1970, 1, 1, 0, 0, 0, 0) {
239                    Ok(epoch) => {
240                        // Add the timestamp seconds to the epoch
241                        match jiff::Span::new().try_seconds(timestamp) {
242                            Ok(duration) => match epoch.checked_add(duration) {
243                                Ok(new_dt) => Ok(Literal::Date(new_dt)),
244                                Err(_) => {
245                                    Err(anyhow!("Failed to create Date from Number({})", number))
246                                }
247                            },
248                            Err(_) => Err(anyhow!("Failed to create Date from Number({})", number)),
249                        }
250                    }
251                    Err(_) => Err(anyhow!("Failed to create Date from Number({})", number)),
252                }
253            }
254            Literal::Task(_, text) => {
255                // Try to parse the task text as a date
256                match parse_date(&text) {
257                    Ok((_, date)) => Ok(Literal::Date(date)),
258                    Err(_) => Err(anyhow!("Failed to parse Task text as Date")),
259                }
260            }
261            Literal::Text(text) => {
262                // Try to parse the text as a date
263                match parse_date(&text) {
264                    Ok((_, date)) => Ok(Literal::Date(date)),
265                    Err(_) => Err(anyhow!("Failed to parse Text({}) as Date", text)),
266                }
267            }
268        }
269    }
270
271    /// Check if this literal represents a number value.
272    pub fn is_number(&self) -> bool {
273        matches!(self, Literal::Number(_))
274    }
275
276    /// Create a number literal from an f64 value.
277    pub fn from_number(n: f64) -> Self {
278        Literal::Number(n)
279    }
280
281    /// Convert this literal to a number representation.
282    ///
283    /// Attempts to convert the literal to a number according to the grammar's
284    /// conversion rules:
285    /// - Boolean: false becomes 0, true becomes 1
286    /// - Date: Converted to Unix timestamp
287    /// - Text: Parsed as number if it represents a valid numeric literal
288    /// - Task: Status determines value (true=1, false=-1, none=0)
289    pub fn as_number(self) -> Result<Literal> {
290        match self {
291            Literal::Empty => Err(anyhow!("Expecting type as Number, found Empty")),
292            Literal::Bool(state) => Ok(Literal::Number(if state { 1.0 } else { 0.0 })),
293            Literal::Date(dt) => {
294                if let Ok(utc_dt) = dt.to_zoned(jiff::tz::TimeZone::UTC) {
295                    let seconds = utc_dt.timestamp().as_second();
296                    Ok(Literal::Number(seconds as f64))
297                } else {
298                    Ok(Literal::Number(0.0))
299                }
300            }
301            Literal::Number(_) => Ok(self),
302            Literal::Task(status, _) => match status {
303                Some(state) => Ok(Literal::Number(if state { 1.0 } else { -1.0 })),
304                None => Ok(Literal::Number(0.0)),
305            },
306            Literal::Text(text) => match text.parse::<f64>() {
307                Ok(number) => Ok(Literal::Number(number)),
308                Err(_) => Err(anyhow!("Expecting Text as Number, found \"{}\"", text)),
309            },
310        }
311    }
312
313    /// Extract a numeric value from this literal.
314    ///
315    /// This is a convenience method for when you specifically need a `f64` number.
316    pub fn to_number(&self) -> Result<f64> {
317        match self {
318            Literal::Empty => Err(anyhow!("Expecting type as Number, found Empty")),
319            Literal::Bool(state) => Ok(if *state { 1.0 } else { 0.0 }),
320            Literal::Date(dt) => {
321                if let Ok(utc_dt) = dt.to_zoned(jiff::tz::TimeZone::UTC) {
322                    let seconds = utc_dt.timestamp().as_second();
323                    Ok(seconds as f64)
324                } else {
325                    Ok(0.0)
326                }
327            }
328            Literal::Number(number) => Ok(*number),
329            Literal::Task(status, _) => match *status {
330                Some(state) => Ok(if state { 1.0 } else { -1.0 }),
331                None => Ok(0.0),
332            },
333            Literal::Text(text) => match text.parse::<f64>() {
334                Ok(number) => Ok(number),
335                Err(_) => Err(anyhow!("Expecting Text as Number, found \"{}\"", text)),
336            },
337        }
338    }
339
340    /// Check if this literal represents a task value.
341    pub fn is_task(&self) -> bool {
342        matches!(self, Literal::Task(..))
343    }
344
345    /// Create a task literal from status and text.
346    pub fn from_task(status: Option<bool>, task: Arc<str>) -> Self {
347        Literal::Task(status, task)
348    }
349
350    /// Convert this literal to a task representation.
351    ///
352    /// Converts the literal to a task according to the grammar's conversion rules:
353    /// - Boolean: Status becomes the boolean value, text becomes "true"/"false"
354    /// - Date: No status, text becomes date string
355    /// - Number: Status based on sign (positive=true, negative=false, zero=none), text becomes number string
356    /// - Text: No status, text remains the same
357    pub fn as_task(self) -> Result<Literal> {
358        match self {
359            Literal::Empty => Err(anyhow!("Expecting type as Task, found Empty")),
360            Literal::Bool(state) => Ok(Literal::Task(Some(state), Arc::from(state.to_string()))),
361            Literal::Date(dt) => Ok(Literal::Task(None, Arc::from(dt.to_string()))),
362            Literal::Number(number) => {
363                let status = if number > 0.0 {
364                    Some(true)
365                } else if number < 0.0 {
366                    Some(false)
367                } else {
368                    None
369                };
370                Ok(Literal::Task(status, Arc::from(number.to_string())))
371            }
372            Literal::Task(..) => Ok(self),
373            Literal::Text(text) => Ok(Literal::Task(None, text)),
374        }
375    }
376
377    /// Check if this literal represents a text value.
378    pub fn is_text(&self) -> bool {
379        matches!(self, Literal::Text(_))
380    }
381
382    /// Create a text literal from a String.
383    pub fn from_text(text: Arc<str>) -> Self {
384        Literal::Text(text)
385    }
386
387    /// Convert this literal to a text representation.
388    ///
389    /// Converts the literal to text according to the grammar's conversion rules:
390    /// - Boolean: "true" or "false"
391    /// - Date: Formatted as date string
392    /// - Number: Formatted as string
393    /// - Task: Text component of the task
394    pub fn as_text(self) -> Result<Literal> {
395        match self {
396            Literal::Empty => Err(anyhow!("Expecting type as Text, found Empty")),
397            Literal::Bool(state) => Ok(Literal::Text(Arc::from(state.to_string()))),
398            Literal::Date(dt) => Ok(Literal::Text(Arc::from(dt.to_string()))),
399            Literal::Number(number) => Ok(Literal::Text(Arc::from(number.to_string()))),
400            Literal::Task(_, text) => Ok(Literal::Text(text)),
401            Literal::Text(_) => Ok(self),
402        }
403    }
404
405    /// Get a string representation of this literal's type.
406    pub fn type_as_string(&self) -> &'static str {
407        match self {
408            Literal::Empty => "Empty",
409            Literal::Bool(_) => "Bool",
410            Literal::Date(_) => "Date",
411            Literal::Number(_) => "Number",
412            Literal::Task(..) => "Task",
413            Literal::Text(_) => "Text",
414        }
415    }
416
417    // Helper function to get type order for consistent sorting
418    fn type_order(&self) -> u8 {
419        match self {
420            Literal::Empty => 0,
421            Literal::Bool(_) => 1,
422            Literal::Number(_) => 2,
423            Literal::Date(_) => 3,
424            Literal::Text(_) => 4,
425            Literal::Task(..) => 5,
426        }
427    }
428
429    fn type_promote(self, literal: &Literal) -> Result<Literal> {
430        match literal {
431            Literal::Task(..) => {
432                match self {
433                    Literal::Text(t) => Ok(Literal::Task(None, t)),
434                    Literal::Date(dt) => Ok(Literal::Task(None, Arc::from(dt.to_string()))),
435                    Literal::Number(n) => Ok(Literal::Task(None, Arc::from(n.to_string()))),
436                    //Literal::Bool(b) => Ok(Literal::Task(Some(b), "".to_owned())),
437                    _ => Err(anyhow!("Type promote to Task failed")),
438                }
439            }
440            Literal::Text(_) => {
441                match self {
442                    Literal::Date(dt) => Ok(Literal::Text(Arc::from(dt.to_string()))),
443                    Literal::Number(n) => Ok(Literal::Text(Arc::from(n.to_string()))),
444                    //Literal::Bool(b) => Ok(Literal::Text(b.to_string())),
445                    _ => Err(anyhow!("Type promote to Text failed")),
446                }
447            }
448            Literal::Date(_) => {
449                match self {
450                    Literal::Number(number) => {
451                        // Convert number to timestamp (assuming it's a Unix timestamp)
452                        let timestamp = number as i64;
453                        match DateTime::new(1970, 1, 1, 0, 0, 0, 0) {
454                            Ok(epoch) => {
455                                // Add the timestamp seconds to the epoch
456                                match jiff::Span::new().try_seconds(timestamp) {
457                                    Ok(duration) => match epoch.checked_add(duration) {
458                                        Ok(new_dt) => Ok(Literal::Date(new_dt)),
459                                        Err(_) => Err(anyhow!(
460                                            "Failed to create Date from Number({})",
461                                            number
462                                        )),
463                                    },
464                                    Err(_) => Err(anyhow!(
465                                        "Failed to create Date from Number({})",
466                                        number
467                                    )),
468                                }
469                            }
470                            Err(_) => Err(anyhow!("Failed to create Date from Number({})", number)),
471                        }
472                    }
473                    _ => Err(anyhow!("Type promote to Date failed")),
474                }
475            }
476            Literal::Number(_) => match self {
477                Literal::Bool(b) => Ok(Literal::Number(if b { 1.0 } else { 0.0 })),
478                _ => Err(anyhow!("Type promote to Task failed")),
479            },
480            _ => Err(anyhow!("Type promote failed")),
481        }
482    }
483
484    pub fn print(&self, writer: &mut Writer) {
485        match self {
486            Literal::Bool(b) => writer.write_bool(*b),
487            Literal::Date(d) => writer.write_date(d),
488            Literal::Empty => {}
489            Literal::Number(n) => writer.write_f64(*n),
490            Literal::Task(status, text) => {
491                match status {
492                    Some(check) => writer.write_str(if *check { "[x] " } else { "[-] " }),
493                    None => writer.write_str("[ ] "),
494                }
495                writer.print(text);
496            },
497            Literal::Text(text) => writer.print(text),
498        }
499    }
500
501    /// Return the formula-string representation (round-trippable by the parser).
502    pub fn to_formula(&self) -> String {
503        let mut writer = Writer::formulizer();
504        self.print(&mut writer);
505        writer.finish()
506    }
507}
508
509impl WriterLike for Literal {
510    fn write(&self, writer: &mut Writer) {
511        self.print(writer);
512    }
513}
514
515impl fmt::Display for Literal {
516    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
517        write!(f, "{}", self.to_stringized())
518    }
519}
520
521impl PartialOrd for Literal {
522    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
523        // Check for special cases
524        match (self, other) {
525            (Literal::Text(t), Literal::Bool(b)) => {
526                if let Ok(a) = t.parse::<bool>() { return a.partial_cmp(b) }
527            },
528            (Literal::Bool(a), Literal::Text(t)) => {
529                if let Ok(b) = t.parse::<bool>() { return a.partial_cmp(&b) }
530            },
531            (Literal::Text(t), Literal::Number(b)) => {
532                if let Ok(a) = t.parse::<f64>() { return a.partial_cmp(b) }
533            },
534            (Literal::Number(a), Literal::Text(t)) => {
535                if let Ok(b) = t.parse::<f64>() { return a.partial_cmp(&b) }
536            }
537            (Literal::Text(t), Literal::Empty) | (Literal::Empty, Literal::Text(t)) => {
538                if t.is_empty() {
539                    return Some(Ordering::Equal);
540                }
541            }
542            _ => {}
543        }
544        // Define a consistent ordering by type first, then value
545        // Type ordering: Empty < Bool < Number < Date < Task < Text
546        let self_type_order = self.type_order();
547        let other_type_order = other.type_order();
548
549        if self_type_order > other_type_order {
550            match other.clone().type_promote(self) {
551                Ok(that) => self.partial_cmp(&that),
552                _ => Some(Ordering::Greater),
553            }
554        } else if self_type_order < other_type_order {
555            match self.clone().type_promote(other) {
556                Ok(this) => this.partial_cmp(other),
557                _ => Some(Ordering::Less),
558            }
559        } else {
560            // Same type, compare values
561            match (self, other) {
562                (Literal::Empty, Literal::Empty) => Some(Ordering::Equal),
563                (Literal::Bool(a), Literal::Bool(b)) => a.partial_cmp(b),
564                (Literal::Date(a), Literal::Date(b)) => a.partial_cmp(b),
565                (Literal::Number(a), Literal::Number(b)) => a.partial_cmp(b),
566                (Literal::Task(status1, text1), Literal::Task(status2, text2)) => {
567                    // Order by status first: Some(true) > None > Some(false)
568                    match (status1, status2) {
569                        (Some(true), Some(false)) => Some(Ordering::Greater),
570                        (Some(true), None) => Some(Ordering::Greater),
571                        (None, Some(true)) => Some(Ordering::Less),
572                        (None, Some(false)) => Some(Ordering::Greater),
573                        (Some(false), Some(true)) => Some(Ordering::Less),
574                        (Some(false), None) => Some(Ordering::Less),
575                        // Same status types or both None, compare texts
576                        (Some(true), Some(true)) | (Some(false), Some(false)) | (None, None) => {
577                            text1.partial_cmp(text2)
578                        }
579                    }
580                }
581                (Literal::Text(a), Literal::Text(b)) => a.partial_cmp(b),
582                _ => Some(Ordering::Equal), // This shouldn't happen due to type ordering check above
583            }
584        }
585    }
586}
587
588impl Eq for Literal {}
589
590impl Ord for Literal {
591    fn cmp(&self, other: &Self) -> Ordering {
592        // Use partial_cmp, but provide a fallback for None cases
593        self.partial_cmp(other).unwrap_or(Ordering::Equal)
594    }
595}
596
597/// Parse a literal value from input according to AIMX literal rules.
598///
599/// Tries boolean, date, number, task, then text, trimming surrounding
600/// whitespace.
601pub fn parse_literal(input: &str) -> IResult<&str, Literal> {
602    delimited(
603        multispace0,
604        alt((
605            map(parse_bool, Literal::Bool),
606            map(parse_date, Literal::Date),
607            map(parse_number, Literal::Number),
608            map(parse_task, |(status, text)| Literal::Task(status, text)),
609            map(parse_text, Literal::Text),
610        )),
611        multispace0
612    ).parse(input)
613}
614
615
616/// Parse a boolean literal: `true` or `false`.
617///
618/// Exact, case-sensitive match; no numeric or fuzzy forms.
619pub fn parse_bool(input: &str) -> IResult<&str, bool> {
620    alt((map(tag("true"), |_| true), map(tag("false"), |_| false))).parse(input)
621}
622
623/// Parse a task literal: `[status] description`.
624///
625/// Status is `+` (Some(true)), `-` (Some(false)), or omitted/space (None).
626/// Description is parsed via [`parse_text`].
627pub fn parse_task(input: &str) -> IResult<&str, (Option<bool>, Arc<str>)> {
628    (
629        delimited(
630            char('['),
631            delimited(
632                multispace0,
633                opt(alt((
634                    value(true, char('+')),
635                    value(false, char('-')),
636                ))),
637                multispace0,
638            ),
639            char(']'),
640        ),
641        multispace0,
642        parse_text,
643    ).parse(input)
644    .map(|(input, (status, _, text))| (input, (status, text)))
645}