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