• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zbraniecki / icu4x / 13958601093

19 Mar 2025 04:17PM UTC coverage: 74.164% (-1.5%) from 75.71%
13958601093

push

github

web-flow
Clean up properties docs (#6315)

58056 of 78281 relevant lines covered (74.16%)

819371.32 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.24
/components/datetime/src/format/time_zone.rs
1
// This file is part of ICU4X. For terms of use, please see the file
2
// called LICENSE at the top level of the ICU4X source tree
3
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4

5
//! A formatter specifically for the time zone.
6

7
use crate::pattern::TimeZoneDataPayloadsBorrowed;
8
use crate::provider::time_zones::MetazoneId;
9
use crate::{format::DateTimeInputUnchecked, provider::fields::FieldLength};
10
use core::fmt;
11
use fixed_decimal::Decimal;
12
use icu_calendar::{Date, Iso};
13
use icu_decimal::DecimalFormatter;
14
use icu_time::provider::MinutesSinceEpoch;
15
use icu_time::{
16
    zone::{TimeZoneVariant, UtcOffset},
17
    Time, TimeZone,
18
};
19
use writeable::Writeable;
20

21
impl crate::provider::time_zones::MetazonePeriod<'_> {
22
    fn resolve(&self, time_zone_id: TimeZone, dt: (Date<Iso>, Time)) -> Option<MetazoneId> {
338✔
23
        use zerovec::ule::AsULE;
24
        let cursor = self.list.get0(&time_zone_id)?;
338✔
25
        let mut metazone_id = None;
251✔
26
        let minutes_since_epoch_walltime = MinutesSinceEpoch::from(dt);
251✔
27
        for (minutes, id) in cursor.iter1() {
487✔
28
            if minutes_since_epoch_walltime >= MinutesSinceEpoch::from_unaligned(*minutes) {
260✔
29
                metazone_id = id.get()
236✔
30
            } else {
31
                break;
32
            }
33
        }
34
        metazone_id
251✔
35
    }
338✔
36
}
37

38
// An enum for time zone format unit.
39
#[derive(Debug, Clone, Copy, PartialEq)]
×
40
pub(super) enum TimeZoneFormatterUnit {
41
    GenericNonLocation(FieldLength),
×
42
    SpecificNonLocation(FieldLength),
×
43
    GenericLocation,
44
    SpecificLocation,
45
    ExemplarCity,
46
    #[allow(dead_code)]
47
    GenericPartialLocation(FieldLength),
×
48
    LocalizedOffset(FieldLength),
×
49
    Iso8601(Iso8601Format),
×
50
    Bcp47Id,
51
}
52

