1use core::str::FromStr;
6
7use crate::TimeZone;
8#[cfg(feature = "alloc")]
9use crate::provider::legacy::TimezoneVariantsOffsetsV1;
10use crate::provider::{TimezonePeriods, TimezonePeriodsV1};
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 const 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 const fn try_from_str(s: &str) -> Result<Self, InvalidOffsetError> {
79 Self::try_from_utf8(s.as_bytes())
80 }
81
82 pub const fn try_from_utf8(mut code_units: &[u8]) -> Result<Self, InvalidOffsetError> {
84 const fn try_get_time_component([tens, ones]: [u8; 2]) -> Option<i32> {
85 let Some(tens) = (tens as char).to_digit(10) else {
86 return None;
87 };
88 let Some(ones) = (ones as char).to_digit(10) else {
89 return None;
90 };
91 Some((tens * 10 + ones) as i32)
92 }
93
94 let offset_sign = match code_units {
95 [b'+', rest @ ..] => {
96 code_units = rest;
97 1
98 }
99 [b'-', rest @ ..] => {
100 code_units = rest;
101 -1
102 }
103 [226, 136, 146, rest @ ..] => {
105 code_units = rest;
106 -1
107 }
108 [b'Z'] => return Ok(Self(0)),
109 _ => return Err(InvalidOffsetError),
110 };
111
112 let hours = match code_units {
113 &[h1, h2, ..] => try_get_time_component([h1, h2]),
114 _ => None,
115 };
116 let Some(hours) = hours else {
117 return Err(InvalidOffsetError);
118 };
119
120 let minutes = match code_units {
121 &[_, _] => Some(0),
123 &[_, _, m1, m2] | &[_, _, b':', m1, m2] => try_get_time_component([m1, m2]),
125 _ => None,
126 };
127
128 let Some(minutes @ ..60) = minutes else {
129 return Err(InvalidOffsetError);
130 };
131
132 Self::try_from_seconds(offset_sign * (hours * 60 + minutes) * 60)
133 }
134
135 #[inline]
137 pub const fn from_seconds_unchecked(seconds: i32) -> Self {
138 Self(seconds)
139 }
140
141 pub const fn to_seconds(self) -> i32 {
143 self.0
144 }
145
146 pub fn is_non_negative(self) -> bool {
148 self.0 >= 0
149 }
150
151 pub fn is_zero(self) -> bool {
153 self.0 == 0
154 }
155
156 pub fn hours_part(self) -> i32 {
158 self.0 / 3600
159 }
160
161 pub fn minutes_part(self) -> u32 {
163 (self.0 % 3600 / 60).unsigned_abs()
164 }
165
166 pub fn seconds_part(self) -> u32 {
168 (self.0 % 60).unsigned_abs()
169 }
170}
171
172impl FromStr for UtcOffset {
173 type Err = InvalidOffsetError;
174
175 #[inline]
176 fn from_str(s: &str) -> Result<Self, Self::Err> {
177 Self::try_from_str(s)
178 }
179}
180
181#[derive(Debug)]
182enum OffsetData {
183 #[cfg(feature = "alloc")] Old(DataPayload<TimezoneVariantsOffsetsV1>),
185 New(DataPayload<TimezonePeriodsV1>),
186}
187
188#[derive(Debug)]
189enum OffsetDataBorrowed<'a> {
190 #[cfg(feature = "alloc")]
191 Old(&'a zerovec::ZeroMap2d<'a, TimeZone, ZoneNameTimestamp, VariantOffsets>),
192 New(&'a TimezonePeriods<'a>),
193}
194
195#[derive(Debug)]
199#[deprecated(
200 since = "2.1.0",
201 note = "this API is a bad approximation of a time zone database"
202)]
203pub struct VariantOffsetsCalculator {
204 offset_period: OffsetData,
205}
206
207#[derive(Debug)]
209#[deprecated(
210 since = "2.1.0",
211 note = "this API is a bad approximation of a time zone database"
212)]
213pub struct VariantOffsetsCalculatorBorrowed<'a> {
214 offset_period: OffsetDataBorrowed<'a>,
215}
216
217#[cfg(feature = "compiled_data")]
218#[allow(deprecated)]
219impl Default for VariantOffsetsCalculatorBorrowed<'static> {
220 fn default() -> Self {
221 VariantOffsetsCalculator::new()
222 }
223}
224
225#[allow(deprecated)]
226impl VariantOffsetsCalculator {
227 #[cfg(feature = "compiled_data")]
233 #[inline]
234 #[expect(clippy::new_ret_no_self)]
235 pub const fn new() -> VariantOffsetsCalculatorBorrowed<'static> {
236 VariantOffsetsCalculatorBorrowed::new()
237 }
238
239 #[cfg(feature = "serde")]
240 #[doc = icu_provider::gen_buffer_unstable_docs!(BUFFER, Self::new)]
241 pub fn try_new_with_buffer_provider(
242 provider: &(impl BufferProvider + ?Sized),
243 ) -> Result<Self, DataError> {
244 use icu_provider::buf::AsDeserializingBufferProvider;
245 {
246 Ok(Self {
247 offset_period: match DataProvider::<TimezonePeriodsV1>::load(
248 &provider.as_deserializing(),
249 Default::default(),
250 ) {
251 Ok(payload) => OffsetData::New(payload.payload),
252 Err(_e) => {
253 #[cfg(feature = "alloc")]
254 {
255 OffsetData::Old(
256 DataProvider::<TimezoneVariantsOffsetsV1>::load(
257 &provider.as_deserializing(),
258 Default::default(),
259 )?
260 .payload,
261 )
262 }
263 #[cfg(not(feature = "alloc"))]
264 return Err(_e);
265 }
266 },
267 })
268 }
269 }
270
271 #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new)]
272 pub fn try_new_unstable(
273 provider: &(impl DataProvider<TimezonePeriodsV1> + ?Sized),
274 ) -> Result<Self, DataError> {
275 let offset_period = provider.load(Default::default())?.payload;
276 Ok(Self {
277 offset_period: OffsetData::New(offset_period),
278 })
279 }
280
281 pub fn as_borrowed(&self) -> VariantOffsetsCalculatorBorrowed<'_> {
285 VariantOffsetsCalculatorBorrowed {
286 offset_period: match self.offset_period {
287 OffsetData::New(ref payload) => OffsetDataBorrowed::New(payload.get()),
288 #[cfg(feature = "alloc")]
289 OffsetData::Old(ref payload) => OffsetDataBorrowed::Old(payload.get()),
290 },
291 }
292 }
293}
294
295#[allow(deprecated)]
296impl VariantOffsetsCalculatorBorrowed<'static> {
297 #[cfg(feature = "compiled_data")]
303 #[inline]
304 pub const fn new() -> Self {
305 Self {
306 offset_period: OffsetDataBorrowed::New(
307 crate::provider::Baked::SINGLETON_TIMEZONE_PERIODS_V1,
308 ),
309 }
310 }
311
312 pub fn static_to_owned(&self) -> VariantOffsetsCalculator {
317 VariantOffsetsCalculator {
318 offset_period: match self.offset_period {
319 OffsetDataBorrowed::New(p) => OffsetData::New(DataPayload::from_static_ref(p)),
320 #[cfg(feature = "alloc")]
321 OffsetDataBorrowed::Old(p) => OffsetData::Old(DataPayload::from_static_ref(p)),
322 },
323 }
324 }
325}
326
327#[allow(deprecated)]
328impl VariantOffsetsCalculatorBorrowed<'_> {
329 pub fn compute_offsets_from_time_zone_and_name_timestamp(
374 &self,
375 time_zone_id: TimeZone,
376 timestamp: ZoneNameTimestamp,
377 ) -> Option<VariantOffsets> {
378 match self.offset_period {
379 OffsetDataBorrowed::New(p) => p.get(time_zone_id, timestamp).map(|(os, _)| os),
380 #[cfg(feature = "alloc")]
381 OffsetDataBorrowed::Old(p) => {
382 use zerovec::ule::AsULE;
383 let mut offsets = None;
384 for (bytes, id) in p.get0(&time_zone_id)?.iter1_copied().rev() {
385 if timestamp >= ZoneNameTimestamp::from_unaligned(*bytes) {
386 offsets = Some(id);
387 break;
388 }
389 }
390 Some(offsets?)
391 }
392 }
393 }
394}
395
396#[deprecated(
397 since = "2.1.0",
398 note = "this API is a bad approximation of a time zone database"
399)]
400pub use crate::provider::VariantOffsets;
401
402#[test]
403#[allow(deprecated)]
404pub fn test_legacy_offsets_data() {
405 use icu_locale_core::subtags::subtag;
406 use icu_provider_blob::BlobDataProvider;
407
408 let c = VariantOffsetsCalculator::try_new_with_buffer_provider(
409 &BlobDataProvider::try_new_from_static_blob(
410 include_bytes!("../../tests/data/offset_periods_old.blob"),
412 )
413 .unwrap(),
414 )
415 .unwrap();
416
417 let tz = TimeZone(subtag!("aqcas"));
418
419 for t in [
420 ZoneNameTimestamp::from_epoch_seconds(0),
421 ZoneNameTimestamp::from_epoch_seconds(1255802400),
422 ZoneNameTimestamp::from_epoch_seconds(1267714800),
423 ZoneNameTimestamp::from_epoch_seconds(1319738400),
424 ZoneNameTimestamp::from_epoch_seconds(1329843600),
425 ZoneNameTimestamp::from_epoch_seconds(1477065600),
426 ZoneNameTimestamp::from_epoch_seconds(1520701200),
427 ZoneNameTimestamp::from_epoch_seconds(1538856000),
428 ZoneNameTimestamp::from_epoch_seconds(1552752000),
429 ZoneNameTimestamp::from_epoch_seconds(1570129200),
430 ZoneNameTimestamp::from_epoch_seconds(1583596800),
431 ZoneNameTimestamp::from_epoch_seconds(1615640400),
432 ZoneNameTimestamp::from_epoch_seconds(1647090000),
433 ZoneNameTimestamp::from_epoch_seconds(1678291200),
434 ] {
435 assert_eq!(
436 c.as_borrowed()
437 .compute_offsets_from_time_zone_and_name_timestamp(tz, t),
438 VariantOffsetsCalculator::new()
439 .compute_offsets_from_time_zone_and_name_timestamp(tz, t),
440 "{t:?}",
441 );
442 }
443}