1use core::str::FromStr;
6
7#[cfg(feature = "alloc")]
8use crate::provider::legacy::TimezoneVariantsOffsetsV1;
9use crate::provider::{TimezonePeriods, TimezonePeriodsV1};
10use crate::TimeZone;
11use icu_provider::prelude::*;
12
13use displaydoc::Display;
14
15use super::ZoneNameTimestamp;
16
17#[derive(Display, Debug, Copy, Clone, PartialEq)]
19#[allow(clippy::exhaustive_structs)]
20pub struct InvalidOffsetError;
21
22#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, PartialOrd, Ord)]
28pub struct UtcOffset(i32);
29
30impl UtcOffset {
31 pub fn try_from_seconds(seconds: i32) -> Result<Self, InvalidOffsetError> {
35 if seconds.unsigned_abs() > 18 * 60 * 60 {
36 Err(InvalidOffsetError)
37 } else {
38 Ok(Self(seconds))
39 }
40 }
41
42 pub const fn zero() -> Self {
44 Self(0)
45 }
46
47 #[inline]
78 pub fn try_from_str(s: &str) -> Result<Self, InvalidOffsetError> {
79 Self::try_from_utf8(s.as_bytes())
80 }
81
82 pub fn try_from_utf8(mut code_units: &[u8]) -> Result<Self, InvalidOffsetError> {
84 fn try_get_time_component([tens, ones]: [u8; 2]) -> Option<i32> {
85 Some(((tens as char).to_digit(10)? * 10 + (ones as char).to_digit(10)?) as i32)
86 }
87
88 let offset_sign = match code_units {
89 [b'+', rest @ ..] => {
90 code_units = rest;
91 1
92 }
93 [b'-', rest @ ..] => {
94 code_units = rest;
95 -1
96 }
97 [226, 136, 146, rest @ ..] => {
99 code_units = rest;
100 -1
101 }
102 [b'Z'] => return Ok(Self(0)),
103 _ => return Err(InvalidOffsetError),
104 };
105
106 let hours = match code_units {
107 &[h1, h2, ..] => try_get_time_component([h1, h2]),
108 _ => None,
109 }
110 .ok_or(InvalidOffsetError)?;
111
112 let minutes = match code_units {
113 &[_, _] => Some(0),
115 &[_, _, m1, m2] | &[_, _, b':', m1, m2] => {
117 try_get_time_component([m1, m2]).filter(|&m| m < 60)
118 }
119 _ => None,
120 }
121 .ok_or(InvalidOffsetError)?;
122
123 Self::try_from_seconds(offset_sign * (hours * 60 + minutes) * 60)
124 }
125
126 #[inline]
128 pub const fn from_seconds_unchecked(seconds: i32) -> Self {
129 Self(seconds)
130 }
131
132 pub const fn to_seconds(self) -> i32 {
134 self.0
135 }
136
137 pub fn is_non_negative(self) -> bool {
139 self.0 >= 0
140 }
141
142 pub fn is_zero(self) -> bool {
144 self.0 == 0
145 }
146
147 pub fn hours_part(self) -> i32 {
149 self.0 / 3600
150 }
151
152 pub fn minutes_part(self) -> u32 {
154 (self.0 % 3600 / 60).unsigned_abs()
155 }
156
157 pub fn seconds_part(self) -> u32 {
159 (self.0 % 60).unsigned_abs()
160 }
161}
162
163impl FromStr for UtcOffset {
164 type Err = InvalidOffsetError;
165
166 #[inline]
167 fn from_str(s: &str) -> Result<Self, Self::Err> {
168 Self::try_from_str(s)
169 }
170}
171
172#[derive(Debug)]
173enum OffsetData {
174 #[cfg(feature = "alloc")] Old(DataPayload<TimezoneVariantsOffsetsV1>),
176 New(DataPayload<TimezonePeriodsV1>),
177}
178
179#[derive(Debug)]
180enum OffsetDataBorrowed<'a> {
181 #[cfg(feature = "alloc")]
182 Old(&'a zerovec::ZeroMap2d<'a, TimeZone, ZoneNameTimestamp, VariantOffsets>),
183 New(&'a TimezonePeriods<'a>),
184}
185
186#[derive(Debug)]
190#[deprecated(
191 since = "2.1.0",
192 note = "this API is a bad approximation of a time zone database"
193)]
194pub struct VariantOffsetsCalculator {
195 offset_period: OffsetData,
196}
197
198#[derive(Debug)]
200#[deprecated(
201 since = "2.1.0",
202 note = "this API is a bad approximation of a time zone database"
203)]
204pub struct VariantOffsetsCalculatorBorrowed<'a> {
205 offset_period: OffsetDataBorrowed<'a>,
206}
207
208#[cfg(feature = "compiled_data")]
209#[allow(deprecated)]
210impl Default for VariantOffsetsCalculatorBorrowed<'static> {
211 fn default() -> Self {
212 VariantOffsetsCalculator::new()
213 }
214}
215
216#[allow(deprecated)]
217impl VariantOffsetsCalculator {
218 #[cfg(feature = "compiled_data")]
224 #[inline]
225 #[expect(clippy::new_ret_no_self)]
226 pub const fn new() -> VariantOffsetsCalculatorBorrowed<'static> {
227 VariantOffsetsCalculatorBorrowed::new()
228 }
229
230 #[cfg(feature = "serde")]
231 #[doc = icu_provider::gen_buffer_unstable_docs!(BUFFER, Self::new)]
232 pub fn try_new_with_buffer_provider(
233 provider: &(impl icu_provider::buf::BufferProvider + ?Sized),
234 ) -> Result<Self, DataError> {
235 use icu_provider::buf::AsDeserializingBufferProvider;
236 {
237 Ok(Self {
238 offset_period: match DataProvider::<TimezonePeriodsV1>::load(
239 &provider.as_deserializing(),
240 Default::default(),
241 ) {
242 Ok(payload) => OffsetData::New(payload.payload),
243 Err(_e) => {
244 #[cfg(feature = "alloc")]
245 {
246 OffsetData::Old(
247 DataProvider::<TimezoneVariantsOffsetsV1>::load(
248 &provider.as_deserializing(),
249 Default::default(),
250 )?
251 .payload,
252 )
253 }
254 #[cfg(not(feature = "alloc"))]
255 return Err(_e);
256 }
257 },
258 })
259 }
260 }
261
262 #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new)]
263 pub fn try_new_unstable(
264 provider: &(impl DataProvider<TimezonePeriodsV1> + ?Sized),
265 ) -> Result<Self, DataError> {
266 let offset_period = provider.load(Default::default())?.payload;
267 Ok(Self {
268 offset_period: OffsetData::New(offset_period),
269 })
270 }
271
272 pub fn as_borrowed(&self) -> VariantOffsetsCalculatorBorrowed<'_> {
276 VariantOffsetsCalculatorBorrowed {
277 offset_period: match self.offset_period {
278 OffsetData::New(ref payload) => OffsetDataBorrowed::New(payload.get()),
279 #[cfg(feature = "alloc")]
280 OffsetData::Old(ref payload) => OffsetDataBorrowed::Old(payload.get()),
281 },
282 }
283 }
284}
285
286#[allow(deprecated)]
287impl VariantOffsetsCalculatorBorrowed<'static> {
288 #[cfg(feature = "compiled_data")]
294 #[inline]
295 pub const fn new() -> Self {
296 Self {
297 offset_period: OffsetDataBorrowed::New(
298 crate::provider::Baked::SINGLETON_TIMEZONE_PERIODS_V1,
299 ),
300 }
301 }
302
303 pub fn static_to_owned(&self) -> VariantOffsetsCalculator {
308 VariantOffsetsCalculator {
309 offset_period: match self.offset_period {
310 OffsetDataBorrowed::New(p) => OffsetData::New(DataPayload::from_static_ref(p)),
311 #[cfg(feature = "alloc")]
312 OffsetDataBorrowed::Old(p) => OffsetData::Old(DataPayload::from_static_ref(p)),
313 },
314 }
315 }
316}
317
318#[allow(deprecated)]
319impl VariantOffsetsCalculatorBorrowed<'_> {
320 pub fn compute_offsets_from_time_zone_and_name_timestamp(
365 &self,
366 time_zone_id: TimeZone,
367 timestamp: ZoneNameTimestamp,
368 ) -> Option<VariantOffsets> {
369 match self.offset_period {
370 OffsetDataBorrowed::New(p) => p.get(time_zone_id, timestamp).map(|(os, _)| os),
371 #[cfg(feature = "alloc")]
372 OffsetDataBorrowed::Old(p) => {
373 use zerovec::ule::AsULE;
374 let mut offsets = None;
375 for (bytes, id) in p.get0(&time_zone_id)?.iter1_copied().rev() {
376 if timestamp >= ZoneNameTimestamp::from_unaligned(*bytes) {
377 offsets = Some(id);
378 break;
379 }
380 }
381 Some(offsets?)
382 }
383 }
384 }
385}
386
387#[deprecated(
388 since = "2.1.0",
389 note = "this API is a bad approximation of a time zone database"
390)]
391pub use crate::provider::VariantOffsets;
392
393#[test]
394#[allow(deprecated)]
395pub fn test_legacy_offsets_data() {
396 use crate::ZonedDateTime;
397 use icu_locale_core::subtags::subtag;
398 use icu_provider_blob::BlobDataProvider;
399
400 let c = VariantOffsetsCalculator::try_new_with_buffer_provider(
401 &BlobDataProvider::try_new_from_static_blob(
402 include_bytes!("../../tests/data/offset_periods_old.blob"),
404 )
405 .unwrap(),
406 )
407 .unwrap();
408
409 let tz = TimeZone(subtag!("aqcas"));
410
411 for timestamp in [
412 "1970-01-01 00:00Z",
413 "2009-10-17 18:00Z",
414 "2010-03-04 15:00Z",
415 "2011-10-27 18:00Z",
416 "2012-02-21 17:00Z",
417 "2016-10-21 16:00Z",
418 "2018-03-10 17:00Z",
419 "2018-10-06 20:00Z",
420 "2019-03-16 16:00Z",
421 "2019-10-03 19:00Z",
422 "2020-03-07 16:00Z",
423 "2021-03-13 13:00Z",
424 "2022-03-12 13:00Z",
425 "2023-03-08 16:00Z",
426 ] {
427 let t = ZoneNameTimestamp::from_zoned_date_time_iso(
428 ZonedDateTime::try_offset_only_from_str(timestamp, icu_calendar::Iso).unwrap(),
429 );
430
431 assert_eq!(
432 c.as_borrowed()
433 .compute_offsets_from_time_zone_and_name_timestamp(tz, t),
434 VariantOffsetsCalculator::new()
435 .compute_offsets_from_time_zone_and_name_timestamp(tz, t),
436 "{timestamp:?}",
437 );
438 }
439}