53
#[derive(Debug)]
×
54
pub(super) enum FormatTimeZoneError {
55
    NamesNotLoaded,
56
    DecimalFormatterNotLoaded,
57
    Fallback,
58
    MissingInputField(&'static str),
×
59
}
60

61
pub(super) trait FormatTimeZone {
62
    /// Tries to write the timezone to the sink. If a DateTimeError is returned, the sink
63
    /// has not been touched, so another format can be attempted.
64
    fn format<W: writeable::PartsWrite + ?Sized>(
65
        &self,
66
        sink: &mut W,
67
        input: &DateTimeInputUnchecked,
68
        data_payloads: TimeZoneDataPayloadsBorrowed,
69
        fdf: Option<&DecimalFormatter>,
70
    ) -> Result<Result<(), FormatTimeZoneError>, fmt::Error>;
71
}
72

73
impl FormatTimeZone for TimeZoneFormatterUnit {
74
    fn format<W: writeable::PartsWrite + ?Sized>(
720✔
75
        &self,
76
        sink: &mut W,
77
        input: &DateTimeInputUnchecked,
78
        data_payloads: TimeZoneDataPayloadsBorrowed,
79
        fdf: Option<&DecimalFormatter>,
80
    ) -> Result<Result<(), FormatTimeZoneError>, fmt::Error> {
81
        match *self {
720✔
82
            Self::GenericNonLocation(length) => {
130✔
83
                GenericNonLocationFormat(length).format(sink, input, data_payloads, fdf)
130✔
84
            }
85
            Self::SpecificNonLocation(length) => {
124✔
86
                SpecificNonLocationFormat(length).format(sink, input, data_payloads, fdf)
124✔
87
            }
88
            Self::GenericLocation => GenericLocationFormat.format(sink, input, data_payloads, fdf),
123✔
89
            Self::SpecificLocation => {
90
                SpecificLocationFormat.format(sink, input, data_payloads, fdf)
26✔
91
            }
92
            Self::ExemplarCity => ExemplarCityFormat.format(sink, input, data_payloads, fdf),
29✔
93
            Self::GenericPartialLocation(length) => {
×
94
                GenericPartialLocationFormat(length).format(sink, input, data_payloads, fdf)
×
95
            }
96
            Self::LocalizedOffset(length) => {
236✔
97
                LocalizedOffsetFormat(length).format(sink, input, data_payloads, fdf)
236✔
98
            }
99
            Self::Iso8601(iso) => iso.format(sink, input, data_payloads, fdf),
51✔
100
            Self::Bcp47Id => Bcp47IdFormat.format(sink, input, data_payloads, fdf),
1✔
101
        }
102
    }
720✔
103
}
104

105
// PT / Pacific Time
106
struct GenericNonLocationFormat(FieldLength);
107

108
impl FormatTimeZone for GenericNonLocationFormat {
109
    /// Writes the time zone in generic non-location format as defined by the UTS-35 spec.
110
    /// <https://unicode.org/reports/tr35/tr35-dates.html#Time_Zone_Format_Terminology>
111
    fn format<W: writeable::PartsWrite + ?Sized>(
130✔
112
        &self,
113
        sink: &mut W,
114
        input: &DateTimeInputUnchecked,
115
        data_payloads: TimeZoneDataPayloadsBorrowed,
116
        _fdf: Option<&DecimalFormatter>,
117
    ) -> Result<Result<(), FormatTimeZoneError>, fmt::Error> {
118
        let Some(time_zone_id) = input.time_zone_id else {
130✔
119
            return Ok(Err(FormatTimeZoneError::MissingInputField("time_zone_id")));
×
120
        };
121
        let Some(local_time) = input.local_time else {
130✔
122
            return Ok(Err(FormatTimeZoneError::MissingInputField("local_time")));
×
123
        };
124
        let Some(generic_names) = (match self.0 {
260✔
125
            FieldLength::Four => data_payloads.mz_generic_long.as_ref(),
56✔
126
            _ => data_payloads.mz_generic_short.as_ref(),
74✔
127
        }) else {
128
            return Ok(Err(FormatTimeZoneError::NamesNotLoaded));
×
129
        };
130
        let Some(standard_names) = (match self.0 {
260✔
131
            FieldLength::Four => data_payloads.mz_standard_long.as_ref(),
56✔
132
            _ => data_payloads.mz_generic_short.as_ref(),
74✔
133
        }) else {
134
            return Ok(Err(FormatTimeZoneError::NamesNotLoaded));
×
135
        };
136
        let Some(metazone_period) = data_payloads.mz_periods else {
130✔
137
            return Ok(Err(FormatTimeZoneError::NamesNotLoaded));
×
138
        };
139

140
        let Some(name) = generic_names
390✔
141
            .overrides
142
            .get(&time_zone_id)
143
            .or_else(|| standard_names.overrides.get(&time_zone_id))
254✔
144
            .or_else(|| {
250✔
145
                let mz = metazone_period.resolve(time_zone_id, local_time)?;
120✔
146
                generic_names
152✔
147
                    .defaults
148
                    .get(&mz)
149
                    .or_else(|| standard_names.defaults.get(&mz))
118✔
150
            })
120✔
151
        else {
152
            return Ok(Err(FormatTimeZoneError::Fallback));
64✔
153
        };
154

155
        sink.write_str(name)?;
196✔
156

157
        Ok(Ok(()))
66✔
158
    }
130✔
159
}
160

161
// PDT / Pacific Daylight Time
162
struct SpecificNonLocationFormat(FieldLength);
163

164
impl FormatTimeZone for SpecificNonLocationFormat {
165
    /// Writes the time zone in short specific non-location format as defined by the UTS-35 spec.
166
    /// <https://unicode.org/reports/tr35/tr35-dates.html#Time_Zone_Format_Terminology>
167
    fn format<W: writeable::PartsWrite + ?Sized>(
124✔
168
        &self,
169
        sink: &mut W,
170
        input: &DateTimeInputUnchecked,
171
        data_payloads: TimeZoneDataPayloadsBorrowed,
172
        _fdf: Option<&DecimalFormatter>,
173
    ) -> Result<Result<(), FormatTimeZoneError>, fmt::Error> {
174
        let Some(time_zone_id) = input.time_zone_id else {
124✔
175
            return Ok(Err(FormatTimeZoneError::MissingInputField("time_zone_id")));
1✔
176
        };
177
        let Some(zone_variant) = input.zone_variant else {
123✔
178
            return Ok(Err(FormatTimeZoneError::MissingInputField("zone_variant")));
×
179
        };
180
        let Some(local_time) = input.local_time else {
123✔
181
            return Ok(Err(FormatTimeZoneError::MissingInputField("local_time")));
×
182
        };
183

184
        let Some(specific) = (match self.0 {
246✔
185
            FieldLength::Four => data_payloads.mz_specific_long.as_ref(),
61✔
186
            _ => data_payloads.mz_specific_short.as_ref(),
62✔
187
        }) else {
188
            return Ok(Err(FormatTimeZoneError::NamesNotLoaded));
1✔
189
        };
190
        let Some(metazone_period) = data_payloads.mz_periods else {
122✔
191
            return Ok(Err(FormatTimeZoneError::NamesNotLoaded));
×
192
        };
193

194
        let name = if zone_variant == TimeZoneVariant::Standard && self.0 == FieldLength::Four {
163✔
195
            let Some(standard_names) = data_payloads.mz_standard_long.as_ref() else {
39✔
196
                return Ok(Err(FormatTimeZoneError::NamesNotLoaded));
×
197
            };
198
            if let Some(n) = specific
91✔
199
                .overrides
200
                .get(&(time_zone_id, TimeZoneVariant::Standard))
39✔
201
            {
202
                n
4✔
203
            } else if let Some(mz) = metazone_period.resolve(time_zone_id, local_time) {
48✔
204
                if specific.use_standard.binary_search(&mz).is_ok() {
30✔
205
                    if let Some(n) = standard_names.defaults.get(&mz) {
13✔
206
                        n
13✔
207
                    } else {
208
                        // The only reason why the name is not in GenericStandard even though we expect it
209
                        // to be, is that it was deduplicated against the generic location format.
210
                        return GenericLocationFormat.format(sink, input, data_payloads, _fdf);
×
211
                    }
212
                } else if let Some(n) = specific.defaults.get(&(mz, TimeZoneVariant::Standard)) {
4✔
213
                    n
214
                } else {
215
                    return Ok(Err(FormatTimeZoneError::Fallback));
4✔
216
                }
217
            } else {
218
                return Ok(Err(FormatTimeZoneError::Fallback));
18✔
219
            }
220
        } else if let Some(n) = specific
249✔
221
            .overrides
222
            .get(&(time_zone_id, zone_variant))
83✔
223
            .or_else(|| {
151✔
224
                specific.defaults.get(&(
68✔
225
                    metazone_period.resolve(time_zone_id, local_time)?,
68✔
226
                    zone_variant,
50✔
227
                ))
228
            })
68✔
229
        {
230
            n
231
        } else {
232
            return Ok(Err(FormatTimeZoneError::Fallback));
42✔
233
        };
234

235
        sink.write_str(name)?;
182✔
236

237
        Ok(Ok(()))
58✔
238
    }
124✔
239
}
240

241
// UTC+7:00
242
struct LocalizedOffsetFormat(FieldLength);
243

244
impl FormatTimeZone for LocalizedOffsetFormat {
245
    /// Writes the time zone in localized offset format according to the CLDR localized hour format.
246
    /// This goes explicitly against the UTS-35 spec, which specifies long or short localized
247
    /// offset formats regardless of locale.
248
    ///
249
    /// You can see more information about our decision to resolve this conflict here:
250
    /// <https://docs.google.com/document/d/16GAqaDRS6hzL8jNYjus5MglSevGBflISM-BrIS7bd4A/edit?usp=sharing>
251
    fn format<W: writeable::PartsWrite + ?Sized>(
236✔
252
        &self,
253
        sink: &mut W,
254
        input: &DateTimeInputUnchecked,
255
        data_payloads: TimeZoneDataPayloadsBorrowed,
256
        formatter: Option<&DecimalFormatter>,
257
    ) -> Result<Result<(), FormatTimeZoneError>, fmt::Error> {
258
        let Some(essentials) = data_payloads.essentials else {
236✔
259
            return Ok(Err(FormatTimeZoneError::NamesNotLoaded));
×
260
        };
261
        let Some(formatter) = formatter else {
236✔
262
            return Ok(Err(FormatTimeZoneError::DecimalFormatterNotLoaded));
×
263
        };
264
        let Some(offset) = input.offset else {
236✔
265
            sink.write_str(&essentials.offset_unknown)?;
30✔
266
            return Ok(Ok(()));
30✔
267
        };
268
        Ok(if offset.is_zero() && self.0 != FieldLength::Four {
412✔
269
            sink.write_str(&essentials.offset_zero)?;
16✔
270
            Ok(())
16✔
271
        } else {
272
            struct FormattedOffset<'a> {
273
                offset: UtcOffset,
274
                separator: &'a str,
275
                formatter: &'a DecimalFormatter,
276
                length: FieldLength,
277
            }
278

279
            impl Writeable for FormattedOffset<'_> {
280
                fn write_to_parts<S: writeable::PartsWrite + ?Sized>(
190✔
281
                    &self,
282
                    sink: &mut S,
283
                ) -> fmt::Result {
284
                    let decimal = {
285
                        let mut decimal = Decimal::from(self.offset.hours_part())
380✔
286
                            .with_sign_display(fixed_decimal::SignDisplay::Always);
190✔
287
                        decimal.pad_start(if self.length == FieldLength::Four {
190✔
288
                            2
98✔
289
                        } else {
290
                            0
92✔
291
                        });
292
                        decimal
190✔
293
                    };
×
294
                    self.formatter.format(&decimal).write_to(sink)?;
190✔
295

296
                    if self.length == FieldLength::Four
190✔
297
                        || self.offset.minutes_part() != 0
92✔
298
                        || self.offset.seconds_part() != 0
80✔
299
                    {
300
                        let mut decimal = Decimal::from(self.offset.minutes_part());
190✔
301
                        decimal.absolute.pad_start(2);
110✔
302
                        sink.write_str(self.separator)?;
110✔
303
                        self.formatter.format(&decimal).write_to(sink)?;
110✔
304
                    }
110✔
305

306
                    if self.offset.seconds_part() != 0 {
×
307
                        sink.write_str(self.separator)?;
2✔
308

309
                        let mut decimal = Decimal::from(self.offset.seconds_part());
2✔
310
                        decimal.absolute.pad_start(2);
2✔
311
                        self.formatter.format(&decimal).write_to(sink)?;
192✔
312
                    }
2✔
313

314
                    Ok(())
190✔
315
                }
570✔
316
            }
317

318
            essentials
616✔
319
                .offset_pattern
320
                .interpolate([FormattedOffset {
190✔
321
                    offset,
322
                    separator: &essentials.offset_separator,
190✔
323
                    formatter,
324
                    length: self.0,
190✔
325
                }])
326
                .write_to(sink)?;
327

328
            Ok(())
190✔
329
        })
330
    }
