Skip to main content

rstime/
parse.rs

1//! Parse module
2//!
3//! Provides functions for parsing date/time strings using format patterns
4//! and ISO 8601 format detection.
5
6use crate::date::Date;
7use crate::datetime::DateTime;
8use crate::error::{RstimeError, RstimeResult};
9use crate::time::Time;
10
11/// Parse a datetime string using a format pattern
12///
13/// # Parameters
14///
15/// * `s` - The input string to parse
16/// * `fmt` - Format pattern using `{TOKEN}` syntax
17///
18/// # Returns
19///
20/// A [`DateTime`] on success, or an error message string.
21///
22/// # Examples
23///
24/// ```rust
25/// use rstime::parse_datetime;
26///
27/// let dt = parse_datetime(
28///     "10/05/2026 14:05",
29///     "{DD}/{MM}/{YYYY} {HH}:{mm}"
30/// ).unwrap();
31/// assert_eq!(dt.date.year, 2026);
32/// assert_eq!(dt.date.month, 5);
33/// assert_eq!(dt.date.day, 10);
34/// ```
35pub fn parse_datetime(s: &str, fmt: &str) -> RstimeResult<DateTime> {
36    let tokens = tokenize_format(fmt);
37    let mut year = 0i32;
38    let mut month = 1u8;
39    let mut day = 1u8;
40    let mut hour = 0u8;
41    let mut minute = 0u8;
42    let mut second = 0u8;
43    let mut millisecond = 0u16;
44    let mut pos = 0usize;
45    let chars: Vec<char> = s.chars().collect();
46
47    for token in &tokens {
48        match token {
49            Token::Literal(lit) => {
50                let lit_chars: Vec<char> = lit.chars().collect();
51                if pos + lit_chars.len() > chars.len() {
52                    return Err(RstimeError::new(format!(
53                        "期望文本 '{}' 但输入已结束",
54                        lit
55                    )));
56                }
57                let actual: String = chars[pos..pos + lit_chars.len()].iter().collect();
58                if actual != *lit {
59                    return Err(RstimeError::new(format!(
60                        "位置 {} 期望 '{}',实际得到 '{}'",
61                        pos, lit, actual
62                    )));
63                }
64                pos += lit_chars.len();
65            }
66            Token::Specifier(spec) => {
67                let value = read_number(&chars, &mut pos, spec.len);
68                match spec.kind {
69                    SpecKind::Year4 => year = value as i32,
70                    SpecKind::Year2 => year = 2000 + value as i32,
71                    SpecKind::Month => month = value as u8,
72                    SpecKind::Day => day = value as u8,
73                    SpecKind::Hour24 => hour = value as u8,
74                    SpecKind::Hour12 => hour = value as u8,
75                    SpecKind::Minute => minute = value as u8,
76                    SpecKind::Second => second = value as u8,
77                    SpecKind::Millisecond => millisecond = value as u16,
78                }
79            }
80        }
81    }
82
83    let date = Date::new(year, month, day);
84    let time = Time::new(hour, minute, second, millisecond);
85    Ok(DateTime::new(date, time))
86}
87
88/// Parse a date string using a format pattern
89///
90/// # Examples
91///
92/// ```rust
93/// use rstime::parse_date;
94///
95/// let d = parse_date("2026-05-10", "{YYYY}-{MM}-{DD}").unwrap();
96/// assert_eq!(d.year, 2026);
97/// assert_eq!(d.month, 5);
98/// assert_eq!(d.day, 10);
99/// ```
100pub fn parse_date(s: &str, fmt: &str) -> RstimeResult<Date> {
101    let dt = parse_datetime(s, fmt)?;
102    Ok(dt.date)
103}
104
105/// Parse a time string using a format pattern
106///
107/// # Examples
108///
109/// ```rust
110/// use rstime::parse_time;
111///
112/// let t = parse_time("14:05:09", "{HH}:{mm}:{ss}").unwrap();
113/// assert_eq!(t.hour, 14);
114/// assert_eq!(t.minute, 5);
115/// assert_eq!(t.second, 9);
116/// ```
117pub fn parse_time(s: &str, fmt: &str) -> RstimeResult<Time> {
118    let dt = parse_datetime(&format!("2000-01-01 {}", s), &format!("{{YYYY}}-{{MM}}-{{DD}} {}", fmt))?;
119    Ok(dt.time)
120}
121
122/// Parse an ISO 8601 formatted datetime string
123///
124/// Supports the following formats:
125/// - `2026-05-10T14:05:09.037` (with milliseconds)
126/// - `2026-05-10T14:05:09` (seconds precision)
127/// - `2026-05-10 14:05:09` (space separator)
128/// - `2026-05-10` (date only, time set to midnight)
129///
130/// # Examples
131///
132/// ```rust
133/// use rstime::{parse_iso8601, Date, Time};
134///
135/// let dt = parse_iso8601("2026-05-10T14:05:09").unwrap();
136/// assert_eq!(dt.date, Date::new(2026, 5, 10));
137/// assert_eq!(dt.time, Time::from_hms(14, 5, 9));
138///
139/// let dt2 = parse_iso8601("2026-05-10").unwrap();
140/// assert_eq!(dt2.time, Time::MIDNIGHT);
141/// ```
142pub fn parse_iso8601(s: &str) -> RstimeResult<DateTime> {
143    let s = s.trim();
144
145    if s.len() >= 23
146        && s.as_bytes()[4] == b'-'
147        && s.as_bytes()[7] == b'-'
148        && (s.as_bytes()[10] == b'T' || s.as_bytes()[10] == b' ')
149        && s.as_bytes()[13] == b':'
150        && s.as_bytes()[16] == b':'
151        && s.as_bytes()[19] == b'.'
152    {
153        return parse_datetime(s, "{YYYY}-{MM}-{DD}T{HH}:{mm}:{ss}.{SSS}");
154    }
155
156    if s.len() >= 19
157        && s.as_bytes()[4] == b'-'
158        && s.as_bytes()[7] == b'-'
159        && (s.as_bytes()[10] == b'T' || s.as_bytes()[10] == b' ')
160    {
161        return parse_datetime(s, "{YYYY}-{MM}-{DD}T{HH}:{mm}:{ss}");
162    }
163
164    if s.len() >= 10 && s.as_bytes()[4] == b'-' && s.as_bytes()[7] == b'-' {
165        let dt = parse_datetime(s, "{YYYY}-{MM}-{DD}")?;
166        return Ok(DateTime::new(dt.date, Time::MIDNIGHT));
167    }
168
169    Err(RstimeError::new(format!("无法识别的 ISO 8601 格式: '{}'", s)))
170}
171
172enum SpecKind {
173    Year4,
174    Year2,
175    Month,
176    Day,
177    Hour24,
178    Hour12,
179    Minute,
180    Second,
181    Millisecond,
182}
183
184struct FormatSpec {
185    kind: SpecKind,
186    len: usize,
187}
188
189enum Token {
190    Literal(String),
191    Specifier(FormatSpec),
192}
193
194fn tokenize_format(fmt: &str) -> Vec<Token> {
195    let mut tokens = Vec::new();
196    let chars: Vec<char> = fmt.chars().collect();
197    let mut i = 0;
198
199    while i < chars.len() {
200        if chars[i] == '{' {
201            let mut end = i + 1;
202            while end < chars.len() && chars[end] != '}' {
203                end += 1;
204            }
205            if end < chars.len() && end > i + 1 {
206                let token: String = chars[i + 1..end].iter().collect();
207                if let Some(spec) = classify_token(&token) {
208                    tokens.push(Token::Specifier(spec));
209                    i = end + 1;
210                    continue;
211                }
212            }
213        }
214        let start = i;
215        while i < chars.len() && (chars[i] != '{' || i == start) {
216            if chars[i] == '{' {
217                let mut j = i + 1;
218                while j < chars.len() && chars[j] != '}' {
219                    j += 1;
220                }
221                if j < chars.len() {
222                    let t: String = chars[i + 1..j].iter().collect();
223                    if classify_token(&t).is_some() {
224                        break;
225                    }
226                }
227            }
228            i += 1;
229        }
230        if i > start {
231            tokens.push(Token::Literal(chars[start..i].iter().collect()));
232        }
233    }
234
235    tokens
236}
237
238fn classify_token(token: &str) -> Option<FormatSpec> {
239    match token {
240        "YYYY" => Some(FormatSpec {
241            kind: SpecKind::Year4,
242            len: 4,
243        }),
244        "YY" => Some(FormatSpec {
245            kind: SpecKind::Year2,
246            len: 2,
247        }),
248        "MM" => Some(FormatSpec {
249            kind: SpecKind::Month,
250            len: 2,
251        }),
252        "M" => Some(FormatSpec {
253            kind: SpecKind::Month,
254            len: 1,
255        }),
256        "DD" => Some(FormatSpec {
257            kind: SpecKind::Day,
258            len: 2,
259        }),
260        "D" => Some(FormatSpec {
261            kind: SpecKind::Day,
262            len: 1,
263        }),
264        "HH" => Some(FormatSpec {
265            kind: SpecKind::Hour24,
266            len: 2,
267        }),
268        "H" => Some(FormatSpec {
269            kind: SpecKind::Hour24,
270            len: 1,
271        }),
272        "hh" => Some(FormatSpec {
273            kind: SpecKind::Hour12,
274            len: 2,
275        }),
276        "h" => Some(FormatSpec {
277            kind: SpecKind::Hour12,
278            len: 1,
279        }),
280        "mm" => Some(FormatSpec {
281            kind: SpecKind::Minute,
282            len: 2,
283        }),
284        "m" => Some(FormatSpec {
285            kind: SpecKind::Minute,
286            len: 1,
287        }),
288        "ss" => Some(FormatSpec {
289            kind: SpecKind::Second,
290            len: 2,
291        }),
292        "s" => Some(FormatSpec {
293            kind: SpecKind::Second,
294            len: 1,
295        }),
296        "SSS" => Some(FormatSpec {
297            kind: SpecKind::Millisecond,
298            len: 3,
299        }),
300        _ => None,
301    }
302}
303
304fn read_number(chars: &[char], pos: &mut usize, len: usize) -> u32 {
305    let end = (*pos + len).min(chars.len());
306    let num_str: String = chars[*pos..end].iter().collect();
307    *pos = end;
308    num_str.parse::<u32>().unwrap_or(0)
309}