1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
// This file is part of ICU4X. For terms of use, please see the file
// called LICENSE at the top level of the ICU4X source tree
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).

use crate::provider::{MetazoneId, TimeZoneBcp47Id};

use crate::metazone::MetazoneCalculator;
use crate::{GmtOffset, TimeZoneError, ZoneVariant};
use core::str::FromStr;
use icu_calendar::{DateTime, Iso};

/// A utility type that can hold time zone information.
///
/// The GMT offset is used as a final fallback for formatting. The other three fields are used
/// for more human-friendly rendering of the time zone.
///
/// This type does not enforce that the four fields are consistent with each other. If they do not
/// represent a real time zone, unexpected results when formatting may occur.
///
/// # Examples
///
/// ```
/// use icu::timezone::{CustomTimeZone, GmtOffset};
///
/// let tz1 = CustomTimeZone {
///     gmt_offset: Some(GmtOffset::default()),
///     time_zone_id: None,
///     metazone_id: None,
///     zone_variant: None,
/// };
///
/// let tz2: CustomTimeZone =
///     "+05:00".parse().expect("Failed to parse a time zone.");
/// ```
#[derive(Debug)]
#[allow(clippy::exhaustive_structs)] // these four fields fully cover the needs of UTS 35
pub struct CustomTimeZone {
    /// The GMT offset in seconds.
    pub gmt_offset: Option<GmtOffset>,
    /// The BCP47 time-zone identifier
    pub time_zone_id: Option<TimeZoneBcp47Id>,
    /// The CLDR metazone identifier
    pub metazone_id: Option<MetazoneId>,
    /// The time variant e.g. daylight or standard
    pub zone_variant: Option<ZoneVariant>,
}

impl CustomTimeZone {
    /// Creates a new [`CustomTimeZone`] with the given GMT offset.
    pub const fn new_with_offset(gmt_offset: GmtOffset) -> Self {
        Self {
            gmt_offset: Some(gmt_offset),
            time_zone_id: None,
            metazone_id: None,
            zone_variant: None,
        }
    }

    /// Creates a time zone with no information.
    ///
    /// One or more fields must be specified before this time zone is usable.
    pub const fn new_empty() -> Self {
        Self {
            gmt_offset: None,
            time_zone_id: None,
            metazone_id: None,
            zone_variant: None,
        }
    }

    /// Creates a new [`CustomTimeZone`] with the GMT offset set to UTC.
    ///
    /// All other fields are left empty.
    pub const fn utc() -> Self {
        Self {
            gmt_offset: Some(GmtOffset::utc()),
            time_zone_id: None,
            metazone_id: None,
            zone_variant: None,
        }
    }

    /// Parse a [`CustomTimeZone`] from a UTF-8 string representing a GMT Offset. See also [`GmtOffset`].
    ///
    ///
    /// # Examples
    ///
    /// ```
    /// use icu::timezone::CustomTimeZone;
    /// use icu::timezone::GmtOffset;
    ///
    /// let tz0: CustomTimeZone = CustomTimeZone::try_from_bytes(b"Z")
    ///     .expect("Failed to parse a time zone");
    /// let tz1: CustomTimeZone = CustomTimeZone::try_from_bytes(b"+02")
    ///     .expect("Failed to parse a time zone");
    /// let tz2: CustomTimeZone = CustomTimeZone::try_from_bytes(b"-0230")
    ///     .expect("Failed to parse a time zone");
    /// let tz3: CustomTimeZone = CustomTimeZone::try_from_bytes(b"+02:30")
    ///     .expect("Failed to parse a time zone");
    ///
    /// assert_eq!(tz0.gmt_offset.map(GmtOffset::offset_seconds), Some(0));
    /// assert_eq!(tz1.gmt_offset.map(GmtOffset::offset_seconds), Some(7200));
    /// assert_eq!(tz2.gmt_offset.map(GmtOffset::offset_seconds), Some(-9000));
    /// assert_eq!(tz3.gmt_offset.map(GmtOffset::offset_seconds), Some(9000));
    /// ```
    pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, TimeZoneError> {
        let gmt_offset = GmtOffset::try_from_bytes(bytes)?;
        Ok(Self {
            gmt_offset: Some(gmt_offset),
            time_zone_id: None,
            metazone_id: None,
            zone_variant: None,
        })
    }

    /// Overwrite the metazone id in MockTimeZone.
    ///
    /// # Examples
    ///
    /// ```
    /// use icu::calendar::DateTime;
    /// use icu::timezone::provider::{MetazoneId, TimeZoneBcp47Id};
    /// use icu::timezone::CustomTimeZone;
    /// use icu::timezone::MetazoneCalculator;
    /// use tinystr::tinystr;
    ///
    /// let mzc = MetazoneCalculator::new();
    /// let mut tz = CustomTimeZone {
    ///     gmt_offset: Some("+11".parse().expect("Failed to parse a GMT offset.")),
    ///     time_zone_id: Some(TimeZoneBcp47Id(tinystr!(8, "gugum"))),
    ///     metazone_id: None,
    ///     zone_variant: None,
    /// };
    /// tz.maybe_calculate_metazone(
    ///     &mzc,
    ///     &DateTime::try_new_iso_datetime(1971, 10, 31, 2, 0, 0).unwrap(),
    /// );
    /// assert_eq!(tz.metazone_id, Some(MetazoneId(tinystr!(4, "guam"))));
    /// ```
    pub fn maybe_calculate_metazone(
        &mut self,
        metazone_calculator: &MetazoneCalculator,
        local_datetime: &DateTime<Iso>,
    ) -> &mut Self {
        if let Some(time_zone_id) = self.time_zone_id {
            self.metazone_id =
                metazone_calculator.compute_metazone_from_time_zone(time_zone_id, local_datetime);
        }
        self
    }
}

impl FromStr for CustomTimeZone {
    type Err = TimeZoneError;

    /// Parse a [`CustomTimeZone`] from a string.
    ///
    /// This utility is for easily creating time zones, not a complete robust solution.
    ///
    /// The offset must range from GMT-12 to GMT+14.
    /// The string must be an ISO-8601 time zone designator:
    /// e.g. Z
    /// e.g. +05
    /// e.g. +0500
    /// e.g. +05:00
    ///
    /// # Examples
    ///
    /// ```
    /// use icu::timezone::CustomTimeZone;
    /// use icu::timezone::GmtOffset;
    ///
    /// let tz0: CustomTimeZone = "Z".parse().expect("Failed to parse a time zone");
    /// let tz1: CustomTimeZone =
    ///     "+02".parse().expect("Failed to parse a time zone");
    /// let tz2: CustomTimeZone =
    ///     "-0230".parse().expect("Failed to parse a time zone");
    /// let tz3: CustomTimeZone =
    ///     "+02:30".parse().expect("Failed to parse a time zone");
    ///
    /// assert_eq!(tz0.gmt_offset.map(GmtOffset::offset_seconds), Some(0));
    /// assert_eq!(tz1.gmt_offset.map(GmtOffset::offset_seconds), Some(7200));
    /// assert_eq!(tz2.gmt_offset.map(GmtOffset::offset_seconds), Some(-9000));
    /// assert_eq!(tz3.gmt_offset.map(GmtOffset::offset_seconds), Some(9000));
    /// ```
    fn from_str(input: &str) -> Result<Self, Self::Err> {
        CustomTimeZone::try_from_bytes(input.as_bytes())
    }
}