236✔
331
}
332

333
// Los Angeles Time
334
struct GenericLocationFormat;
335

336
impl FormatTimeZone for GenericLocationFormat {
337
    /// Writes the time zone in generic location format as defined by the UTS-35 spec.
338
    /// e.g. France Time
339
    /// <https://unicode.org/reports/tr35/tr35-dates.html#Time_Zone_Format_Terminology>
340
    fn format<W: writeable::PartsWrite + ?Sized>(
123✔
341
        &self,
342
        sink: &mut W,
343
        input: &DateTimeInputUnchecked,
344
        data_payloads: TimeZoneDataPayloadsBorrowed,
345
        _decimal_formatter: Option<&DecimalFormatter>,
346
    ) -> Result<Result<(), FormatTimeZoneError>, fmt::Error> {
347
        let Some(time_zone_id) = input.time_zone_id else {
123✔
348
            return Ok(Err(FormatTimeZoneError::MissingInputField("time_zone_id")));
×
349
        };
350

351
        let Some(locations) = data_payloads.locations else {
123✔
352
            return Ok(Err(FormatTimeZoneError::NamesNotLoaded));
×
353
        };
354

355
        let Some(locations_root) = data_payloads.locations_root else {
123✔
356
            return Ok(Err(FormatTimeZoneError::NamesNotLoaded));
×
357
        };
358

359
        let Some(location) = locations
123✔
360
            .locations
361
            .get(&time_zone_id)
362
            .or_else(|| locations_root.locations.get(&time_zone_id))
123✔
363
        else {
364
            return Ok(Err(FormatTimeZoneError::Fallback));
54✔
365
        };
366

367
        locations
261✔
368
            .pattern_generic
369
            .interpolate([location])
69✔
370
            .write_to(sink)?;
371

372
        Ok(Ok(()))
69✔
373
    }
123✔
374
}
375

