tu/
lib.rs

1use chrono::prelude::{DateTime, Utc};
2
3mod chrono_english {
4  pub mod errors;
5  pub mod lib;
6  pub mod parser;
7  pub mod types;
8}
9
10use chrono_english::lib::{parse_date_string, DateError, Dialect};
11
12const DIALECT: Dialect = Dialect::Us;
13
14// TODO: Remove after https://github.com/chronotope/chrono/issues/1228
15fn append_min_if_only_hour(input: &str) -> String {
16  if input.len() >= 3 {
17    let last_three = &input[input.len() - 3..];
18
19    // Check if first char is '+' and then two digits
20    if last_three.starts_with('+')
21      && last_three.chars().nth(1).map(|ch| ch.is_ascii_digit()) == Some(true)
22      && last_three.chars().nth(2).map(|ch| ch.is_ascii_digit()) == Some(true)
23    {
24      return format!("{input}:00");
25    }
26  }
27
28  if input.len() >= 2 {
29    let last_two_char = &input[input.len() - 2..];
30
31    if last_two_char.starts_with('+') {
32      let last_char_opt = last_two_char.chars().nth(1);
33      if let Some(last_char) = last_char_opt {
34        if last_char.is_ascii_digit() {
35          let without_last_two = &input[..input.len() - 2];
36          return format!("{without_last_two}+0{last_char}:00");
37        }
38      }
39    }
40  }
41
42  input.to_string()
43}
44
45/// Parse date arguments and convert to UTC timestamp
46pub fn parse_date_args(
47  args: &[String],
48  now: DateTime<Utc>,
49) -> Result<DateTime<Utc>, DateError> {
50  // Remove "in" or "at" from the beginning
51  let args_combined = append_min_if_only_hour(
52    {
53      let combined = args.join(" ");
54      if let Some(stripped) = combined.strip_prefix("in a ") {
55        format!("1 {stripped}")
56      }
57      else if let Some(stripped) = combined.strip_prefix("in an ") {
58        format!("1 {stripped}")
59      }
60      else if let Some(stripped) = combined.strip_prefix("in ") {
61        stripped.to_string()
62      }
63      else if let Some(stripped) = combined.strip_prefix("at ") {
64        stripped.to_string()
65      }
66      else {
67        combined
68      }
69    }
70    .trim(),
71  );
72
73  // Check if it's a Unix timestamp (all digits)
74  if args_combined.chars().all(|c| c.is_ascii_digit()) {
75    if let Ok(timestamp) = args_combined.parse::<i64>() {
76      // Try as millisecond timestamp first (if it's a reasonable size)
77      // Millisecond timestamps are typically 13 digits long
78      // We use a heuristic: if the timestamp has exactly 13 digits and dividing by 1000
79      // gives a reasonable Unix timestamp (after 2001), treat it as milliseconds
80      if args_combined.len() == 13 && timestamp / 1000 >= 1_000_000_000 {
81        let seconds = timestamp / 1000;
82        let nanoseconds = (timestamp % 1000) * 1_000_000;
83        if let Some(datetime) =
84          DateTime::from_timestamp(seconds, nanoseconds as u32)
85        {
86          return Ok(datetime);
87        }
88      }
89
90      // Fall back to regular second-based timestamp
91      if let Some(datetime) = DateTime::from_timestamp(timestamp, 0) {
92        return Ok(datetime);
93      }
94    }
95  }
96
97  DateTime::parse_from_rfc2822(&args_combined)
98    .or_else(|_| DateTime::parse_from_rfc3339(&args_combined))
99    .map(|datetime| datetime.with_timezone(&Utc))
100    .or_else(|_| parse_date_string(&args_combined, now, DIALECT))
101}
102
103pub fn to_iso(date: DateTime<Utc>) -> String {
104  date.to_rfc3339().replace("+00:00", "Z")
105}
106
107pub fn parse_print(now: DateTime<Utc>, s: &str) -> String {
108  to_iso(parse_date_string(s, now, DIALECT).unwrap())
109}