1 | // This is a part of Chrono. |
2 | // See README.md and LICENSE.txt for details. |
3 | |
4 | /*! |
5 | `strftime`/`strptime`-inspired date and time formatting syntax. |
6 | |
7 | ## Specifiers |
8 | |
9 | The following specifiers are available both to formatting and parsing. |
10 | |
11 | | Spec. | Example | Description | |
12 | |-------|----------|----------------------------------------------------------------------------| |
13 | | | | **DATE SPECIFIERS:** | |
14 | | `%Y` | `2001` | The full proleptic Gregorian year, zero-padded to 4 digits. chrono supports years from -262144 to 262143. Note: years before 1 BCE or after 9999 CE, require an initial sign (+/-).| |
15 | | `%C` | `20` | The proleptic Gregorian year divided by 100, zero-padded to 2 digits. [^1] | |
16 | | `%y` | `01` | The proleptic Gregorian year modulo 100, zero-padded to 2 digits. [^1] | |
17 | | | | | |
18 | | `%q` | `1` | Quarter of year (1-4) | |
19 | | `%m` | `07` | Month number (01--12), zero-padded to 2 digits. | |
20 | | `%b` | `Jul` | Abbreviated month name. Always 3 letters. | |
21 | | `%B` | `July` | Full month name. Also accepts corresponding abbreviation in parsing. | |
22 | | `%h` | `Jul` | Same as `%b`. | |
23 | | | | | |
24 | | `%d` | `08` | Day number (01--31), zero-padded to 2 digits. | |
25 | | `%e` | ` 8` | Same as `%d` but space-padded. Same as `%_d`. | |
26 | | | | | |
27 | | `%a` | `Sun` | Abbreviated weekday name. Always 3 letters. | |
28 | | `%A` | `Sunday` | Full weekday name. Also accepts corresponding abbreviation in parsing. | |
29 | | `%w` | `0` | Sunday = 0, Monday = 1, ..., Saturday = 6. | |
30 | | `%u` | `7` | Monday = 1, Tuesday = 2, ..., Sunday = 7. (ISO 8601) | |
31 | | | | | |
32 | | `%U` | `28` | Week number starting with Sunday (00--53), zero-padded to 2 digits. [^2] | |
33 | | `%W` | `27` | Same as `%U`, but week 1 starts with the first Monday in that year instead.| |
34 | | | | | |
35 | | `%G` | `2001` | Same as `%Y` but uses the year number in ISO 8601 week date. [^3] | |
36 | | `%g` | `01` | Same as `%y` but uses the year number in ISO 8601 week date. [^3] | |
37 | | `%V` | `27` | Same as `%U` but uses the week number in ISO 8601 week date (01--53). [^3] | |
38 | | | | | |
39 | | `%j` | `189` | Day of the year (001--366), zero-padded to 3 digits. | |
40 | | | | | |
41 | | `%D` | `07/08/01` | Month-day-year format. Same as `%m/%d/%y`. | |
42 | | `%x` | `07/08/01` | Locale's date representation (e.g., 12/31/99). | |
43 | | `%F` | `2001-07-08` | Year-month-day format (ISO 8601). Same as `%Y-%m-%d`. | |
44 | | `%v` | ` 8-Jul-2001` | Day-month-year format. Same as `%e-%b-%Y`. | |
45 | | | | | |
46 | | | | **TIME SPECIFIERS:** | |
47 | | `%H` | `00` | Hour number (00--23), zero-padded to 2 digits. | |
48 | | `%k` | ` 0` | Same as `%H` but space-padded. Same as `%_H`. | |
49 | | `%I` | `12` | Hour number in 12-hour clocks (01--12), zero-padded to 2 digits. | |
50 | | `%l` | `12` | Same as `%I` but space-padded. Same as `%_I`. | |
51 | | | | | |
52 | | `%P` | `am` | `am` or `pm` in 12-hour clocks. | |
53 | | `%p` | `AM` | `AM` or `PM` in 12-hour clocks. | |
54 | | | | | |
55 | | `%M` | `34` | Minute number (00--59), zero-padded to 2 digits. | |
56 | | `%S` | `60` | Second number (00--60), zero-padded to 2 digits. [^4] | |
57 | | `%f` | `26490000` | Number of nanoseconds since last whole second. [^7] | |
58 | | `%.f` | `.026490`| Decimal fraction of a second. Consumes the leading dot. [^7] | |
59 | | `%.3f`| `.026` | Decimal fraction of a second with a fixed length of 3. | |
60 | | `%.6f`| `.026490` | Decimal fraction of a second with a fixed length of 6. | |
61 | | `%.9f`| `.026490000` | Decimal fraction of a second with a fixed length of 9. | |
62 | | `%3f` | `026` | Decimal fraction of a second like `%.3f` but without the leading dot. | |
63 | | `%6f` | `026490` | Decimal fraction of a second like `%.6f` but without the leading dot. | |
64 | | `%9f` | `026490000` | Decimal fraction of a second like `%.9f` but without the leading dot. | |
65 | | | | | |
66 | | `%R` | `00:34` | Hour-minute format. Same as `%H:%M`. | |
67 | | `%T` | `00:34:60` | Hour-minute-second format. Same as `%H:%M:%S`. | |
68 | | `%X` | `00:34:60` | Locale's time representation (e.g., 23:13:48). | |
69 | | `%r` | `12:34:60 AM` | Locale's 12 hour clock time. (e.g., 11:11:04 PM). Falls back to `%X` if the locale does not have a 12 hour clock format. | |
70 | | | | | |
71 | | | | **TIME ZONE SPECIFIERS:** | |
72 | | `%Z` | `ACST` | Local time zone name. Skips all non-whitespace characters during parsing. Identical to `%:z` when formatting. [^8] | |
73 | | `%z` | `+0930` | Offset from the local time to UTC (with UTC being `+0000`). | |
74 | | `%:z` | `+09:30` | Same as `%z` but with a colon. | |
75 | |`%::z`|`+09:30:00`| Offset from the local time to UTC with seconds. | |
76 | |`%:::z`| `+09` | Offset from the local time to UTC without minutes. | |
77 | | `%#z` | `+09` | *Parsing only:* Same as `%z` but allows minutes to be missing or present. | |
78 | | | | | |
79 | | | | **DATE & TIME SPECIFIERS:** | |
80 | |`%c`|`Sun Jul 8 00:34:60 2001`|Locale's date and time (e.g., Thu Mar 3 23:05:25 2005). | |
81 | | `%+` | `2001-07-08T00:34:60.026490+09:30` | ISO 8601 / RFC 3339 date & time format. [^5] | |
82 | | | | | |
83 | | `%s` | `994518299` | UNIX timestamp, the number of seconds since 1970-01-01 00:00 UTC. [^6]| |
84 | | | | | |
85 | | | | **SPECIAL SPECIFIERS:** | |
86 | | `%t` | | Literal tab (`\t`). | |
87 | | `%n` | | Literal newline (`\n`). | |
88 | | `%%` | | Literal percent sign. | |
89 | |
90 | It is possible to override the default padding behavior of numeric specifiers `%?`. |
91 | This is not allowed for other specifiers and will result in the `BAD_FORMAT` error. |
92 | |
93 | Modifier | Description |
94 | -------- | ----------- |
95 | `%-?` | Suppresses any padding including spaces and zeroes. (e.g. `%j` = `012`, `%-j` = `12`) |
96 | `%_?` | Uses spaces as a padding. (e.g. `%j` = `012`, `%_j` = ` 12`) |
97 | `%0?` | Uses zeroes as a padding. (e.g. `%e` = ` 9`, `%0e` = `09`) |
98 | |
99 | Notes: |
100 | |
101 | [^1]: `%C`, `%y`: |
102 | This is floor division, so 100 BCE (year number -99) will print `-1` and `99` respectively. |
103 | For `%y`, values greater or equal to 70 are interpreted as being in the 20th century, |
104 | values smaller than 70 in the 21st century. |
105 | |
106 | [^2]: `%U`: |
107 | Week 1 starts with the first Sunday in that year. |
108 | It is possible to have week 0 for days before the first Sunday. |
109 | |
110 | [^3]: `%G`, `%g`, `%V`: |
111 | Week 1 is the first week with at least 4 days in that year. |
112 | Week 0 does not exist, so this should be used with `%G` or `%g`. |
113 | |
114 | [^4]: `%S`: |
115 | It accounts for leap seconds, so `60` is possible. |
116 | |
117 | [^5]: `%+`: Same as `%Y-%m-%dT%H:%M:%S%.f%:z`, i.e. 0, 3, 6 or 9 fractional |
118 | digits for seconds and colons in the time zone offset. |
119 | <br> |
120 | <br> |
121 | This format also supports having a `Z` or `UTC` in place of `%:z`. They |
122 | are equivalent to `+00:00`. |
123 | <br> |
124 | <br> |
125 | Note that all `T`, `Z`, and `UTC` are parsed case-insensitively. |
126 | <br> |
127 | <br> |
128 | The typical `strftime` implementations have different (and locale-dependent) |
129 | formats for this specifier. While Chrono's format for `%+` is far more |
130 | stable, it is best to avoid this specifier if you want to control the exact |
131 | output. |
132 | |
133 | [^6]: `%s`: |
134 | This is not padded and can be negative. |
135 | For the purpose of Chrono, it only accounts for non-leap seconds |
136 | so it slightly differs from ISO C `strftime` behavior. |
137 | |
138 | [^7]: `%f`, `%.f`: |
139 | <br> |
140 | `%f` and `%.f` are notably different formatting specifiers.<br> |
141 | `%f` counts the number of nanoseconds since the last whole second, while `%.f` is a fraction of a |
142 | second.<br> |
143 | Example: 7μs is formatted as `7000` with `%f`, and formatted as `.000007` with `%.f`. |
144 | |
145 | [^8]: `%Z`: |
146 | Since `chrono` is not aware of timezones beyond their offsets, this specifier |
147 | **only prints the offset** when used for formatting. The timezone abbreviation |
148 | will NOT be printed. See [this issue](https://github.com/chronotope/chrono/issues/960) |
149 | for more information. |
150 | <br> |
151 | <br> |
152 | Offset will not be populated from the parsed data, nor will it be validated. |
153 | Timezone is completely ignored. Similar to the glibc `strptime` treatment of |
154 | this format code. |
155 | <br> |
156 | <br> |
157 | It is not possible to reliably convert from an abbreviation to an offset, |
158 | for example CDT can mean either Central Daylight Time (North America) or |
159 | China Daylight Time. |
160 | */ |
161 | |
162 | #[cfg (feature = "alloc" )] |
163 | extern crate alloc; |
164 | |
165 | #[cfg (any(feature = "alloc" , feature = "std" ))] |
166 | use super::{BAD_FORMAT, ParseError}; |
167 | use super::{Fixed, InternalInternal, Item, Numeric, Pad}; |
168 | #[cfg (feature = "unstable-locales" )] |
169 | use super::{Locale, locales}; |
170 | use super::{fixed, internal_fixed, num, num0, nums}; |
171 | #[cfg (all(feature = "alloc" , not(feature = "std" ), not(test)))] |
172 | use alloc::vec::Vec; |
173 | |
174 | /// Parsing iterator for `strftime`-like format strings. |
175 | /// |
176 | /// See the [`format::strftime` module](crate::format::strftime) for supported formatting |
177 | /// specifiers. |
178 | /// |
179 | /// `StrftimeItems` is used in combination with more low-level methods such as [`format::parse()`] |
180 | /// or [`format_with_items`]. |
181 | /// |
182 | /// If formatting or parsing date and time values is not performance-critical, the methods |
183 | /// [`parse_from_str`] and [`format`] on types such as [`DateTime`](crate::DateTime) are easier to |
184 | /// use. |
185 | /// |
186 | /// [`format`]: crate::DateTime::format |
187 | /// [`format_with_items`]: crate::DateTime::format |
188 | /// [`parse_from_str`]: crate::DateTime::parse_from_str |
189 | /// [`DateTime`]: crate::DateTime |
190 | /// [`format::parse()`]: crate::format::parse() |
191 | #[derive (Clone, Debug)] |
192 | pub struct StrftimeItems<'a> { |
193 | /// Remaining portion of the string. |
194 | remainder: &'a str, |
195 | /// If the current specifier is composed of multiple formatting items (e.g. `%+`), |
196 | /// `queue` stores a slice of `Item`s that have to be returned one by one. |
197 | queue: &'static [Item<'static>], |
198 | #[cfg (feature = "unstable-locales" )] |
199 | locale_str: &'a str, |
200 | #[cfg (feature = "unstable-locales" )] |
201 | locale: Option<Locale>, |
202 | } |
203 | |
204 | impl<'a> StrftimeItems<'a> { |
205 | /// Creates a new parsing iterator from a `strftime`-like format string. |
206 | /// |
207 | /// # Errors |
208 | /// |
209 | /// While iterating [`Item::Error`] will be returned if the format string contains an invalid |
210 | /// or unrecognized formatting specifier. |
211 | /// |
212 | /// # Example |
213 | /// |
214 | /// ``` |
215 | /// use chrono::format::*; |
216 | /// |
217 | /// let strftime_parser = StrftimeItems::new("%F" ); // %F: year-month-day (ISO 8601) |
218 | /// |
219 | /// const ISO8601_YMD_ITEMS: &[Item<'static>] = &[ |
220 | /// Item::Numeric(Numeric::Year, Pad::Zero), |
221 | /// Item::Literal("-" ), |
222 | /// Item::Numeric(Numeric::Month, Pad::Zero), |
223 | /// Item::Literal("-" ), |
224 | /// Item::Numeric(Numeric::Day, Pad::Zero), |
225 | /// ]; |
226 | /// assert!(strftime_parser.eq(ISO8601_YMD_ITEMS.iter().cloned())); |
227 | /// ``` |
228 | #[must_use ] |
229 | pub const fn new(s: &'a str) -> StrftimeItems<'a> { |
230 | #[cfg (not(feature = "unstable-locales" ))] |
231 | { |
232 | StrftimeItems { remainder: s, queue: &[] } |
233 | } |
234 | #[cfg (feature = "unstable-locales" )] |
235 | { |
236 | StrftimeItems { remainder: s, queue: &[], locale_str: "" , locale: None } |
237 | } |
238 | } |
239 | |
240 | /// Creates a new parsing iterator from a `strftime`-like format string, with some formatting |
241 | /// specifiers adjusted to match [`Locale`]. |
242 | /// |
243 | /// Note: `StrftimeItems::new_with_locale` only localizes the *format*. You usually want to |
244 | /// combine it with other locale-aware methods such as |
245 | /// [`DateTime::format_localized_with_items`] to get things like localized month or day names. |
246 | /// |
247 | /// The `%x` formatting specifier will use the local date format, `%X` the local time format, |
248 | /// and `%c` the local format for date and time. |
249 | /// `%r` will use the local 12-hour clock format (e.g., 11:11:04 PM). Not all locales have such |
250 | /// a format, in which case we fall back to a 24-hour clock (`%X`). |
251 | /// |
252 | /// See the [`format::strftime` module](crate::format::strftime) for all supported formatting |
253 | /// specifiers. |
254 | /// |
255 | /// [`DateTime::format_localized_with_items`]: crate::DateTime::format_localized_with_items |
256 | /// |
257 | /// # Errors |
258 | /// |
259 | /// While iterating [`Item::Error`] will be returned if the format string contains an invalid |
260 | /// or unrecognized formatting specifier. |
261 | /// |
262 | /// # Example |
263 | /// |
264 | /// ``` |
265 | /// # #[cfg(feature = "alloc")] { |
266 | /// use chrono::format::{Locale, StrftimeItems}; |
267 | /// use chrono::{FixedOffset, TimeZone}; |
268 | /// |
269 | /// let dt = FixedOffset::east_opt(9 * 60 * 60) |
270 | /// .unwrap() |
271 | /// .with_ymd_and_hms(2023, 7, 11, 0, 34, 59) |
272 | /// .unwrap(); |
273 | /// |
274 | /// // Note: you usually want to combine `StrftimeItems::new_with_locale` with other |
275 | /// // locale-aware methods such as `DateTime::format_localized_with_items`. |
276 | /// // We use the regular `format_with_items` to show only how the formatting changes. |
277 | /// |
278 | /// let fmtr = dt.format_with_items(StrftimeItems::new_with_locale("%x", Locale::en_US)); |
279 | /// assert_eq!(fmtr.to_string(), "07/11/2023"); |
280 | /// let fmtr = dt.format_with_items(StrftimeItems::new_with_locale("%x", Locale::ko_KR)); |
281 | /// assert_eq!(fmtr.to_string(), "2023년 07월 11일"); |
282 | /// let fmtr = dt.format_with_items(StrftimeItems::new_with_locale("%x", Locale::ja_JP)); |
283 | /// assert_eq!(fmtr.to_string(), "2023年07月11日"); |
284 | /// # } |
285 | /// ``` |
286 | #[cfg (feature = "unstable-locales" )] |
287 | #[must_use ] |
288 | pub const fn new_with_locale(s: &'a str, locale: Locale) -> StrftimeItems<'a> { |
289 | StrftimeItems { remainder: s, queue: &[], locale_str: "" , locale: Some(locale) } |
290 | } |
291 | |
292 | /// Parse format string into a `Vec` of formatting [`Item`]'s. |
293 | /// |
294 | /// If you need to format or parse multiple values with the same format string, it is more |
295 | /// efficient to convert it to a `Vec` of formatting [`Item`]'s than to re-parse the format |
296 | /// string on every use. |
297 | /// |
298 | /// The `format_with_items` methods on [`DateTime`], [`NaiveDateTime`], [`NaiveDate`] and |
299 | /// [`NaiveTime`] accept the result for formatting. [`format::parse()`] can make use of it for |
300 | /// parsing. |
301 | /// |
302 | /// [`DateTime`]: crate::DateTime::format_with_items |
303 | /// [`NaiveDateTime`]: crate::NaiveDateTime::format_with_items |
304 | /// [`NaiveDate`]: crate::NaiveDate::format_with_items |
305 | /// [`NaiveTime`]: crate::NaiveTime::format_with_items |
306 | /// [`format::parse()`]: crate::format::parse() |
307 | /// |
308 | /// # Errors |
309 | /// |
310 | /// Returns an error if the format string contains an invalid or unrecognized formatting |
311 | /// specifier. |
312 | /// |
313 | /// # Example |
314 | /// |
315 | /// ``` |
316 | /// use chrono::format::{parse, Parsed, StrftimeItems}; |
317 | /// use chrono::NaiveDate; |
318 | /// |
319 | /// let fmt_items = StrftimeItems::new("%e %b %Y %k.%M" ).parse()?; |
320 | /// let datetime = NaiveDate::from_ymd_opt(2023, 7, 11).unwrap().and_hms_opt(9, 0, 0).unwrap(); |
321 | /// |
322 | /// // Formatting |
323 | /// assert_eq!( |
324 | /// datetime.format_with_items(fmt_items.as_slice().iter()).to_string(), |
325 | /// "11 Jul 2023 9.00" |
326 | /// ); |
327 | /// |
328 | /// // Parsing |
329 | /// let mut parsed = Parsed::new(); |
330 | /// parse(&mut parsed, "11 Jul 2023 9.00" , fmt_items.as_slice().iter())?; |
331 | /// let parsed_dt = parsed.to_naive_datetime_with_offset(0)?; |
332 | /// assert_eq!(parsed_dt, datetime); |
333 | /// # Ok::<(), chrono::ParseError>(()) |
334 | /// ``` |
335 | #[cfg (any(feature = "alloc" , feature = "std" ))] |
336 | pub fn parse(self) -> Result<Vec<Item<'a>>, ParseError> { |
337 | self.into_iter() |
338 | .map(|item| match item == Item::Error { |
339 | false => Ok(item), |
340 | true => Err(BAD_FORMAT), |
341 | }) |
342 | .collect() |
343 | } |
344 | |
345 | /// Parse format string into a `Vec` of [`Item`]'s that contain no references to slices of the |
346 | /// format string. |
347 | /// |
348 | /// A `Vec` created with [`StrftimeItems::parse`] contains references to the format string, |
349 | /// binding the lifetime of the `Vec` to that string. [`StrftimeItems::parse_to_owned`] will |
350 | /// convert the references to owned types. |
351 | /// |
352 | /// # Errors |
353 | /// |
354 | /// Returns an error if the format string contains an invalid or unrecognized formatting |
355 | /// specifier. |
356 | /// |
357 | /// # Example |
358 | /// |
359 | /// ``` |
360 | /// use chrono::format::{Item, ParseError, StrftimeItems}; |
361 | /// use chrono::NaiveDate; |
362 | /// |
363 | /// fn format_items(date_fmt: &str, time_fmt: &str) -> Result<Vec<Item<'static>>, ParseError> { |
364 | /// // `fmt_string` is dropped at the end of this function. |
365 | /// let fmt_string = format!("{} {}" , date_fmt, time_fmt); |
366 | /// StrftimeItems::new(&fmt_string).parse_to_owned() |
367 | /// } |
368 | /// |
369 | /// let fmt_items = format_items("%e %b %Y" , "%k.%M" )?; |
370 | /// let datetime = NaiveDate::from_ymd_opt(2023, 7, 11).unwrap().and_hms_opt(9, 0, 0).unwrap(); |
371 | /// |
372 | /// assert_eq!( |
373 | /// datetime.format_with_items(fmt_items.as_slice().iter()).to_string(), |
374 | /// "11 Jul 2023 9.00" |
375 | /// ); |
376 | /// # Ok::<(), ParseError>(()) |
377 | /// ``` |
378 | #[cfg (any(feature = "alloc" , feature = "std" ))] |
379 | pub fn parse_to_owned(self) -> Result<Vec<Item<'static>>, ParseError> { |
380 | self.into_iter() |
381 | .map(|item| match item == Item::Error { |
382 | false => Ok(item.to_owned()), |
383 | true => Err(BAD_FORMAT), |
384 | }) |
385 | .collect() |
386 | } |
387 | } |
388 | |
389 | const HAVE_ALTERNATES: &str = "z" ; |
390 | |
391 | impl<'a> Iterator for StrftimeItems<'a> { |
392 | type Item = Item<'a>; |
393 | |
394 | fn next(&mut self) -> Option<Item<'a>> { |
395 | // We have items queued to return from a specifier composed of multiple formatting items. |
396 | if let Some((item: &Item<'_>, remainder: &[Item<'_>])) = self.queue.split_first() { |
397 | self.queue = remainder; |
398 | return Some(item.clone()); |
399 | } |
400 | |
401 | // We are in the middle of parsing the localized formatting string of a specifier. |
402 | #[cfg (feature = "unstable-locales" )] |
403 | if !self.locale_str.is_empty() { |
404 | let (remainder, item) = self.parse_next_item(self.locale_str)?; |
405 | self.locale_str = remainder; |
406 | return Some(item); |
407 | } |
408 | |
409 | // Normal: we are parsing the formatting string. |
410 | let (remainder: &'a str, item: Item<'a>) = self.parse_next_item(self.remainder)?; |
411 | self.remainder = remainder; |
412 | Some(item) |
413 | } |
414 | } |
415 | |
416 | impl<'a> StrftimeItems<'a> { |
417 | fn parse_next_item(&mut self, mut remainder: &'a str) -> Option<(&'a str, Item<'a>)> { |
418 | use InternalInternal::*; |
419 | use Item::{Literal, Space}; |
420 | use Numeric::*; |
421 | |
422 | static D_FMT: &[Item<'static>] = |
423 | &[num0(Month), Literal("/" ), num0(Day), Literal("/" ), num0(YearMod100)]; |
424 | static D_T_FMT: &[Item<'static>] = &[ |
425 | fixed(Fixed::ShortWeekdayName), |
426 | Space(" " ), |
427 | fixed(Fixed::ShortMonthName), |
428 | Space(" " ), |
429 | nums(Day), |
430 | Space(" " ), |
431 | num0(Hour), |
432 | Literal(":" ), |
433 | num0(Minute), |
434 | Literal(":" ), |
435 | num0(Second), |
436 | Space(" " ), |
437 | num0(Year), |
438 | ]; |
439 | static T_FMT: &[Item<'static>] = |
440 | &[num0(Hour), Literal(":" ), num0(Minute), Literal(":" ), num0(Second)]; |
441 | static T_FMT_AMPM: &[Item<'static>] = &[ |
442 | num0(Hour12), |
443 | Literal(":" ), |
444 | num0(Minute), |
445 | Literal(":" ), |
446 | num0(Second), |
447 | Space(" " ), |
448 | fixed(Fixed::UpperAmPm), |
449 | ]; |
450 | |
451 | match remainder.chars().next() { |
452 | // we are done |
453 | None => None, |
454 | |
455 | // the next item is a specifier |
456 | Some('%' ) => { |
457 | remainder = &remainder[1..]; |
458 | |
459 | macro_rules! next { |
460 | () => { |
461 | match remainder.chars().next() { |
462 | Some(x) => { |
463 | remainder = &remainder[x.len_utf8()..]; |
464 | x |
465 | } |
466 | None => return Some((remainder, Item::Error)), // premature end of string |
467 | } |
468 | }; |
469 | } |
470 | |
471 | let spec = next!(); |
472 | let pad_override = match spec { |
473 | '-' => Some(Pad::None), |
474 | '0' => Some(Pad::Zero), |
475 | '_' => Some(Pad::Space), |
476 | _ => None, |
477 | }; |
478 | let is_alternate = spec == '#' ; |
479 | let spec = if pad_override.is_some() || is_alternate { next!() } else { spec }; |
480 | if is_alternate && !HAVE_ALTERNATES.contains(spec) { |
481 | return Some((remainder, Item::Error)); |
482 | } |
483 | |
484 | macro_rules! queue { |
485 | [$head:expr, $($tail:expr),+ $(,)*] => ({ |
486 | const QUEUE: &'static [Item<'static>] = &[$($tail),+]; |
487 | self.queue = QUEUE; |
488 | $head |
489 | }) |
490 | } |
491 | #[cfg (not(feature = "unstable-locales" ))] |
492 | macro_rules! queue_from_slice { |
493 | ($slice:expr) => {{ |
494 | self.queue = &$slice[1..]; |
495 | $slice[0].clone() |
496 | }}; |
497 | } |
498 | |
499 | let item = match spec { |
500 | 'A' => fixed(Fixed::LongWeekdayName), |
501 | 'B' => fixed(Fixed::LongMonthName), |
502 | 'C' => num0(YearDiv100), |
503 | 'D' => { |
504 | queue![num0(Month), Literal("/" ), num0(Day), Literal("/" ), num0(YearMod100)] |
505 | } |
506 | 'F' => queue![num0(Year), Literal("-" ), num0(Month), Literal("-" ), num0(Day)], |
507 | 'G' => num0(IsoYear), |
508 | 'H' => num0(Hour), |
509 | 'I' => num0(Hour12), |
510 | 'M' => num0(Minute), |
511 | 'P' => fixed(Fixed::LowerAmPm), |
512 | 'R' => queue![num0(Hour), Literal(":" ), num0(Minute)], |
513 | 'S' => num0(Second), |
514 | 'T' => { |
515 | queue![num0(Hour), Literal(":" ), num0(Minute), Literal(":" ), num0(Second)] |
516 | } |
517 | 'U' => num0(WeekFromSun), |
518 | 'V' => num0(IsoWeek), |
519 | 'W' => num0(WeekFromMon), |
520 | #[cfg (not(feature = "unstable-locales" ))] |
521 | 'X' => queue_from_slice!(T_FMT), |
522 | #[cfg (feature = "unstable-locales" )] |
523 | 'X' => self.switch_to_locale_str(locales::t_fmt, T_FMT), |
524 | 'Y' => num0(Year), |
525 | 'Z' => fixed(Fixed::TimezoneName), |
526 | 'a' => fixed(Fixed::ShortWeekdayName), |
527 | 'b' | 'h' => fixed(Fixed::ShortMonthName), |
528 | #[cfg (not(feature = "unstable-locales" ))] |
529 | 'c' => queue_from_slice!(D_T_FMT), |
530 | #[cfg (feature = "unstable-locales" )] |
531 | 'c' => self.switch_to_locale_str(locales::d_t_fmt, D_T_FMT), |
532 | 'd' => num0(Day), |
533 | 'e' => nums(Day), |
534 | 'f' => num0(Nanosecond), |
535 | 'g' => num0(IsoYearMod100), |
536 | 'j' => num0(Ordinal), |
537 | 'k' => nums(Hour), |
538 | 'l' => nums(Hour12), |
539 | 'm' => num0(Month), |
540 | 'n' => Space(" \n" ), |
541 | 'p' => fixed(Fixed::UpperAmPm), |
542 | 'q' => num(Quarter), |
543 | #[cfg (not(feature = "unstable-locales" ))] |
544 | 'r' => queue_from_slice!(T_FMT_AMPM), |
545 | #[cfg (feature = "unstable-locales" )] |
546 | 'r' => { |
547 | if self.locale.is_some() |
548 | && locales::t_fmt_ampm(self.locale.unwrap()).is_empty() |
549 | { |
550 | // 12-hour clock not supported by this locale. Switch to 24-hour format. |
551 | self.switch_to_locale_str(locales::t_fmt, T_FMT) |
552 | } else { |
553 | self.switch_to_locale_str(locales::t_fmt_ampm, T_FMT_AMPM) |
554 | } |
555 | } |
556 | 's' => num(Timestamp), |
557 | 't' => Space(" \t" ), |
558 | 'u' => num(WeekdayFromMon), |
559 | 'v' => { |
560 | queue![ |
561 | nums(Day), |
562 | Literal("-" ), |
563 | fixed(Fixed::ShortMonthName), |
564 | Literal("-" ), |
565 | num0(Year) |
566 | ] |
567 | } |
568 | 'w' => num(NumDaysFromSun), |
569 | #[cfg (not(feature = "unstable-locales" ))] |
570 | 'x' => queue_from_slice!(D_FMT), |
571 | #[cfg (feature = "unstable-locales" )] |
572 | 'x' => self.switch_to_locale_str(locales::d_fmt, D_FMT), |
573 | 'y' => num0(YearMod100), |
574 | 'z' => { |
575 | if is_alternate { |
576 | internal_fixed(TimezoneOffsetPermissive) |
577 | } else { |
578 | fixed(Fixed::TimezoneOffset) |
579 | } |
580 | } |
581 | '+' => fixed(Fixed::RFC3339), |
582 | ':' => { |
583 | if remainder.starts_with("::z" ) { |
584 | remainder = &remainder[3..]; |
585 | fixed(Fixed::TimezoneOffsetTripleColon) |
586 | } else if remainder.starts_with(":z" ) { |
587 | remainder = &remainder[2..]; |
588 | fixed(Fixed::TimezoneOffsetDoubleColon) |
589 | } else if remainder.starts_with('z' ) { |
590 | remainder = &remainder[1..]; |
591 | fixed(Fixed::TimezoneOffsetColon) |
592 | } else { |
593 | Item::Error |
594 | } |
595 | } |
596 | '.' => match next!() { |
597 | '3' => match next!() { |
598 | 'f' => fixed(Fixed::Nanosecond3), |
599 | _ => Item::Error, |
600 | }, |
601 | '6' => match next!() { |
602 | 'f' => fixed(Fixed::Nanosecond6), |
603 | _ => Item::Error, |
604 | }, |
605 | '9' => match next!() { |
606 | 'f' => fixed(Fixed::Nanosecond9), |
607 | _ => Item::Error, |
608 | }, |
609 | 'f' => fixed(Fixed::Nanosecond), |
610 | _ => Item::Error, |
611 | }, |
612 | '3' => match next!() { |
613 | 'f' => internal_fixed(Nanosecond3NoDot), |
614 | _ => Item::Error, |
615 | }, |
616 | '6' => match next!() { |
617 | 'f' => internal_fixed(Nanosecond6NoDot), |
618 | _ => Item::Error, |
619 | }, |
620 | '9' => match next!() { |
621 | 'f' => internal_fixed(Nanosecond9NoDot), |
622 | _ => Item::Error, |
623 | }, |
624 | '%' => Literal("%" ), |
625 | _ => Item::Error, // no such specifier |
626 | }; |
627 | |
628 | // Adjust `item` if we have any padding modifier. |
629 | // Not allowed on non-numeric items or on specifiers composed out of multiple |
630 | // formatting items. |
631 | if let Some(new_pad) = pad_override { |
632 | match item { |
633 | Item::Numeric(ref kind, _pad) if self.queue.is_empty() => { |
634 | Some((remainder, Item::Numeric(kind.clone(), new_pad))) |
635 | } |
636 | _ => Some((remainder, Item::Error)), |
637 | } |
638 | } else { |
639 | Some((remainder, item)) |
640 | } |
641 | } |
642 | |
643 | // the next item is space |
644 | Some(c) if c.is_whitespace() => { |
645 | // `%` is not a whitespace, so `c != '%'` is redundant |
646 | let nextspec = |
647 | remainder.find(|c: char| !c.is_whitespace()).unwrap_or(remainder.len()); |
648 | assert!(nextspec > 0); |
649 | let item = Space(&remainder[..nextspec]); |
650 | remainder = &remainder[nextspec..]; |
651 | Some((remainder, item)) |
652 | } |
653 | |
654 | // the next item is literal |
655 | _ => { |
656 | let nextspec = remainder |
657 | .find(|c: char| c.is_whitespace() || c == '%' ) |
658 | .unwrap_or(remainder.len()); |
659 | assert!(nextspec > 0); |
660 | let item = Literal(&remainder[..nextspec]); |
661 | remainder = &remainder[nextspec..]; |
662 | Some((remainder, item)) |
663 | } |
664 | } |
665 | } |
666 | |
667 | #[cfg (feature = "unstable-locales" )] |
668 | fn switch_to_locale_str( |
669 | &mut self, |
670 | localized_fmt_str: impl Fn(Locale) -> &'static str, |
671 | fallback: &'static [Item<'static>], |
672 | ) -> Item<'a> { |
673 | if let Some(locale) = self.locale { |
674 | assert!(self.locale_str.is_empty()); |
675 | let (fmt_str, item) = self.parse_next_item(localized_fmt_str(locale)).unwrap(); |
676 | self.locale_str = fmt_str; |
677 | item |
678 | } else { |
679 | self.queue = &fallback[1..]; |
680 | fallback[0].clone() |
681 | } |
682 | } |
683 | } |
684 | |
685 | #[cfg (test)] |
686 | mod tests { |
687 | use super::StrftimeItems; |
688 | use crate::format::Item::{self, Literal, Space}; |
689 | #[cfg (feature = "unstable-locales" )] |
690 | use crate::format::Locale; |
691 | use crate::format::{Fixed, InternalInternal, Numeric::*}; |
692 | use crate::format::{fixed, internal_fixed, num, num0, nums}; |
693 | #[cfg (feature = "alloc" )] |
694 | use crate::{DateTime, FixedOffset, NaiveDate, TimeZone, Timelike, Utc}; |
695 | |
696 | #[test ] |
697 | fn test_strftime_items() { |
698 | fn parse_and_collect(s: &str) -> Vec<Item<'_>> { |
699 | // map any error into `[Item::Error]`. useful for easy testing. |
700 | eprintln!("test_strftime_items: parse_and_collect({:?})" , s); |
701 | let items = StrftimeItems::new(s); |
702 | let items = items.map(|spec| if spec == Item::Error { None } else { Some(spec) }); |
703 | items.collect::<Option<Vec<_>>>().unwrap_or_else(|| vec![Item::Error]) |
704 | } |
705 | |
706 | assert_eq!(parse_and_collect("" ), []); |
707 | assert_eq!(parse_and_collect(" " ), [Space(" " )]); |
708 | assert_eq!(parse_and_collect(" " ), [Space(" " )]); |
709 | // ne! |
710 | assert_ne!(parse_and_collect(" " ), [Space(" " ), Space(" " )]); |
711 | // eq! |
712 | assert_eq!(parse_and_collect(" " ), [Space(" " )]); |
713 | assert_eq!(parse_and_collect("a" ), [Literal("a" )]); |
714 | assert_eq!(parse_and_collect("ab" ), [Literal("ab" )]); |
715 | assert_eq!(parse_and_collect("😽" ), [Literal("😽" )]); |
716 | assert_eq!(parse_and_collect("a😽" ), [Literal("a😽" )]); |
717 | assert_eq!(parse_and_collect("😽a" ), [Literal("😽a" )]); |
718 | assert_eq!(parse_and_collect(" 😽" ), [Space(" " ), Literal("😽" )]); |
719 | assert_eq!(parse_and_collect("😽 " ), [Literal("😽" ), Space(" " )]); |
720 | // ne! |
721 | assert_ne!(parse_and_collect("😽😽" ), [Literal("😽" )]); |
722 | assert_ne!(parse_and_collect("😽" ), [Literal("😽😽" )]); |
723 | assert_ne!(parse_and_collect("😽😽" ), [Literal("😽😽" ), Literal("😽" )]); |
724 | // eq! |
725 | assert_eq!(parse_and_collect("😽😽" ), [Literal("😽😽" )]); |
726 | assert_eq!(parse_and_collect(" \t\n\r " ), [Space(" \t\n\r " )]); |
727 | assert_eq!(parse_and_collect("hello?" ), [Literal("hello?" )]); |
728 | assert_eq!( |
729 | parse_and_collect("a b \t\nc" ), |
730 | [Literal("a" ), Space(" " ), Literal("b" ), Space(" \t\n" ), Literal("c" )] |
731 | ); |
732 | assert_eq!(parse_and_collect("100%%" ), [Literal("100" ), Literal("%" )]); |
733 | assert_eq!( |
734 | parse_and_collect("100%% ok" ), |
735 | [Literal("100" ), Literal("%" ), Space(" " ), Literal("ok" )] |
736 | ); |
737 | assert_eq!(parse_and_collect("%%PDF-1.0" ), [Literal("%" ), Literal("PDF-1.0" )]); |
738 | assert_eq!( |
739 | parse_and_collect("%Y-%m-%d" ), |
740 | [num0(Year), Literal("-" ), num0(Month), Literal("-" ), num0(Day)] |
741 | ); |
742 | assert_eq!(parse_and_collect("😽 " ), [Literal("😽" ), Space(" " )]); |
743 | assert_eq!(parse_and_collect("😽😽" ), [Literal("😽😽" )]); |
744 | assert_eq!(parse_and_collect("😽😽😽" ), [Literal("😽😽😽" )]); |
745 | assert_eq!(parse_and_collect("😽😽 😽" ), [Literal("😽😽" ), Space(" " ), Literal("😽" )]); |
746 | assert_eq!(parse_and_collect("😽😽a 😽" ), [Literal("😽😽a" ), Space(" " ), Literal("😽" )]); |
747 | assert_eq!(parse_and_collect("😽😽a b😽" ), [Literal("😽😽a" ), Space(" " ), Literal("b😽" )]); |
748 | assert_eq!( |
749 | parse_and_collect("😽😽a b😽c" ), |
750 | [Literal("😽😽a" ), Space(" " ), Literal("b😽c" )] |
751 | ); |
752 | assert_eq!(parse_and_collect("😽😽 " ), [Literal("😽😽" ), Space(" " )]); |
753 | assert_eq!(parse_and_collect("😽😽 😽" ), [Literal("😽😽" ), Space(" " ), Literal("😽" )]); |
754 | assert_eq!(parse_and_collect(" 😽" ), [Space(" " ), Literal("😽" )]); |
755 | assert_eq!(parse_and_collect(" 😽 " ), [Space(" " ), Literal("😽" ), Space(" " )]); |
756 | assert_eq!( |
757 | parse_and_collect(" 😽 😽" ), |
758 | [Space(" " ), Literal("😽" ), Space(" " ), Literal("😽" )] |
759 | ); |
760 | assert_eq!( |
761 | parse_and_collect(" 😽 😽 " ), |
762 | [Space(" " ), Literal("😽" ), Space(" " ), Literal("😽" ), Space(" " )] |
763 | ); |
764 | assert_eq!( |
765 | parse_and_collect(" 😽 😽 " ), |
766 | [Space(" " ), Literal("😽" ), Space(" " ), Literal("😽" ), Space(" " )] |
767 | ); |
768 | assert_eq!( |
769 | parse_and_collect(" 😽 😽😽 " ), |
770 | [Space(" " ), Literal("😽" ), Space(" " ), Literal("😽😽" ), Space(" " )] |
771 | ); |
772 | assert_eq!(parse_and_collect(" 😽😽" ), [Space(" " ), Literal("😽😽" )]); |
773 | assert_eq!(parse_and_collect(" 😽😽 " ), [Space(" " ), Literal("😽😽" ), Space(" " )]); |
774 | assert_eq!( |
775 | parse_and_collect(" 😽😽 " ), |
776 | [Space(" " ), Literal("😽😽" ), Space(" " )] |
777 | ); |
778 | assert_eq!( |
779 | parse_and_collect(" 😽😽 " ), |
780 | [Space(" " ), Literal("😽😽" ), Space(" " )] |
781 | ); |
782 | assert_eq!(parse_and_collect(" 😽😽 " ), [Space(" " ), Literal("😽😽" ), Space(" " )]); |
783 | assert_eq!( |
784 | parse_and_collect(" 😽 😽😽 " ), |
785 | [Space(" " ), Literal("😽" ), Space(" " ), Literal("😽😽" ), Space(" " )] |
786 | ); |
787 | assert_eq!( |
788 | parse_and_collect(" 😽 😽はい😽 ハンバーガー" ), |
789 | [ |
790 | Space(" " ), |
791 | Literal("😽" ), |
792 | Space(" " ), |
793 | Literal("😽はい😽" ), |
794 | Space(" " ), |
795 | Literal("ハンバーガー" ) |
796 | ] |
797 | ); |
798 | assert_eq!( |
799 | parse_and_collect("%%😽%%😽" ), |
800 | [Literal("%" ), Literal("😽" ), Literal("%" ), Literal("😽" )] |
801 | ); |
802 | assert_eq!(parse_and_collect("%Y--%m" ), [num0(Year), Literal("--" ), num0(Month)]); |
803 | assert_eq!(parse_and_collect("[%F]" ), parse_and_collect("[%Y-%m-%d]" )); |
804 | assert_eq!(parse_and_collect("100%%😽" ), [Literal("100" ), Literal("%" ), Literal("😽" )]); |
805 | assert_eq!( |
806 | parse_and_collect("100%%😽%%a" ), |
807 | [Literal("100" ), Literal("%" ), Literal("😽" ), Literal("%" ), Literal("a" )] |
808 | ); |
809 | assert_eq!(parse_and_collect("😽100%%" ), [Literal("😽100" ), Literal("%" )]); |
810 | assert_eq!(parse_and_collect("%m %d" ), [num0(Month), Space(" " ), num0(Day)]); |
811 | assert_eq!(parse_and_collect("%" ), [Item::Error]); |
812 | assert_eq!(parse_and_collect("%%" ), [Literal("%" )]); |
813 | assert_eq!(parse_and_collect("%%%" ), [Item::Error]); |
814 | assert_eq!(parse_and_collect("%a" ), [fixed(Fixed::ShortWeekdayName)]); |
815 | assert_eq!(parse_and_collect("%aa" ), [fixed(Fixed::ShortWeekdayName), Literal("a" )]); |
816 | assert_eq!(parse_and_collect("%%a%" ), [Item::Error]); |
817 | assert_eq!(parse_and_collect("%😽" ), [Item::Error]); |
818 | assert_eq!(parse_and_collect("%😽😽" ), [Item::Error]); |
819 | assert_eq!(parse_and_collect("%%%%" ), [Literal("%" ), Literal("%" )]); |
820 | assert_eq!( |
821 | parse_and_collect("%%%%ハンバーガー" ), |
822 | [Literal("%" ), Literal("%" ), Literal("ハンバーガー" )] |
823 | ); |
824 | assert_eq!(parse_and_collect("foo%?" ), [Item::Error]); |
825 | assert_eq!(parse_and_collect("bar%42" ), [Item::Error]); |
826 | assert_eq!(parse_and_collect("quux% +" ), [Item::Error]); |
827 | assert_eq!(parse_and_collect("%.Z" ), [Item::Error]); |
828 | assert_eq!(parse_and_collect("%:Z" ), [Item::Error]); |
829 | assert_eq!(parse_and_collect("%-Z" ), [Item::Error]); |
830 | assert_eq!(parse_and_collect("%0Z" ), [Item::Error]); |
831 | assert_eq!(parse_and_collect("%_Z" ), [Item::Error]); |
832 | assert_eq!(parse_and_collect("%.j" ), [Item::Error]); |
833 | assert_eq!(parse_and_collect("%:j" ), [Item::Error]); |
834 | assert_eq!(parse_and_collect("%-j" ), [num(Ordinal)]); |
835 | assert_eq!(parse_and_collect("%0j" ), [num0(Ordinal)]); |
836 | assert_eq!(parse_and_collect("%_j" ), [nums(Ordinal)]); |
837 | assert_eq!(parse_and_collect("%.e" ), [Item::Error]); |
838 | assert_eq!(parse_and_collect("%:e" ), [Item::Error]); |
839 | assert_eq!(parse_and_collect("%-e" ), [num(Day)]); |
840 | assert_eq!(parse_and_collect("%0e" ), [num0(Day)]); |
841 | assert_eq!(parse_and_collect("%_e" ), [nums(Day)]); |
842 | assert_eq!(parse_and_collect("%z" ), [fixed(Fixed::TimezoneOffset)]); |
843 | assert_eq!(parse_and_collect("%:z" ), [fixed(Fixed::TimezoneOffsetColon)]); |
844 | assert_eq!(parse_and_collect("%Z" ), [fixed(Fixed::TimezoneName)]); |
845 | assert_eq!(parse_and_collect("%ZZZZ" ), [fixed(Fixed::TimezoneName), Literal("ZZZ" )]); |
846 | assert_eq!(parse_and_collect("%Z😽" ), [fixed(Fixed::TimezoneName), Literal("😽" )]); |
847 | assert_eq!( |
848 | parse_and_collect("%#z" ), |
849 | [internal_fixed(InternalInternal::TimezoneOffsetPermissive)] |
850 | ); |
851 | assert_eq!(parse_and_collect("%#m" ), [Item::Error]); |
852 | } |
853 | |
854 | #[test ] |
855 | #[cfg (feature = "alloc" )] |
856 | fn test_strftime_docs() { |
857 | let dt = FixedOffset::east_opt(34200) |
858 | .unwrap() |
859 | .from_local_datetime( |
860 | &NaiveDate::from_ymd_opt(2001, 7, 8) |
861 | .unwrap() |
862 | .and_hms_nano_opt(0, 34, 59, 1_026_490_708) |
863 | .unwrap(), |
864 | ) |
865 | .unwrap(); |
866 | |
867 | // date specifiers |
868 | assert_eq!(dt.format("%Y" ).to_string(), "2001" ); |
869 | assert_eq!(dt.format("%C" ).to_string(), "20" ); |
870 | assert_eq!(dt.format("%y" ).to_string(), "01" ); |
871 | assert_eq!(dt.format("%q" ).to_string(), "3" ); |
872 | assert_eq!(dt.format("%m" ).to_string(), "07" ); |
873 | assert_eq!(dt.format("%b" ).to_string(), "Jul" ); |
874 | assert_eq!(dt.format("%B" ).to_string(), "July" ); |
875 | assert_eq!(dt.format("%h" ).to_string(), "Jul" ); |
876 | assert_eq!(dt.format("%d" ).to_string(), "08" ); |
877 | assert_eq!(dt.format("%e" ).to_string(), " 8" ); |
878 | assert_eq!(dt.format("%e" ).to_string(), dt.format("%_d" ).to_string()); |
879 | assert_eq!(dt.format("%a" ).to_string(), "Sun" ); |
880 | assert_eq!(dt.format("%A" ).to_string(), "Sunday" ); |
881 | assert_eq!(dt.format("%w" ).to_string(), "0" ); |
882 | assert_eq!(dt.format("%u" ).to_string(), "7" ); |
883 | assert_eq!(dt.format("%U" ).to_string(), "27" ); |
884 | assert_eq!(dt.format("%W" ).to_string(), "27" ); |
885 | assert_eq!(dt.format("%G" ).to_string(), "2001" ); |
886 | assert_eq!(dt.format("%g" ).to_string(), "01" ); |
887 | assert_eq!(dt.format("%V" ).to_string(), "27" ); |
888 | assert_eq!(dt.format("%j" ).to_string(), "189" ); |
889 | assert_eq!(dt.format("%D" ).to_string(), "07/08/01" ); |
890 | assert_eq!(dt.format("%x" ).to_string(), "07/08/01" ); |
891 | assert_eq!(dt.format("%F" ).to_string(), "2001-07-08" ); |
892 | assert_eq!(dt.format("%v" ).to_string(), " 8-Jul-2001" ); |
893 | |
894 | // time specifiers |
895 | assert_eq!(dt.format("%H" ).to_string(), "00" ); |
896 | assert_eq!(dt.format("%k" ).to_string(), " 0" ); |
897 | assert_eq!(dt.format("%k" ).to_string(), dt.format("%_H" ).to_string()); |
898 | assert_eq!(dt.format("%I" ).to_string(), "12" ); |
899 | assert_eq!(dt.format("%l" ).to_string(), "12" ); |
900 | assert_eq!(dt.format("%l" ).to_string(), dt.format("%_I" ).to_string()); |
901 | assert_eq!(dt.format("%P" ).to_string(), "am" ); |
902 | assert_eq!(dt.format("%p" ).to_string(), "AM" ); |
903 | assert_eq!(dt.format("%M" ).to_string(), "34" ); |
904 | assert_eq!(dt.format("%S" ).to_string(), "60" ); |
905 | assert_eq!(dt.format("%f" ).to_string(), "026490708" ); |
906 | assert_eq!(dt.format("%.f" ).to_string(), ".026490708" ); |
907 | assert_eq!(dt.with_nanosecond(1_026_490_000).unwrap().format("%.f" ).to_string(), ".026490" ); |
908 | assert_eq!(dt.format("%.3f" ).to_string(), ".026" ); |
909 | assert_eq!(dt.format("%.6f" ).to_string(), ".026490" ); |
910 | assert_eq!(dt.format("%.9f" ).to_string(), ".026490708" ); |
911 | assert_eq!(dt.format("%3f" ).to_string(), "026" ); |
912 | assert_eq!(dt.format("%6f" ).to_string(), "026490" ); |
913 | assert_eq!(dt.format("%9f" ).to_string(), "026490708" ); |
914 | assert_eq!(dt.format("%R" ).to_string(), "00:34" ); |
915 | assert_eq!(dt.format("%T" ).to_string(), "00:34:60" ); |
916 | assert_eq!(dt.format("%X" ).to_string(), "00:34:60" ); |
917 | assert_eq!(dt.format("%r" ).to_string(), "12:34:60 AM" ); |
918 | |
919 | // time zone specifiers |
920 | //assert_eq!(dt.format("%Z").to_string(), "ACST"); |
921 | assert_eq!(dt.format("%z" ).to_string(), "+0930" ); |
922 | assert_eq!(dt.format("%:z" ).to_string(), "+09:30" ); |
923 | assert_eq!(dt.format("%::z" ).to_string(), "+09:30:00" ); |
924 | assert_eq!(dt.format("%:::z" ).to_string(), "+09" ); |
925 | |
926 | // date & time specifiers |
927 | assert_eq!(dt.format("%c" ).to_string(), "Sun Jul 8 00:34:60 2001" ); |
928 | assert_eq!(dt.format("%+" ).to_string(), "2001-07-08T00:34:60.026490708+09:30" ); |
929 | |
930 | assert_eq!( |
931 | dt.with_timezone(&Utc).format("%+" ).to_string(), |
932 | "2001-07-07T15:04:60.026490708+00:00" |
933 | ); |
934 | assert_eq!( |
935 | dt.with_timezone(&Utc), |
936 | DateTime::parse_from_str("2001-07-07T15:04:60.026490708Z" , "%+" ).unwrap() |
937 | ); |
938 | assert_eq!( |
939 | dt.with_timezone(&Utc), |
940 | DateTime::parse_from_str("2001-07-07T15:04:60.026490708UTC" , "%+" ).unwrap() |
941 | ); |
942 | assert_eq!( |
943 | dt.with_timezone(&Utc), |
944 | DateTime::parse_from_str("2001-07-07t15:04:60.026490708utc" , "%+" ).unwrap() |
945 | ); |
946 | |
947 | assert_eq!( |
948 | dt.with_nanosecond(1_026_490_000).unwrap().format("%+" ).to_string(), |
949 | "2001-07-08T00:34:60.026490+09:30" |
950 | ); |
951 | assert_eq!(dt.format("%s" ).to_string(), "994518299" ); |
952 | |
953 | // special specifiers |
954 | assert_eq!(dt.format("%t" ).to_string(), " \t" ); |
955 | assert_eq!(dt.format("%n" ).to_string(), " \n" ); |
956 | assert_eq!(dt.format("%%" ).to_string(), "%" ); |
957 | |
958 | // complex format specifiers |
959 | assert_eq!(dt.format(" %Y%d%m%%%%%t%H%M%S \t" ).to_string(), " 20010807%% \t003460 \t" ); |
960 | assert_eq!( |
961 | dt.format(" %Y%d%m%%%%%t%H:%P:%M%S%:::z \t" ).to_string(), |
962 | " 20010807%% \t00:am:3460+09 \t" |
963 | ); |
964 | } |
965 | |
966 | #[test ] |
967 | #[cfg (all(feature = "unstable-locales" , feature = "alloc" ))] |
968 | fn test_strftime_docs_localized() { |
969 | let dt = FixedOffset::east_opt(34200) |
970 | .unwrap() |
971 | .with_ymd_and_hms(2001, 7, 8, 0, 34, 59) |
972 | .unwrap() |
973 | .with_nanosecond(1_026_490_708) |
974 | .unwrap(); |
975 | |
976 | // date specifiers |
977 | assert_eq!(dt.format_localized("%b" , Locale::fr_BE).to_string(), "jui" ); |
978 | assert_eq!(dt.format_localized("%B" , Locale::fr_BE).to_string(), "juillet" ); |
979 | assert_eq!(dt.format_localized("%h" , Locale::fr_BE).to_string(), "jui" ); |
980 | assert_eq!(dt.format_localized("%a" , Locale::fr_BE).to_string(), "dim" ); |
981 | assert_eq!(dt.format_localized("%A" , Locale::fr_BE).to_string(), "dimanche" ); |
982 | assert_eq!(dt.format_localized("%D" , Locale::fr_BE).to_string(), "07/08/01" ); |
983 | assert_eq!(dt.format_localized("%x" , Locale::fr_BE).to_string(), "08/07/01" ); |
984 | assert_eq!(dt.format_localized("%F" , Locale::fr_BE).to_string(), "2001-07-08" ); |
985 | assert_eq!(dt.format_localized("%v" , Locale::fr_BE).to_string(), " 8-jui-2001" ); |
986 | |
987 | // time specifiers |
988 | assert_eq!(dt.format_localized("%P" , Locale::fr_BE).to_string(), "" ); |
989 | assert_eq!(dt.format_localized("%p" , Locale::fr_BE).to_string(), "" ); |
990 | assert_eq!(dt.format_localized("%R" , Locale::fr_BE).to_string(), "00:34" ); |
991 | assert_eq!(dt.format_localized("%T" , Locale::fr_BE).to_string(), "00:34:60" ); |
992 | assert_eq!(dt.format_localized("%X" , Locale::fr_BE).to_string(), "00:34:60" ); |
993 | assert_eq!(dt.format_localized("%r" , Locale::fr_BE).to_string(), "00:34:60" ); |
994 | |
995 | // date & time specifiers |
996 | assert_eq!( |
997 | dt.format_localized("%c" , Locale::fr_BE).to_string(), |
998 | "dim 08 jui 2001 00:34:60 +09:30" |
999 | ); |
1000 | |
1001 | let nd = NaiveDate::from_ymd_opt(2001, 7, 8).unwrap(); |
1002 | |
1003 | // date specifiers |
1004 | assert_eq!(nd.format_localized("%b" , Locale::de_DE).to_string(), "Jul" ); |
1005 | assert_eq!(nd.format_localized("%B" , Locale::de_DE).to_string(), "Juli" ); |
1006 | assert_eq!(nd.format_localized("%h" , Locale::de_DE).to_string(), "Jul" ); |
1007 | assert_eq!(nd.format_localized("%a" , Locale::de_DE).to_string(), "So" ); |
1008 | assert_eq!(nd.format_localized("%A" , Locale::de_DE).to_string(), "Sonntag" ); |
1009 | assert_eq!(nd.format_localized("%D" , Locale::de_DE).to_string(), "07/08/01" ); |
1010 | assert_eq!(nd.format_localized("%x" , Locale::de_DE).to_string(), "08.07.2001" ); |
1011 | assert_eq!(nd.format_localized("%F" , Locale::de_DE).to_string(), "2001-07-08" ); |
1012 | assert_eq!(nd.format_localized("%v" , Locale::de_DE).to_string(), " 8-Jul-2001" ); |
1013 | } |
1014 | |
1015 | /// Ensure parsing a timestamp with the parse-only stftime formatter "%#z" does |
1016 | /// not cause a panic. |
1017 | /// |
1018 | /// See <https://github.com/chronotope/chrono/issues/1139>. |
1019 | #[test ] |
1020 | #[cfg (feature = "alloc" )] |
1021 | fn test_parse_only_timezone_offset_permissive_no_panic() { |
1022 | use crate::NaiveDate; |
1023 | use crate::{FixedOffset, TimeZone}; |
1024 | use std::fmt::Write; |
1025 | |
1026 | let dt = FixedOffset::east_opt(34200) |
1027 | .unwrap() |
1028 | .from_local_datetime( |
1029 | &NaiveDate::from_ymd_opt(2001, 7, 8) |
1030 | .unwrap() |
1031 | .and_hms_nano_opt(0, 34, 59, 1_026_490_708) |
1032 | .unwrap(), |
1033 | ) |
1034 | .unwrap(); |
1035 | |
1036 | let mut buf = String::new(); |
1037 | let _ = write!(buf, "{}" , dt.format("%#z" )).expect_err("parse-only formatter should fail" ); |
1038 | } |
1039 | |
1040 | #[test ] |
1041 | #[cfg (all(feature = "unstable-locales" , feature = "alloc" ))] |
1042 | fn test_strftime_localized_korean() { |
1043 | let dt = FixedOffset::east_opt(34200) |
1044 | .unwrap() |
1045 | .with_ymd_and_hms(2001, 7, 8, 0, 34, 59) |
1046 | .unwrap() |
1047 | .with_nanosecond(1_026_490_708) |
1048 | .unwrap(); |
1049 | |
1050 | // date specifiers |
1051 | assert_eq!(dt.format_localized("%b" , Locale::ko_KR).to_string(), " 7월" ); |
1052 | assert_eq!(dt.format_localized("%B" , Locale::ko_KR).to_string(), "7월" ); |
1053 | assert_eq!(dt.format_localized("%h" , Locale::ko_KR).to_string(), " 7월" ); |
1054 | assert_eq!(dt.format_localized("%a" , Locale::ko_KR).to_string(), "일" ); |
1055 | assert_eq!(dt.format_localized("%A" , Locale::ko_KR).to_string(), "일요일" ); |
1056 | assert_eq!(dt.format_localized("%D" , Locale::ko_KR).to_string(), "07/08/01" ); |
1057 | assert_eq!(dt.format_localized("%x" , Locale::ko_KR).to_string(), "2001년 07월 08일" ); |
1058 | assert_eq!(dt.format_localized("%F" , Locale::ko_KR).to_string(), "2001-07-08" ); |
1059 | assert_eq!(dt.format_localized("%v" , Locale::ko_KR).to_string(), " 8- 7월-2001" ); |
1060 | assert_eq!(dt.format_localized("%r" , Locale::ko_KR).to_string(), "오전 12시 34분 60초" ); |
1061 | |
1062 | // date & time specifiers |
1063 | assert_eq!( |
1064 | dt.format_localized("%c" , Locale::ko_KR).to_string(), |
1065 | "2001년 07월 08일 (일) 오전 12시 34분 60초" |
1066 | ); |
1067 | } |
1068 | |
1069 | #[test ] |
1070 | #[cfg (all(feature = "unstable-locales" , feature = "alloc" ))] |
1071 | fn test_strftime_localized_japanese() { |
1072 | let dt = FixedOffset::east_opt(34200) |
1073 | .unwrap() |
1074 | .with_ymd_and_hms(2001, 7, 8, 0, 34, 59) |
1075 | .unwrap() |
1076 | .with_nanosecond(1_026_490_708) |
1077 | .unwrap(); |
1078 | |
1079 | // date specifiers |
1080 | assert_eq!(dt.format_localized("%b" , Locale::ja_JP).to_string(), " 7月" ); |
1081 | assert_eq!(dt.format_localized("%B" , Locale::ja_JP).to_string(), "7月" ); |
1082 | assert_eq!(dt.format_localized("%h" , Locale::ja_JP).to_string(), " 7月" ); |
1083 | assert_eq!(dt.format_localized("%a" , Locale::ja_JP).to_string(), "日" ); |
1084 | assert_eq!(dt.format_localized("%A" , Locale::ja_JP).to_string(), "日曜日" ); |
1085 | assert_eq!(dt.format_localized("%D" , Locale::ja_JP).to_string(), "07/08/01" ); |
1086 | assert_eq!(dt.format_localized("%x" , Locale::ja_JP).to_string(), "2001年07月08日" ); |
1087 | assert_eq!(dt.format_localized("%F" , Locale::ja_JP).to_string(), "2001-07-08" ); |
1088 | assert_eq!(dt.format_localized("%v" , Locale::ja_JP).to_string(), " 8- 7月-2001" ); |
1089 | assert_eq!(dt.format_localized("%r" , Locale::ja_JP).to_string(), "午前12時34分60秒" ); |
1090 | |
1091 | // date & time specifiers |
1092 | assert_eq!( |
1093 | dt.format_localized("%c" , Locale::ja_JP).to_string(), |
1094 | "2001年07月08日 00時34分60秒" |
1095 | ); |
1096 | } |
1097 | |
1098 | #[test ] |
1099 | #[cfg (all(feature = "unstable-locales" , feature = "alloc" ))] |
1100 | fn test_strftime_localized_time() { |
1101 | let dt1 = Utc.with_ymd_and_hms(2024, 2, 9, 6, 54, 32).unwrap(); |
1102 | let dt2 = Utc.with_ymd_and_hms(2024, 2, 9, 18, 54, 32).unwrap(); |
1103 | // Some of these locales gave issues before pure-rust-locales 0.8.0 with chrono 0.4.27+ |
1104 | assert_eq!(dt1.format_localized("%X" , Locale::nl_NL).to_string(), "06:54:32" ); |
1105 | assert_eq!(dt2.format_localized("%X" , Locale::nl_NL).to_string(), "18:54:32" ); |
1106 | assert_eq!(dt1.format_localized("%X" , Locale::en_US).to_string(), "06:54:32 AM" ); |
1107 | assert_eq!(dt2.format_localized("%X" , Locale::en_US).to_string(), "06:54:32 PM" ); |
1108 | assert_eq!(dt1.format_localized("%X" , Locale::hy_AM).to_string(), "06:54:32" ); |
1109 | assert_eq!(dt2.format_localized("%X" , Locale::hy_AM).to_string(), "18:54:32" ); |
1110 | assert_eq!(dt1.format_localized("%X" , Locale::chr_US).to_string(), "06:54:32 ᏌᎾᎴ" ); |
1111 | assert_eq!(dt2.format_localized("%X" , Locale::chr_US).to_string(), "06:54:32 ᏒᎯᏱᎢᏗᏢ" ); |
1112 | } |
1113 | |
1114 | #[test ] |
1115 | #[cfg (all(feature = "unstable-locales" , target_pointer_width = "64" ))] |
1116 | fn test_type_sizes() { |
1117 | use core::mem::size_of; |
1118 | assert_eq!(size_of::<Item>(), 24); |
1119 | assert_eq!(size_of::<StrftimeItems>(), 56); |
1120 | assert_eq!(size_of::<Locale>(), 2); |
1121 | } |
1122 | |
1123 | #[test ] |
1124 | #[cfg (all(feature = "unstable-locales" , target_pointer_width = "32" ))] |
1125 | fn test_type_sizes() { |
1126 | use core::mem::size_of; |
1127 | assert_eq!(size_of::<Item>(), 12); |
1128 | assert_eq!(size_of::<StrftimeItems>(), 28); |
1129 | assert_eq!(size_of::<Locale>(), 2); |
1130 | } |
1131 | |
1132 | #[test ] |
1133 | #[cfg (any(feature = "alloc" , feature = "std" ))] |
1134 | fn test_strftime_parse() { |
1135 | let fmt_str = StrftimeItems::new("%Y-%m-%dT%H:%M:%S%z" ); |
1136 | let fmt_items = fmt_str.parse().unwrap(); |
1137 | let dt = Utc.with_ymd_and_hms(2014, 5, 7, 12, 34, 56).unwrap(); |
1138 | assert_eq!(&dt.format_with_items(fmt_items.iter()).to_string(), "2014-05-07T12:34:56+0000" ); |
1139 | } |
1140 | } |
1141 | |