376
// Los Angeles Daylight Time
377
struct SpecificLocationFormat;
378

379
impl FormatTimeZone for SpecificLocationFormat {
380
    /// Writes the time zone in a specific location format as defined by the UTS-35 spec.
381
    /// e.g. France Time
382
    /// <https://unicode.org/reports/tr35/tr35-dates.html#Time_Zone_Format_Terminology>
383
    fn format<W: writeable::PartsWrite + ?Sized>(
26✔
384
        &self,
385
        sink: &mut W,
386
        input: &DateTimeInputUnchecked,
387
        data_payloads: TimeZoneDataPayloadsBorrowed,
388
        _decimal_formatter: Option<&DecimalFormatter>,
389
    ) -> Result<Result<(), FormatTimeZoneError>, fmt::Error> {
390
        let Some(time_zone_id) = input.time_zone_id else {
26✔
391
            return Ok(Err(FormatTimeZoneError::MissingInputField("time_zone_id")));
×
392
        };
393
        let Some(zone_variant) = input.zone_variant else {
26✔
394
            return Ok(Err(FormatTimeZoneError::MissingInputField("zone_variant")));
×
395
        };
396
        let Some(locations) = data_payloads.locations else {
26✔
397
            return Ok(Err(FormatTimeZoneError::NamesNotLoaded));
×
398
        };
399
        let Some(locations_root) = data_payloads.locations_root else {
26✔
400
            return Ok(Err(FormatTimeZoneError::NamesNotLoaded));
×
401
        };
402

403
        let Some(location) = locations
26✔
404
            .locations
405
            .get(&time_zone_id)
406
            .or_else(|| locations_root.locations.get(&time_zone_id))
26✔
407
        else {
408
            return Ok(Err(FormatTimeZoneError::Fallback));
12✔
409
        };
410

411
        match zone_variant {
68✔
412
            TimeZoneVariant::Standard => &locations.pattern_standard,
10✔
413
            TimeZoneVariant::Daylight => &locations.pattern_daylight,
4✔
414
            // Compiles out due to tilde dependency on `icu_time`
415
            _ => unreachable!(),
416
        }
417
        .interpolate([location])
14✔
418
        .write_to(sink)?;
419

420
        Ok(Ok(()))
14✔
421
    }
26✔
422
}
423

