Skip to content

Commit

Permalink
Add icu_timezone crate (#2265)
Browse files Browse the repository at this point in the history
* Add timezones crate

* Move MockTimeZone and timezone provider types

* Move over GmtOffset and add error type

* Move MetaZonePeriodV1 over to icu_timezone

* rename to CustomTimeZone

* Move metazone calculator
  • Loading branch information
Manishearth authored Jul 29, 2022
1 parent cd04bf4 commit 169ee04
Show file tree
Hide file tree
Showing 39 changed files with 828 additions and 346 deletions.
20 changes: 20 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ members = [
"components/normalizer",
"components/plurals",
"components/properties",
"components/timezone",
"experimental/bies",
"experimental/casemapping",
"experimental/char16trie",
Expand Down
3 changes: 0 additions & 3 deletions components/calendar/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ pub enum DateTimeError {
/// The minimum value
min: isize,
},
/// The time zone offset was invalid.
#[displaydoc("Failed to parse time-zone offset")]
InvalidTimeZoneOffset,
/// Out of range
// TODO(Manishearth) turn this into a proper variant
OutOfRange,
Expand Down
117 changes: 0 additions & 117 deletions components/calendar/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -433,123 +433,6 @@ impl Time {
}
}

/// The GMT offset in seconds for a mock time zone
#[derive(Copy, Clone, Debug, Default)]
pub struct GmtOffset(i32);

impl GmtOffset {
/// Attempt to create a [`GmtOffset`] from a seconds input. It returns an error when the seconds
/// overflows or underflows.
pub fn try_new(seconds: i32) -> Result<Self, DateTimeError> {
// Valid range is from GMT-12 to GMT+14 in seconds.
if seconds < -(12 * 60 * 60) {
Err(DateTimeError::Underflow {
field: "GmtOffset",
min: -(12 * 60 * 60),
})
} else if seconds > (14 * 60 * 60) {
Err(DateTimeError::Overflow {
field: "GmtOffset",
max: (14 * 60 * 60),
})
} else {
Ok(Self(seconds))
}
}

/// Returns the raw offset value in seconds.
pub fn raw_offset_seconds(&self) -> i32 {
self.0
}

/// Returns `true` if the [`GmtOffset`] is positive, otherwise `false`.
pub fn is_positive(&self) -> bool {
self.0 >= 0
}

/// Returns `true` if the [`GmtOffset`] is zero, otherwise `false`.
pub fn is_zero(&self) -> bool {
self.0 == 0
}

/// Returns `true` if the [`GmtOffset`] has non-zero minutes, otherwise `false`.
pub fn has_minutes(&self) -> bool {
self.0 % 3600 / 60 > 0
}

/// Returns `true` if the [`GmtOffset`] has non-zero seconds, otherwise `false`.
pub fn has_seconds(&self) -> bool {
self.0 % 3600 % 60 > 0
}
}

impl FromStr for GmtOffset {
type Err = DateTimeError;

/// Parse a [`GmtOffset`] from a string.
///
/// 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::datetime::date::GmtOffset;
///
/// let offset0: GmtOffset = "Z".parse().expect("Failed to parse a GMT offset.");
/// let offset1: GmtOffset = "-09".parse().expect("Failed to parse a GMT offset.");
/// let offset2: GmtOffset = "-0930".parse().expect("Failed to parse a GMT offset.");
/// let offset3: GmtOffset = "-09:30".parse().expect("Failed to parse a GMT offset.");
/// ```
fn from_str(input: &str) -> Result<Self, Self::Err> {
let offset_sign = match input.chars().next() {
Some('+') => 1,
/* ASCII */ Some('-') => -1,
/* U+2212 */ Some('−') => -1,
Some('Z') => return Ok(Self(0)),
_ => return Err(DateTimeError::InvalidTimeZoneOffset),
};

let seconds = match input.chars().count() {
/* ±hh */
3 => {
#[allow(clippy::indexing_slicing)]
// TODO(#1668) Clippy exceptions need docs or fixing.
let hour: u8 = input[1..3].parse()?;
offset_sign * (hour as i32 * 60 * 60)
}
/* ±hhmm */
5 => {
#[allow(clippy::indexing_slicing)]
// TODO(#1668) Clippy exceptions need docs or fixing.
let hour: u8 = input[1..3].parse()?;
#[allow(clippy::indexing_slicing)]
// TODO(#1668) Clippy exceptions need docs or fixing.
let minute: u8 = input[3..5].parse()?;
offset_sign * (hour as i32 * 60 * 60 + minute as i32 * 60)
}
/* ±hh:mm */
6 => {
#[allow(clippy::indexing_slicing)]
// TODO(#1668) Clippy exceptions need docs or fixing.
let hour: u8 = input[1..3].parse()?;
#[allow(clippy::indexing_slicing)]
// TODO(#1668) Clippy exceptions need docs or fixing.
let minute: u8 = input[4..6].parse()?;
offset_sign * (hour as i32 * 60 * 60 + minute as i32 * 60)
}
#[allow(clippy::panic)] // TODO(#1668) Clippy exceptions need docs or fixing.
_ => panic!("Invalid time-zone designator"),
};

Self::try_new(seconds)
}
}

/// A weekday in a 7-day week, according to ISO-8601.
///
/// The discriminant values correspond to ISO-8601 weekday numbers (Monday = 1, Sunday = 7).
Expand Down
7 changes: 4 additions & 3 deletions components/datetime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ icu_locid = { version = "0.6", path = "../locid" }
icu_plurals = { version = "0.6", path = "../plurals" }
icu_provider = { version = "0.6", path = "../../provider/core", features = ["macros"] }
icu_calendar = { version = "0.6", path = "../calendar" }
icu_timezone = { version = "0.6", path = "../timezone" }
writeable = { version = "0.4", path = "../../utils/writeable" }
litemap = { version = "0.4.0", path = "../../utils/litemap" }
tinystr = { path = "../../utils/tinystr", version = "0.6.0", features = ["alloc", "zerovec"], default-features = false }
Expand All @@ -51,7 +52,7 @@ fixed_decimal = { version = "0.3", path = "../../utils/fixed_decimal" }

[dev-dependencies]
criterion = "0.3"
icu = { path = "../icu", default-features = false }
icu = { path = "../icu", default-features = false, features = ["experimental"] }
icu_benchmark_macros = { version = "0.6", path = "../../tools/benchmark/macros" }
icu_provider = { version = "0.6", path = "../../provider/core" }
icu_provider_adapters = { path = "../../provider/adapters" }
Expand All @@ -71,8 +72,8 @@ bench = false # This option is required for Benchmark CI
std = ["icu_provider/std", "icu_locid/std", "icu_calendar/std"]
default = []
bench = ["serde"]
serde = ["dep:serde", "litemap/serde", "zerovec/serde", "tinystr/serde", "smallvec/serde", "icu_calendar/serde", "icu_decimal/serde", "icu_provider/serde", "icu_plurals/serde"]
datagen = ["serde", "icu_calendar/datagen", "icu_provider/datagen", "std", "databake"]
serde = ["dep:serde", "litemap/serde", "zerovec/serde", "tinystr/serde", "smallvec/serde", "icu_calendar/serde", "icu_decimal/serde", "icu_provider/serde", "icu_plurals/serde", "icu_timezone/serde"]
datagen = ["serde", "icu_calendar/datagen", "icu_timezone/datagen", "icu_provider/datagen", "std", "databake"]

[[bench]]
name = "datetime"
Expand Down
13 changes: 7 additions & 6 deletions components/datetime/benches/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ use std::fmt::Write;
use icu_calendar::{DateTime, Gregorian};
use icu_datetime::DateTimeFormatter;
use icu_datetime::{
mock::{parse_gregorian_from_str, parse_zoned_gregorian_from_str, time_zone::MockTimeZone},
mock::{parse_gregorian_from_str, parse_zoned_gregorian_from_str},
time_zone::TimeZoneFormatterOptions,
ZonedDateTimeFormatter,
};
use icu_locid::Locale;
use icu_timezone::CustomTimeZone;

fn datetime_benches(c: &mut Criterion) {
let provider = icu_testdata::get_provider();
Expand Down Expand Up @@ -63,7 +64,7 @@ fn datetime_benches(c: &mut Criterion) {
group.bench_function("zoned_datetime_overview", |b| {
b.iter(|| {
for fx in &fxs.0 {
let datetimes: Vec<(DateTime<Gregorian>, MockTimeZone)> = fx
let datetimes: Vec<(DateTime<Gregorian>, CustomTimeZone)> = fx
.values
.iter()
.map(|value| parse_zoned_gregorian_from_str(value).unwrap())
Expand Down Expand Up @@ -221,7 +222,7 @@ fn datetime_benches(c: &mut Criterion) {
group.bench_function("ZonedDateTimeFormatter/format_to_write", |b| {
b.iter(|| {
for fx in &fxs.0 {
let datetimes: Vec<(DateTime<Gregorian>, MockTimeZone)> = fx
let datetimes: Vec<(DateTime<Gregorian>, CustomTimeZone)> = fx
.values
.iter()
.map(|value| parse_zoned_gregorian_from_str(value).unwrap())
Expand Down Expand Up @@ -255,7 +256,7 @@ fn datetime_benches(c: &mut Criterion) {
group.bench_function("ZonedDateTimeFormatter/format_to_string", |b| {
b.iter(|| {
for fx in &fxs.0 {
let datetimes: Vec<(DateTime<Gregorian>, MockTimeZone)> = fx
let datetimes: Vec<(DateTime<Gregorian>, CustomTimeZone)> = fx
.values
.iter()
.map(|value| parse_zoned_gregorian_from_str(value).unwrap())
Expand Down Expand Up @@ -286,7 +287,7 @@ fn datetime_benches(c: &mut Criterion) {
group.bench_function("FormattedZonedDateTime/format", |b| {
b.iter(|| {
for fx in &fxs.0 {
let datetimes: Vec<(DateTime<Gregorian>, MockTimeZone)> = fx
let datetimes: Vec<(DateTime<Gregorian>, CustomTimeZone)> = fx
.values
.iter()
.map(|value| parse_zoned_gregorian_from_str(value).unwrap())
Expand Down Expand Up @@ -321,7 +322,7 @@ fn datetime_benches(c: &mut Criterion) {
group.bench_function("FormattedZonedDateTime/to_string", |b| {
b.iter(|| {
for fx in &fxs.0 {
let datetimes: Vec<(DateTime<Gregorian>, MockTimeZone)> = fx
let datetimes: Vec<(DateTime<Gregorian>, CustomTimeZone)> = fx
.values
.iter()
.map(|value| parse_zoned_gregorian_from_str(value).unwrap())
Expand Down
4 changes: 2 additions & 2 deletions components/datetime/src/any/zoned_datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ use icu_plurals::provider::OrdinalV1Marker;
///
/// ```
/// use icu::calendar::{DateTime, Gregorian};
/// use icu::datetime::mock::time_zone::MockTimeZone;
/// use icu::timezone::CustomTimeZone;
/// use icu::datetime::{options::length, any::ZonedAnyDateTimeFormatter};
/// use icu::locid::locale;
/// use icu_datetime::TimeZoneFormatterOptions;
Expand All @@ -67,7 +67,7 @@ use icu_plurals::provider::OrdinalV1Marker;
/// .expect("Failed to construct DateTime.");
/// let any_datetime = datetime.to_any();
///
/// let time_zone: MockTimeZone = "+05:00".parse().expect("Timezone should parse");
/// let time_zone: CustomTimeZone = "+05:00".parse().expect("Timezone should parse");
///
/// let value = zdtf.format_to_string(&any_datetime, &time_zone).expect("calendars should match");
///
Expand Down
19 changes: 19 additions & 0 deletions components/datetime/src/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use icu_calendar::any_calendar::AnyCalendarKind;
use icu_calendar::Calendar;
use icu_calendar::{arithmetic::week_of, AsCalendar, Date, DateTime, Iso};
use icu_provider::DataLocale;
use icu_timezone::{CustomTimeZone, GmtOffset};
use tinystr::TinyStr8;

// TODO (Manishearth) fix up imports to directly import from icu_calendar
Expand Down Expand Up @@ -465,3 +466,21 @@ impl<A: AsCalendar> IsoTimeInput for DateTime<A> {
Some(self.time.nanosecond)
}
}

impl TimeZoneInput for CustomTimeZone {
fn gmt_offset(&self) -> Option<GmtOffset> {
self.gmt_offset
}

fn time_zone_id(&self) -> Option<TimeZoneBcp47Id> {
self.time_zone_id
}

fn metazone_id(&self) -> Option<MetaZoneId> {
self.metazone_id
}

fn time_variant(&self) -> Option<TinyStr8> {
self.time_variant
}
}
2 changes: 0 additions & 2 deletions components/datetime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,6 @@ pub mod datetime;
mod error;
pub mod fields;
mod format;
/// A module for metazone id calculation.
pub mod metazone;
pub mod mock;
pub mod options;
#[doc(hidden)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@
//! and examples.

use core::str::FromStr;
use either::Either;
use icu_calendar::{DateTime, DateTimeError, Gregorian};
use time_zone::MockTimeZone;

/// Temporary time zone input utilities.
pub mod time_zone;
use icu_timezone::{CustomTimeZone, TimeZoneError};

/// Temporary function for parsing a `DateTime<Gregorian>`
///
Expand Down Expand Up @@ -68,7 +66,7 @@ pub fn parse_gregorian_from_str(input: &str) -> Result<DateTime<Gregorian>, Date
Ok(datetime)
}

/// Parse a [`DateTime`] and [`MockTimeZone`] from a string.
/// Parse a [`DateTime`] and [`CustomTimeZone`] from a string.
///
/// This utility is for easily creating dates, not a complete robust solution. The
/// string must take a specific form of the ISO 8601 format:
Expand All @@ -87,14 +85,14 @@ pub fn parse_gregorian_from_str(input: &str) -> Result<DateTime<Gregorian>, Date
/// ```
pub fn parse_zoned_gregorian_from_str(
input: &str,
) -> Result<(DateTime<Gregorian>, MockTimeZone), DateTimeError> {
let datetime = parse_gregorian_from_str(input)?;
) -> Result<(DateTime<Gregorian>, CustomTimeZone), Either<DateTimeError, TimeZoneError>> {
let datetime = parse_gregorian_from_str(input).map_err(Either::Left)?;
let time_zone = match input
.rfind(|c| c == '+' || /* ASCII */ c == '-' || /* U+2212 */ c == '−' || c == 'Z')
{
#[allow(clippy::indexing_slicing)] // TODO(#1668) Clippy exceptions need docs or fixing.
Some(index) => FromStr::from_str(&input[index..])?,
None => return Err(DateTimeError::InvalidTimeZoneOffset),
Some(index) => FromStr::from_str(&input[index..]).map_err(Either::Right)?,
None => return Err(Either::Right(TimeZoneError::InvalidOffset)),
};

Ok((datetime, time_zone))
Expand Down
Loading

0 comments on commit 169ee04

Please sign in to comment.