1 | // This is a part of Chrono. |
2 | // See README.md and LICENSE.txt for details. |
3 | |
4 | //! Date and time formatting routines. |
5 | |
6 | #[cfg (all(feature = "alloc" , not(feature = "std" ), not(test)))] |
7 | use alloc::string::{String, ToString}; |
8 | #[cfg (feature = "alloc" )] |
9 | use core::borrow::Borrow; |
10 | #[cfg (feature = "alloc" )] |
11 | use core::fmt::Display; |
12 | use core::fmt::{self, Write}; |
13 | |
14 | #[cfg (feature = "alloc" )] |
15 | use crate::offset::Offset; |
16 | #[cfg (any(feature = "alloc" , feature = "serde" ))] |
17 | use crate::{Datelike, FixedOffset, NaiveDateTime, Timelike}; |
18 | #[cfg (feature = "alloc" )] |
19 | use crate::{NaiveDate, NaiveTime, Weekday}; |
20 | |
21 | #[cfg (feature = "alloc" )] |
22 | use super::locales; |
23 | #[cfg (any(feature = "alloc" , feature = "serde" ))] |
24 | use super::{Colons, OffsetFormat, OffsetPrecision, Pad}; |
25 | #[cfg (feature = "alloc" )] |
26 | use super::{Fixed, InternalFixed, InternalInternal, Item, Numeric}; |
27 | #[cfg (feature = "alloc" )] |
28 | use locales::*; |
29 | |
30 | /// A *temporary* object which can be used as an argument to `format!` or others. |
31 | /// This is normally constructed via `format` methods of each date and time type. |
32 | #[cfg (feature = "alloc" )] |
33 | #[derive (Debug)] |
34 | pub struct DelayedFormat<I> { |
35 | /// The date view, if any. |
36 | date: Option<NaiveDate>, |
37 | /// The time view, if any. |
38 | time: Option<NaiveTime>, |
39 | /// The name and local-to-UTC difference for the offset (timezone), if any. |
40 | off: Option<(String, FixedOffset)>, |
41 | /// An iterator returning formatting items. |
42 | items: I, |
43 | /// Locale used for text. |
44 | /// ZST if the `unstable-locales` feature is not enabled. |
45 | locale: Locale, |
46 | } |
47 | |
48 | #[cfg (feature = "alloc" )] |
49 | impl<'a, I: Iterator<Item = B> + Clone, B: Borrow<Item<'a>>> DelayedFormat<I> { |
50 | /// Makes a new `DelayedFormat` value out of local date and time. |
51 | #[must_use ] |
52 | pub fn new(date: Option<NaiveDate>, time: Option<NaiveTime>, items: I) -> DelayedFormat<I> { |
53 | DelayedFormat { date, time, off: None, items, locale: default_locale() } |
54 | } |
55 | |
56 | /// Makes a new `DelayedFormat` value out of local date and time and UTC offset. |
57 | #[must_use ] |
58 | pub fn new_with_offset<Off>( |
59 | date: Option<NaiveDate>, |
60 | time: Option<NaiveTime>, |
61 | offset: &Off, |
62 | items: I, |
63 | ) -> DelayedFormat<I> |
64 | where |
65 | Off: Offset + Display, |
66 | { |
67 | let name_and_diff = (offset.to_string(), offset.fix()); |
68 | DelayedFormat { date, time, off: Some(name_and_diff), items, locale: default_locale() } |
69 | } |
70 | |
71 | /// Makes a new `DelayedFormat` value out of local date and time and locale. |
72 | #[cfg (feature = "unstable-locales" )] |
73 | #[must_use ] |
74 | pub fn new_with_locale( |
75 | date: Option<NaiveDate>, |
76 | time: Option<NaiveTime>, |
77 | items: I, |
78 | locale: Locale, |
79 | ) -> DelayedFormat<I> { |
80 | DelayedFormat { date, time, off: None, items, locale } |
81 | } |
82 | |
83 | /// Makes a new `DelayedFormat` value out of local date and time, UTC offset and locale. |
84 | #[cfg (feature = "unstable-locales" )] |
85 | #[must_use ] |
86 | pub fn new_with_offset_and_locale<Off>( |
87 | date: Option<NaiveDate>, |
88 | time: Option<NaiveTime>, |
89 | offset: &Off, |
90 | items: I, |
91 | locale: Locale, |
92 | ) -> DelayedFormat<I> |
93 | where |
94 | Off: Offset + Display, |
95 | { |
96 | let name_and_diff = (offset.to_string(), offset.fix()); |
97 | DelayedFormat { date, time, off: Some(name_and_diff), items, locale } |
98 | } |
99 | |
100 | fn format(&self, w: &mut impl Write) -> fmt::Result { |
101 | for item in self.items.clone() { |
102 | match *item.borrow() { |
103 | Item::Literal(s) | Item::Space(s) => w.write_str(s), |
104 | #[cfg (feature = "alloc" )] |
105 | Item::OwnedLiteral(ref s) | Item::OwnedSpace(ref s) => w.write_str(s), |
106 | Item::Numeric(ref spec, pad) => self.format_numeric(w, spec, pad), |
107 | Item::Fixed(ref spec) => self.format_fixed(w, spec), |
108 | Item::Error => Err(fmt::Error), |
109 | }?; |
110 | } |
111 | Ok(()) |
112 | } |
113 | |
114 | #[cfg (feature = "alloc" )] |
115 | fn format_numeric(&self, w: &mut impl Write, spec: &Numeric, pad: Pad) -> fmt::Result { |
116 | use self::Numeric::*; |
117 | |
118 | fn write_one(w: &mut impl Write, v: u8) -> fmt::Result { |
119 | w.write_char((b'0' + v) as char) |
120 | } |
121 | |
122 | fn write_two(w: &mut impl Write, v: u8, pad: Pad) -> fmt::Result { |
123 | let ones = b'0' + v % 10; |
124 | match (v / 10, pad) { |
125 | (0, Pad::None) => {} |
126 | (0, Pad::Space) => w.write_char(' ' )?, |
127 | (tens, _) => w.write_char((b'0' + tens) as char)?, |
128 | } |
129 | w.write_char(ones as char) |
130 | } |
131 | |
132 | #[inline ] |
133 | fn write_year(w: &mut impl Write, year: i32, pad: Pad) -> fmt::Result { |
134 | if (1000..=9999).contains(&year) { |
135 | // fast path |
136 | write_hundreds(w, (year / 100) as u8)?; |
137 | write_hundreds(w, (year % 100) as u8) |
138 | } else { |
139 | write_n(w, 4, year as i64, pad, !(0..10_000).contains(&year)) |
140 | } |
141 | } |
142 | |
143 | fn write_n( |
144 | w: &mut impl Write, |
145 | n: usize, |
146 | v: i64, |
147 | pad: Pad, |
148 | always_sign: bool, |
149 | ) -> fmt::Result { |
150 | if always_sign { |
151 | match pad { |
152 | Pad::None => write!(w, " {:+}" , v), |
153 | Pad::Zero => write!(w, " {:+01$}" , v, n + 1), |
154 | Pad::Space => write!(w, " {:+1$}" , v, n + 1), |
155 | } |
156 | } else { |
157 | match pad { |
158 | Pad::None => write!(w, " {}" , v), |
159 | Pad::Zero => write!(w, " {:01$}" , v, n), |
160 | Pad::Space => write!(w, " {:1$}" , v, n), |
161 | } |
162 | } |
163 | } |
164 | |
165 | match (spec, self.date, self.time) { |
166 | (Year, Some(d), _) => write_year(w, d.year(), pad), |
167 | (YearDiv100, Some(d), _) => write_two(w, d.year().div_euclid(100) as u8, pad), |
168 | (YearMod100, Some(d), _) => write_two(w, d.year().rem_euclid(100) as u8, pad), |
169 | (IsoYear, Some(d), _) => write_year(w, d.iso_week().year(), pad), |
170 | (IsoYearDiv100, Some(d), _) => { |
171 | write_two(w, d.iso_week().year().div_euclid(100) as u8, pad) |
172 | } |
173 | (IsoYearMod100, Some(d), _) => { |
174 | write_two(w, d.iso_week().year().rem_euclid(100) as u8, pad) |
175 | } |
176 | (Month, Some(d), _) => write_two(w, d.month() as u8, pad), |
177 | (Day, Some(d), _) => write_two(w, d.day() as u8, pad), |
178 | (WeekFromSun, Some(d), _) => write_two(w, d.weeks_from(Weekday::Sun) as u8, pad), |
179 | (WeekFromMon, Some(d), _) => write_two(w, d.weeks_from(Weekday::Mon) as u8, pad), |
180 | (IsoWeek, Some(d), _) => write_two(w, d.iso_week().week() as u8, pad), |
181 | (NumDaysFromSun, Some(d), _) => write_one(w, d.weekday().num_days_from_sunday() as u8), |
182 | (WeekdayFromMon, Some(d), _) => write_one(w, d.weekday().number_from_monday() as u8), |
183 | (Ordinal, Some(d), _) => write_n(w, 3, d.ordinal() as i64, pad, false), |
184 | (Hour, _, Some(t)) => write_two(w, t.hour() as u8, pad), |
185 | (Hour12, _, Some(t)) => write_two(w, t.hour12().1 as u8, pad), |
186 | (Minute, _, Some(t)) => write_two(w, t.minute() as u8, pad), |
187 | (Second, _, Some(t)) => { |
188 | write_two(w, (t.second() + t.nanosecond() / 1_000_000_000) as u8, pad) |
189 | } |
190 | (Nanosecond, _, Some(t)) => { |
191 | write_n(w, 9, (t.nanosecond() % 1_000_000_000) as i64, pad, false) |
192 | } |
193 | (Timestamp, Some(d), Some(t)) => { |
194 | let offset = self.off.as_ref().map(|(_, o)| i64::from(o.local_minus_utc())); |
195 | let timestamp = d.and_time(t).and_utc().timestamp() - offset.unwrap_or(0); |
196 | write_n(w, 9, timestamp, pad, false) |
197 | } |
198 | (Internal(_), _, _) => Ok(()), // for future expansion |
199 | _ => Err(fmt::Error), // insufficient arguments for given format |
200 | } |
201 | } |
202 | |
203 | #[cfg (feature = "alloc" )] |
204 | fn format_fixed(&self, w: &mut impl Write, spec: &Fixed) -> fmt::Result { |
205 | use Fixed::*; |
206 | use InternalInternal::*; |
207 | |
208 | match (spec, self.date, self.time, self.off.as_ref()) { |
209 | (ShortMonthName, Some(d), _, _) => { |
210 | w.write_str(short_months(self.locale)[d.month0() as usize]) |
211 | } |
212 | (LongMonthName, Some(d), _, _) => { |
213 | w.write_str(long_months(self.locale)[d.month0() as usize]) |
214 | } |
215 | (ShortWeekdayName, Some(d), _, _) => w.write_str( |
216 | short_weekdays(self.locale)[d.weekday().num_days_from_sunday() as usize], |
217 | ), |
218 | (LongWeekdayName, Some(d), _, _) => { |
219 | w.write_str(long_weekdays(self.locale)[d.weekday().num_days_from_sunday() as usize]) |
220 | } |
221 | (LowerAmPm, _, Some(t), _) => { |
222 | let ampm = if t.hour12().0 { am_pm(self.locale)[1] } else { am_pm(self.locale)[0] }; |
223 | for c in ampm.chars().flat_map(|c| c.to_lowercase()) { |
224 | w.write_char(c)? |
225 | } |
226 | Ok(()) |
227 | } |
228 | (UpperAmPm, _, Some(t), _) => { |
229 | let ampm = if t.hour12().0 { am_pm(self.locale)[1] } else { am_pm(self.locale)[0] }; |
230 | w.write_str(ampm) |
231 | } |
232 | (Nanosecond, _, Some(t), _) => { |
233 | let nano = t.nanosecond() % 1_000_000_000; |
234 | if nano == 0 { |
235 | Ok(()) |
236 | } else { |
237 | w.write_str(decimal_point(self.locale))?; |
238 | if nano % 1_000_000 == 0 { |
239 | write!(w, " {:03}" , nano / 1_000_000) |
240 | } else if nano % 1_000 == 0 { |
241 | write!(w, " {:06}" , nano / 1_000) |
242 | } else { |
243 | write!(w, " {:09}" , nano) |
244 | } |
245 | } |
246 | } |
247 | (Nanosecond3, _, Some(t), _) => { |
248 | w.write_str(decimal_point(self.locale))?; |
249 | write!(w, " {:03}" , t.nanosecond() / 1_000_000 % 1000) |
250 | } |
251 | (Nanosecond6, _, Some(t), _) => { |
252 | w.write_str(decimal_point(self.locale))?; |
253 | write!(w, " {:06}" , t.nanosecond() / 1_000 % 1_000_000) |
254 | } |
255 | (Nanosecond9, _, Some(t), _) => { |
256 | w.write_str(decimal_point(self.locale))?; |
257 | write!(w, " {:09}" , t.nanosecond() % 1_000_000_000) |
258 | } |
259 | (Internal(InternalFixed { val: Nanosecond3NoDot }), _, Some(t), _) => { |
260 | write!(w, " {:03}" , t.nanosecond() / 1_000_000 % 1_000) |
261 | } |
262 | (Internal(InternalFixed { val: Nanosecond6NoDot }), _, Some(t), _) => { |
263 | write!(w, " {:06}" , t.nanosecond() / 1_000 % 1_000_000) |
264 | } |
265 | (Internal(InternalFixed { val: Nanosecond9NoDot }), _, Some(t), _) => { |
266 | write!(w, " {:09}" , t.nanosecond() % 1_000_000_000) |
267 | } |
268 | (TimezoneName, _, _, Some((tz_name, _))) => write!(w, " {}" , tz_name), |
269 | (TimezoneOffset | TimezoneOffsetZ, _, _, Some((_, off))) => { |
270 | let offset_format = OffsetFormat { |
271 | precision: OffsetPrecision::Minutes, |
272 | colons: Colons::Maybe, |
273 | allow_zulu: *spec == TimezoneOffsetZ, |
274 | padding: Pad::Zero, |
275 | }; |
276 | offset_format.format(w, *off) |
277 | } |
278 | (TimezoneOffsetColon | TimezoneOffsetColonZ, _, _, Some((_, off))) => { |
279 | let offset_format = OffsetFormat { |
280 | precision: OffsetPrecision::Minutes, |
281 | colons: Colons::Colon, |
282 | allow_zulu: *spec == TimezoneOffsetColonZ, |
283 | padding: Pad::Zero, |
284 | }; |
285 | offset_format.format(w, *off) |
286 | } |
287 | (TimezoneOffsetDoubleColon, _, _, Some((_, off))) => { |
288 | let offset_format = OffsetFormat { |
289 | precision: OffsetPrecision::Seconds, |
290 | colons: Colons::Colon, |
291 | allow_zulu: false, |
292 | padding: Pad::Zero, |
293 | }; |
294 | offset_format.format(w, *off) |
295 | } |
296 | (TimezoneOffsetTripleColon, _, _, Some((_, off))) => { |
297 | let offset_format = OffsetFormat { |
298 | precision: OffsetPrecision::Hours, |
299 | colons: Colons::None, |
300 | allow_zulu: false, |
301 | padding: Pad::Zero, |
302 | }; |
303 | offset_format.format(w, *off) |
304 | } |
305 | (RFC2822, Some(d), Some(t), Some((_, off))) => { |
306 | write_rfc2822(w, crate::NaiveDateTime::new(d, t), *off) |
307 | } |
308 | (RFC3339, Some(d), Some(t), Some((_, off))) => write_rfc3339( |
309 | w, |
310 | crate::NaiveDateTime::new(d, t), |
311 | *off, |
312 | SecondsFormat::AutoSi, |
313 | false, |
314 | ), |
315 | _ => Err(fmt::Error), // insufficient arguments for given format |
316 | } |
317 | } |
318 | } |
319 | |
320 | #[cfg (feature = "alloc" )] |
321 | impl<'a, I: Iterator<Item = B> + Clone, B: Borrow<Item<'a>>> Display for DelayedFormat<I> { |
322 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
323 | let mut result: String = String::new(); |
324 | self.format(&mut result)?; |
325 | f.pad(&result) |
326 | } |
327 | } |
328 | |
329 | /// Tries to format given arguments with given formatting items. |
330 | /// Internally used by `DelayedFormat`. |
331 | #[cfg (feature = "alloc" )] |
332 | #[deprecated (since = "0.4.32" , note = "Use DelayedFormat::fmt instead" )] |
333 | pub fn format<'a, I, B>( |
334 | w: &mut fmt::Formatter, |
335 | date: Option<&NaiveDate>, |
336 | time: Option<&NaiveTime>, |
337 | off: Option<&(String, FixedOffset)>, |
338 | items: I, |
339 | ) -> fmt::Result |
340 | where |
341 | I: Iterator<Item = B> + Clone, |
342 | B: Borrow<Item<'a>>, |
343 | { |
344 | DelayedFormat { |
345 | date: date.copied(), |
346 | time: time.copied(), |
347 | off: off.cloned(), |
348 | items, |
349 | locale: default_locale(), |
350 | } |
351 | .fmt(w) |
352 | } |
353 | |
354 | /// Formats single formatting item |
355 | #[cfg (feature = "alloc" )] |
356 | #[deprecated (since = "0.4.32" , note = "Use DelayedFormat::fmt instead" )] |
357 | pub fn format_item( |
358 | w: &mut fmt::Formatter, |
359 | date: Option<&NaiveDate>, |
360 | time: Option<&NaiveTime>, |
361 | off: Option<&(String, FixedOffset)>, |
362 | item: &Item<'_>, |
363 | ) -> fmt::Result { |
364 | DelayedFormat { |
365 | date: date.copied(), |
366 | time: time.copied(), |
367 | off: off.cloned(), |
368 | items: [item].into_iter(), |
369 | locale: default_locale(), |
370 | } |
371 | .fmt(w) |
372 | } |
373 | |
374 | #[cfg (any(feature = "alloc" , feature = "serde" ))] |
375 | impl OffsetFormat { |
376 | /// Writes an offset from UTC with the format defined by `self`. |
377 | fn format(&self, w: &mut impl Write, off: FixedOffset) -> fmt::Result { |
378 | let off = off.local_minus_utc(); |
379 | if self.allow_zulu && off == 0 { |
380 | w.write_char('Z' )?; |
381 | return Ok(()); |
382 | } |
383 | let (sign, off) = if off < 0 { ('-' , -off) } else { ('+' , off) }; |
384 | |
385 | let hours; |
386 | let mut mins = 0; |
387 | let mut secs = 0; |
388 | let precision = match self.precision { |
389 | OffsetPrecision::Hours => { |
390 | // Minutes and seconds are simply truncated |
391 | hours = (off / 3600) as u8; |
392 | OffsetPrecision::Hours |
393 | } |
394 | OffsetPrecision::Minutes | OffsetPrecision::OptionalMinutes => { |
395 | // Round seconds to the nearest minute. |
396 | let minutes = (off + 30) / 60; |
397 | mins = (minutes % 60) as u8; |
398 | hours = (minutes / 60) as u8; |
399 | if self.precision == OffsetPrecision::OptionalMinutes && mins == 0 { |
400 | OffsetPrecision::Hours |
401 | } else { |
402 | OffsetPrecision::Minutes |
403 | } |
404 | } |
405 | OffsetPrecision::Seconds |
406 | | OffsetPrecision::OptionalSeconds |
407 | | OffsetPrecision::OptionalMinutesAndSeconds => { |
408 | let minutes = off / 60; |
409 | secs = (off % 60) as u8; |
410 | mins = (minutes % 60) as u8; |
411 | hours = (minutes / 60) as u8; |
412 | if self.precision != OffsetPrecision::Seconds && secs == 0 { |
413 | if self.precision == OffsetPrecision::OptionalMinutesAndSeconds && mins == 0 { |
414 | OffsetPrecision::Hours |
415 | } else { |
416 | OffsetPrecision::Minutes |
417 | } |
418 | } else { |
419 | OffsetPrecision::Seconds |
420 | } |
421 | } |
422 | }; |
423 | let colons = self.colons == Colons::Colon; |
424 | |
425 | if hours < 10 { |
426 | if self.padding == Pad::Space { |
427 | w.write_char(' ' )?; |
428 | } |
429 | w.write_char(sign)?; |
430 | if self.padding == Pad::Zero { |
431 | w.write_char('0' )?; |
432 | } |
433 | w.write_char((b'0' + hours) as char)?; |
434 | } else { |
435 | w.write_char(sign)?; |
436 | write_hundreds(w, hours)?; |
437 | } |
438 | if let OffsetPrecision::Minutes | OffsetPrecision::Seconds = precision { |
439 | if colons { |
440 | w.write_char(':' )?; |
441 | } |
442 | write_hundreds(w, mins)?; |
443 | } |
444 | if let OffsetPrecision::Seconds = precision { |
445 | if colons { |
446 | w.write_char(':' )?; |
447 | } |
448 | write_hundreds(w, secs)?; |
449 | } |
450 | Ok(()) |
451 | } |
452 | } |
453 | |
454 | /// Specific formatting options for seconds. This may be extended in the |
455 | /// future, so exhaustive matching in external code is not recommended. |
456 | /// |
457 | /// See the `TimeZone::to_rfc3339_opts` function for usage. |
458 | #[derive (Clone, Copy, Debug, Eq, PartialEq, Hash)] |
459 | #[allow (clippy::manual_non_exhaustive)] |
460 | pub enum SecondsFormat { |
461 | /// Format whole seconds only, with no decimal point nor subseconds. |
462 | Secs, |
463 | |
464 | /// Use fixed 3 subsecond digits. This corresponds to [Fixed::Nanosecond3]. |
465 | Millis, |
466 | |
467 | /// Use fixed 6 subsecond digits. This corresponds to [Fixed::Nanosecond6]. |
468 | Micros, |
469 | |
470 | /// Use fixed 9 subsecond digits. This corresponds to [Fixed::Nanosecond9]. |
471 | Nanos, |
472 | |
473 | /// Automatically select one of `Secs`, `Millis`, `Micros`, or `Nanos` to display all available |
474 | /// non-zero sub-second digits. This corresponds to [Fixed::Nanosecond]. |
475 | AutoSi, |
476 | |
477 | // Do not match against this. |
478 | #[doc (hidden)] |
479 | __NonExhaustive, |
480 | } |
481 | |
482 | /// Writes the date, time and offset to the string. same as `%Y-%m-%dT%H:%M:%S%.f%:z` |
483 | #[inline ] |
484 | #[cfg (any(feature = "alloc" , feature = "serde" ))] |
485 | pub(crate) fn write_rfc3339( |
486 | w: &mut impl Write, |
487 | dt: NaiveDateTime, |
488 | off: FixedOffset, |
489 | secform: SecondsFormat, |
490 | use_z: bool, |
491 | ) -> fmt::Result { |
492 | let year = dt.date().year(); |
493 | if (0..=9999).contains(&year) { |
494 | write_hundreds(w, (year / 100) as u8)?; |
495 | write_hundreds(w, (year % 100) as u8)?; |
496 | } else { |
497 | // ISO 8601 requires the explicit sign for out-of-range years |
498 | write!(w, " {:+05}" , year)?; |
499 | } |
500 | w.write_char('-' )?; |
501 | write_hundreds(w, dt.date().month() as u8)?; |
502 | w.write_char('-' )?; |
503 | write_hundreds(w, dt.date().day() as u8)?; |
504 | |
505 | w.write_char('T' )?; |
506 | |
507 | let (hour, min, mut sec) = dt.time().hms(); |
508 | let mut nano = dt.nanosecond(); |
509 | if nano >= 1_000_000_000 { |
510 | sec += 1; |
511 | nano -= 1_000_000_000; |
512 | } |
513 | write_hundreds(w, hour as u8)?; |
514 | w.write_char(':' )?; |
515 | write_hundreds(w, min as u8)?; |
516 | w.write_char(':' )?; |
517 | let sec = sec; |
518 | write_hundreds(w, sec as u8)?; |
519 | |
520 | match secform { |
521 | SecondsFormat::Secs => {} |
522 | SecondsFormat::Millis => write!(w, ". {:03}" , nano / 1_000_000)?, |
523 | SecondsFormat::Micros => write!(w, ". {:06}" , nano / 1000)?, |
524 | SecondsFormat::Nanos => write!(w, ". {:09}" , nano)?, |
525 | SecondsFormat::AutoSi => { |
526 | if nano == 0 { |
527 | } else if nano % 1_000_000 == 0 { |
528 | write!(w, ". {:03}" , nano / 1_000_000)? |
529 | } else if nano % 1_000 == 0 { |
530 | write!(w, ". {:06}" , nano / 1_000)? |
531 | } else { |
532 | write!(w, ". {:09}" , nano)? |
533 | } |
534 | } |
535 | SecondsFormat::__NonExhaustive => unreachable!(), |
536 | }; |
537 | |
538 | OffsetFormat { |
539 | precision: OffsetPrecision::Minutes, |
540 | colons: Colons::Colon, |
541 | allow_zulu: use_z, |
542 | padding: Pad::Zero, |
543 | } |
544 | .format(w, off) |
545 | } |
546 | |
547 | #[cfg (feature = "alloc" )] |
548 | /// write datetimes like `Tue, 1 Jul 2003 10:52:37 +0200`, same as `%a, %d %b %Y %H:%M:%S %z` |
549 | pub(crate) fn write_rfc2822( |
550 | w: &mut impl Write, |
551 | dt: NaiveDateTime, |
552 | off: FixedOffset, |
553 | ) -> fmt::Result { |
554 | let year = dt.year(); |
555 | // RFC2822 is only defined on years 0 through 9999 |
556 | if !(0..=9999).contains(&year) { |
557 | return Err(fmt::Error); |
558 | } |
559 | |
560 | let english = default_locale(); |
561 | |
562 | w.write_str(short_weekdays(english)[dt.weekday().num_days_from_sunday() as usize])?; |
563 | w.write_str(", " )?; |
564 | let day = dt.day(); |
565 | if day < 10 { |
566 | w.write_char((b'0' + day as u8) as char)?; |
567 | } else { |
568 | write_hundreds(w, day as u8)?; |
569 | } |
570 | w.write_char(' ' )?; |
571 | w.write_str(short_months(english)[dt.month0() as usize])?; |
572 | w.write_char(' ' )?; |
573 | write_hundreds(w, (year / 100) as u8)?; |
574 | write_hundreds(w, (year % 100) as u8)?; |
575 | w.write_char(' ' )?; |
576 | |
577 | let (hour, min, sec) = dt.time().hms(); |
578 | write_hundreds(w, hour as u8)?; |
579 | w.write_char(':' )?; |
580 | write_hundreds(w, min as u8)?; |
581 | w.write_char(':' )?; |
582 | let sec = sec + dt.nanosecond() / 1_000_000_000; |
583 | write_hundreds(w, sec as u8)?; |
584 | w.write_char(' ' )?; |
585 | OffsetFormat { |
586 | precision: OffsetPrecision::Minutes, |
587 | colons: Colons::None, |
588 | allow_zulu: false, |
589 | padding: Pad::Zero, |
590 | } |
591 | .format(w, off) |
592 | } |
593 | |
594 | /// Equivalent to `{:02}` formatting for n < 100. |
595 | pub(crate) fn write_hundreds(w: &mut impl Write, n: u8) -> fmt::Result { |
596 | if n >= 100 { |
597 | return Err(fmt::Error); |
598 | } |
599 | |
600 | let tens: u8 = b'0' + n / 10; |
601 | let ones: u8 = b'0' + n % 10; |
602 | w.write_char(tens as char)?; |
603 | w.write_char(ones as char) |
604 | } |
605 | |
606 | #[cfg (test)] |
607 | #[cfg (feature = "alloc" )] |
608 | mod tests { |
609 | use super::{Colons, OffsetFormat, OffsetPrecision, Pad}; |
610 | use crate::FixedOffset; |
611 | #[cfg (feature = "alloc" )] |
612 | use crate::{NaiveDate, NaiveTime, TimeZone, Timelike, Utc}; |
613 | |
614 | #[test ] |
615 | #[cfg (feature = "alloc" )] |
616 | fn test_date_format() { |
617 | let d = NaiveDate::from_ymd_opt(2012, 3, 4).unwrap(); |
618 | assert_eq!(d.format("%Y,%C,%y,%G,%g" ).to_string(), "2012,20,12,2012,12" ); |
619 | assert_eq!(d.format("%m,%b,%h,%B" ).to_string(), "03,Mar,Mar,March" ); |
620 | assert_eq!(d.format("%d,%e" ).to_string(), "04, 4" ); |
621 | assert_eq!(d.format("%U,%W,%V" ).to_string(), "10,09,09" ); |
622 | assert_eq!(d.format("%a,%A,%w,%u" ).to_string(), "Sun,Sunday,0,7" ); |
623 | assert_eq!(d.format("%j" ).to_string(), "064" ); // since 2012 is a leap year |
624 | assert_eq!(d.format("%D,%x" ).to_string(), "03/04/12,03/04/12" ); |
625 | assert_eq!(d.format("%F" ).to_string(), "2012-03-04" ); |
626 | assert_eq!(d.format("%v" ).to_string(), " 4-Mar-2012" ); |
627 | assert_eq!(d.format("%t%n%%%n%t" ).to_string(), " \t\n% \n\t" ); |
628 | |
629 | // non-four-digit years |
630 | assert_eq!( |
631 | NaiveDate::from_ymd_opt(12345, 1, 1).unwrap().format("%Y" ).to_string(), |
632 | "+12345" |
633 | ); |
634 | assert_eq!(NaiveDate::from_ymd_opt(1234, 1, 1).unwrap().format("%Y" ).to_string(), "1234" ); |
635 | assert_eq!(NaiveDate::from_ymd_opt(123, 1, 1).unwrap().format("%Y" ).to_string(), "0123" ); |
636 | assert_eq!(NaiveDate::from_ymd_opt(12, 1, 1).unwrap().format("%Y" ).to_string(), "0012" ); |
637 | assert_eq!(NaiveDate::from_ymd_opt(1, 1, 1).unwrap().format("%Y" ).to_string(), "0001" ); |
638 | assert_eq!(NaiveDate::from_ymd_opt(0, 1, 1).unwrap().format("%Y" ).to_string(), "0000" ); |
639 | assert_eq!(NaiveDate::from_ymd_opt(-1, 1, 1).unwrap().format("%Y" ).to_string(), "-0001" ); |
640 | assert_eq!(NaiveDate::from_ymd_opt(-12, 1, 1).unwrap().format("%Y" ).to_string(), "-0012" ); |
641 | assert_eq!(NaiveDate::from_ymd_opt(-123, 1, 1).unwrap().format("%Y" ).to_string(), "-0123" ); |
642 | assert_eq!(NaiveDate::from_ymd_opt(-1234, 1, 1).unwrap().format("%Y" ).to_string(), "-1234" ); |
643 | assert_eq!( |
644 | NaiveDate::from_ymd_opt(-12345, 1, 1).unwrap().format("%Y" ).to_string(), |
645 | "-12345" |
646 | ); |
647 | |
648 | // corner cases |
649 | assert_eq!( |
650 | NaiveDate::from_ymd_opt(2007, 12, 31).unwrap().format("%G,%g,%U,%W,%V" ).to_string(), |
651 | "2008,08,52,53,01" |
652 | ); |
653 | assert_eq!( |
654 | NaiveDate::from_ymd_opt(2010, 1, 3).unwrap().format("%G,%g,%U,%W,%V" ).to_string(), |
655 | "2009,09,01,00,53" |
656 | ); |
657 | } |
658 | |
659 | #[test ] |
660 | #[cfg (feature = "alloc" )] |
661 | fn test_time_format() { |
662 | let t = NaiveTime::from_hms_nano_opt(3, 5, 7, 98765432).unwrap(); |
663 | assert_eq!(t.format("%H,%k,%I,%l,%P,%p" ).to_string(), "03, 3,03, 3,am,AM" ); |
664 | assert_eq!(t.format("%M" ).to_string(), "05" ); |
665 | assert_eq!(t.format("%S,%f,%.f" ).to_string(), "07,098765432,.098765432" ); |
666 | assert_eq!(t.format("%.3f,%.6f,%.9f" ).to_string(), ".098,.098765,.098765432" ); |
667 | assert_eq!(t.format("%R" ).to_string(), "03:05" ); |
668 | assert_eq!(t.format("%T,%X" ).to_string(), "03:05:07,03:05:07" ); |
669 | assert_eq!(t.format("%r" ).to_string(), "03:05:07 AM" ); |
670 | assert_eq!(t.format("%t%n%%%n%t" ).to_string(), " \t\n% \n\t" ); |
671 | |
672 | let t = NaiveTime::from_hms_micro_opt(3, 5, 7, 432100).unwrap(); |
673 | assert_eq!(t.format("%S,%f,%.f" ).to_string(), "07,432100000,.432100" ); |
674 | assert_eq!(t.format("%.3f,%.6f,%.9f" ).to_string(), ".432,.432100,.432100000" ); |
675 | |
676 | let t = NaiveTime::from_hms_milli_opt(3, 5, 7, 210).unwrap(); |
677 | assert_eq!(t.format("%S,%f,%.f" ).to_string(), "07,210000000,.210" ); |
678 | assert_eq!(t.format("%.3f,%.6f,%.9f" ).to_string(), ".210,.210000,.210000000" ); |
679 | |
680 | let t = NaiveTime::from_hms_opt(3, 5, 7).unwrap(); |
681 | assert_eq!(t.format("%S,%f,%.f" ).to_string(), "07,000000000," ); |
682 | assert_eq!(t.format("%.3f,%.6f,%.9f" ).to_string(), ".000,.000000,.000000000" ); |
683 | |
684 | // corner cases |
685 | assert_eq!( |
686 | NaiveTime::from_hms_opt(13, 57, 9).unwrap().format("%r" ).to_string(), |
687 | "01:57:09 PM" |
688 | ); |
689 | assert_eq!( |
690 | NaiveTime::from_hms_milli_opt(23, 59, 59, 1_000).unwrap().format("%X" ).to_string(), |
691 | "23:59:60" |
692 | ); |
693 | } |
694 | |
695 | #[test ] |
696 | #[cfg (feature = "alloc" )] |
697 | fn test_datetime_format() { |
698 | let dt = |
699 | NaiveDate::from_ymd_opt(2010, 9, 8).unwrap().and_hms_milli_opt(7, 6, 54, 321).unwrap(); |
700 | assert_eq!(dt.format("%c" ).to_string(), "Wed Sep 8 07:06:54 2010" ); |
701 | assert_eq!(dt.format("%s" ).to_string(), "1283929614" ); |
702 | assert_eq!(dt.format("%t%n%%%n%t" ).to_string(), " \t\n% \n\t" ); |
703 | |
704 | // a horror of leap second: coming near to you. |
705 | let dt = NaiveDate::from_ymd_opt(2012, 6, 30) |
706 | .unwrap() |
707 | .and_hms_milli_opt(23, 59, 59, 1_000) |
708 | .unwrap(); |
709 | assert_eq!(dt.format("%c" ).to_string(), "Sat Jun 30 23:59:60 2012" ); |
710 | assert_eq!(dt.format("%s" ).to_string(), "1341100799" ); // not 1341100800, it's intentional. |
711 | } |
712 | |
713 | #[test ] |
714 | #[cfg (feature = "alloc" )] |
715 | fn test_datetime_format_alignment() { |
716 | let datetime = Utc |
717 | .with_ymd_and_hms(2007, 1, 2, 12, 34, 56) |
718 | .unwrap() |
719 | .with_nanosecond(123456789) |
720 | .unwrap(); |
721 | |
722 | // Item::Literal, odd number of padding bytes. |
723 | let percent = datetime.format("%%" ); |
724 | assert_eq!(" %" , format!("{:>4}" , percent)); |
725 | assert_eq!("% " , format!("{:<4}" , percent)); |
726 | assert_eq!(" % " , format!("{:^4}" , percent)); |
727 | |
728 | // Item::Numeric, custom non-ASCII padding character |
729 | let year = datetime.format("%Y" ); |
730 | assert_eq!("——2007" , format!("{:—>6}" , year)); |
731 | assert_eq!("2007——" , format!("{:—<6}" , year)); |
732 | assert_eq!("—2007—" , format!("{:—^6}" , year)); |
733 | |
734 | // Item::Fixed |
735 | let tz = datetime.format("%Z" ); |
736 | assert_eq!(" UTC" , format!("{:>5}" , tz)); |
737 | assert_eq!("UTC " , format!("{:<5}" , tz)); |
738 | assert_eq!(" UTC " , format!("{:^5}" , tz)); |
739 | |
740 | // [Item::Numeric, Item::Space, Item::Literal, Item::Space, Item::Numeric] |
741 | let ymd = datetime.format("%Y %B %d" ); |
742 | assert_eq!(" 2007 January 02" , format!("{:>17}" , ymd)); |
743 | assert_eq!("2007 January 02 " , format!("{:<17}" , ymd)); |
744 | assert_eq!(" 2007 January 02 " , format!("{:^17}" , ymd)); |
745 | |
746 | // Truncated |
747 | let time = datetime.format("%T%.6f" ); |
748 | assert_eq!("12:34:56.1234" , format!("{:.13}" , time)); |
749 | } |
750 | |
751 | #[test ] |
752 | fn test_offset_formatting() { |
753 | fn check_all(precision: OffsetPrecision, expected: [[&str; 7]; 12]) { |
754 | fn check( |
755 | precision: OffsetPrecision, |
756 | colons: Colons, |
757 | padding: Pad, |
758 | allow_zulu: bool, |
759 | offsets: [FixedOffset; 7], |
760 | expected: [&str; 7], |
761 | ) { |
762 | let offset_format = OffsetFormat { precision, colons, allow_zulu, padding }; |
763 | for (offset, expected) in offsets.iter().zip(expected.iter()) { |
764 | let mut output = String::new(); |
765 | offset_format.format(&mut output, *offset).unwrap(); |
766 | assert_eq!(&output, expected); |
767 | } |
768 | } |
769 | // +03:45, -03:30, +11:00, -11:00:22, +02:34:26, -12:34:30, +00:00 |
770 | let offsets = [ |
771 | FixedOffset::east_opt(13_500).unwrap(), |
772 | FixedOffset::east_opt(-12_600).unwrap(), |
773 | FixedOffset::east_opt(39_600).unwrap(), |
774 | FixedOffset::east_opt(-39_622).unwrap(), |
775 | FixedOffset::east_opt(9266).unwrap(), |
776 | FixedOffset::east_opt(-45270).unwrap(), |
777 | FixedOffset::east_opt(0).unwrap(), |
778 | ]; |
779 | check(precision, Colons::Colon, Pad::Zero, false, offsets, expected[0]); |
780 | check(precision, Colons::Colon, Pad::Zero, true, offsets, expected[1]); |
781 | check(precision, Colons::Colon, Pad::Space, false, offsets, expected[2]); |
782 | check(precision, Colons::Colon, Pad::Space, true, offsets, expected[3]); |
783 | check(precision, Colons::Colon, Pad::None, false, offsets, expected[4]); |
784 | check(precision, Colons::Colon, Pad::None, true, offsets, expected[5]); |
785 | check(precision, Colons::None, Pad::Zero, false, offsets, expected[6]); |
786 | check(precision, Colons::None, Pad::Zero, true, offsets, expected[7]); |
787 | check(precision, Colons::None, Pad::Space, false, offsets, expected[8]); |
788 | check(precision, Colons::None, Pad::Space, true, offsets, expected[9]); |
789 | check(precision, Colons::None, Pad::None, false, offsets, expected[10]); |
790 | check(precision, Colons::None, Pad::None, true, offsets, expected[11]); |
791 | // `Colons::Maybe` should format the same as `Colons::None` |
792 | check(precision, Colons::Maybe, Pad::Zero, false, offsets, expected[6]); |
793 | check(precision, Colons::Maybe, Pad::Zero, true, offsets, expected[7]); |
794 | check(precision, Colons::Maybe, Pad::Space, false, offsets, expected[8]); |
795 | check(precision, Colons::Maybe, Pad::Space, true, offsets, expected[9]); |
796 | check(precision, Colons::Maybe, Pad::None, false, offsets, expected[10]); |
797 | check(precision, Colons::Maybe, Pad::None, true, offsets, expected[11]); |
798 | } |
799 | check_all( |
800 | OffsetPrecision::Hours, |
801 | [ |
802 | ["+03" , "-03" , "+11" , "-11" , "+02" , "-12" , "+00" ], |
803 | ["+03" , "-03" , "+11" , "-11" , "+02" , "-12" , "Z" ], |
804 | [" +3" , " -3" , "+11" , "-11" , " +2" , "-12" , " +0" ], |
805 | [" +3" , " -3" , "+11" , "-11" , " +2" , "-12" , "Z" ], |
806 | ["+3" , "-3" , "+11" , "-11" , "+2" , "-12" , "+0" ], |
807 | ["+3" , "-3" , "+11" , "-11" , "+2" , "-12" , "Z" ], |
808 | ["+03" , "-03" , "+11" , "-11" , "+02" , "-12" , "+00" ], |
809 | ["+03" , "-03" , "+11" , "-11" , "+02" , "-12" , "Z" ], |
810 | [" +3" , " -3" , "+11" , "-11" , " +2" , "-12" , " +0" ], |
811 | [" +3" , " -3" , "+11" , "-11" , " +2" , "-12" , "Z" ], |
812 | ["+3" , "-3" , "+11" , "-11" , "+2" , "-12" , "+0" ], |
813 | ["+3" , "-3" , "+11" , "-11" , "+2" , "-12" , "Z" ], |
814 | ], |
815 | ); |
816 | check_all( |
817 | OffsetPrecision::Minutes, |
818 | [ |
819 | ["+03:45" , "-03:30" , "+11:00" , "-11:00" , "+02:34" , "-12:35" , "+00:00" ], |
820 | ["+03:45" , "-03:30" , "+11:00" , "-11:00" , "+02:34" , "-12:35" , "Z" ], |
821 | [" +3:45" , " -3:30" , "+11:00" , "-11:00" , " +2:34" , "-12:35" , " +0:00" ], |
822 | [" +3:45" , " -3:30" , "+11:00" , "-11:00" , " +2:34" , "-12:35" , "Z" ], |
823 | ["+3:45" , "-3:30" , "+11:00" , "-11:00" , "+2:34" , "-12:35" , "+0:00" ], |
824 | ["+3:45" , "-3:30" , "+11:00" , "-11:00" , "+2:34" , "-12:35" , "Z" ], |
825 | ["+0345" , "-0330" , "+1100" , "-1100" , "+0234" , "-1235" , "+0000" ], |
826 | ["+0345" , "-0330" , "+1100" , "-1100" , "+0234" , "-1235" , "Z" ], |
827 | [" +345" , " -330" , "+1100" , "-1100" , " +234" , "-1235" , " +000" ], |
828 | [" +345" , " -330" , "+1100" , "-1100" , " +234" , "-1235" , "Z" ], |
829 | ["+345" , "-330" , "+1100" , "-1100" , "+234" , "-1235" , "+000" ], |
830 | ["+345" , "-330" , "+1100" , "-1100" , "+234" , "-1235" , "Z" ], |
831 | ], |
832 | ); |
833 | #[rustfmt::skip] |
834 | check_all( |
835 | OffsetPrecision::Seconds, |
836 | [ |
837 | ["+03:45:00" , "-03:30:00" , "+11:00:00" , "-11:00:22" , "+02:34:26" , "-12:34:30" , "+00:00:00" ], |
838 | ["+03:45:00" , "-03:30:00" , "+11:00:00" , "-11:00:22" , "+02:34:26" , "-12:34:30" , "Z" ], |
839 | [" +3:45:00" , " -3:30:00" , "+11:00:00" , "-11:00:22" , " +2:34:26" , "-12:34:30" , " +0:00:00" ], |
840 | [" +3:45:00" , " -3:30:00" , "+11:00:00" , "-11:00:22" , " +2:34:26" , "-12:34:30" , "Z" ], |
841 | ["+3:45:00" , "-3:30:00" , "+11:00:00" , "-11:00:22" , "+2:34:26" , "-12:34:30" , "+0:00:00" ], |
842 | ["+3:45:00" , "-3:30:00" , "+11:00:00" , "-11:00:22" , "+2:34:26" , "-12:34:30" , "Z" ], |
843 | ["+034500" , "-033000" , "+110000" , "-110022" , "+023426" , "-123430" , "+000000" ], |
844 | ["+034500" , "-033000" , "+110000" , "-110022" , "+023426" , "-123430" , "Z" ], |
845 | [" +34500" , " -33000" , "+110000" , "-110022" , " +23426" , "-123430" , " +00000" ], |
846 | [" +34500" , " -33000" , "+110000" , "-110022" , " +23426" , "-123430" , "Z" ], |
847 | ["+34500" , "-33000" , "+110000" , "-110022" , "+23426" , "-123430" , "+00000" ], |
848 | ["+34500" , "-33000" , "+110000" , "-110022" , "+23426" , "-123430" , "Z" ], |
849 | ], |
850 | ); |
851 | check_all( |
852 | OffsetPrecision::OptionalMinutes, |
853 | [ |
854 | ["+03:45" , "-03:30" , "+11" , "-11" , "+02:34" , "-12:35" , "+00" ], |
855 | ["+03:45" , "-03:30" , "+11" , "-11" , "+02:34" , "-12:35" , "Z" ], |
856 | [" +3:45" , " -3:30" , "+11" , "-11" , " +2:34" , "-12:35" , " +0" ], |
857 | [" +3:45" , " -3:30" , "+11" , "-11" , " +2:34" , "-12:35" , "Z" ], |
858 | ["+3:45" , "-3:30" , "+11" , "-11" , "+2:34" , "-12:35" , "+0" ], |
859 | ["+3:45" , "-3:30" , "+11" , "-11" , "+2:34" , "-12:35" , "Z" ], |
860 | ["+0345" , "-0330" , "+11" , "-11" , "+0234" , "-1235" , "+00" ], |
861 | ["+0345" , "-0330" , "+11" , "-11" , "+0234" , "-1235" , "Z" ], |
862 | [" +345" , " -330" , "+11" , "-11" , " +234" , "-1235" , " +0" ], |
863 | [" +345" , " -330" , "+11" , "-11" , " +234" , "-1235" , "Z" ], |
864 | ["+345" , "-330" , "+11" , "-11" , "+234" , "-1235" , "+0" ], |
865 | ["+345" , "-330" , "+11" , "-11" , "+234" , "-1235" , "Z" ], |
866 | ], |
867 | ); |
868 | check_all( |
869 | OffsetPrecision::OptionalSeconds, |
870 | [ |
871 | ["+03:45" , "-03:30" , "+11:00" , "-11:00:22" , "+02:34:26" , "-12:34:30" , "+00:00" ], |
872 | ["+03:45" , "-03:30" , "+11:00" , "-11:00:22" , "+02:34:26" , "-12:34:30" , "Z" ], |
873 | [" +3:45" , " -3:30" , "+11:00" , "-11:00:22" , " +2:34:26" , "-12:34:30" , " +0:00" ], |
874 | [" +3:45" , " -3:30" , "+11:00" , "-11:00:22" , " +2:34:26" , "-12:34:30" , "Z" ], |
875 | ["+3:45" , "-3:30" , "+11:00" , "-11:00:22" , "+2:34:26" , "-12:34:30" , "+0:00" ], |
876 | ["+3:45" , "-3:30" , "+11:00" , "-11:00:22" , "+2:34:26" , "-12:34:30" , "Z" ], |
877 | ["+0345" , "-0330" , "+1100" , "-110022" , "+023426" , "-123430" , "+0000" ], |
878 | ["+0345" , "-0330" , "+1100" , "-110022" , "+023426" , "-123430" , "Z" ], |
879 | [" +345" , " -330" , "+1100" , "-110022" , " +23426" , "-123430" , " +000" ], |
880 | [" +345" , " -330" , "+1100" , "-110022" , " +23426" , "-123430" , "Z" ], |
881 | ["+345" , "-330" , "+1100" , "-110022" , "+23426" , "-123430" , "+000" ], |
882 | ["+345" , "-330" , "+1100" , "-110022" , "+23426" , "-123430" , "Z" ], |
883 | ], |
884 | ); |
885 | check_all( |
886 | OffsetPrecision::OptionalMinutesAndSeconds, |
887 | [ |
888 | ["+03:45" , "-03:30" , "+11" , "-11:00:22" , "+02:34:26" , "-12:34:30" , "+00" ], |
889 | ["+03:45" , "-03:30" , "+11" , "-11:00:22" , "+02:34:26" , "-12:34:30" , "Z" ], |
890 | [" +3:45" , " -3:30" , "+11" , "-11:00:22" , " +2:34:26" , "-12:34:30" , " +0" ], |
891 | [" +3:45" , " -3:30" , "+11" , "-11:00:22" , " +2:34:26" , "-12:34:30" , "Z" ], |
892 | ["+3:45" , "-3:30" , "+11" , "-11:00:22" , "+2:34:26" , "-12:34:30" , "+0" ], |
893 | ["+3:45" , "-3:30" , "+11" , "-11:00:22" , "+2:34:26" , "-12:34:30" , "Z" ], |
894 | ["+0345" , "-0330" , "+11" , "-110022" , "+023426" , "-123430" , "+00" ], |
895 | ["+0345" , "-0330" , "+11" , "-110022" , "+023426" , "-123430" , "Z" ], |
896 | [" +345" , " -330" , "+11" , "-110022" , " +23426" , "-123430" , " +0" ], |
897 | [" +345" , " -330" , "+11" , "-110022" , " +23426" , "-123430" , "Z" ], |
898 | ["+345" , "-330" , "+11" , "-110022" , "+23426" , "-123430" , "+0" ], |
899 | ["+345" , "-330" , "+11" , "-110022" , "+23426" , "-123430" , "Z" ], |
900 | ], |
901 | ); |
902 | } |
903 | } |
904 | |