424
// Los Angeles
425
struct ExemplarCityFormat;
426

427
impl FormatTimeZone for ExemplarCityFormat {
428
    /// Writes the time zone exemplar city format as defined by the UTS-35 spec.
429
    /// e.g. Los Angeles
430
    /// <https://unicode.org/reports/tr35/tr35-dates.html#Time_Zone_Format_Terminology>
431
    fn format<W: writeable::PartsWrite + ?Sized>(
29✔
432
        &self,
433
        sink: &mut W,
434
        input: &DateTimeInputUnchecked,
435
        data_payloads: TimeZoneDataPayloadsBorrowed,
436
        _fdf: Option<&DecimalFormatter>,
437
    ) -> Result<Result<(), FormatTimeZoneError>, fmt::Error> {
438
        let Some(time_zone_id) = input.time_zone_id else {
29✔
439
            return Ok(Err(FormatTimeZoneError::MissingInputField("time_zone_id")));
×
440
        };
441
        let Some(exemplars) = data_payloads.exemplars else {
29✔
442
            return Ok(Err(FormatTimeZoneError::NamesNotLoaded));
×
443
        };
444
        let Some(exemplars_root) = data_payloads.exemplars_root else {
29✔
445
            return Ok(Err(FormatTimeZoneError::NamesNotLoaded));
×
446
        };
447
        let Some(locations) = data_payloads.locations else {
29✔
448
            return Ok(Err(FormatTimeZoneError::NamesNotLoaded));
×
449
        };
450
        let Some(locations_root) = data_payloads.locations_root else {
29✔
451
            return Ok(Err(FormatTimeZoneError::NamesNotLoaded));
×
452
        };
453

454
        let Some(location) = exemplars
29✔
455
            .exemplars
456
            .get(&time_zone_id)
457
            .or_else(|| exemplars_root.exemplars.get(&time_zone_id))
29✔
458
            .or_else(|| locations.locations.get(&time_zone_id))
12✔
459
            .or_else(|| locations_root.locations.get(&time_zone_id))
12✔
460
            .or_else(|| exemplars.exemplars.get(&TimeZone::unknown()))
4✔
461
            .or_else(|| exemplars_root.exemplars.get(&TimeZone::unknown()))
4✔
462
        else {
463
            return Ok(Err(FormatTimeZoneError::Fallback));
×
464
        };
465

466
        location.write_to(sink)?;
58✔
467

468
        Ok(Ok(()))
29✔
469
    }
29✔
470
}
471

