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