472
// Pacific Time (Los Angeles) / PT (Los Angeles)
473
struct GenericPartialLocationFormat(FieldLength);
474

475
impl FormatTimeZone for GenericPartialLocationFormat {
476
    /// Writes the time zone in a long generic partial location format as defined by the UTS-35 spec.
477
    /// <https://unicode.org/reports/tr35/tr35-dates.html#Time_Zone_Format_Terminology>
478
    fn format<W: writeable::PartsWrite + ?Sized>(
×
479
        &self,
480
        sink: &mut W,
481
        input: &DateTimeInputUnchecked,
482
        data_payloads: TimeZoneDataPayloadsBorrowed,
483
        _fdf: Option<&DecimalFormatter>,
484
    ) -> Result<Result<(), FormatTimeZoneError>, fmt::Error> {
485
        let Some(time_zone_id) = input.time_zone_id else {
×
486
            return Ok(Err(FormatTimeZoneError::MissingInputField("time_zone_id")));
×
487
        };
488
        let Some(local_time) = input.local_time else {
×
489
            return Ok(Err(FormatTimeZoneError::MissingInputField("local_time")));
×
490
        };
491

492
        let Some(locations) = data_payloads.locations else {
×
493
            return Ok(Err(FormatTimeZoneError::NamesNotLoaded));
×
494
        };
495
        let Some(locations_root) = data_payloads.locations_root else {
×
496
            return Ok(Err(FormatTimeZoneError::NamesNotLoaded));
×
497
        };
498
        let Some(non_locations) = (match self.0 {
×
499
            FieldLength::Four => data_payloads.mz_generic_long.as_ref(),
×
500
            _ => data_payloads.mz_generic_short.as_ref(),
×
501
        }) else {
502
            return Ok(Err(FormatTimeZoneError::NamesNotLoaded));
×
503
        };
504
        let Some(metazone_period) = data_payloads.mz_periods else {
×
505
            return Ok(Err(FormatTimeZoneError::NamesNotLoaded));
×
506
        };
507
        let Some(location) = locations
×
508
            .locations
509
            .get(&time_zone_id)
510
            .or_else(|| locations_root.locations.get(&time_zone_id))
×
511
        else {
512
            return Ok(Err(FormatTimeZoneError::Fallback));
×
513
        };
514
        let Some(non_location) = non_locations.overrides.get(&time_zone_id).or_else(|| {
×
515
            non_locations
×
516
                .defaults
517
                .get(&metazone_period.resolve(time_zone_id, local_time)?)
×
518
        }) else {
×
519
            return Ok(Err(FormatTimeZoneError::Fallback));
×
520
        };
521

522
        locations
×
523
            .pattern_partial_location
524
            .interpolate((location, non_location))
×
525
            .write_to(sink)?;
526

527
        Ok(Ok(()))
×
528
    }
×
529
}
530

531
/// Whether the minutes field should be optional or required in ISO-8601 format.
532
#[derive(Debug, Clone, Copy, PartialEq)]
45✔
533
enum Minutes {
534
    /// Minutes are always displayed.
535
    Required,
536

537
    /// Minutes are displayed only if they are non-zero.
538
    Optional,
539
}
540

541
/// Whether the seconds field should be optional or excluded in ISO-8601 format.
542
#[derive(Debug, Clone, Copy, PartialEq)]
41✔
543
enum Seconds {
544
    /// Seconds are displayed only if they are non-zero.
545
    Optional,
546

547
    /// Seconds are not displayed.
548
    Never,
549
}
550

551
#[derive(Debug, Clone, Copy, PartialEq)]
×
552
pub(crate) struct Iso8601Format {
553
    // 1000 vs 10:00
554
    extended: bool,
×
555
    // 00:00 vs Z
556
    z: bool,
×
557
    minutes: Minutes,
×
558
    seconds: Seconds,
×
559
}
560

561
impl Iso8601Format {
562
    pub(crate) fn with_z(length: FieldLength) -> Self {
40✔
563
        match length {
40✔
564
            FieldLength::One => Self {
6✔
565
                extended: false,
566
                z: true,
567
                minutes: Minutes::Optional,
6✔
568
                seconds: Seconds::Never,
6✔
569
            },
6✔
570
            FieldLength::Two => Self {
3✔
571
                extended: false,
572
                z: true,
573
                minutes: Minutes::Required,
3✔
574
                seconds: Seconds::Never,
3✔
575
            },
3✔
576
            FieldLength::Three => Self {
3✔
577
                extended: true,
578
                z: true,
579
                minutes: Minutes::Required,
3✔
580
                seconds: Seconds::Never,
3✔
581
            },
3✔
582
            FieldLength::Four => Self {
3✔
583
                extended: false,
584
                z: true,
585
                minutes: Minutes::Required,
3✔
586
                seconds: Seconds::Optional,
3✔
587
            },
3✔
588
            _ => Self {
25✔
589
                extended: true,
590
                z: true,
591
                minutes: Minutes::Required,
25✔
592
                seconds: Seconds::Optional,
25✔
593
            },
25✔
594
        }
595
    }
40✔
596

597
    pub(crate) fn without_z(length: FieldLength) -> Self {
40✔
598
        match length {
40✔
599
            FieldLength::One => Self {
3✔
600
                extended: false,
601
                z: false,
602
                minutes: Minutes::Optional,
3✔
603
                seconds: Seconds::Never,
3✔
604
            },
3✔
605
            FieldLength::Two => Self {
3✔
606
                extended: false,
607
                z: false,
608
                minutes: Minutes::Required,
3✔
609
                seconds: Seconds::Never,
3✔
610
            },
3✔
611
            FieldLength::Three => Self {
3✔
612
                extended: true,
613
                z: false,
614
                minutes: Minutes::Required,
3✔
615
                seconds: Seconds::Never,
3✔
616
            },
3✔
617
            FieldLength::Four => Self {
28✔
618
                extended: false,
619
                z: false,
620
                minutes: Minutes::Required,
28✔
621
                seconds: Seconds::Optional,
28✔
622
            },
28✔
623
            _ => Self {
3✔
624
                extended: true,
625
                z: false,
626
                minutes: Minutes::Required,
3✔
627
                seconds: Seconds::Optional,
3✔
628
            },
3✔
629
        }
630
    }
40✔
631
}
632

633
impl FormatTimeZone for Iso8601Format {
634
    /// Writes a [`UtcOffset`](crate::input::UtcOffset) in ISO-8601 format according to the
635
    /// given formatting options.
636
    ///
637
    /// [`IsoFormat`] determines whether the format should be Basic or Extended,
638
    /// and whether a zero-offset should be formatted numerically or with
639
    /// The UTC indicator: "Z"
640
    /// - Basic    e.g. +0800
641
    /// - Extended e.g. +08:00
642
    ///
643
    /// [`Minutes`] can be required or optional.
644
    /// [`Seconds`] can be optional or never.
645
    fn format<W: writeable::PartsWrite + ?Sized>(
51✔
646
        &self,
647
        sink: &mut W,
648
        input: &DateTimeInputUnchecked,
649
        _data_payloads: TimeZoneDataPayloadsBorrowed,
650
        _fdf: Option<&DecimalFormatter>,
651
    ) -> Result<Result<(), FormatTimeZoneError>, fmt::Error> {
652
        let Some(offset) = input.offset else {
51✔
653
            sink.write_str("+?")?;
51✔
654
            return Ok(Ok(()));
4✔
655
        };
656
        self.format_infallible(sink, offset).map(|()| Ok(()))
94✔
657
    }
51✔
658
}
659

660
impl Iso8601Format {
661
    pub(crate) fn format_infallible<W: writeable::PartsWrite + ?Sized>(
48✔
662
        self,
663
        sink: &mut W,
664
        offset: UtcOffset,
665
    ) -> Result<(), fmt::Error> {
666
        if offset.is_zero() && self.z {
48✔
667
            return sink.write_char('Z');
7✔
668
        }
669

670
        // Always in latin digits according to spec
671
        {
41✔
672
            let mut fd = Decimal::from(offset.hours_part())
82✔
673
                .with_sign_display(fixed_decimal::SignDisplay::Always);
41✔
674
            fd.pad_start(2);
41✔
675
            fd
41✔
676
        }
×
677
        .write_to(sink)?;
41✔
678

679
        if self.minutes == Minutes::Required
41✔
680
            || (self.minutes == Minutes::Optional && offset.minutes_part() != 0)
4✔
681
        {
682
            if self.extended {
37✔
683
                sink.write_char(':')?;
15✔
684
            }
685
            {
37✔
686
                let mut fd = Decimal::from(offset.minutes_part());
37✔
687
                fd.pad_start(2);
37✔
688
                fd
37✔
689
            }
×
690
            .write_to(sink)?;
37✔
691
        }
692

693
        if self.seconds == Seconds::Optional && offset.seconds_part() != 0 {
41✔
694
            if self.extended {
×
695
                sink.write_char(':')?;
×
696
            }
697
            {
48✔
698
                let mut fd = Decimal::from(offset.seconds_part());
×
699
                fd.pad_start(2);
×
700
                fd
×
701
            }
×
702
            .write_to(sink)?;
×
703
        }
704

705
        Ok(())
41✔
706
    }
48✔
707
}
708

709
// It is only used for pattern in special case and not public to users.
710
struct Bcp47IdFormat;
711

712
impl FormatTimeZone for Bcp47IdFormat {
713
    fn format<W: writeable::PartsWrite + ?Sized>(
1✔
714
        &self,
715
        sink: &mut W,
716
        input: &DateTimeInputUnchecked,
717
        _data_payloads: TimeZoneDataPayloadsBorrowed,
718
        _fdf: Option<&DecimalFormatter>,
719
    ) -> Result<Result<(), FormatTimeZoneError>, fmt::Error> {
720
        let time_zone_id = input
2✔
721
            .time_zone_id
722
            .unwrap_or(TimeZone(tinystr::tinystr!(8, "unk")));
1✔
723

724
        sink.write_str(&time_zone_id)?;
2✔
725

726
        Ok(Ok(()))
1✔
727
    }
1✔
728
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc