diff --git a/CHANGELOG.md b/CHANGELOG.md index 047d318..f7bddc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [0.5.0] - 2023-10-08 +Astrolabe can now get the local timezone offset on UNIX systems. +```rust +use astrolabe::{DateTime, Offset, OffsetUtilities, Precision}; + +// Equivalent to `DateTime::now().set_offset(Offset::Local)` +let now = DateTime::now_local(); +// Prints for example: +// 2023-10-08T08:30:00+02:00 +println!("{}", now.format_rfc3339(Precision::Seconds)); +assert_eq!(Offset::Local, now.get_offset()); +``` + +Also, we decided to panic in case of a date overflow. This should not happen if the library is used correctly, as the valid date range is between `30. June -5879611`..=`12. July 5879611`. This change makes the API much easier to use, as many functions only returned a `Result` because of this edge case. + +### Added +- Enums + - [`Offset`](https://docs.rs/astrolabe/0.5.0/astrolabe/enum.Offset.html) - Represents an offset from UTC. `Fixed` or `Local` + +### Changed +- Updated the [`OffsetUtilities`](https://docs.rs/astrolabe/0.5.0/astrolabe/trait.OffsetUtilities.html) trait to use the [`Offset`](https://docs.rs/astrolabe/0.5.0/astrolabe/enum.Offset.html) enum +- The `add_*` and `sub_*` functions from the [`DateUtilities`](https://docs.rs/astrolabe/0.5.0/astrolabe/trait.DateUtilities.html) and [`TimeUtilities`](https://docs.rs/astrolabe/0.5.0/astrolabe/trait.TimeUtilities.html) traits now return `Self` instead of `Result` +- `from_timestamp` from the [`DateUtilities`](https://docs.rs/astrolabe/0.5.0/astrolabe/trait.DateUtilities.html) trait now return `Self` instead of `Result` + ## [0.4.0] - 2023-05-18 ### Added - Structs diff --git a/Cargo.toml b/Cargo.toml index 12829aa..ece1555 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ homepage = "https://github.com/giyomoon/astrolabe" documentation = "https://docs.rs/astrolabe/" repository = "https://github.com/giyomoon/astrolabe" readme = "README.md" -authors = ["Jasmin "] +authors = ["Jasmin "] license = "MIT OR Apache-2.0" keywords = ["date", "time"] categories = ["date-and-time"] diff --git a/LICENSE-APACHE b/LICENSE-APACHE index cf95792..485a157 100644 --- a/LICENSE-APACHE +++ b/LICENSE-APACHE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2022 GiyoMoon + Copyright 2022-2023 Jasmin Noetzli Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/LICENSE-MIT b/LICENSE-MIT index 74709b5..b626415 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 GiyoMoon +Copyright (c) 2022-2023 Jasmin Noetzli Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index aacdbc0..15dc472 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,8 @@ Crate | - - Example + + Examples @@ -72,10 +72,12 @@ Astrolabe is a date and time library for Rust which aims to be feature rich, lig - **Manipulation** functions to add, subtract, set and clear date units - **Cron** expression parser - **Timezone** offset +- **Local** timezone on UNIX platforms - **Zero** dependencies - **Serde** serializing and deserializing (With feature flag `serde`) -## Example +## Examples +### Basic A basic example which demonstrates creating, formatting and manipulating a `DateTime` instance. ```rust @@ -89,15 +91,55 @@ assert_eq!("2022/05/02", date_time.format("yyyy/MM/dd")); // Create a new instance with a modified DateTime // The previous instance is not modified and is still in scope -let modified_dt = date_time - .add_hours(11).unwrap() - .add_minutes(23).unwrap(); +let modified_dt = date_time.add_hours(11).add_minutes(23); assert_eq!("2022/05/02 11:23:00", modified_dt.format("yyyy/MM/dd HH:mm:ss")); assert_eq!("2022-05-02T11:23:00Z", modified_dt.format_rfc3339(Precision::Seconds)); ``` To see all implementations for the `DateTime` struct, check out it's [documentation](https://docs.rs/astrolabe/latest/astrolabe/struct.DateTime.html). +### Local timezone (UNIX systems only) +Astrolabe can parse the timezone from `/etc/localtime` to get the local UTC offset. This only works on UNIX systems. + +```rust +use astrolabe::{DateTime, Offset, OffsetUtilities, Precision}; + +// Equivalent to `DateTime::now().set_offset(Offset::Local)` +let now = DateTime::now_local(); + +// Prints for example: +// 2023-10-08T08:30:00+02:00 +println!("{}", now.format_rfc3339(Precision::Seconds)); +assert_eq!(Offset::Local, now.get_offset()); +``` +See [`Offset`](https://docs.rs/astrolabe/latest/astrolabe/enum.Offset.html) + +### CRON parsing +```rust +use astrolabe::CronSchedule; + +// Every 5 minutes +let schedule = CronSchedule::parse("*/5 * * * *").unwrap(); +for date in schedule.take(3) { + println!("{}", date); +} +// Prints for example: +// 2022-05-02 16:15:00 +// 2022-05-02 16:20:00 +// 2022-05-02 16:25:00 + +// Every weekday at 10:00 +let schedule = CronSchedule::parse("0 10 * * Mon-Fri").unwrap(); +for date in schedule.take(3) { + println!("{}", date.format("yyyy-MM-dd HH:mm:ss eeee")); +} +// Prints for example: +// 2022-05-03 10:00:00 Tuesday +// 2022-05-04 10:00:00 Wednesday +// 2022-05-05 10:00:00 Thursday +``` +See [`CronSchedule`](https://docs.rs/astrolabe/latest/astrolabe/struct.CronSchedule.html) + ## MSRV This crate uses the Rust 2021 Edition and requires at least version `1.56`. diff --git a/src/cron.rs b/src/cron.rs index ddae7b5..f9deccb 100644 --- a/src/cron.rs +++ b/src/cron.rs @@ -225,14 +225,14 @@ impl Iterator for CronSchedule { _ => now, }; - let mut next = last.add_minutes(1).ok()?; + let mut next = last.add_minutes(1); let dom_restricted = self.days_of_month.len() != 31; let dow_restricted = self.days_of_week.len() != 7; loop { if !self.months.contains(&(next.month() as u8)) { - next = next.add_months(1).ok()?.clear_until_day(); + next = next.add_months(1).clear_until_day(); continue; } @@ -252,17 +252,17 @@ impl Iterator for CronSchedule { && !self.days_of_month.contains(&day_of_month)) || (dow_restricted && !dom_restricted && !self.days_of_week.contains(&day_of_week)) { - next = next.add_days(1).ok()?.clear_until_hour(); + next = next.add_days(1).clear_until_hour(); continue; } if !self.hours.contains(&(next.hour() as u8)) { - next = next.add_hours(1).ok()?.clear_until_minute(); + next = next.add_hours(1).clear_until_minute(); continue; } if !self.minutes.contains(&(next.minute() as u8)) { - next = next.add_minutes(1).ok()?.clear_until_second(); + next = next.add_minutes(1).clear_until_second(); continue; } @@ -306,7 +306,7 @@ fn parse_cron_part( } else if let Some(step) = part.strip_prefix("*/") { let step: u8 = step .parse() - .map_err(|_| format!("Can't parse step value to u8: {step}"))?; + .map_err(|_| format!("Can't parse step value to u8: {}", step))?; if step == 0 { return Err("Step value can't be 0".to_string()); } @@ -368,7 +368,7 @@ fn parse_value(value: &str, cron_type: &CronPartType) -> Result { CronPartType::DayOfWeek if value == "7" => 0, _ => value .parse::() - .map_err(|_| format!("Can't parse value to u8: {value}"))?, + .map_err(|_| format!("Can't parse value to u8: {}", value))?, }) } @@ -479,15 +479,14 @@ mod cron_tests { "2022/01/20 00:00:00", ]; cron_next("0 0 20 * mon", expected, now); + } - // Test if iterator returns none at overflow + #[test] + #[should_panic] + // Test if iterator returns none at overflow + fn overflow() { let now = DateTime::from_ymdhms(5_879_611, 7, 12, 23, 59, 0).unwrap(); - cron_next_none("* * * * *", now); - let now = DateTime::from_ymdhms(5_879_611, 7, 12, 23, 58, 0).unwrap(); - cron_next_none("* * * 8 *", now); - cron_next_none("* * 13 * *", now); - cron_next_none("* 0 * * *", now); - cron_next_none("0 * * * *", now); + CronSchedule::parse("* * * * *", Some(now)).unwrap().next(); } fn cron_next(cron: &str, expected: Vec<&str>, now: DateTime) { @@ -498,11 +497,4 @@ mod cron_tests { assert_eq!(expected, next.format("yyyy/MM/dd HH:mm:ss")); } } - - fn cron_next_none(cron: &str, now: DateTime) { - assert!(CronSchedule::parse(cron, Some(now)) - .unwrap() - .next() - .is_none()); - } } diff --git a/src/date.rs b/src/date.rs index f5cdd71..f29c5b1 100644 --- a/src/date.rs +++ b/src/date.rs @@ -29,7 +29,7 @@ use std::{ /// See the [`DateUtilities`](#impl-DateUtilities-for-Date) implementation for get, set and manipulation methods. /// /// Range: `30. June -5879611`..=`12. July 5879611`. Please note that year 0 does not exist. After year -1 follows year 1. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)] pub struct Date { pub(crate) days: i32, } @@ -123,7 +123,7 @@ impl Date { // Use day of year if present, otherwise use month + day of month Ok(if date.day_of_year.is_some() { - let days = year_doy_to_days(date.year.unwrap_or(1), date.day_of_year.unwrap())?; + let days = year_doy_to_days(date.year.unwrap_or(1), date.day_of_year.unwrap(), false)?; Self { days } } else { Self::from_ymd( @@ -250,7 +250,7 @@ impl DateUtilities for Date { days_to_wday(self.days, false) as u8 } - fn from_timestamp(timestamp: i64) -> Result { + fn from_timestamp(timestamp: i64) -> Self { let days = (timestamp / SECS_PER_DAY_U64 as i64 + DAYS_TO_1970_I64 - i64::from( timestamp.is_negative() && timestamp.unsigned_abs() % SECS_PER_DAY_U64 != 0, @@ -265,9 +265,14 @@ impl DateUtilities for Date { - 1, timestamp as i128, ) - })?; + }); - Ok(Self { days }) + let days = match days { + Ok(days) => days, + Err(e) => panic!("{}", e), + }; + + Self { days } } fn timestamp(&self) -> i64 { @@ -294,34 +299,70 @@ impl DateUtilities for Date { Ok(Self { days: new_days }) } - fn add_years(&self, years: u32) -> Result { - let new_days = add_years(self.days, years)?; - Ok(Self { days: new_days }) + fn add_years(&self, years: u32) -> Self { + let new_days = add_years(self.days, years); + + let new_days = match new_days { + Ok(new_days) => new_days, + Err(e) => panic!("{}", e), + }; + + Self { days: new_days } } - fn add_months(&self, months: u32) -> Result { - let new_days = add_months(self.days, months)?; - Ok(Self { days: new_days }) + fn add_months(&self, months: u32) -> Self { + let new_days = add_months(self.days, months); + + let new_days = match new_days { + Ok(new_days) => new_days, + Err(e) => panic!("{}", e), + }; + + Self { days: new_days } } - fn add_days(&self, days: u32) -> Result { - let new_days = add_days(self.days, days)?; - Ok(Self { days: new_days }) + fn add_days(&self, days: u32) -> Self { + let new_days = add_days(self.days, days); + + let new_days = match new_days { + Ok(new_days) => new_days, + Err(e) => panic!("{}", e), + }; + + Self { days: new_days } } - fn sub_years(&self, years: u32) -> Result { - let new_days = sub_years(self.days, years)?; - Ok(Self { days: new_days }) + fn sub_years(&self, years: u32) -> Self { + let new_days = sub_years(self.days, years); + + let new_days = match new_days { + Ok(new_days) => new_days, + Err(e) => panic!("{}", e), + }; + + Self { days: new_days } } - fn sub_months(&self, months: u32) -> Result { - let new_days = sub_months(self.days, months)?; - Ok(Self { days: new_days }) + fn sub_months(&self, months: u32) -> Self { + let new_days = sub_months(self.days, months); + + let new_days = match new_days { + Ok(new_days) => new_days, + Err(e) => panic!("{}", e), + }; + + Self { days: new_days } } - fn sub_days(&self, days: u32) -> Result { - let new_days = sub_days(self.days, days)?; - Ok(Self { days: new_days }) + fn sub_days(&self, days: u32) -> Self { + let new_days = sub_days(self.days, days); + + let new_days = match new_days { + Ok(new_days) => new_days, + Err(e) => panic!("{}", e), + }; + + Self { days: new_days } } fn clear_until_year(&self) -> Self { diff --git a/src/datetime.rs b/src/datetime.rs index 6e2570a..f0ab133 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -1,14 +1,11 @@ +use crate::offset::Offset; use crate::util::constants::DAYS_TO_1970; use crate::{ - errors::{ - invalid_format::create_invalid_format, - out_of_range::{create_custom_oor, create_simple_oor}, - AstrolabeError, - }, + errors::{invalid_format::create_invalid_format, AstrolabeError}, util::{ constants::{ - DAYS_TO_1970_I64, NANOS_PER_DAY, NANOS_PER_SEC, SECS_PER_DAY, SECS_PER_DAY_U64, - SECS_PER_HOUR, SECS_PER_HOUR_U64, SECS_PER_MINUTE, SECS_PER_MINUTE_U64, + DAYS_TO_1970_I64, NANOS_PER_DAY, NANOS_PER_SEC, SECS_PER_DAY_U64, SECS_PER_HOUR_U64, + SECS_PER_MINUTE_U64, }, date::{ convert::{ @@ -63,11 +60,11 @@ use std::{ /// [`OffsetUtilities`](#impl-OffsetUtilities-for-DateTime) implements methods for setting and getting the offset. /// /// Range: `30. June -5879611 00:00:00`..=`12. July 5879611 23:59:59`. Please note that year 0 does not exist. After year -1 follows year 1. -#[derive(Debug, Default, Copy, Clone, Eq)] +#[derive(Debug, Default, Clone, Copy, Eq)] pub struct DateTime { pub(crate) days: i32, pub(crate) nanoseconds: u64, - pub(crate) offset: i32, + pub(crate) offset: Offset, } impl DateTime { @@ -90,10 +87,22 @@ impl DateTime { Self { days: days as i32, nanoseconds, - offset: 0, + offset: Offset::default(), } } + /// Creates a new [`DateTime`] instance with [`SystemTime::now()`] with the local timezone as the offset. + /// + /// ```rust + /// # use astrolabe::{DateTime, DateUtilities, Offset, OffsetUtilities}; + /// let date_time = DateTime::now_local(); + /// assert!(2021 < date_time.year()); + /// assert_eq!(date_time.get_offset(), Offset::Local); + /// ``` + pub fn now_local() -> Self { + Self::now().set_offset(Offset::Local) + } + /// Creates a new [`DateTime`] instance from year, month, day (day of month), hour, minute and seconds. /// /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided values are invalid. @@ -116,7 +125,7 @@ impl DateTime { Ok(Self { days, nanoseconds: seconds * NANOS_PER_SEC, - offset: 0, + offset: Offset::default(), }) } @@ -154,7 +163,7 @@ impl DateTime { Ok(Self { days, nanoseconds: 0, - offset: 0, + offset: Offset::default(), }) } @@ -187,7 +196,7 @@ impl DateTime { Ok(Self { days: 0, nanoseconds: seconds * NANOS_PER_SEC, - offset: 0, + offset: Offset::default(), }) } @@ -286,12 +295,12 @@ impl DateTime { let days = date_to_days(year, month, day)?; let seconds = time_to_day_seconds(hour, minute, second)? as u64; - Self { + Ok(Self { days, nanoseconds: seconds * NANOS_PER_SEC + nanos, - offset: 0, + offset: Offset::default(), } - .as_offset(offset) + .as_offset(Offset::Fixed(offset))) } /// Format as an RFC 3339 timestamp (`2022-05-02T15:30:20Z`). @@ -379,7 +388,7 @@ impl DateTime { // Use day of year if present, otherwise use month + day of month let mut date_time = if date.day_of_year.is_some() { - let days = year_doy_to_days(date.year.unwrap_or(1), date.day_of_year.unwrap())?; + let days = year_doy_to_days(date.year.unwrap_or(1), date.day_of_year.unwrap(), false)?; Self { days, ..Default::default() @@ -413,7 +422,7 @@ impl DateTime { date_time = date_time.set_time(Time::from_nanos(nanoseconds)?); if let Some(offset) = time.offset { - date_time = date_time.set_offset(offset)?; + date_time = date_time.set_offset(Offset::from_seconds(offset)?); } Ok(date_time) @@ -513,8 +522,9 @@ impl DateTime { /// ``` /// pub fn format(&self, format: &str) -> String { + let offset_seconds = self.offset.resolve(); let parts = parse_format_string(format); - let (days, nanoseconds) = add_offset_to_dn(self.days, self.nanoseconds, self.offset); + let (days, nanoseconds) = add_offset_to_dn(self.days, self.nanoseconds, offset_seconds); parts .iter() @@ -532,7 +542,7 @@ impl DateTime { .collect::>(); } - format_part(part, days, nanoseconds, self.offset) + format_part(part, days, nanoseconds, offset_seconds) .chars() .collect::>() }) @@ -557,37 +567,41 @@ impl DateTime { impl DateUtilities for DateTime { fn year(&self) -> i32 { - let days = add_offset_to_dn(self.days, self.nanoseconds, self.offset).0; + let days = add_offset_to_dn(self.days, self.nanoseconds, self.offset.resolve()).0; days_to_date(days).0 } fn month(&self) -> u32 { - let days = add_offset_to_dn(self.days, self.nanoseconds, self.offset).0; + let days = add_offset_to_dn(self.days, self.nanoseconds, self.offset.resolve()).0; days_to_date(days).1 } fn day(&self) -> u32 { - let days = add_offset_to_dn(self.days, self.nanoseconds, self.offset).0; + let days = add_offset_to_dn(self.days, self.nanoseconds, self.offset.resolve()).0; days_to_date(days).2 } fn day_of_year(&self) -> u32 { - let days = add_offset_to_dn(self.days, self.nanoseconds, self.offset).0; + let days = add_offset_to_dn(self.days, self.nanoseconds, self.offset.resolve()).0; days_to_doy(days) } fn weekday(&self) -> u8 { - let days = add_offset_to_dn(self.days, self.nanoseconds, self.offset).0; + let days = add_offset_to_dn(self.days, self.nanoseconds, self.offset.resolve()).0; days_to_wday(days, false) as u8 } - fn from_timestamp(timestamp: i64) -> Result { - Self::from_seconds(timestamp + DAYS_TO_1970_I64 * SECS_PER_DAY_U64 as i64) + fn from_timestamp(timestamp: i64) -> Self { + let date_time = Self::from_seconds(timestamp + DAYS_TO_1970_I64 * SECS_PER_DAY_U64 as i64); + match date_time { + Ok(date_time) => date_time, + Err(e) => panic!("{}", e), + } } fn timestamp(&self) -> i64 { @@ -595,111 +609,145 @@ impl DateUtilities for DateTime { } fn set_year(&self, year: i32) -> Result { - let (days, nanoseconds) = add_offset_to_dn(self.days, self.nanoseconds, self.offset); + let offset_seconds = self.offset.resolve(); + let (days, nanoseconds) = add_offset_to_dn(self.days, self.nanoseconds, offset_seconds); let new_days = set_year(days, year)?; Ok(Self { - days: remove_offset_from_dn(new_days, nanoseconds, self.offset).0, + days: remove_offset_from_dn(new_days, nanoseconds, offset_seconds).0, nanoseconds: self.nanoseconds, offset: self.offset, }) } fn set_month(&self, month: u32) -> Result { - let (days, nanoseconds) = add_offset_to_dn(self.days, self.nanoseconds, self.offset); + let offset_seconds = self.offset.resolve(); + let (days, nanoseconds) = add_offset_to_dn(self.days, self.nanoseconds, offset_seconds); let new_days = set_month(days, month)?; Ok(Self { - days: remove_offset_from_dn(new_days, nanoseconds, self.offset).0, + days: remove_offset_from_dn(new_days, nanoseconds, offset_seconds).0, nanoseconds: self.nanoseconds, offset: self.offset, }) } fn set_day(&self, day: u32) -> Result { - let (days, nanoseconds) = add_offset_to_dn(self.days, self.nanoseconds, self.offset); + let offset_seconds = self.offset.resolve(); + let (days, nanoseconds) = add_offset_to_dn(self.days, self.nanoseconds, offset_seconds); let new_days = set_day(days, day)?; Ok(Self { - days: remove_offset_from_dn(new_days, nanoseconds, self.offset).0, + days: remove_offset_from_dn(new_days, nanoseconds, offset_seconds).0, nanoseconds: self.nanoseconds, offset: self.offset, }) } fn set_day_of_year(&self, day_of_year: u32) -> Result { - let (days, nanoseconds) = add_offset_to_dn(self.days, self.nanoseconds, self.offset); + let offset_seconds = self.offset.resolve(); + let (days, nanoseconds) = add_offset_to_dn(self.days, self.nanoseconds, offset_seconds); let new_days = set_day_of_year(days, day_of_year)?; Ok(Self { - days: remove_offset_from_dn(new_days, nanoseconds, self.offset).0, + days: remove_offset_from_dn(new_days, nanoseconds, offset_seconds).0, nanoseconds: self.nanoseconds, offset: self.offset, }) } - fn add_years(&self, years: u32) -> Result { - let new_days = add_years(self.days, years)?; + fn add_years(&self, years: u32) -> Self { + let new_days = add_years(self.days, years); - Ok(Self { + let new_days = match new_days { + Ok(new_days) => new_days, + Err(e) => panic!("{}", e), + }; + + Self { days: new_days, nanoseconds: self.nanoseconds, offset: self.offset, - }) + } } - fn add_months(&self, months: u32) -> Result { - let new_days = add_months(self.days, months)?; + fn add_months(&self, months: u32) -> Self { + let new_days = add_months(self.days, months); - Ok(Self { + let new_days = match new_days { + Ok(new_days) => new_days, + Err(e) => panic!("{}", e), + }; + + Self { days: new_days, nanoseconds: self.nanoseconds, offset: self.offset, - }) + } } - fn add_days(&self, days: u32) -> Result { - let new_days = add_days(self.days, days)?; + fn add_days(&self, days: u32) -> Self { + let new_days = add_days(self.days, days); - Ok(Self { + let new_days = match new_days { + Ok(new_days) => new_days, + Err(e) => panic!("{}", e), + }; + + Self { days: new_days, nanoseconds: self.nanoseconds, offset: self.offset, - }) + } } - fn sub_years(&self, years: u32) -> Result { - let new_days = sub_years(self.days, years)?; + fn sub_years(&self, years: u32) -> Self { + let new_days = sub_years(self.days, years); - Ok(Self { + let new_days = match new_days { + Ok(new_days) => new_days, + Err(e) => panic!("{}", e), + }; + + Self { days: new_days, nanoseconds: self.nanoseconds, offset: self.offset, - }) + } } - fn sub_months(&self, months: u32) -> Result { - let new_days = sub_months(self.days, months)?; + fn sub_months(&self, months: u32) -> Self { + let new_days = sub_months(self.days, months); - Ok(Self { + let new_days = match new_days { + Ok(new_days) => new_days, + Err(e) => panic!("{}", e), + }; + + Self { days: new_days, nanoseconds: self.nanoseconds, offset: self.offset, - }) + } } - fn sub_days(&self, days: u32) -> Result { - let new_days = sub_days(self.days, days)?; + fn sub_days(&self, days: u32) -> Self { + let new_days = sub_days(self.days, days); - Ok(Self { + let new_days = match new_days { + Ok(new_days) => new_days, + Err(e) => panic!("{}", e), + }; + + Self { days: new_days, nanoseconds: self.nanoseconds, offset: self.offset, - }) + } } fn clear_until_year(&self) -> Self { @@ -768,47 +816,49 @@ impl DateUtilities for DateTime { impl TimeUtilities for DateTime { fn hour(&self) -> u32 { - let nanoseconds = add_offset_to_dn(self.days, self.nanoseconds, self.offset).1; + let nanoseconds = add_offset_to_dn(self.days, self.nanoseconds, self.offset.resolve()).1; nanos_to_time(nanoseconds).0 } fn minute(&self) -> u32 { - let nanoseconds = add_offset_to_dn(self.days, self.nanoseconds, self.offset).1; + let nanoseconds = add_offset_to_dn(self.days, self.nanoseconds, self.offset.resolve()).1; nanos_to_time(nanoseconds).1 } fn second(&self) -> u32 { - let nanoseconds = add_offset_to_dn(self.days, self.nanoseconds, self.offset).1; + let nanoseconds = add_offset_to_dn(self.days, self.nanoseconds, self.offset.resolve()).1; nanos_to_time(nanoseconds).2 } fn milli(&self) -> u32 { - let nanoseconds = add_offset_to_dn(self.days, self.nanoseconds, self.offset).1; + let nanoseconds = add_offset_to_dn(self.days, self.nanoseconds, self.offset.resolve()).1; nanos_to_subsecond(nanoseconds).0 } fn micro(&self) -> u32 { - let nanoseconds = add_offset_to_dn(self.days, self.nanoseconds, self.offset).1; + let nanoseconds = add_offset_to_dn(self.days, self.nanoseconds, self.offset.resolve()).1; nanos_to_subsecond(nanoseconds).1 } fn nano(&self) -> u32 { - let nanoseconds = add_offset_to_dn(self.days, self.nanoseconds, self.offset).1; + let nanoseconds = add_offset_to_dn(self.days, self.nanoseconds, self.offset.resolve()).1; nanos_to_subsecond(nanoseconds).2 } fn set_hour(&self, hour: u32) -> Result { - let (days, nanos) = add_offset_to_dn(self.days, self.nanoseconds, self.offset); + let offset_seconds = self.offset.resolve(); + + let (days, nanos) = add_offset_to_dn(self.days, self.nanoseconds, offset_seconds); let new_nanos = set_hour(nanos, hour)?; - let (new_days, new_nanos) = remove_offset_from_dn(days, new_nanos, self.offset); + let (new_days, new_nanos) = remove_offset_from_dn(days, new_nanos, offset_seconds); Ok(Self { days: new_days, @@ -818,11 +868,13 @@ impl TimeUtilities for DateTime { } fn set_minute(&self, minute: u32) -> Result { - let (days, nanos) = add_offset_to_dn(self.days, self.nanoseconds, self.offset); + let offset_seconds = self.offset.resolve(); + + let (days, nanos) = add_offset_to_dn(self.days, self.nanoseconds, offset_seconds); let new_nanos = set_minute(nanos, minute)?; - let (new_days, new_nanos) = remove_offset_from_dn(days, new_nanos, self.offset); + let (new_days, new_nanos) = remove_offset_from_dn(days, new_nanos, offset_seconds); Ok(Self { days: new_days, @@ -832,11 +884,13 @@ impl TimeUtilities for DateTime { } fn set_second(&self, second: u32) -> Result { - let (days, nanos) = add_offset_to_dn(self.days, self.nanoseconds, self.offset); + let offset_seconds = self.offset.resolve(); + + let (days, nanos) = add_offset_to_dn(self.days, self.nanoseconds, offset_seconds); let new_nanos = set_second(nanos, second)?; - let (new_days, new_nanos) = remove_offset_from_dn(days, new_nanos, self.offset); + let (new_days, new_nanos) = remove_offset_from_dn(days, new_nanos, offset_seconds); Ok(Self { days: new_days, @@ -846,11 +900,13 @@ impl TimeUtilities for DateTime { } fn set_milli(&self, milli: u32) -> Result { - let (days, nanos) = add_offset_to_dn(self.days, self.nanoseconds, self.offset); + let offset_seconds = self.offset.resolve(); + + let (days, nanos) = add_offset_to_dn(self.days, self.nanoseconds, offset_seconds); let new_nanos = set_milli(nanos, milli)?; - let (new_days, new_nanos) = remove_offset_from_dn(days, new_nanos, self.offset); + let (new_days, new_nanos) = remove_offset_from_dn(days, new_nanos, offset_seconds); Ok(Self { days: new_days, @@ -860,11 +916,13 @@ impl TimeUtilities for DateTime { } fn set_micro(&self, micro: u32) -> Result { - let (days, nanos) = add_offset_to_dn(self.days, self.nanoseconds, self.offset); + let offset_seconds = self.offset.resolve(); + + let (days, nanos) = add_offset_to_dn(self.days, self.nanoseconds, offset_seconds); let new_nanos = set_micro(nanos, micro)?; - let (new_days, new_nanos) = remove_offset_from_dn(days, new_nanos, self.offset); + let (new_days, new_nanos) = remove_offset_from_dn(days, new_nanos, offset_seconds); Ok(Self { days: new_days, @@ -874,11 +932,13 @@ impl TimeUtilities for DateTime { } fn set_nano(&self, nano: u32) -> Result { - let (days, nanos) = add_offset_to_dn(self.days, self.nanoseconds, self.offset); + let offset_seconds = self.offset.resolve(); + + let (days, nanos) = add_offset_to_dn(self.days, self.nanoseconds, offset_seconds); let new_nanos = set_nano(nanos, nano)?; - let (new_days, new_nanos) = remove_offset_from_dn(days, new_nanos, self.offset); + let (new_days, new_nanos) = remove_offset_from_dn(days, new_nanos, offset_seconds); Ok(Self { days: new_days, @@ -887,225 +947,230 @@ impl TimeUtilities for DateTime { }) } - fn add_hours(&self, hours: u32) -> Result { + /// Panics if the provided value would result in an out of range datetime. + fn add_hours(&self, hours: u32) -> Self { let total_nanos = self.days as i128 * NANOS_PER_DAY as i128 + add_hours(self.nanoseconds, hours) as i128; - let (days, nanoseconds) = nanos_to_days_nanos(total_nanos).map_err(|_| { - create_custom_oor(format!( + let (days, nanoseconds) = nanos_to_days_nanos(total_nanos).unwrap_or_else(|_| { + panic!( "Adding {} hours would result into an out of range datetime", hours - )) - })?; + ) + }); - Ok(Self { + Self { days, nanoseconds, offset: self.offset, - }) + } } - fn add_minutes(&self, minutes: u32) -> Result { + /// Panics if the provided value would result in an out of range datetime. + fn add_minutes(&self, minutes: u32) -> Self { let total_nanos = self.days as i128 * NANOS_PER_DAY as i128 + add_minutes(self.nanoseconds, minutes) as i128; - let (days, nanoseconds) = nanos_to_days_nanos(total_nanos).map_err(|_| { - create_custom_oor(format!( + let (days, nanoseconds) = nanos_to_days_nanos(total_nanos).unwrap_or_else(|_| { + panic!( "Adding {} minutes would result into an out of range datetime", minutes - )) - })?; + ) + }); - Ok(Self { + Self { days, nanoseconds, offset: self.offset, - }) + } } - fn add_seconds(&self, seconds: u32) -> Result { + /// Panics if the provided value would result in an out of range datetime. + fn add_seconds(&self, seconds: u32) -> Self { let total_nanos = self.days as i128 * NANOS_PER_DAY as i128 + add_seconds(self.nanoseconds, seconds) as i128; - let (days, nanoseconds) = nanos_to_days_nanos(total_nanos).map_err(|_| { - create_custom_oor(format!( + let (days, nanoseconds) = nanos_to_days_nanos(total_nanos).unwrap_or_else(|_| { + panic!( "Adding {} seconds would result into an out of range datetime", seconds - )) - })?; + ) + }); - Ok(Self { + Self { days, nanoseconds, offset: self.offset, - }) + } } - fn add_millis(&self, millis: u32) -> Result { + fn add_millis(&self, millis: u32) -> Self { let total_nanos = self.days as i128 * NANOS_PER_DAY as i128 + add_millis(self.nanoseconds, millis) as i128; - let (days, nanoseconds) = nanos_to_days_nanos(total_nanos).map_err(|_| { - create_custom_oor(format!( + let (days, nanoseconds) = nanos_to_days_nanos(total_nanos).unwrap_or_else(|_| { + panic!( "Adding {} milliseconds would result into an out of range datetime", millis - )) - })?; + ) + }); - Ok(Self { + Self { days, nanoseconds, offset: self.offset, - }) + } } - fn add_micros(&self, micros: u32) -> Result { + fn add_micros(&self, micros: u32) -> Self { let total_nanos = self.days as i128 * NANOS_PER_DAY as i128 + add_micros(self.nanoseconds, micros) as i128; - let (days, nanoseconds) = nanos_to_days_nanos(total_nanos).map_err(|_| { - create_custom_oor(format!( + let (days, nanoseconds) = nanos_to_days_nanos(total_nanos).unwrap_or_else(|_| { + panic!( "Adding {} microseconds would result into an out of range datetime", micros - )) - })?; + ) + }); - Ok(Self { + Self { days, nanoseconds, offset: self.offset, - }) + } } - fn add_nanos(&self, nanos: u32) -> Result { + fn add_nanos(&self, nanos: u32) -> Self { let total_nanos = self.days as i128 * NANOS_PER_DAY as i128 + self.nanoseconds as i128 + nanos as i128; - let (days, nanoseconds) = nanos_to_days_nanos(total_nanos).map_err(|_| { - create_custom_oor(format!( + let (days, nanoseconds) = nanos_to_days_nanos(total_nanos).unwrap_or_else(|_| { + panic!( "Adding {} nanoseconds would result into an out of range datetime", nanos - )) - })?; + ) + }); - Ok(Self { + Self { days, nanoseconds, offset: self.offset, - }) + } } - fn sub_hours(&self, hours: u32) -> Result { + fn sub_hours(&self, hours: u32) -> Self { let total_nanos = self.days as i128 * NANOS_PER_DAY as i128 + sub_hours(self.nanoseconds as i64, hours) as i128; - let (days, nanoseconds) = nanos_to_days_nanos(total_nanos).map_err(|_| { - create_custom_oor(format!( + let (days, nanoseconds) = nanos_to_days_nanos(total_nanos).unwrap_or_else(|_| { + panic!( "Subtracting {} hours would result into an out of range datetime", hours - )) - })?; + ) + }); - Ok(Self { + Self { days, nanoseconds, offset: self.offset, - }) + } } - fn sub_minutes(&self, minutes: u32) -> Result { + fn sub_minutes(&self, minutes: u32) -> Self { let total_nanos = self.days as i128 * NANOS_PER_DAY as i128 + sub_minutes(self.nanoseconds as i64, minutes) as i128; - let (days, nanoseconds) = nanos_to_days_nanos(total_nanos).map_err(|_| { - create_custom_oor(format!( + let (days, nanoseconds) = nanos_to_days_nanos(total_nanos).unwrap_or_else(|_| { + panic!( "Subtracting {} minutes would result into an out of range datetime", minutes - )) - })?; + ) + }); - Ok(Self { + Self { days, nanoseconds, offset: self.offset, - }) + } } - fn sub_seconds(&self, seconds: u32) -> Result { + fn sub_seconds(&self, seconds: u32) -> Self { let total_nanos = self.days as i128 * NANOS_PER_DAY as i128 + sub_seconds(self.nanoseconds as i64, seconds) as i128; - let (days, nanoseconds) = nanos_to_days_nanos(total_nanos).map_err(|_| { - create_custom_oor(format!( + let (days, nanoseconds) = nanos_to_days_nanos(total_nanos).unwrap_or_else(|_| { + panic!( "Subtracting {} seconds would result into an out of range datetime", seconds - )) - })?; + ) + }); - Ok(Self { + Self { days, nanoseconds, offset: self.offset, - }) + } } - fn sub_millis(&self, millis: u32) -> Result { + fn sub_millis(&self, millis: u32) -> Self { let total_nanos = self.days as i128 * NANOS_PER_DAY as i128 + sub_millis(self.nanoseconds as i64, millis) as i128; - let (days, nanoseconds) = nanos_to_days_nanos(total_nanos).map_err(|_| { - create_custom_oor(format!( + let (days, nanoseconds) = nanos_to_days_nanos(total_nanos).unwrap_or_else(|_| { + panic!( "Subtracting {} milliseconds would result into an out of range datetime", millis - )) - })?; + ) + }); - Ok(Self { + Self { days, nanoseconds, offset: self.offset, - }) + } } - fn sub_micros(&self, micros: u32) -> Result { + fn sub_micros(&self, micros: u32) -> Self { let total_nanos = self.days as i128 * NANOS_PER_DAY as i128 + sub_micros(self.nanoseconds as i64, micros) as i128; - let (days, nanoseconds) = nanos_to_days_nanos(total_nanos).map_err(|_| { - create_custom_oor(format!( + let (days, nanoseconds) = nanos_to_days_nanos(total_nanos).unwrap_or_else(|_| { + panic!( "Subtracting {} microseconds would result into an out of range datetime", micros - )) - })?; + ) + }); - Ok(Self { + Self { days, nanoseconds, offset: self.offset, - }) + } } - fn sub_nanos(&self, nanos: u32) -> Result { + fn sub_nanos(&self, nanos: u32) -> Self { let total_nanos = self.days as i128 * NANOS_PER_DAY as i128 + self.nanoseconds as i128 - nanos as i128; - let (days, nanoseconds) = nanos_to_days_nanos(total_nanos).map_err(|_| { - create_custom_oor(format!( + let (days, nanoseconds) = nanos_to_days_nanos(total_nanos).unwrap_or_else(|_| { + panic!( "Subtracting {} nanoseconds would result into an out of range datetime", nanos - )) - })?; + ) + }); - Ok(Self { + Self { days, nanoseconds, offset: self.offset, - }) + } } fn clear_until_hour(&self) -> Self { - let (days, _) = add_offset_to_dn(self.days, self.nanoseconds, self.offset); - let (days, nanos) = remove_offset_from_dn(days, 0, self.offset); + let offset_seconds = self.offset.resolve(); + + let (days, _) = add_offset_to_dn(self.days, self.nanoseconds, offset_seconds); + let (days, nanos) = remove_offset_from_dn(days, 0, offset_seconds); Self { days, nanoseconds: nanos, @@ -1114,9 +1179,11 @@ impl TimeUtilities for DateTime { } fn clear_until_minute(&self) -> Self { - let (days, nanoseconds) = add_offset_to_dn(self.days, self.nanoseconds, self.offset); + let offset_seconds = self.offset.resolve(); + + let (days, nanoseconds) = add_offset_to_dn(self.days, self.nanoseconds, offset_seconds); let (days, nanoseconds) = - remove_offset_from_dn(days, clear_nanos_until_minute(nanoseconds), self.offset); + remove_offset_from_dn(days, clear_nanos_until_minute(nanoseconds), offset_seconds); Self { days, nanoseconds, @@ -1125,9 +1192,11 @@ impl TimeUtilities for DateTime { } fn clear_until_second(&self) -> Self { - let (days, nanoseconds) = add_offset_to_dn(self.days, self.nanoseconds, self.offset); + let offset_seconds = self.offset.resolve(); + + let (days, nanoseconds) = add_offset_to_dn(self.days, self.nanoseconds, offset_seconds); let (days, nanoseconds) = - remove_offset_from_dn(days, clear_nanos_until_second(nanoseconds), self.offset); + remove_offset_from_dn(days, clear_nanos_until_second(nanoseconds), offset_seconds); Self { days, nanoseconds, @@ -1136,9 +1205,11 @@ impl TimeUtilities for DateTime { } fn clear_until_milli(&self) -> Self { - let (days, nanoseconds) = add_offset_to_dn(self.days, self.nanoseconds, self.offset); + let offset_seconds = self.offset.resolve(); + + let (days, nanoseconds) = add_offset_to_dn(self.days, self.nanoseconds, offset_seconds); let (days, nanoseconds) = - remove_offset_from_dn(days, clear_nanos_until_milli(nanoseconds), self.offset); + remove_offset_from_dn(days, clear_nanos_until_milli(nanoseconds), offset_seconds); Self { days, nanoseconds, @@ -1147,9 +1218,11 @@ impl TimeUtilities for DateTime { } fn clear_until_micro(&self) -> Self { - let (days, nanoseconds) = add_offset_to_dn(self.days, self.nanoseconds, self.offset); + let offset_seconds = self.offset.resolve(); + + let (days, nanoseconds) = add_offset_to_dn(self.days, self.nanoseconds, offset_seconds); let (days, nanoseconds) = - remove_offset_from_dn(days, clear_nanos_until_micro(nanoseconds), self.offset); + remove_offset_from_dn(days, clear_nanos_until_micro(nanoseconds), offset_seconds); Self { days, nanoseconds, @@ -1158,9 +1231,11 @@ impl TimeUtilities for DateTime { } fn clear_until_nano(&self) -> Self { - let (days, nanoseconds) = add_offset_to_dn(self.days, self.nanoseconds, self.offset); + let offset_seconds = self.offset.resolve(); + + let (days, nanoseconds) = add_offset_to_dn(self.days, self.nanoseconds, offset_seconds); let (days, nanoseconds) = - remove_offset_from_dn(days, clear_nanos_until_nanos(nanoseconds), self.offset); + remove_offset_from_dn(days, clear_nanos_until_nanos(nanoseconds), offset_seconds); Self { days, nanoseconds, @@ -1263,72 +1338,31 @@ impl TimeUtilities for DateTime { // ######################################## impl OffsetUtilities for DateTime { - fn set_offset_hms(&self, hour: i32, minute: u32, second: u32) -> Result { - let mut seconds = time_to_day_seconds(hour.unsigned_abs(), minute, second)? as i32; - seconds = if hour.is_negative() { - -seconds - } else { - seconds - }; - - self.set_offset(seconds) - } + fn set_offset(&self, offset: Offset) -> Self { + let offset_seconds = offset.resolve(); - fn as_offset_hms(&self, hour: i32, minute: u32, second: u32) -> Result { - let mut seconds = time_to_day_seconds(hour.unsigned_abs(), minute, second)? as i32; - seconds = if hour.is_negative() { - -seconds - } else { - seconds - }; - - let new_nanos = self.as_nanos() - seconds as i128 * NANOS_PER_SEC as i128; - - Ok(Self::from_nanos(new_nanos)?.set_offset(seconds).unwrap()) - } - - fn get_offset_hms(&self) -> (i32, u32, u32) { - let hour = self.offset / SECS_PER_HOUR as i32; - let minute = self.offset % SECS_PER_HOUR as i32 / SECS_PER_MINUTE as i32; - let second = self.offset % SECS_PER_MINUTE as i32; - - (hour, minute.unsigned_abs(), second.unsigned_abs()) - } - - fn set_offset(&self, seconds: i32) -> Result { - if seconds <= -(SECS_PER_DAY as i32) || seconds >= SECS_PER_DAY as i32 { - return Err(create_simple_oor( - "seconds", - -(SECS_PER_DAY as i128) + 1, - SECS_PER_DAY as i128 - 1, - seconds as i128, - )); - } - - let offset_days = (self.as_seconds() + seconds as i64) / SECS_PER_DAY_U64 as i64; - let offset_nanos = (self.nanoseconds / NANOS_PER_SEC) as i64 + seconds as i64; + let offset_days = (self.as_seconds() + offset_seconds as i64) / SECS_PER_DAY_U64 as i64; + let offset_nanos = (self.nanoseconds / NANOS_PER_SEC) as i64 + offset_seconds as i64; if offset_days < i32::MIN as i64 || offset_days > i32::MAX as i64 || (offset_days == i32::MIN as i64 && offset_nanos.is_negative()) { - return Err(create_custom_oor( - "Offset would result in an out of range date".to_string(), - )); + panic!("Offset would result in an out of range date"); } - Ok(Self { + Self { days: self.days, nanoseconds: self.nanoseconds, - offset: seconds, - }) + offset, + } } - fn as_offset(&self, seconds: i32) -> Result { - let new_nanos = self.as_nanos() - seconds as i128 * NANOS_PER_SEC as i128; - Self::from_nanos(new_nanos)?.set_offset(seconds) + fn as_offset(&self, offset: Offset) -> Self { + let new_nanos = self.as_nanos() - offset.resolve() as i128 * NANOS_PER_SEC as i128; + Self::from_nanos(new_nanos).unwrap().set_offset(offset) } - fn get_offset(&self) -> i32 { + fn get_offset(&self) -> Offset { self.offset } } @@ -1347,7 +1381,7 @@ impl DateTime { Ok(Self { days, nanoseconds, - offset: 0, + offset: Offset::default(), }) } @@ -1363,7 +1397,7 @@ impl DateTime { Ok(Self { days, nanoseconds, - offset: 0, + offset: Offset::default(), }) } @@ -1394,7 +1428,7 @@ impl From for DateTime { Self { days: value.days, nanoseconds: 0, - offset: 0, + offset: Offset::default(), } } } @@ -1403,7 +1437,7 @@ impl From<&Date> for DateTime { Self { days: value.days, nanoseconds: 0, - offset: 0, + offset: Offset::default(), } } } diff --git a/src/lib.rs b/src/lib.rs index e197b42..9c40004 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,11 +6,14 @@ //! - **Manipulation** functions to add, subtract, set and clear date units //! - **Cron** expression parser //! - **Timezone** offset +//! - **Local** timezone on UNIX platforms //! - **Zero** dependencies //! - **Serde** serializing and deserializing (With feature flag `serde`) //! -//! ### Example -//! A basic example which demonstrates creating, formatting and manipulating a [`DateTime`] instance. +//! ## Examples +//! ### Basic +//! A basic example which demonstrates creating, formatting and manipulating a `DateTime` instance. +//! //! ```rust //! use astrolabe::{DateTime, TimeUtilities, Precision}; //! @@ -22,14 +25,55 @@ //! //! // Create a new instance with a modified DateTime //! // The previous instance is not modified and is still in scope -//! let modified_dt = date_time -//! .add_hours(11).unwrap() -//! .add_minutes(23).unwrap(); +//! let modified_dt = date_time.add_hours(11).add_minutes(23); //! //! assert_eq!("2022/05/02 11:23:00", modified_dt.format("yyyy/MM/dd HH:mm:ss")); //! assert_eq!("2022-05-02T11:23:00Z", modified_dt.format_rfc3339(Precision::Seconds)); //! ``` -//! To see all implementations for the [`DateTime`] struct, check out it's [documentation](DateTime). +//! To see all implementations for the `DateTime` struct, check out it's [documentation](https://docs.rs/astrolabe/latest/astrolabe/struct.DateTime.html). +//! +//! ### Local timezone (UNIX systems only) +//! Astrolabe can parse the timezone from `/etc/localtime` to get the local UTC offset. This only works on UNIX systems. +//! +//! ```rust +//! use astrolabe::{DateTime, Offset, OffsetUtilities, Precision}; +//! +//! // Equivalent to `DateTime::now().set_offset(Offset::Local)` +//! let now = DateTime::now_local(); +//! +//! // Prints for example: +//! // 2023-10-08T08:30:00+02:00 +//! println!("{}", now.format_rfc3339(Precision::Seconds)); +//! assert_eq!(Offset::Local, now.get_offset()); +//! ``` +//! See [`Offset`](https://docs.rs/astrolabe/latest/astrolabe/enum.Offset.html) +//! +//! ### CRON parsing +//! ```rust +//! use astrolabe::CronSchedule; +//! +//! // Every 5 minutes +//! let schedule = CronSchedule::parse("*/5 * * * *").unwrap(); +//! for date in schedule.take(3) { +//! println!("{}", date); +//! } +//! // Prints for example: +//! // 2022-05-02 16:15:00 +//! // 2022-05-02 16:20:00 +//! // 2022-05-02 16:25:00 +//! +//! // Every weekday at 10:00 +//! let schedule = CronSchedule::parse("0 10 * * Mon-Fri").unwrap(); +//! for date in schedule.take(3) { +//! println!("{}", date.format("yyyy-MM-dd HH:mm:ss eeee")); +//! } +//! // Prints for example: +//! // 2022-05-03 10:00:00 Tuesday +//! // 2022-05-04 10:00:00 Wednesday +//! // 2022-05-05 10:00:00 Thursday +//! ``` +//! See [`CronSchedule`](https://docs.rs/astrolabe/latest/astrolabe/struct.CronSchedule.html) +//! #![doc( html_logo_url = "https://raw.githubusercontent.com/giyomoon/astrolabe/main/assets/logo.svg", @@ -45,6 +89,8 @@ mod cron; mod date; mod datetime; pub mod errors; +mod local; +mod offset; mod shared; mod time; mod util; @@ -52,5 +98,6 @@ mod util; pub use self::cron::CronSchedule; pub use self::date::Date; pub use self::datetime::DateTime; +pub use self::offset::Offset; pub use self::shared::{DateUtilities, OffsetUtilities, Precision, TimeUtilities}; pub use self::time::Time; diff --git a/src/local/cursor.rs b/src/local/cursor.rs new file mode 100644 index 0000000..67e150b --- /dev/null +++ b/src/local/cursor.rs @@ -0,0 +1,75 @@ +use super::errors::TimeZoneError; + +/// Helper to read data from a byte slice +pub(super) struct Cursor<'a> { + /// Slice representing the remaining data to be read + remaining: &'a [u8], +} + +impl<'a> Cursor<'a> { + pub(super) fn new(bytes: &'a [u8]) -> Self { + Self { remaining: bytes } + } + + pub(super) fn read_exact(&mut self, len: usize) -> Result<&'a [u8], TimeZoneError> { + if self.remaining.len() < len { + return Err(TimeZoneError::Cursor("End of byte slice reached")); + } + let (data, remaining) = self.remaining.split_at(len); + self.remaining = remaining; + Ok(data) + } + + pub(super) fn read_until(&mut self, char: char) -> &'a [u8] { + let mut index = 0; + for byte in self.remaining.iter() { + if *byte == char as u8 { + break; + } + index += 1; + } + let (data, remaining) = self.remaining.split_at(index); + self.remaining = remaining; + data + } + + pub(super) fn read_while(&mut self, until: impl Fn(&u8) -> bool) -> &'a [u8] { + let mut index = 0; + for byte in self.remaining.iter() { + if !until(byte) { + break; + } + index += 1; + } + let (data, remaining) = self.remaining.split_at(index); + self.remaining = remaining; + data + } + + pub(super) fn read_tag(&mut self, bytes: &[u8]) -> Result<&'a [u8], TimeZoneError> { + if self.remaining.len() < bytes.len() { + return Err(TimeZoneError::Cursor("End of byte slice reached")); + } + let (data, remaining) = self.remaining.split_at(bytes.len()); + if data != bytes { + return Err(TimeZoneError::Cursor("Unexpected bytes read")); + } + self.remaining = remaining; + Ok(data) + } + + pub(super) fn remaining(&self) -> &'a [u8] { + self.remaining + } + + pub(super) fn empty(&self) -> bool { + self.remaining.is_empty() + } + + pub(super) fn get_next(&self) -> Result { + if self.remaining.is_empty() { + return Err(TimeZoneError::Cursor("End of byte slice reached")); + } + Ok(self.remaining[0]) + } +} diff --git a/src/local/data_block.rs b/src/local/data_block.rs new file mode 100644 index 0000000..dccd7ed --- /dev/null +++ b/src/local/data_block.rs @@ -0,0 +1,47 @@ +use super::{ + cursor::Cursor, + errors::TimeZoneError, + header::{Header, Version}, +}; + +/// TZif data block +pub(super) struct DataBlock<'a> { + /// Size in bytes of each transition time. + /// 4 bytes for version 1, 8 bytes for version 2 and 3 TZif files + pub(super) time_size: usize, + /// Transition times at which the rules for computing local time change + pub(super) transition_times: &'a [u8], + /// Index into the local time types array + pub(super) transition_types: &'a [u8], + /// Local time types specifying UTC offsets and DST + pub(super) local_time_types: &'a [u8], + _time_zone_designations: &'a [u8], + _leap_seconds: &'a [u8], + _standard_wall: &'a [u8], + _ut_local: &'a [u8], +} + +impl<'a> DataBlock<'a> { + /// Parse a TZif data block + pub(super) fn parse( + cursor: &mut Cursor<'a>, + header: &Header, + version: Version, + ) -> Result { + let time_size = match version { + Version::V1 => 4, + Version::V2 | Version::V3 => 8, + }; + + Ok(Self { + time_size, + transition_times: cursor.read_exact(header.transition_count * time_size)?, + transition_types: cursor.read_exact(header.transition_count)?, + local_time_types: cursor.read_exact(header.type_count * 6)?, + _time_zone_designations: cursor.read_exact(header.char_count)?, + _leap_seconds: cursor.read_exact(header.leap_count * (time_size + 4))?, + _standard_wall: cursor.read_exact(header.isstd_count)?, + _ut_local: cursor.read_exact(header.isut_count)?, + }) + } +} diff --git a/src/local/errors.rs b/src/local/errors.rs new file mode 100644 index 0000000..09ec63f --- /dev/null +++ b/src/local/errors.rs @@ -0,0 +1,94 @@ +use std::{array::TryFromSliceError, fmt::Display, num::ParseIntError, str::Utf8Error}; + +/// TZif parsing errors +#[derive(Debug)] +pub(crate) enum TimeZoneError { + Cursor(&'static str), + InvalidTzFile(&'static str), + UnsupportedTzFile(&'static str), + TryFromSliceError(TryFromSliceError), + Utf8Error(Utf8Error), + ParseIntError(ParseIntError), +} + +impl Display for TimeZoneError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TimeZoneError::Cursor(error) => { + write!(f, "Error when parsing a TZif file: Cursor Error: {}", error) + } + TimeZoneError::InvalidTzFile(error) => { + write!( + f, + "Error when parsing a TZif file: Invalid Tzif file: {}", + error + ) + } + TimeZoneError::UnsupportedTzFile(error) => { + write!( + f, + "Error when parsing a TZif file: Unsupported Tzif file: {}", + error + ) + } + TimeZoneError::TryFromSliceError(error) => { + write!(f, "Error when parsing a TZif file: {}", error) + } + TimeZoneError::Utf8Error(error) => { + write!(f, "Error when parsing a TZif file: {}", error) + } + TimeZoneError::ParseIntError(error) => { + write!(f, "Error when parsing a TZif file: {}", error) + } + } + } +} + +impl From for TimeZoneError { + fn from(e: TryFromSliceError) -> Self { + TimeZoneError::TryFromSliceError(e) + } +} + +impl From for TimeZoneError { + fn from(e: Utf8Error) -> Self { + TimeZoneError::Utf8Error(e) + } +} + +impl From for TimeZoneError { + fn from(e: ParseIntError) -> Self { + TimeZoneError::ParseIntError(e) + } +} + +#[cfg(test)] +mod tests { + use crate::local::errors::TimeZoneError; + + #[test] + fn display() { + let string = " 0"; + println!( + "{}", + string + .parse::() + .map_err(TimeZoneError::from) + .unwrap_err() + ); + + let invalid_utf8 = b"\xFF"; + #[allow(invalid_from_utf8)] + let parsed = std::str::from_utf8(invalid_utf8) + .map_err(TimeZoneError::from) + .unwrap_err(); + println!("{}", parsed); + println!("{:?}", parsed); + + let bytes: &[u8] = b"12"; + #[allow(invalid_from_utf8)] + let slice_result: Result<[u8; 4], _> = bytes.try_into().map_err(TimeZoneError::from); + assert!(slice_result.is_err()); + println!("{}", slice_result.unwrap_err()); + } +} diff --git a/src/local/header.rs b/src/local/header.rs new file mode 100644 index 0000000..d8122ae --- /dev/null +++ b/src/local/header.rs @@ -0,0 +1,72 @@ +use crate::util::constants::BUG_MSG; + +use super::{cursor::Cursor, errors::TimeZoneError}; + +/// TZif version +#[derive(Clone, Copy, PartialEq)] +pub(super) enum Version { + /// Version 1 + V1, + /// Version 2 + V2, + /// Version 3 + V3, +} + +/// TZif header +pub(super) struct Header { + /// TZif version + pub(super) ver: Version, + /// Number of UT/local indicators + pub(super) isut_count: usize, + /// Number of standard/wall indicators + pub(super) isstd_count: usize, + /// Number of leap-second records + pub(super) leap_count: usize, + /// Number of transition times + pub(super) transition_count: usize, + /// Number of local time type records + pub(super) type_count: usize, + /// Number of time zone designations bytes + pub(super) char_count: usize, +} + +impl Header { + /// Parses the TZif header + pub(super) fn parse(cursor: &mut Cursor) -> Result { + let magic = cursor.read_exact(4)?; + if magic != *b"TZif" { + return Err(TimeZoneError::InvalidTzFile("TZif magic not found")); + } + + let ver = match cursor.read_exact(1)? { + [0x00] => Version::V1, + [0x32] => Version::V2, + [0x33] => Version::V3, + _ => { + return Err(TimeZoneError::UnsupportedTzFile( + "TZif version not supported. Only version 1, 2 and 3 are supported", + )) + } + }; + + cursor.read_exact(15)?; + + let isut_count = u32::from_be_bytes(cursor.read_exact(4)?.try_into().expect(BUG_MSG)); + let isstd_count = u32::from_be_bytes(cursor.read_exact(4)?.try_into().expect(BUG_MSG)); + let leap_count = u32::from_be_bytes(cursor.read_exact(4)?.try_into().expect(BUG_MSG)); + let transition_count = u32::from_be_bytes(cursor.read_exact(4)?.try_into().expect(BUG_MSG)); + let type_count = u32::from_be_bytes(cursor.read_exact(4)?.try_into().expect(BUG_MSG)); + let char_count = u32::from_be_bytes(cursor.read_exact(4)?.try_into().expect(BUG_MSG)); + + Ok(Self { + ver, + isut_count: isut_count as usize, + isstd_count: isstd_count as usize, + leap_count: leap_count as usize, + transition_count: transition_count as usize, + type_count: type_count as usize, + char_count: char_count as usize, + }) + } +} diff --git a/src/local/mod.rs b/src/local/mod.rs new file mode 100644 index 0000000..475ddeb --- /dev/null +++ b/src/local/mod.rs @@ -0,0 +1,6 @@ +mod cursor; +mod data_block; +mod errors; +mod header; +pub(crate) mod timezone; +mod transition_rule; diff --git a/src/local/timezone.rs b/src/local/timezone.rs new file mode 100644 index 0000000..7e3e095 --- /dev/null +++ b/src/local/timezone.rs @@ -0,0 +1,700 @@ +use super::{ + data_block::DataBlock, + errors::TimeZoneError, + header::{Header, Version}, + transition_rule::TransitionRule, +}; +use crate::{local::cursor::Cursor, util::constants::BUG_MSG}; + +/// TimeZone containing parsed TZif data +#[derive(Debug, PartialEq)] +pub(crate) struct TimeZone { + /// Transition times of the time zone + transitions: Vec, + /// Local time types of the time zone + local_time_types: Vec, + /// Extra transition time + extra_rule: Option, +} + +impl TimeZone { + /// Parses the TZif file at `/etc/localtime` to a TimeZone + pub(crate) fn from_tzif(bytes: &[u8]) -> Result { + let mut cursor = Cursor::new(bytes); + let header = Header::parse(&mut cursor)?; + + let (header, data_block, footer) = match header.ver { + Version::V1 => { + let data_block = DataBlock::parse(&mut cursor, &header, Version::V1)?; + (header, data_block, None) + } + Version::V2 | Version::V3 => { + // Remove Version 1 data block + DataBlock::parse(&mut cursor, &header, Version::V1)?; + + let header = Header::parse(&mut cursor)?; + let data_block = DataBlock::parse(&mut cursor, &header, header.ver)?; + let footer = cursor.remaining(); + (header, data_block, Some(footer)) + } + }; + + let mut transitions: Vec = Vec::with_capacity(header.transition_count); + + for (transition_time, &transition_type) in data_block + .transition_times + .chunks_exact(data_block.time_size) + .zip(data_block.transition_types) + { + let transition_time = match header.ver { + Version::V1 => { + i32::from_be_bytes(transition_time.try_into().expect(BUG_MSG)).into() + } + Version::V2 | Version::V3 => { + i64::from_be_bytes(transition_time.try_into().expect(BUG_MSG)) + } + }; + let time_type_index = transition_type as usize; + transitions.push(Transition::new(transition_time, time_type_index)); + } + + let mut local_time_types = Vec::with_capacity(header.type_count); + + for local_time_type in data_block.local_time_types.chunks_exact(6) { + let utoff = i32::from_be_bytes(local_time_type[0..4].try_into().expect(BUG_MSG)); + let dst = local_time_type[4] != 0; + local_time_types.push(LocalTimeType::new(utoff, dst)); + } + + let extra_rule = if let Some(footer) = footer { + TransitionRule::from_tz_string(footer, header.ver == Version::V3)? + } else { + None + }; + + Ok(Self { + transitions, + local_time_types, + extra_rule, + }) + } + + pub(crate) fn to_local_time_type(&self, timestamp: i64) -> LocalTimeType { + match self.transitions[..] { + [] => match &self.extra_rule { + Some(rule) => match rule { + TransitionRule::Fixed(local_time_type) => local_time_type.clone(), + TransitionRule::Alternate(altt) => { + let std_end_timestamp = altt.local_std_end_timestamp(timestamp); + let dst_end_timestamp = altt.local_dst_end_timestamp(timestamp); + + let std_end_unix = std_end_timestamp - altt.std.utoff as i64; + let dst_end_unix = dst_end_timestamp - altt.dst.utoff as i64; + + match timestamp { + // std end is before dst end + // timestamp is after time changed to dst + timestamp + if std_end_unix < dst_end_unix + && std_end_unix <= timestamp + && timestamp < dst_end_unix => + { + altt.dst.clone() + } + // std is before dst + // timestamp is in std range + _ if std_end_unix < dst_end_unix => altt.std.clone(), + // dst end is before std end + // timestamp is after time changed to std + timestamp + if dst_end_unix < std_end_unix + && dst_end_unix <= timestamp + && timestamp < std_end_unix => + { + altt.std.clone() + } + _ => altt.dst.clone(), + } + } + }, + None => self.local_time_types[0].clone(), + }, + _ => { + let mut local_time_type_index = 0; + for transition in self.transitions.iter().rev() { + if transition.unix_leap_time < timestamp { + local_time_type_index = transition.local_time_type_index; + break; + } + } + self.local_time_types[local_time_type_index].clone() + } + } + } +} + +/// Transition of a TZif file +#[derive(Debug, Eq, PartialEq)] +struct Transition { + /// Unix leap time + unix_leap_time: i64, + /// Index specifying the local time type of the transition + local_time_type_index: usize, +} + +impl Transition { + fn new(unix_leap_time: i64, local_time_type_index: usize) -> Self { + Self { + unix_leap_time, + local_time_type_index, + } + } +} + +/// Local time type of a TZif file +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct LocalTimeType { + /// UTC offset + pub(crate) utoff: i32, + /// If the local time type is considered DST + _dst: bool, +} + +impl LocalTimeType { + pub(super) fn new(utoff: i32, dst: bool) -> Self { + Self { utoff, _dst: dst } + } +} + +#[cfg(test)] +mod local_tests { + use crate::local::{ + header::Version, + timezone::{LocalTimeType, TimeZone, Transition}, + transition_rule::{AlternateLocalTimeType, RuleDay, TransitionRule}, + }; + + #[test] + fn test_v1_file() { + let bytes = b"TZif\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\x1b\0\0\0\0\0\0\0\x01\0\0\0\x04\0\0\0\0\0\0UTC\0\x04\xb2\x58\0\0\0\0\x01\x05\xa4\xec\x01\0\0\0\x02\x07\x86\x1f\x82\0\0\0\x03\x09\x67\x53\x03\0\0\0\x04\x0b\x48\x86\x84\0\0\0\x05\x0d\x2b\x0b\x85\0\0\0\x06\x0f\x0c\x3f\x06\0\0\0\x07\x10\xed\x72\x87\0\0\0\x08\x12\xce\xa6\x08\0\0\0\x09\x15\x9f\xca\x89\0\0\0\x0a\x17\x80\xfe\x0a\0\0\0\x0b\x19\x62\x31\x8b\0\0\0\x0c\x1d\x25\xea\x0c\0\0\0\x0d\x21\xda\xe5\x0d\0\0\0\x0e\x25\x9e\x9d\x8e\0\0\0\x0f\x27\x7f\xd1\x0f\0\0\0\x10\x2a\x50\xf5\x90\0\0\0\x11\x2c\x32\x29\x11\0\0\0\x12\x2e\x13\x5c\x92\0\0\0\x13\x30\xe7\x24\x13\0\0\0\x14\x33\xb8\x48\x94\0\0\0\x15\x36\x8c\x10\x15\0\0\0\x16\x43\xb7\x1b\x96\0\0\0\x17\x49\x5c\x07\x97\0\0\0\x18\x4f\xef\x93\x18\0\0\0\x19\x55\x93\x2d\x99\0\0\0\x1a\x58\x68\x46\x9a\0\0\0\x1b\0\0"; + + let time_zone = TimeZone::from_tzif(bytes).unwrap(); + + let time_zone_result = TimeZone { + transitions: vec![], + local_time_types: vec![LocalTimeType::new(0, false)], + extra_rule: None, + }; + + assert_eq!(time_zone, time_zone_result); + } + + #[test] + fn test_v2_file() { + let bytes = b"TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x06\0\0\0\x06\0\0\0\0\0\0\0\x07\0\0\0\x06\0\0\0\x14\x80\0\0\0\xbb\x05\x43\x48\xbb\x21\x71\x58\xcb\x89\x3d\xc8\xd2\x23\xf4\x70\xd2\x61\x49\x38\xd5\x8d\x73\x48\x01\x02\x01\x03\x04\x01\x05\xff\xff\x6c\x02\0\0\xff\xff\x6c\x58\0\x04\xff\xff\x7a\x68\x01\x08\xff\xff\x7a\x68\x01\x0c\xff\xff\x7a\x68\x01\x10\xff\xff\x73\x60\0\x04LMT\0HST\0HDT\0HWT\0HPT\0\0\0\0\0\x01\0\0\0\0\0\x01\0TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x06\0\0\0\x06\0\0\0\0\0\0\0\x07\0\0\0\x06\0\0\0\x14\xff\xff\xff\xff\x74\xe0\x70\xbe\xff\xff\xff\xff\xbb\x05\x43\x48\xff\xff\xff\xff\xbb\x21\x71\x58\xff\xff\xff\xff\xcb\x89\x3d\xc8\xff\xff\xff\xff\xd2\x23\xf4\x70\xff\xff\xff\xff\xd2\x61\x49\x38\xff\xff\xff\xff\xd5\x8d\x73\x48\x01\x02\x01\x03\x04\x01\x05\xff\xff\x6c\x02\0\0\xff\xff\x6c\x58\0\x04\xff\xff\x7a\x68\x01\x08\xff\xff\x7a\x68\x01\x0c\xff\xff\x7a\x68\x01\x10\xff\xff\x73\x60\0\x04LMT\0HST\0HDT\0HWT\0HPT\0\0\0\0\0\x01\0\0\0\0\0\x01\0\x0aHST10\x0a"; + + let time_zone = TimeZone::from_tzif(bytes).unwrap(); + + let time_zone_result = TimeZone { + transitions: vec![ + Transition::new(-2334101314, 1), + Transition::new(-1157283000, 2), + Transition::new(-1155436200, 1), + Transition::new(-880198200, 3), + Transition::new(-769395600, 4), + Transition::new(-765376200, 1), + Transition::new(-712150200, 5), + ], + local_time_types: vec![ + LocalTimeType::new(-37886, false), + LocalTimeType::new(-37800, false), + LocalTimeType::new(-34200, true), + LocalTimeType::new(-34200, true), + LocalTimeType::new(-34200, true), + LocalTimeType::new(-36000, false), + ], + extra_rule: Some(TransitionRule::Fixed(LocalTimeType::new(-36000, false))), + }; + + assert_eq!(time_zone, time_zone_result); + + assert_eq!( + time_zone.to_local_time_type(-1156939200), + LocalTimeType::new(-34200, true) + ); + assert_eq!( + time_zone.to_local_time_type(1546300800), + LocalTimeType::new(-36000, false) + ); + } + + #[test] + fn test_v3_file() { + let bytes = b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x04\0\0\x1c\x20\0\0IST\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\x04\0\0\0\0\x7f\xe8\x17\x80\0\0\0\x1c\x20\0\0IST\0\x01\x01\x0aIST-2IDT,M3.4.4/26,M10.5.0\x0a"; + + let time_zone = TimeZone::from_tzif(bytes).unwrap(); + + let time_zone_result = TimeZone { + transitions: vec![Transition::new(2145916800, 0)], + local_time_types: vec![LocalTimeType::new(7200, false)], + extra_rule: Some(TransitionRule::Alternate(AlternateLocalTimeType::new( + LocalTimeType::new(7200, false), + RuleDay::MonthWeekDay(3, 4, 4), + 93600, + LocalTimeType::new(10800, true), + RuleDay::MonthWeekDay(10, 5, 0), + 7200, + ))), + }; + + assert_eq!(time_zone, time_zone_result); + } + + #[test] + fn footer_julian_day() { + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1CEST,J100,J200\x0a"; + + let time_zone = TimeZone::from_tzif(bytes).unwrap(); + assert_eq!( + Some(TransitionRule::Alternate(AlternateLocalTimeType::new( + LocalTimeType::new(3600, false), + RuleDay::JulianDayWithoutLeap(100), + 7200, + LocalTimeType::new(7200, true), + RuleDay::JulianDayWithoutLeap(200), + 7200, + ))), + time_zone.extra_rule + ); + assert_eq!( + LocalTimeType::new(3600, false), + time_zone.to_local_time_type(1672531200) + ); + assert_eq!( + LocalTimeType::new(3600, false), + time_zone.to_local_time_type(1681088400 - 1) + ); + assert_eq!( + LocalTimeType::new(7200, true), + time_zone.to_local_time_type(1681088400) + ); + assert_eq!( + LocalTimeType::new(7200, true), + time_zone.to_local_time_type(1689724800 - 1) + ); + assert_eq!( + LocalTimeType::new(3600, false), + time_zone.to_local_time_type(1689724800) + ); + assert_eq!( + LocalTimeType::new(3600, false), + time_zone.to_local_time_type(1704067199) + ); + + assert_eq!( + LocalTimeType::new(3600, false), + time_zone.to_local_time_type(1704067200) + ); + assert_eq!( + LocalTimeType::new(3600, false), + time_zone.to_local_time_type(1712710800 - 1) + ); + assert_eq!( + LocalTimeType::new(7200, true), + time_zone.to_local_time_type(1712710800) + ); + assert_eq!( + LocalTimeType::new(7200, true), + time_zone.to_local_time_type(1721347200 - 1) + ); + assert_eq!( + LocalTimeType::new(3600, false), + time_zone.to_local_time_type(1721347200) + ); + assert_eq!( + LocalTimeType::new(3600, false), + time_zone.to_local_time_type(1735689599) + ); + + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1CEST,99,199\x0a"; + let time_zone = TimeZone::from_tzif(bytes).unwrap(); + assert_eq!( + Some(TransitionRule::Alternate(AlternateLocalTimeType::new( + LocalTimeType::new(3600, false), + RuleDay::JulianDayWithLeap(99), + 7200, + LocalTimeType::new(7200, true), + RuleDay::JulianDayWithLeap(199), + 7200, + ))), + time_zone.extra_rule + ); + assert_eq!( + LocalTimeType::new(3600, false), + time_zone.to_local_time_type(1672531200) + ); + assert_eq!( + LocalTimeType::new(3600, false), + time_zone.to_local_time_type(1681088400 - 1) + ); + assert_eq!( + LocalTimeType::new(7200, true), + time_zone.to_local_time_type(1681088400) + ); + assert_eq!( + LocalTimeType::new(7200, true), + time_zone.to_local_time_type(1689724800 - 1) + ); + assert_eq!( + LocalTimeType::new(3600, false), + time_zone.to_local_time_type(1689724800) + ); + assert_eq!( + LocalTimeType::new(3600, false), + time_zone.to_local_time_type(1704067199) + ); + + assert_eq!( + LocalTimeType::new(3600, false), + time_zone.to_local_time_type(1704067200) + ); + assert_eq!( + LocalTimeType::new(3600, false), + time_zone.to_local_time_type(1712624400 - 1) + ); + assert_eq!( + LocalTimeType::new(7200, true), + time_zone.to_local_time_type(1712624400) + ); + assert_eq!( + LocalTimeType::new(7200, true), + time_zone.to_local_time_type(1721260800 - 1) + ); + assert_eq!( + LocalTimeType::new(3600, false), + time_zone.to_local_time_type(1721260800) + ); + assert_eq!( + LocalTimeType::new(3600, false), + time_zone.to_local_time_type(1735689599) + ); + } + + #[test] + fn footer() { + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0CET-1CEST,J100,J200"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0a:character\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0a\0\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0a-1CEST+2,M3.3.0,M10.3.0\x0a"; + let time_zone = TimeZone::from_tzif(bytes).unwrap(); + assert_eq!( + LocalTimeType::new(3600, false), + time_zone.to_local_time_type(1672531200) + ); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1:0:0CEST,M3.3.0,M10.3.0\x0a"; + let time_zone = TimeZone::from_tzif(bytes).unwrap(); + assert_eq!( + LocalTimeType::new(3600, false), + time_zone.to_local_time_type(1672531200) + ); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET25:0:0CEST,M3.3.0,M10.3.0\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET1:60:0CEST,M3.3.0,M10.3.0\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET1:0:60CEST,M3.3.0,M10.3.0\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1CEST,M3.3.0/1:60,M10.3.0\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1CEST,M3.3.0/1:59:60,M10.3.0\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1CEST,M3.3.0/167,M10.3.0\x0a"; + assert!(TimeZone::from_tzif(bytes).is_ok()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1CEST,M3.3.0/168,M10.3.0\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1CEST,M3.3.0/-167,M10.3.0\x0a"; + assert!(TimeZone::from_tzif(bytes).is_ok()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1CEST,M3.3.0/-168,M10.3.0\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1CEST,Goob\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1CEST,M3.3.0/-167,M10.3.0\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0a\xFF\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1<\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1CEST\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1CEST-60\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1CEST,10\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1CEST,10,10/18000\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-a\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1:a\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1:0:a\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1CEST,10/A\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1CEST,\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1CEST,J\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1CEST,M\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1CEST,M3\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1CEST,M3.\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1CEST,M3.5\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1CEST,M3.5.\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1\x0a"; + assert!(TimeZone::from_tzif(bytes).is_err()); + } + + #[test] + fn footer_month_week_day() { + let bytes = b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1CEST,M3.5.0,M10.5.0\x0a"; + let time_zone = TimeZone::from_tzif(bytes).unwrap(); + assert_eq!( + Some(TransitionRule::Alternate(AlternateLocalTimeType::new( + LocalTimeType::new(3600, false), + RuleDay::MonthWeekDay(3, 5, 0), + 7200, + LocalTimeType::new(7200, true), + RuleDay::MonthWeekDay(10, 5, 0), + 7200, + ))), + time_zone.extra_rule + ); + assert_eq!( + LocalTimeType::new(3600, false), + time_zone.to_local_time_type(1672531200) + ); + assert_eq!( + LocalTimeType::new(3600, false), + time_zone.to_local_time_type(1679792400 - 1) + ); + assert_eq!( + LocalTimeType::new(7200, true), + time_zone.to_local_time_type(1679792400) + ); + assert_eq!( + LocalTimeType::new(7200, true), + time_zone.to_local_time_type(1698537600 - 1) + ); + assert_eq!( + LocalTimeType::new(3600, false), + time_zone.to_local_time_type(1698537600) + ); + assert_eq!( + LocalTimeType::new(3600, false), + time_zone.to_local_time_type(1704067199) + ); + + let bytes = b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1CEST,M10.5.0,M3.5.0\x0a"; + let time_zone = TimeZone::from_tzif(bytes).unwrap(); + assert_eq!( + Some(TransitionRule::Alternate(AlternateLocalTimeType::new( + LocalTimeType::new(3600, false), + RuleDay::MonthWeekDay(10, 5, 0), + 7200, + LocalTimeType::new(7200, true), + RuleDay::MonthWeekDay(3, 5, 0), + 7200, + ))), + time_zone.extra_rule + ); + assert_eq!( + LocalTimeType::new(7200, true), + time_zone.to_local_time_type(1672531200) + ); + assert_eq!( + LocalTimeType::new(7200, true), + time_zone.to_local_time_type(1679788800 - 1) + ); + assert_eq!( + LocalTimeType::new(3600, false), + time_zone.to_local_time_type(1679788800) + ); + assert_eq!( + LocalTimeType::new(3600, false), + time_zone.to_local_time_type(1698541200 - 1) + ); + assert_eq!( + LocalTimeType::new(7200, true), + time_zone.to_local_time_type(1698541200) + ); + assert_eq!( + LocalTimeType::new(7200, true), + time_zone.to_local_time_type(1704067199) + ); + let bytes = b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x0aCET-1\x0a"; + let time_zone = TimeZone::from_tzif(bytes).unwrap(); + assert_eq!( + Some(TransitionRule::Fixed(LocalTimeType::new(3600, false))), + time_zone.extra_rule + ); + assert_eq!( + LocalTimeType::new(3600, false), + time_zone.to_local_time_type(1704067199) + ); + } + + #[test] + fn invalid_tzif_file() { + let result = TimeZone::from_tzif(b""); + assert!(result.is_err()); + println!("{}", result.unwrap_err()); + let result = TimeZone::from_tzif(b"InvalidMagic"); + assert!(result.is_err()); + println!("{}", result.unwrap_err()); + let result = TimeZone::from_tzif(b"TZif4"); + assert!(result.is_err()); + println!("{}", result.unwrap_err()); + assert!(TimeZone::from_tzif(b"TZif").is_err()); + assert!(TimeZone::from_tzif(b"TZif3").is_err()); + + let header = b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; + assert!(TimeZone::from_tzif(header).is_err()); + let header = b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\xFF\xFF\xFF\xFF"; + assert!(TimeZone::from_tzif(header).is_err()); + let header = b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF"; + assert!(TimeZone::from_tzif(header.as_slice()).is_err()); + let header = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF"; + assert!(TimeZone::from_tzif(header.as_slice()).is_err()); + let header = b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF"; + assert!(TimeZone::from_tzif(header.as_slice()).is_err()); + let header = b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF"; + assert!(TimeZone::from_tzif(header.as_slice()).is_err()); + + let bytes = b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFFTZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x0a\x0f\x0a"; + let result = TimeZone::from_tzif(bytes); + assert!(result.is_err()); + + let bytes = b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFFTZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x0a\xc0\x0a"; + let result = TimeZone::from_tzif(bytes); + assert!(result.is_err()); + + let mut header = b"TZif3".to_vec(); + header.append(&mut vec![b'\0'; 15]); + header.append(&mut vec![b'\0'; 4 * 6]); + + let footer: Vec = b"\x0aCET-1CEST0M3.5.0,M10.5.0\x0a".to_vec(); + let bytes: Vec = [header.clone(), header.clone(), footer].concat(); + assert!(TimeZone::from_tzif(bytes.as_slice()).is_err()); + let footer: Vec = b"\x0aCET-1CEST0\x0a".to_vec(); + let bytes: Vec = [header.clone(), header.clone(), footer].concat(); + assert!(TimeZone::from_tzif(bytes.as_slice()).is_err()); + let footer: Vec = b"\x0a\x0a".to_vec(); + let bytes: Vec = [header.clone(), header.clone(), footer].concat(); + assert!(TimeZone::from_tzif(bytes.as_slice()).is_err()); + } + + #[test] + fn data_block() { + let bytes = b"TZif\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\0\0\0\0\0\xC8"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = b"TZif\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\0\0\0\0\0\x65\x22\x9F\xAB"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = b"TZif\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\0\x65\x22\x9F\xAB\x01"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = b"TZif\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\x01\x65\x22\x9F\xAB\x01\x00\x00\x00\x00\x00\x00"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = b"TZif\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\x01\0\0\0\x01\x65\x22\x9F\xAB\x01\x00\x00\x00\x00\x00\x00\x00"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = b"TZif\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\x01\0\0\0\x01\0\0\0\x01\x65\x22\x9F\xAB\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = b"TZif\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\x01\0\0\0\x01\0\0\0\x01\0\0\0\x01\x65\x22\x9F\xAB\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = + b"TZif\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\0\0\0\0\0\0\0\0\x0E\x10\x01\0"; + assert!(TimeZone::from_tzif(bytes).is_ok()); + let bytes = + b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\0\0\0\0\0\0\0\0\x0E\x10\x01\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\0\0\0\0\0\0\0\0\x0E\x10\x01"; + assert!(TimeZone::from_tzif(bytes).is_err()); + let bytes = b"TZif\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\0\0\0\x0E\x10\0\0"; + let time_zone = TimeZone::from_tzif(bytes).unwrap(); + assert_eq!( + LocalTimeType::new(3600, false), + time_zone.to_local_time_type(1704067199) + ); + } + + #[test] + fn default_impl() { + let mut header = b"TZif3".to_vec(); + header.append(&mut vec![b'\0'; 15]); + header.append(&mut vec![b'\0'; 4 * 6]); + + let footer: Vec = b"\x0aCET-1CEST,M3.5.0,M10.5.0\x0a".to_vec(); + let bytes: Vec = [header.clone(), header.clone(), footer].concat(); + let time_zone = TimeZone::from_tzif(bytes.as_slice()).unwrap(); + + assert_eq!( + "TimeZone { transitions: [], local_time_types: [], extra_rule: Some(Alternate(AlternateLocalTimeType { std: LocalTimeType { utoff: 3600, _dst: false }, std_end: MonthWeekDay(3, 5, 0), std_end_time: 7200, dst: LocalTimeType { utoff: 7200, _dst: true }, dst_end: MonthWeekDay(10, 5, 0), dst_end_time: 7200 })) }", + format!("{:?}", time_zone) + ); + + let bytes = + b"TZif\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\0\0\0\0\0\0\0\0\x0E\x10\x01\0"; + let time_zone = TimeZone::from_tzif(bytes).unwrap(); + + assert_eq!( + "Transition { unix_leap_time: 0, local_time_type_index: 0 }", + format!("{:?}", time_zone.transitions[0]) + ); + + let version = Version::V3; + #[allow(clippy::clone_on_copy)] + let _ = version.clone(); + } +} diff --git a/src/local/transition_rule.rs b/src/local/transition_rule.rs new file mode 100644 index 0000000..397a1e5 --- /dev/null +++ b/src/local/transition_rule.rs @@ -0,0 +1,294 @@ +use std::{num::ParseIntError, str::FromStr}; + +use crate::{ + util::{ + constants::{BUG_MSG, SECS_PER_DAY}, + date::convert::{weekdays_in_month, year_doy_to_days, year_month_to_doy}, + }, + DateTime, DateUtilities, +}; + +use super::{cursor::Cursor, errors::TimeZoneError, timezone::LocalTimeType}; + +/// Transition rule of a TZif file (Present in footer of Version 2/3 TZif files) +#[derive(Debug, PartialEq)] +pub(super) enum TransitionRule { + /// Fixed local time type + Fixed(LocalTimeType), + /// Alternate local time types + Alternate(AlternateLocalTimeType), +} + +impl TransitionRule { + pub(super) fn from_tz_string( + footer: &[u8], + string_extensions: bool, + ) -> Result, TimeZoneError> { + let footer = std::str::from_utf8(footer)?; + + if !footer.starts_with('\n') || !footer.ends_with('\n') { + return Err(TimeZoneError::InvalidTzFile("Invalid footer")); + }; + + let tz_string = footer.trim_matches(|c: char| c.is_ascii_whitespace()); + + if tz_string.starts_with(':') || tz_string.contains('\0') { + return Err(TimeZoneError::InvalidTzFile("Invalid footer")); + } + + let mut cursor = Cursor::new(tz_string.as_bytes()); + + remove_designation(&mut cursor)?; + + let std_offset = parse_tz_string_offset(&mut cursor)?; + + if cursor.empty() { + return Ok(Some(TransitionRule::Fixed(LocalTimeType::new( + -std_offset, + false, + )))); + } + + remove_designation(&mut cursor)?; + + let dst_offset = match cursor.remaining().first() { + Some(&b',') => std_offset - 3600, + Some(_) => parse_tz_string_offset(&mut cursor)?, + None => return Err(TimeZoneError::InvalidTzFile("Invalid footer")), + }; + + cursor.read_tag(b",")?; + + let (std_end, std_end_time) = parse_tz_string_rule(&mut cursor, string_extensions)?; + + cursor.read_tag(b",")?; + + let (dst_end, dst_end_time) = parse_tz_string_rule(&mut cursor, string_extensions)?; + + Ok(Some(TransitionRule::Alternate( + AlternateLocalTimeType::new( + LocalTimeType::new(-std_offset, false), + std_end, + std_end_time, + LocalTimeType::new(-dst_offset, true), + dst_end, + dst_end_time, + ), + ))) + } +} + +/// Alternate local time type +#[derive(Debug, PartialEq)] +pub(super) struct AlternateLocalTimeType { + pub(super) std: LocalTimeType, + std_end: RuleDay, + std_end_time: u32, + pub(super) dst: LocalTimeType, + dst_end: RuleDay, + dst_end_time: u32, +} + +impl AlternateLocalTimeType { + pub(super) fn new( + std: LocalTimeType, + std_end: RuleDay, + std_end_time: u32, + dst: LocalTimeType, + dst_end: RuleDay, + dst_end_time: u32, + ) -> Self { + Self { + std, + std_end, + std_end_time, + dst, + dst_end, + dst_end_time, + } + } + + pub(super) fn local_std_end_timestamp(&self, timestamp: i64) -> i64 { + rule_to_local_timestamp(&self.std_end, self.std_end_time as i32, timestamp) + } + + pub(super) fn local_dst_end_timestamp(&self, timestamp: i64) -> i64 { + rule_to_local_timestamp(&self.dst_end, self.dst_end_time as i32, timestamp) + } +} + +fn rule_to_local_timestamp(start: &RuleDay, time: i32, timestamp: i64) -> i64 { + let date_days = match start { + RuleDay::JulianDayWithoutLeap(doy) => { + let year = DateTime::from_timestamp(timestamp).year(); + year_doy_to_days(year, *doy, true).unwrap() + } + RuleDay::JulianDayWithLeap(doy) => { + let year = DateTime::from_timestamp(timestamp).year(); + year_doy_to_days(year, doy + 1, false).unwrap() + } + RuleDay::MonthWeekDay(month, week, day) => { + let year = DateTime::from_timestamp(timestamp).year(); + + let weekdays_in_month = weekdays_in_month(year, *month as u32, *day); + + let day_of_month = match week { + 5 => weekdays_in_month.last().unwrap(), + _ => &weekdays_in_month[*week as usize - 1], + }; + + let (start, _) = year_month_to_doy(year, *month as u32).unwrap(); + year_doy_to_days(year, start + day_of_month, false).unwrap() + } + }; + let time = time as i64; + DateTime::from_seconds(date_days as i64 * SECS_PER_DAY as i64 + time) + .unwrap() + .timestamp() +} + +fn remove_designation(cursor: &mut Cursor) -> Result<(), TimeZoneError> { + if cursor.get_next()? == b'<' { + cursor.read_until('>'); + cursor.read_exact(1)?; + } else { + cursor.read_while(|c: &u8| c.is_ascii_alphabetic()); + }; + Ok(()) +} + +fn parse_hms(cursor: &mut Cursor) -> Result<(i32, i32, i32, i32), TimeZoneError> { + let next = cursor.get_next()?; + let direction = if next == b'-' { + cursor.read_exact(1).expect(BUG_MSG); + -1 + } else { + if next == b'+' { + cursor.read_exact(1).expect(BUG_MSG); + }; + 1 + }; + + let hour: i32 = parse_int(cursor.read_while(|c: &u8| c.is_ascii_digit()))?; + + let mut minute = 0; + let mut second = 0; + + if !cursor.empty() && cursor.get_next().expect(BUG_MSG) == b':' { + cursor.read_exact(1).expect(BUG_MSG); + minute = parse_int(cursor.read_while(|c: &u8| c.is_ascii_digit()))?; + if !cursor.empty() && cursor.get_next().expect(BUG_MSG) == b':' { + cursor.read_exact(1).expect(BUG_MSG); + second = parse_int(cursor.read_while(|c: &u8| c.is_ascii_digit()))?; + }; + }; + + Ok((direction, hour, minute, second)) +} + +fn parse_tz_string_offset(cursor: &mut Cursor) -> Result { + let (direction, hour, minute, second) = parse_hms(cursor)?; + + if !(0..=24).contains(&hour) { + return Err(TimeZoneError::InvalidTzFile( + "Invalid day time hour in footer", + )); + } + if !(0..=59).contains(&minute) { + return Err(TimeZoneError::InvalidTzFile( + "Invalid day time minute in footer", + )); + } + if !(0..=59).contains(&second) { + return Err(TimeZoneError::InvalidTzFile( + "Invalid day time second in footer", + )); + } + + Ok(direction * (hour * 3600 + minute * 60 + second)) +} + +fn parse_tz_string_offset_extended(cursor: &mut Cursor) -> Result { + let (direction, hour, minute, second) = parse_hms(cursor)?; + + if !(-167..=167).contains(&hour) { + return Err(TimeZoneError::InvalidTzFile( + "Invalid day time hour in footer", + )); + } + if !(0..=59).contains(&minute) { + return Err(TimeZoneError::InvalidTzFile( + "Invalid day time minute in footer", + )); + } + if !(0..=59).contains(&second) { + return Err(TimeZoneError::InvalidTzFile( + "Invalid day time second in footer", + )); + } + + Ok(direction * (hour * 3600 + minute * 60 + second)) +} + +/// Parse integer from a slice of bytes +fn parse_int>(bytes: &[u8]) -> Result { + std::str::from_utf8(bytes) + .expect(BUG_MSG) + .parse() + .map_err(TimeZoneError::from) +} + +fn parse_tz_string_rule( + cursor: &mut Cursor, + string_extensions: bool, +) -> Result<(RuleDay, u32), TimeZoneError> { + let day = match cursor.get_next()? { + b'J' => { + cursor.read_exact(1).expect(BUG_MSG); + let day = parse_int(cursor.read_while(|c: &u8| c.is_ascii_digit()))?; + RuleDay::JulianDayWithoutLeap(day) + } + byte if byte.is_ascii_digit() => { + let day = parse_int(cursor.read_while(|c: &u8| c.is_ascii_digit())).expect(BUG_MSG); + RuleDay::JulianDayWithLeap(day) + } + b'M' => { + cursor.read_exact(1).expect(BUG_MSG); + let month = parse_int(cursor.read_until('.'))?; + + cursor.read_exact(1)?; + let week = parse_int(cursor.read_until('.'))?; + + cursor.read_exact(1)?; + let day = parse_int(cursor.read_while(|c| c.is_ascii_digit()))?; + + RuleDay::MonthWeekDay(month, week, day) + } + _ => return Err(TimeZoneError::InvalidTzFile("Invalid footer")), + }; + + let time = if !cursor.empty() && cursor.get_next().expect(BUG_MSG) == b'/' { + cursor.read_exact(1).expect(BUG_MSG); + if string_extensions { + parse_tz_string_offset_extended(cursor)? as u32 + } else { + parse_tz_string_offset(cursor)? as u32 + } + } else { + 2 * 3600 + }; + + Ok((day, time)) +} + +#[derive(Debug, PartialEq)] +pub(super) enum RuleDay { + /// Julian day (1..=365). February 29 is never counted + JulianDayWithoutLeap(u32), + /// Zero based Julian day (0..=365). February 29 is counted in leap years + JulianDayWithLeap(u32), + /// Month, week, day (1..=12, 1..=5, 0..=6) + /// Week 5 means the last week of the month + /// Day zero is Sunday + MonthWeekDay(u8, u8, u8), +} diff --git a/src/offset.rs b/src/offset.rs new file mode 100644 index 0000000..431074f --- /dev/null +++ b/src/offset.rs @@ -0,0 +1,93 @@ +use std::fs; + +use crate::{ + errors::{out_of_range::create_simple_oor, AstrolabeError}, + local::timezone::TimeZone, + util::{ + constants::{SECS_PER_DAY, SECS_PER_HOUR, SECS_PER_MINUTE}, + time::convert::time_to_day_seconds, + }, + DateTime, DateUtilities, +}; + +/// Represents an offset from UTC +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Offset { + /// Fixed offset in seconds + Fixed(i32), + /// Local timezone. Only works on UNIX systems. On other systems, this is equivalent to `Fixed(0)`. + Local, +} + +impl Offset { + /// Creates a fixed offset from seconds + /// + /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided seconds are not the range `-86_399..=86_399` (`UTC-23:59:59-UTC+23:59:59`). + pub fn from_seconds(seconds: i32) -> Result { + if seconds <= -(SECS_PER_DAY as i32) || seconds >= SECS_PER_DAY as i32 { + return Err(create_simple_oor( + "seconds", + -(SECS_PER_DAY as i128) + 1, + SECS_PER_DAY as i128 - 1, + seconds as i128, + )); + } + + Ok(Self::Fixed(seconds)) + } + + /// Creates a fixed offset from hours, minutes and seconds. + /// + /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided offset is not between `UTC-23:59:59` and `UTC+23:59:59`. + pub fn from_hms(hour: i32, minute: u32, second: u32) -> Result { + let mut seconds = time_to_day_seconds(hour.unsigned_abs(), minute, second)? as i32; + seconds = if hour.is_negative() { + -seconds + } else { + seconds + }; + + Ok(Self::Fixed(seconds)) + } + + /// Resolves the offset to seconds from UTC + pub fn resolve(self) -> i32 { + match self { + Self::Fixed(offset) => offset, + Self::Local => { + #[cfg(not(unix))] + return 0; + #[cfg(unix)] + return { + let result = fs::read("/etc/localtime"); + match result { + Ok(bytes) => { + TimeZone::from_tzif(&bytes) + .unwrap() + .to_local_time_type(DateTime::now().timestamp()) + .utoff + } + Err(_) => 0, + } + }; + } + } + } + + /// Returns the offset as hours, minutes and seconds. + pub fn resolve_hms(self) -> (i32, u32, u32) { + let offset_seconds = self.resolve(); + + let hour = offset_seconds / SECS_PER_HOUR as i32; + let minute = offset_seconds % SECS_PER_HOUR as i32 / SECS_PER_MINUTE as i32; + let second = offset_seconds % SECS_PER_MINUTE as i32; + + (hour, minute.unsigned_abs(), second.unsigned_abs()) + } +} + +impl Default for Offset { + fn default() -> Self { + Self::Fixed(0) + } +} diff --git a/src/shared.rs b/src/shared.rs index 9a1b6e7..12caeec 100644 --- a/src/shared.rs +++ b/src/shared.rs @@ -1,4 +1,4 @@ -use crate::errors::AstrolabeError; +use crate::{errors::AstrolabeError, offset::Offset}; /// Used for specifing the precision for RFC 3339 timestamps. #[derive(Debug, Clone, PartialEq, Eq)] @@ -32,53 +32,51 @@ pub trait DateUtilities: Sized { /// Creates a date from a unix timestamp (non-leap seconds since January 1, 1970 00:00:00 UTC). /// - /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided timestamp would result in an out of range date. - fn from_timestamp(timestamp: i64) -> Result; + /// Panics if the provided timestamp would result in an out of range date. + fn from_timestamp(timestamp: i64) -> Self; /// Returns the number of non-leap seconds since January 1, 1970 00:00:00 UTC. (Negative if date is before) fn timestamp(&self) -> i64; - /// Sets the year to the provided value. - /// - /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided value is out of range. + /// Sets the year to the provided value. Has to be in range `-5879611..=5879611`. fn set_year(&self, year: i32) -> Result; - /// Sets the month of the year to the provided value. + /// Sets the month of the year to the provided value. Has to be in range `1..=12`. /// /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided value is out of range. fn set_month(&self, month: u32) -> Result; - /// Sets the day of the month to the provided value. + /// Sets the day of the month to the provided value. Has to be in range `1..=31` and cannot be greater than the number of days in the current month. /// /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided value is out of range. fn set_day(&self, day: u32) -> Result; - /// Sets the day of the year to the provided value. + /// Sets the day of the year to the provided value. Has to be in range `1..=365` or `1..=366` in case of a leap year. /// /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided value is out of range. fn set_day_of_year(&self, day_of_year: u32) -> Result; /// Adds the provided years to the current date. /// - /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided value would result in an out of range date. - fn add_years(&self, years: u32) -> Result; + /// Panics if the provided value would result in an out of range date. + fn add_years(&self, years: u32) -> Self; /// Adds the provided months to the current date. /// - /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided value would result in an out of range date. - fn add_months(&self, months: u32) -> Result; + /// Panics if the provided value would result in an out of range date. + fn add_months(&self, months: u32) -> Self; /// Adds the provided days to the current date. /// - /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided value would result in an out of range date. - fn add_days(&self, days: u32) -> Result; + /// Panics if the provided value would result in an out of range date. + fn add_days(&self, days: u32) -> Self; /// Subtracts the provided years from the current date. /// - /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided value would result in an out of range date. - fn sub_years(&self, years: u32) -> Result; + /// Panics if the provided value would result in an out of range date. + fn sub_years(&self, years: u32) -> Self; /// Subtracts the provided months from the current date. /// - /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided value would result in an out of range date. - fn sub_months(&self, months: u32) -> Result; + /// Panics if the provided value would result in an out of range date. + fn sub_months(&self, months: u32) -> Self; /// Subtracts the provided days from the current date. /// - /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided value would result in an out of range date. - fn sub_days(&self, days: u32) -> Result; + /// Panics if the provided value would result in an out of range date. + fn sub_days(&self, days: u32) -> Self; /// Clears date/time units until the year (inclusive). fn clear_until_year(&self) -> Self; @@ -112,80 +110,56 @@ pub trait TimeUtilities: Sized { /// Returns the nanosecond of the second (`0-999_999_999`). fn nano(&self) -> u32; - /// Sets the hour to the provided value. + /// Sets the hour to the provided value. Has to be in range `0..=23`. /// /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided value is out of range. fn set_hour(&self, hour: u32) -> Result; - /// Sets the minute to the provided value. + /// Sets the minute to the provided value. Has to be in range `0..=59`. /// /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided value is out of range. fn set_minute(&self, minute: u32) -> Result; - /// Sets the second to the provided value. + /// Sets the second to the provided value. Has to be in range `0..=59`. /// /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided value is out of range. fn set_second(&self, second: u32) -> Result; - /// Sets the millisecond to the provided value. + /// Sets the millisecond to the provided value. Has to be in range `0..=100`. /// /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided value is out of range. fn set_milli(&self, milli: u32) -> Result; - /// Sets the microsecond to the provided value. + /// Sets the microsecond to the provided value. Has to be in range `0..=100_000`. /// /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided value is out of range. fn set_micro(&self, micro: u32) -> Result; - /// Sets the nanosecond to the provided value. + /// Sets the nanosecond to the provided value. Has to be in range `0..=100_000_000`. /// /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided value is out of range. fn set_nano(&self, nano: u32) -> Result; /// Adds the provided hours. - /// - /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided value would result in an out of range time. - fn add_hours(&self, hours: u32) -> Result; + fn add_hours(&self, hours: u32) -> Self; /// Adds the provided minutes. - /// - /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided value would result in an out of range time. - fn add_minutes(&self, minutes: u32) -> Result; + fn add_minutes(&self, minutes: u32) -> Self; /// Adds the provided seconds. - /// - /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided value would result in an out of range time. - fn add_seconds(&self, seconds: u32) -> Result; + fn add_seconds(&self, seconds: u32) -> Self; /// Adds the provided milliseconds. - /// - /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided value would result in an out of range time. - fn add_millis(&self, millis: u32) -> Result; + fn add_millis(&self, millis: u32) -> Self; /// Adds the provided microseconds. - /// - /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided value would result in an out of range time. - fn add_micros(&self, micros: u32) -> Result; + fn add_micros(&self, micros: u32) -> Self; /// Adds the provided nanoseconds. - /// - /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided value would result in an out of range time. - fn add_nanos(&self, nanos: u32) -> Result; + fn add_nanos(&self, nanos: u32) -> Self; /// Subtracts the provided hours. - /// - /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided value would result in an out of range time. - fn sub_hours(&self, hours: u32) -> Result; + fn sub_hours(&self, hours: u32) -> Self; /// Subtracts the provided minutes. - /// - /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided value would result in an out of range time. - fn sub_minutes(&self, minutes: u32) -> Result; + fn sub_minutes(&self, minutes: u32) -> Self; /// Subtracts the provided seconds. - /// - /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided value would result in an out of range time. - fn sub_seconds(&self, seconds: u32) -> Result; + fn sub_seconds(&self, seconds: u32) -> Self; /// Subtracts the provided milliseconds. - /// - /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided value would result in an out of range time. - fn sub_millis(&self, millis: u32) -> Result; + fn sub_millis(&self, millis: u32) -> Self; /// Subtracts the provided microseconds. - /// - /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided value would result in an out of range time. - fn sub_micros(&self, micros: u32) -> Result; + fn sub_micros(&self, micros: u32) -> Self; /// Subtracts the provided nanoseconds. - /// - /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided value would result in an out of range time. - fn sub_nanos(&self, nanos: u32) -> Result; + fn sub_nanos(&self, nanos: u32) -> Self; /// Clears date/time units until the hour (inclusive). fn clear_until_hour(&self) -> Self; @@ -225,42 +199,20 @@ pub trait TimeUtilities: Sized { /// The offset affects all `format`, `get` and `set` functions. /// Used by [`DateTime`](crate::DateTime) and [`Time`](crate::Time). pub trait OffsetUtilities: Sized { - /// Sets the offset from hours, minutes and seconds. - /// - /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided offset is not between `UTC-23:59:59` and `UTC+23:59:59`. - /// - /// Examples: - /// - `UTC+1` is `offset_from_hms(1, 0, 0)` - /// - `UTC-1` is `offset_from_hms(-1, 0, 0)`. - fn set_offset_hms(&self, hour: i32, minute: u32, second: u32) -> Result; - /// Sets the offset from hours, minutes and seconds, assuming the current instance has the provided offset applied. The new instance will have the specified offset and the datetime itself will be converted to `UTC`. - /// - /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided offset is not between `UTC-23:59:59` and `UTC+23:59:59`. + /// Sets the offset /// /// Examples: - /// - `UTC+1` is `as_offset_hms(1, 0, 0)` - /// - `UTC-1` is `as_offset_hms(-1, 0, 0)`. - fn as_offset_hms(&self, hour: i32, minute: u32, second: u32) -> Result; - - /// Returns the offset as hours, minutes and seconds. - fn get_offset_hms(&self) -> (i32, u32, u32); - - /// Sets the offset from hours, minutes and seconds. - /// - /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided offset is not between `UTC-23:59:59` and `UTC+23:59:59`. - /// - /// Examples: - /// - `UTC+1` is `offset_from_seconds(3600)` - /// - `UTC-1` is `offset_from_seconds(-3600)`. - fn set_offset(&self, seconds: i32) -> Result; - /// Sets the offset from seconds, assuming the current instance has the provided offset applied. The new instance will have the specified offset and the datetime itself will be converted to `UTC`. - /// - /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided offset is not between `UTC-23:59:59` and `UTC+23:59:59`. + /// - `UTC+1` is `set_offset(Offset::Fixed(3600))` + /// - `UTC-1` is `set_offset(Offset::Fixed(-3600))` + /// - To set the offset to the local timezone, use `set_offset(Offset::Local)` + fn set_offset(&self, offset: Offset) -> Self; + /// Sets the offset, assuming the current instance has the provided offset applied. The new instance will have the specified offset and the datetime itself will be converted to `UTC`. /// /// Examples: - /// - `UTC+1` is `as_offset(3600)` - /// - `UTC-1` is `as_offset(-3600)`. - fn as_offset(&self, seconds: i32) -> Result; - /// Returns the offset as seconds. - fn get_offset(&self) -> i32; + /// - `UTC+1` is `as_offset(Offset::Fixed(3600))` + /// - `UTC-1` is `as_offset(Offset::Fixed(-3600))`. + /// - To set the offset to the local timezone, use `as_offset(Offset::Local)` + fn as_offset(&self, offset: Offset) -> Self; + /// Returns the offset + fn get_offset(&self) -> Offset; } diff --git a/src/time.rs b/src/time.rs index 3a46d61..20d6b8a 100644 --- a/src/time.rs +++ b/src/time.rs @@ -1,12 +1,9 @@ use crate::{ - errors::{ - out_of_range::{create_custom_oor, create_simple_oor}, - AstrolabeError, - }, + errors::{out_of_range::create_simple_oor, AstrolabeError}, util::{ constants::{ - BUG_MSG, NANOS_PER_DAY, NANOS_PER_SEC, SECS_PER_DAY, SECS_PER_DAY_U64, SECS_PER_HOUR, - SECS_PER_HOUR_U64, SECS_PER_MINUTE, SECS_PER_MINUTE_U64, + BUG_MSG, NANOS_PER_DAY, NANOS_PER_SEC, SECS_PER_DAY, SECS_PER_DAY_U64, + SECS_PER_HOUR_U64, SECS_PER_MINUTE_U64, }, format::format_time_part, offset::{add_offset_to_nanos, remove_offset_from_nanos}, @@ -28,7 +25,7 @@ use crate::{ }, }, }, - DateTime, OffsetUtilities, TimeUtilities, + DateTime, Offset, OffsetUtilities, TimeUtilities, }; use std::{ cmp, @@ -43,10 +40,10 @@ use std::{ /// See the [`TimeUtilities`](#impl-TimeUtilities-for-Time) implementation for get, set and manipulation methods. /// /// [`OffsetUtilities`](#impl-OffsetUtilities-for-Time) implements methods for setting and getting the offset. -#[derive(Debug, Default, Copy, Clone, Eq)] +#[derive(Debug, Default, Clone, Copy, Eq)] pub struct Time { pub(crate) nanoseconds: u64, - pub(crate) offset: i32, + pub(crate) offset: Offset, } impl Time { @@ -65,10 +62,22 @@ impl Time { duration.as_secs() % SECS_PER_DAY_U64 * NANOS_PER_SEC + duration.subsec_nanos() as u64; Self { nanoseconds, - offset: 0, + offset: Offset::default(), } } + /// Creates a new [`Time`] instance with [`SystemTime::now()`] with the local timezone as the offset. + /// + /// ```rust + /// # use astrolabe::{Time, Offset, OffsetUtilities}; + /// let time = Time::now_local(); + /// println!("{}", time); + /// assert_eq!(time.get_offset(), Offset::Local); + /// ``` + pub fn now_local() -> Self { + Self::now().set_offset(Offset::Local) + } + /// Creates a new [`Time`] instance from hour, minute and seconds. /// /// Returns an [`OutOfRange`](AstrolabeError::OutOfRange) error if the provided time is invalid. @@ -83,7 +92,7 @@ impl Time { Ok(Self { nanoseconds: seconds * NANOS_PER_SEC, - offset: 0, + offset: Offset::default(), }) } @@ -127,7 +136,7 @@ impl Time { } Ok(Self { nanoseconds: seconds as u64 * NANOS_PER_SEC, - offset: 0, + offset: Offset::default(), }) } @@ -162,7 +171,7 @@ impl Time { } Ok(Self { nanoseconds: nanos, - offset: 0, + offset: Offset::default(), }) } @@ -249,7 +258,7 @@ impl Time { nanoseconds += time.nanos.unwrap_or(0); Ok(if let Some(offset) = time.offset { - Self::from_nanos(nanoseconds)?.set_offset(offset)? + Self::from_nanos(nanoseconds)?.set_offset(Offset::from_seconds(offset)?) } else { Self::from_nanos(nanoseconds)? }) @@ -316,6 +325,8 @@ impl Time { /// ``` /// pub fn format(&self, format: &str) -> String { + let offset_seconds = self.offset.resolve(); + let parts = parse_format_string(format); parts .iter() @@ -335,8 +346,8 @@ impl Time { format_time_part( part, - add_offset_to_nanos(self.nanoseconds, self.offset), - self.offset, + add_offset_to_nanos(self.nanoseconds, offset_seconds), + offset_seconds, ) .chars() .collect::>() @@ -360,47 +371,49 @@ impl Time { impl TimeUtilities for Time { fn hour(&self) -> u32 { - let nanos = add_offset_to_nanos(self.nanoseconds, self.offset); + let nanos = add_offset_to_nanos(self.nanoseconds, self.offset.resolve()); nanos_to_time(nanos).0 } fn minute(&self) -> u32 { - let nanos = add_offset_to_nanos(self.nanoseconds, self.offset); + let nanos = add_offset_to_nanos(self.nanoseconds, self.offset.resolve()); nanos_to_time(nanos).1 } fn second(&self) -> u32 { - let nanos = add_offset_to_nanos(self.nanoseconds, self.offset); + let nanos = add_offset_to_nanos(self.nanoseconds, self.offset.resolve()); nanos_to_time(nanos).2 } fn milli(&self) -> u32 { - let nanos = add_offset_to_nanos(self.nanoseconds, self.offset); + let nanos = add_offset_to_nanos(self.nanoseconds, self.offset.resolve()); nanos_to_subsecond(nanos).0 } fn micro(&self) -> u32 { - let nanos = add_offset_to_nanos(self.nanoseconds, self.offset); + let nanos = add_offset_to_nanos(self.nanoseconds, self.offset.resolve()); nanos_to_subsecond(nanos).1 } fn nano(&self) -> u32 { - let nanos = add_offset_to_nanos(self.nanoseconds, self.offset); + let nanos = add_offset_to_nanos(self.nanoseconds, self.offset.resolve()); nanos_to_subsecond(nanos).2 } fn set_hour(&self, hour: u32) -> Result { - let nanos = add_offset_to_nanos(self.nanoseconds, self.offset); + let offset_seconds = self.offset.resolve(); + + let nanos = add_offset_to_nanos(self.nanoseconds, offset_seconds); let new_nanos = set_hour(nanos, hour)?; - let new_nanos = remove_offset_from_nanos(new_nanos, self.offset); + let new_nanos = remove_offset_from_nanos(new_nanos, offset_seconds); Ok(Self { nanoseconds: new_nanos, @@ -409,11 +422,13 @@ impl TimeUtilities for Time { } fn set_minute(&self, minute: u32) -> Result { - let nanos = add_offset_to_nanos(self.nanoseconds, self.offset); + let offset_seconds = self.offset.resolve(); + + let nanos = add_offset_to_nanos(self.nanoseconds, offset_seconds); let new_nanos = set_minute(nanos, minute)?; - let new_nanos = remove_offset_from_nanos(new_nanos, self.offset); + let new_nanos = remove_offset_from_nanos(new_nanos, offset_seconds); Ok(Self { nanoseconds: new_nanos, @@ -422,11 +437,13 @@ impl TimeUtilities for Time { } fn set_second(&self, second: u32) -> Result { - let nanos = add_offset_to_nanos(self.nanoseconds, self.offset); + let offset_seconds = self.offset.resolve(); + + let nanos = add_offset_to_nanos(self.nanoseconds, offset_seconds); let new_nanos = set_second(nanos, second)?; - let new_nanos = remove_offset_from_nanos(new_nanos, self.offset); + let new_nanos = remove_offset_from_nanos(new_nanos, offset_seconds); Ok(Self { nanoseconds: new_nanos, @@ -435,11 +452,13 @@ impl TimeUtilities for Time { } fn set_milli(&self, milli: u32) -> Result { - let nanos = add_offset_to_nanos(self.nanoseconds, self.offset); + let offset_seconds = self.offset.resolve(); + + let nanos = add_offset_to_nanos(self.nanoseconds, offset_seconds); let new_nanos = set_milli(nanos, milli)?; - let new_nanos = remove_offset_from_nanos(new_nanos, self.offset); + let new_nanos = remove_offset_from_nanos(new_nanos, offset_seconds); Ok(Self { nanoseconds: new_nanos, @@ -448,11 +467,13 @@ impl TimeUtilities for Time { } fn set_micro(&self, micro: u32) -> Result { - let nanos = add_offset_to_nanos(self.nanoseconds, self.offset); + let offset_seconds = self.offset.resolve(); + + let nanos = add_offset_to_nanos(self.nanoseconds, offset_seconds); let new_nanos = set_micro(nanos, micro)?; - let new_nanos = remove_offset_from_nanos(new_nanos, self.offset); + let new_nanos = remove_offset_from_nanos(new_nanos, offset_seconds); Ok(Self { nanoseconds: new_nanos, @@ -461,11 +482,13 @@ impl TimeUtilities for Time { } fn set_nano(&self, nano: u32) -> Result { - let nanos = add_offset_to_nanos(self.nanoseconds, self.offset); + let offset_seconds = self.offset.resolve(); + + let nanos = add_offset_to_nanos(self.nanoseconds, offset_seconds); let new_nanos = set_nano(nanos, nano)?; - let new_nanos = remove_offset_from_nanos(new_nanos, self.offset); + let new_nanos = remove_offset_from_nanos(new_nanos, offset_seconds); Ok(Self { nanoseconds: new_nanos, @@ -473,122 +496,108 @@ impl TimeUtilities for Time { }) } - fn add_hours(&self, hours: u32) -> Result { - Self::from_nanos(add_hours(self.nanoseconds, hours))?.set_offset(self.offset) + /// Wraps around from `23:59:59` to `00:00:00` + fn add_hours(&self, hours: u32) -> Self { + Self::from_nanos(add_hours(self.nanoseconds, hours) % (SECS_PER_DAY_U64 * NANOS_PER_SEC)) + .expect(BUG_MSG) + .set_offset(self.offset) } - fn add_minutes(&self, minutes: u32) -> Result { - Self::from_nanos(add_minutes(self.nanoseconds, minutes))?.set_offset(self.offset) + /// Wraps around from `23:59:59` to `00:00:00` + fn add_minutes(&self, minutes: u32) -> Self { + Self::from_nanos( + add_minutes(self.nanoseconds, minutes) % (SECS_PER_DAY_U64 * NANOS_PER_SEC), + ) + .expect(BUG_MSG) + .set_offset(self.offset) } - fn add_seconds(&self, seconds: u32) -> Result { - Self::from_nanos(add_seconds(self.nanoseconds, seconds))?.set_offset(self.offset) + /// Wraps around from `23:59:59` to `00:00:00` + fn add_seconds(&self, seconds: u32) -> Self { + Self::from_nanos( + add_seconds(self.nanoseconds, seconds) % (SECS_PER_DAY_U64 * NANOS_PER_SEC), + ) + .expect(BUG_MSG) + .set_offset(self.offset) } - fn add_millis(&self, millis: u32) -> Result { - Self::from_nanos(add_millis(self.nanoseconds, millis))?.set_offset(self.offset) + /// Wraps around from `23:59:59` to `00:00:00` + fn add_millis(&self, millis: u32) -> Self { + Self::from_nanos(add_millis(self.nanoseconds, millis) % (SECS_PER_DAY_U64 * NANOS_PER_SEC)) + .expect(BUG_MSG) + .set_offset(self.offset) } - fn add_micros(&self, micros: u32) -> Result { - Self::from_nanos(add_micros(self.nanoseconds, micros))?.set_offset(self.offset) + /// Wraps around from `23:59:59` to `00:00:00` + fn add_micros(&self, micros: u32) -> Self { + Self::from_nanos(add_micros(self.nanoseconds, micros) % (SECS_PER_DAY_U64 * NANOS_PER_SEC)) + .expect(BUG_MSG) + .set_offset(self.offset) } - fn add_nanos(&self, nanos: u32) -> Result { - Self::from_nanos(self.nanoseconds + nanos as u64)?.set_offset(self.offset) + /// Wraps around from `23:59:59` to `00:00:00` + fn add_nanos(&self, nanos: u32) -> Self { + Self::from_nanos((self.nanoseconds + nanos as u64) % (SECS_PER_DAY_U64 * NANOS_PER_SEC)) + .expect(BUG_MSG) + .set_offset(self.offset) } - fn sub_hours(&self, hours: u32) -> Result { - Self::from_nanos( - sub_hours(self.nanoseconds as i64, hours) - .try_into() - .map_err(|_| { - create_custom_oor(format!( - "Subtracting {} hours would result into an out of range time", - hours - )) - })?, - ) - .expect(BUG_MSG) - .set_offset(self.offset) + /// Wraps around from `00:00:00` to `23:59:59` + fn sub_hours(&self, hours: u32) -> Self { + let new_nanos = sub_hours(self.nanoseconds as i64, hours); + let rhs = SECS_PER_DAY_U64 as i64 * NANOS_PER_SEC as i64; + Self::from_nanos(new_nanos.rem_euclid(rhs) as u64) + .expect(BUG_MSG) + .set_offset(self.offset) } - fn sub_minutes(&self, minutes: u32) -> Result { - Self::from_nanos( - sub_minutes(self.nanoseconds as i64, minutes) - .try_into() - .map_err(|_| { - create_custom_oor(format!( - "Subtracting {} minutes would result into an out of range time", - minutes - )) - })?, - ) - .expect(BUG_MSG) - .set_offset(self.offset) + /// Wraps around from `00:00:00` to `23:59:59` + fn sub_minutes(&self, minutes: u32) -> Self { + let new_nanos = sub_minutes(self.nanoseconds as i64, minutes); + let rhs = SECS_PER_DAY_U64 as i64 * NANOS_PER_SEC as i64; + Self::from_nanos(new_nanos.rem_euclid(rhs) as u64) + .expect(BUG_MSG) + .set_offset(self.offset) } - fn sub_seconds(&self, seconds: u32) -> Result { - Self::from_nanos( - sub_seconds(self.nanoseconds as i64, seconds) - .try_into() - .map_err(|_| { - create_custom_oor(format!( - "Subtracting {} seconds would result into an out of range time", - seconds - )) - })?, - ) - .expect(BUG_MSG) - .set_offset(self.offset) + /// Wraps around from `00:00:00` to `23:59:59` + fn sub_seconds(&self, seconds: u32) -> Self { + let new_nanos = sub_seconds(self.nanoseconds as i64, seconds); + let rhs = SECS_PER_DAY_U64 as i64 * NANOS_PER_SEC as i64; + Self::from_nanos(new_nanos.rem_euclid(rhs) as u64) + .expect(BUG_MSG) + .set_offset(self.offset) } - fn sub_millis(&self, millis: u32) -> Result { - Self::from_nanos( - sub_millis(self.nanoseconds as i64, millis) - .try_into() - .map_err(|_| { - create_custom_oor(format!( - "Subtracting {} milliseconds would result into an out of range time", - millis - )) - })?, - ) - .expect(BUG_MSG) - .set_offset(self.offset) + /// Wraps around from `00:00:00` to `23:59:59` + fn sub_millis(&self, millis: u32) -> Self { + let new_nanos = sub_millis(self.nanoseconds as i64, millis); + let rhs = SECS_PER_DAY_U64 as i64 * NANOS_PER_SEC as i64; + Self::from_nanos(new_nanos.rem_euclid(rhs) as u64) + .expect(BUG_MSG) + .set_offset(self.offset) } - fn sub_micros(&self, micros: u32) -> Result { - Self::from_nanos( - sub_micros(self.nanoseconds as i64, micros) - .try_into() - .map_err(|_| { - create_custom_oor(format!( - "Subtracting {} microseconds would result into an out of range time", - micros - )) - })?, - ) - .expect(BUG_MSG) - .set_offset(self.offset) + /// Wraps around from `00:00:00` to `23:59:59` + fn sub_micros(&self, micros: u32) -> Self { + let new_nanos = sub_micros(self.nanoseconds as i64, micros); + let rhs = SECS_PER_DAY_U64 as i64 * NANOS_PER_SEC as i64; + Self::from_nanos(new_nanos.rem_euclid(rhs) as u64) + .expect(BUG_MSG) + .set_offset(self.offset) } - fn sub_nanos(&self, nanos: u32) -> Result { - Self::from_nanos( - (self.nanoseconds as i64 - nanos as i64) - .try_into() - .map_err(|_| { - create_custom_oor(format!( - "Subtracting {} nanoseconds would result into an out of range time", - nanos - )) - })?, - ) - .expect(BUG_MSG) - .set_offset(self.offset) + /// Wraps around from `00:00:00` to `23:59:59` + fn sub_nanos(&self, nanos: u32) -> Self { + let new_nanos = self.nanoseconds as i64 - nanos as i64; + let rhs = SECS_PER_DAY_U64 as i64 * NANOS_PER_SEC as i64; + Self::from_nanos(new_nanos.rem_euclid(rhs) as u64) + .expect(BUG_MSG) + .set_offset(self.offset) } fn clear_until_hour(&self) -> Self { - let nanoseconds = remove_offset_from_nanos(0, self.offset); + let nanoseconds = remove_offset_from_nanos(0, self.offset.resolve()); Self { nanoseconds, offset: self.offset, @@ -596,9 +605,11 @@ impl TimeUtilities for Time { } fn clear_until_minute(&self) -> Self { - let nanoseconds = add_offset_to_nanos(self.nanoseconds, self.offset); + let offset_seconds = self.offset.resolve(); + + let nanoseconds = add_offset_to_nanos(self.nanoseconds, offset_seconds); let nanoseconds = - remove_offset_from_nanos(clear_nanos_until_minute(nanoseconds), self.offset); + remove_offset_from_nanos(clear_nanos_until_minute(nanoseconds), offset_seconds); Self { nanoseconds, offset: self.offset, @@ -606,9 +617,11 @@ impl TimeUtilities for Time { } fn clear_until_second(&self) -> Self { - let nanoseconds = add_offset_to_nanos(self.nanoseconds, self.offset); + let offset_seconds = self.offset.resolve(); + + let nanoseconds = add_offset_to_nanos(self.nanoseconds, offset_seconds); let nanoseconds = - remove_offset_from_nanos(clear_nanos_until_second(nanoseconds), self.offset); + remove_offset_from_nanos(clear_nanos_until_second(nanoseconds), offset_seconds); Self { nanoseconds, offset: self.offset, @@ -616,9 +629,11 @@ impl TimeUtilities for Time { } fn clear_until_milli(&self) -> Self { - let nanoseconds = add_offset_to_nanos(self.nanoseconds, self.offset); + let offset_seconds = self.offset.resolve(); + + let nanoseconds = add_offset_to_nanos(self.nanoseconds, offset_seconds); let nanoseconds = - remove_offset_from_nanos(clear_nanos_until_milli(nanoseconds), self.offset); + remove_offset_from_nanos(clear_nanos_until_milli(nanoseconds), offset_seconds); Self { nanoseconds, offset: self.offset, @@ -626,9 +641,11 @@ impl TimeUtilities for Time { } fn clear_until_micro(&self) -> Self { - let nanoseconds = add_offset_to_nanos(self.nanoseconds, self.offset); + let offset_seconds = self.offset.resolve(); + + let nanoseconds = add_offset_to_nanos(self.nanoseconds, offset_seconds); let nanoseconds = - remove_offset_from_nanos(clear_nanos_until_micro(nanoseconds), self.offset); + remove_offset_from_nanos(clear_nanos_until_micro(nanoseconds), offset_seconds); Self { nanoseconds, offset: self.offset, @@ -636,9 +653,11 @@ impl TimeUtilities for Time { } fn clear_until_nano(&self) -> Self { - let nanoseconds = add_offset_to_nanos(self.nanoseconds, self.offset); + let offset_seconds = self.offset.resolve(); + + let nanoseconds = add_offset_to_nanos(self.nanoseconds, offset_seconds); let nanoseconds = - remove_offset_from_nanos(clear_nanos_until_nanos(nanoseconds), self.offset); + remove_offset_from_nanos(clear_nanos_until_nanos(nanoseconds), offset_seconds); Self { nanoseconds, offset: self.offset, @@ -740,63 +759,19 @@ impl TimeUtilities for Time { // ######################################## impl OffsetUtilities for Time { - fn set_offset_hms(&self, hour: i32, minute: u32, second: u32) -> Result { - let mut seconds = time_to_day_seconds(hour.unsigned_abs(), minute, second)? as i32; - seconds = if hour.is_negative() { - -seconds - } else { - seconds - }; - - Ok(self.set_offset(seconds).unwrap()) - } - - fn as_offset_hms(&self, hour: i32, minute: u32, second: u32) -> Result { - let mut offset_secs = time_to_day_seconds(hour.unsigned_abs(), minute, second)? as i32; - offset_secs = if hour.is_negative() { - -offset_secs - } else { - offset_secs - }; - - let new_seconds = remove_offset_from_nanos(self.nanoseconds, offset_secs); - - Ok(Self::from_nanos(new_seconds) - .unwrap() - .set_offset(offset_secs) - .unwrap()) - } - - fn get_offset_hms(&self) -> (i32, u32, u32) { - let hour = self.offset / SECS_PER_HOUR as i32; - let minute = self.offset % SECS_PER_HOUR as i32 / SECS_PER_MINUTE as i32; - let second = self.offset % SECS_PER_MINUTE as i32; - - (hour, minute.unsigned_abs(), second.unsigned_abs()) - } - - fn set_offset(&self, seconds: i32) -> Result { - if seconds <= -(SECS_PER_DAY as i32) || seconds >= SECS_PER_DAY as i32 { - return Err(create_simple_oor( - "seconds", - -(SECS_PER_DAY as i128) + 1, - SECS_PER_DAY as i128 - 1, - seconds as i128, - )); - } - - Ok(Self { + fn set_offset(&self, offset: Offset) -> Self { + Self { nanoseconds: self.nanoseconds, - offset: seconds, - }) + offset, + } } - fn as_offset(&self, seconds: i32) -> Result { - let new_seconds = remove_offset_from_nanos(self.nanoseconds, seconds); - Self::from_nanos(new_seconds).unwrap().set_offset(seconds) + fn as_offset(&self, offset: Offset) -> Self { + let new_seconds = remove_offset_from_nanos(self.nanoseconds, offset.resolve()); + Self::from_nanos(new_seconds).unwrap().set_offset(offset) } - fn get_offset(&self) -> i32 { + fn get_offset(&self) -> Offset { self.offset } } diff --git a/src/util/date/convert.rs b/src/util/date/convert.rs index e2feb39..6ed51b2 100644 --- a/src/util/date/convert.rs +++ b/src/util/date/convert.rs @@ -103,12 +103,21 @@ pub(crate) fn date_to_days(year: i32, month: u32, day: u32) -> Result Result { +pub(crate) fn year_doy_to_days( + year: i32, + mut doy: u32, + ignore_leap: bool, +) -> Result { validate_doy(year, doy)?; let leap_years = leap_years(year); doy -= 1; + // Ignores leap day if ignore_leap is true + if ignore_leap && is_leap_year(year) && doy >= 60 { + doy += 1; + } + Ok(if year.is_negative() { doy = if is_leap_year(year) { 366 } else { 365 } - doy; (year + 1) * 365 - leap_years as i32 - doy as i32 @@ -168,6 +177,33 @@ pub(crate) fn days_to_wday(days: i32, monday_first: bool) -> u32 { (days.unsigned_abs() % 7 + if monday_first { 0 } else { 1 }) % 7 } +/// Get a list of specific weekdays in a month +pub(crate) fn weekdays_in_month(year: i32, month: u32, weekday: u8) -> Vec { + let (_, days) = year_month_to_doy(year, month).unwrap(); + + let start_days = date_to_days(year, month, 1).unwrap(); + + let mut weekday_index = 0; + for index in 0..=6 { + if days_to_wday(start_days + index as i32, false) == weekday as u32 { + weekday_index = index; + break; + } + } + + let mut weekdays = Vec::new(); + for index in 0..=5 { + let day = weekday_index + 1 + index * 7; + if day <= days { + weekdays.push(day); + } else { + break; + } + } + + weekdays +} + /// Converts days to week of year /// Formula taken from https://tondering.dk/claus/cal/week.php#calcweekno pub(crate) fn days_to_wyear(days: i32) -> u32 { diff --git a/src/util/date/manipulate.rs b/src/util/date/manipulate.rs index e9581ac..90ef8d1 100644 --- a/src/util/date/manipulate.rs +++ b/src/util/date/manipulate.rs @@ -25,7 +25,7 @@ pub(crate) fn set_day(days: i32, day: u32) -> Result { pub(crate) fn set_day_of_year(days: i32, day_of_year: u32) -> Result { let year = days_to_date(days).0; - year_doy_to_days(year, day_of_year) + year_doy_to_days(year, day_of_year, false) } pub(crate) fn add_years(days: i32, years: u32) -> Result { diff --git a/tests/date.rs b/tests/date.rs index 8c8c71b..f1dacaf 100644 --- a/tests/date.rs +++ b/tests/date.rs @@ -42,7 +42,7 @@ mod date_tests { #[test] fn ord() { let date = Date::default(); - let date_2 = date.add_days(1).unwrap(); + let date_2 = date.add_days(1); assert!(date < date_2); assert_eq!(std::cmp::Ordering::Less, date.cmp(&date_2)); } @@ -161,167 +161,151 @@ mod date_tests { #[test] fn timestamp() { - assert_eq!(0, Date::from_timestamp(0).unwrap().timestamp()); - assert_eq!( - "1970/01/01", - Date::from_timestamp(0).unwrap().format("yyyy/MM/dd") - ); - assert_eq!( - "1969/12/31", - Date::from_timestamp(-1).unwrap().format("yyyy/MM/dd") - ); + assert_eq!(0, Date::from_timestamp(0).timestamp()); + assert_eq!("1970/01/01", Date::from_timestamp(0).format("yyyy/MM/dd")); + assert_eq!("1969/12/31", Date::from_timestamp(-1).format("yyyy/MM/dd")); assert_eq!( 185_480_451_504_000, - Date::from_timestamp(185_480_451_590_399) - .unwrap() - .timestamp() + Date::from_timestamp(185_480_451_590_399).timestamp() ); assert_eq!( "5879611/07/12", - Date::from_timestamp(185_480_451_590_399) - .unwrap() - .format("yyyy/MM/dd") + Date::from_timestamp(185_480_451_590_399).format("yyyy/MM/dd") ); assert_eq!( -185_604_722_784_000, - Date::from_timestamp(-185_604_722_784_000) - .unwrap() - .timestamp() + Date::from_timestamp(-185_604_722_784_000).timestamp() ); assert_eq!( "-5879611/06/23", - Date::from_timestamp(-185_604_722_784_000) - .unwrap() - .format("yyyy/MM/dd") + Date::from_timestamp(-185_604_722_784_000).format("yyyy/MM/dd") ); + } - assert!(Date::from_timestamp(185_480_451_590_400).is_err()); - assert!(Date::from_timestamp(-185_604_722_784_001).is_err()); + #[test] + #[should_panic] + fn timestamp_overflow() { + Date::from_timestamp(185_480_451_590_400); + } + + #[test] + #[should_panic] + fn timestamp_underflow() { + Date::from_timestamp(-185_604_722_784_001); } #[test] fn add_sub() { let date = Date::from_ymd(1970, 1, 1).unwrap(); - let modified = date.add_days(123).unwrap(); + let modified = date.add_days(123); assert_eq!(10627200, modified.timestamp()); - let modified = date.add_months(11).unwrap(); + let modified = date.add_months(11); assert_eq!("1970-12-01", modified.format("yyyy-MM-dd")); - let modified = date.add_months(12).unwrap(); + let modified = date.add_months(12); assert_eq!("1971-01-01", modified.format("yyyy-MM-dd")); - let modified = date.add_months(14).unwrap(); + let modified = date.add_months(14); assert_eq!("1971-03-01", modified.format("yyyy-MM-dd")); // Leap year cases - let modified = date.add_days(30).unwrap(); + let modified = date.add_days(30); assert_eq!("1970-01-31", modified.format("yyyy-MM-dd")); - let modified = modified.add_months(1).unwrap(); + let modified = modified.add_months(1); assert_eq!("1970-02-28", modified.format("yyyy-MM-dd")); - let modified = modified.add_years(2).unwrap(); + let modified = modified.add_years(2); assert_eq!("1972-02-28", modified.format("yyyy-MM-dd")); - let modified = date.add_years(2).unwrap().add_days(30).unwrap(); + let modified = date.add_years(2).add_days(30); assert_eq!("1972-01-31", modified.format("yyyy-MM-dd")); - let modified = modified.add_months(1).unwrap(); + let modified = modified.add_months(1); assert_eq!("1972-02-29", modified.format("yyyy-MM-dd")); let date = Date::from_ymd(1971, 1, 1).unwrap(); - let modified = date.sub_months(1).unwrap(); + let modified = date.sub_months(1); assert_eq!("1970-12-01", modified.format("yyyy-MM-dd")); let date = Date::from_ymd(1972, 3, 31).unwrap(); - let modified = date.sub_months(1).unwrap(); + let modified = date.sub_months(1); assert_eq!("1972-02-29", modified.format("yyyy-MM-dd")); - let modified = modified.sub_months(1).unwrap(); + let modified = modified.sub_months(1); assert_eq!("1972-01-29", modified.format("yyyy-MM-dd")); - let date = Date::from_ymd(5_879_611, 7, 12).unwrap(); - assert!(date.add_days(1).is_err()); - let date = Date::from_ymd(5_879_611, 6, 13).unwrap(); - assert!(date.add_months(1).is_err()); - let date = Date::from_ymd(5_879_610, 7, 13).unwrap(); - assert!(date.add_years(1).is_err()); - - let date = Date::from_ymd(-5_879_611, 6, 23).unwrap(); - assert!(date.sub_days(1).is_err()); - let date = Date::from_ymd(-5_879_611, 7, 22).unwrap(); - assert!(date.sub_months(1).is_err()); - let date = Date::from_ymd(-5_879_610, 6, 22).unwrap(); - assert!(date.sub_years(1).is_err()); - let date = Date::from_ymd(1, 1, 1).unwrap(); - assert_eq!( - "-0001-12-31", - date.sub_days(1).unwrap().format("yyyy-MM-dd") - ); + assert_eq!("-0001-12-31", date.sub_days(1).format("yyyy-MM-dd")); - assert_eq!( - "-0001-12-01", - date.sub_months(1).unwrap().format("yyyy-MM-dd") - ); + assert_eq!("-0001-12-01", date.sub_months(1).format("yyyy-MM-dd")); - assert_eq!( - "-0001-01-01", - date.sub_years(1).unwrap().format("yyyy-MM-dd") - ); + assert_eq!("-0001-01-01", date.sub_years(1).format("yyyy-MM-dd")); let date = Date::from_ymd(-1, 12, 31).unwrap(); - assert_eq!( - "-0001-12-30", - date.sub_days(1).unwrap().format("yyyy-MM-dd") - ); + assert_eq!("-0001-12-30", date.sub_days(1).format("yyyy-MM-dd")); - assert_eq!( - "-0001-11-30", - date.sub_months(1).unwrap().format("yyyy-MM-dd") - ); + assert_eq!("-0001-11-30", date.sub_months(1).format("yyyy-MM-dd")); - assert_eq!( - "-0002-12-31", - date.sub_years(1).unwrap().format("yyyy-MM-dd") - ); + assert_eq!("-0002-12-31", date.sub_years(1).format("yyyy-MM-dd")); let date = Date::from_ymd(1, 1, 1).unwrap(); - assert_eq!("0001-01-02", date.add_days(1).unwrap().format("yyyy-MM-dd")); + assert_eq!("0001-01-02", date.add_days(1).format("yyyy-MM-dd")); - assert_eq!( - "0001-02-01", - date.add_months(1).unwrap().format("yyyy-MM-dd") - ); + assert_eq!("0001-02-01", date.add_months(1).format("yyyy-MM-dd")); - assert_eq!( - "0002-01-01", - date.add_years(1).unwrap().format("yyyy-MM-dd") - ); + assert_eq!("0002-01-01", date.add_years(1).format("yyyy-MM-dd")); let date = Date::from_ymd(-1, 12, 31).unwrap(); - assert_eq!("0001-01-01", date.add_days(1).unwrap().format("yyyy-MM-dd")); + assert_eq!("0001-01-01", date.add_days(1).format("yyyy-MM-dd")); - assert_eq!( - "0001-01-31", - date.add_months(1).unwrap().format("yyyy-MM-dd") - ); + assert_eq!("0001-01-31", date.add_months(1).format("yyyy-MM-dd")); - assert_eq!( - "0001-12-31", - date.add_years(1).unwrap().format("yyyy-MM-dd") - ); + assert_eq!("0001-12-31", date.add_years(1).format("yyyy-MM-dd")); let date = Date::from_ymd(2020, 2, 29).unwrap(); - assert_eq!( - "2021-02-28", - date.add_years(1).unwrap().format("yyyy-MM-dd") - ); - assert_eq!( - "2019-02-28", - date.sub_years(1).unwrap().format("yyyy-MM-dd") - ); + assert_eq!("2021-02-28", date.add_years(1).format("yyyy-MM-dd")); + assert_eq!("2019-02-28", date.sub_years(1).format("yyyy-MM-dd")); let date = Date::from_ymd(1, 1, 1).unwrap(); - assert_eq!( - "0001-02-01", - date.add_months(1).unwrap().format("yyyy-MM-dd") - ); + assert_eq!("0001-02-01", date.add_months(1).format("yyyy-MM-dd")); + } + + #[test] + #[should_panic] + fn add_overflow_days() { + let date = Date::from_ymd(5_879_611, 7, 12).unwrap(); + date.add_days(1); + } + + #[test] + #[should_panic] + fn add_overflow_months() { + let date = Date::from_ymd(5_879_611, 6, 13).unwrap(); + date.add_months(1); + } + + #[test] + #[should_panic] + fn add_overflow_years() { + let date = Date::from_ymd(5_879_610, 7, 13).unwrap(); + date.add_years(1); + } + + #[test] + #[should_panic] + fn sub_underflow_days() { + let date = Date::from_ymd(-5_879_611, 6, 23).unwrap(); + date.sub_days(1); + } + + #[test] + #[should_panic] + fn sub_underflow_months() { + let date = Date::from_ymd(-5_879_611, 7, 22).unwrap(); + date.sub_months(1); + } + + #[test] + #[should_panic] + fn sub_underflow_years() { + let date = Date::from_ymd(-5_879_610, 6, 22).unwrap(); + date.sub_years(1); } #[test] diff --git a/tests/datetime.rs b/tests/datetime.rs index 2490089..acb7204 100644 --- a/tests/datetime.rs +++ b/tests/datetime.rs @@ -3,14 +3,14 @@ mod datetime_tests { use std::time::Duration; use astrolabe::{ - Date, DateTime, DateUtilities, OffsetUtilities, Precision, Time, TimeUtilities, + Date, DateTime, DateUtilities, Offset, OffsetUtilities, Precision, Time, TimeUtilities, }; #[test] fn debug() { let date_time = DateTime::default(); assert_eq!( - "DateTime { days: 0, nanoseconds: 0, offset: 0 }", + "DateTime { days: 0, nanoseconds: 0, offset: Fixed(0) }", format!("{:?}", date_time) ); } @@ -54,7 +54,7 @@ mod datetime_tests { #[test] fn ord() { let date_time = DateTime::default(); - let date_time_2 = date_time.add_days(1).unwrap(); + let date_time_2 = date_time.add_days(1); assert!(date_time < date_time_2); assert_eq!(std::cmp::Ordering::Less, date_time.cmp(&date_time_2)); } @@ -62,6 +62,7 @@ mod datetime_tests { #[test] fn now() { assert!(2021 < DateTime::now().year()); + assert!(2021 < DateTime::now_local().year()); } #[test] @@ -271,41 +272,40 @@ mod datetime_tests { #[test] fn timestamp() { - assert_eq!(0, DateTime::from_timestamp(0).unwrap().timestamp()); + assert_eq!(0, DateTime::from_timestamp(0).timestamp()); assert_eq!( 185_480_451_590_399, - DateTime::from_timestamp(185_480_451_590_399) - .unwrap() - .timestamp() + DateTime::from_timestamp(185_480_451_590_399).timestamp() ); assert_eq!( "5879611/07/12 23:59:59", - DateTime::from_timestamp(185_480_451_590_399) - .unwrap() - .format("yyyy/MM/dd HH:mm:ss") + DateTime::from_timestamp(185_480_451_590_399).format("yyyy/MM/dd HH:mm:ss") ); assert_eq!( -185_604_722_784_000, - DateTime::from_timestamp(-185_604_722_784_000) - .unwrap() - .timestamp() + DateTime::from_timestamp(-185_604_722_784_000).timestamp() ); assert_eq!( "-5879611/06/23 00:00:00", - DateTime::from_timestamp(-185_604_722_784_000) - .unwrap() - .format("yyyy/MM/dd HH:mm:ss") + DateTime::from_timestamp(-185_604_722_784_000).format("yyyy/MM/dd HH:mm:ss") ); assert_eq!( "-5879611/06/23 00:00:01", - DateTime::from_timestamp(-185_604_722_783_999) - .unwrap() - .format("yyyy/MM/dd HH:mm:ss") + DateTime::from_timestamp(-185_604_722_783_999).format("yyyy/MM/dd HH:mm:ss") ); + } - assert!(DateTime::from_timestamp(185_480_451_590_400).is_err()); - assert!(DateTime::from_timestamp(-185_604_722_784_001).is_err()); + #[test] + #[should_panic] + fn timestamp_overflow() { + DateTime::from_timestamp(185_480_451_590_400); + } + + #[test] + #[should_panic] + fn timestamp_underflow() { + DateTime::from_timestamp(-185_604_722_784_001); } #[test] @@ -315,7 +315,7 @@ mod datetime_tests { "2022-05-02T15:30:20Z", date_time.format_rfc3339(Precision::Seconds) ); - assert_eq!(0, date_time.get_offset()); + assert_eq!(0, date_time.get_offset().resolve()); assert_eq!(1651505420, date_time.timestamp()); let date_time = DateTime::parse_rfc3339("2022-05-02T15:30:20+12:34").unwrap(); @@ -323,7 +323,7 @@ mod datetime_tests { "2022-05-02T15:30:20+12:34", date_time.format_rfc3339(Precision::Seconds) ); - assert_eq!(45240, date_time.get_offset()); + assert_eq!(45240, date_time.get_offset().resolve()); assert_eq!(1651460180, date_time.timestamp()); let date_time = DateTime::parse_rfc3339("2022-05-02T15:30:20-12:34").unwrap(); @@ -331,7 +331,7 @@ mod datetime_tests { "2022-05-02T15:30:20-12:34", date_time.format_rfc3339(Precision::Seconds) ); - assert_eq!(-45240, date_time.get_offset()); + assert_eq!(-45240, date_time.get_offset().resolve()); assert_eq!(1651550660, date_time.timestamp()); let date_time = DateTime::parse_rfc3339("2022-05-02T15:30:20.1Z").unwrap(); @@ -339,7 +339,7 @@ mod datetime_tests { "2022-05-02T15:30:20Z", date_time.format_rfc3339(Precision::Seconds) ); - assert_eq!(0, date_time.get_offset()); + assert_eq!(0, date_time.get_offset().resolve()); assert_eq!(1651505420, date_time.timestamp()); assert_eq!(100000000, date_time.nano()); @@ -348,7 +348,7 @@ mod datetime_tests { "2022-05-02T15:30:20Z", date_time.format_rfc3339(Precision::Seconds) ); - assert_eq!(0, date_time.get_offset()); + assert_eq!(0, date_time.get_offset().resolve()); assert_eq!(1651505420, date_time.timestamp()); assert_eq!(123456789, date_time.nano()); @@ -357,7 +357,7 @@ mod datetime_tests { "2022-05-02T15:30:20+12:34", date_time.format_rfc3339(Precision::Seconds) ); - assert_eq!(45240, date_time.get_offset()); + assert_eq!(45240, date_time.get_offset().resolve()); assert_eq!(1651460180, date_time.timestamp()); assert_eq!(123456789, date_time.nano()); @@ -437,13 +437,12 @@ mod datetime_tests { let with_offset = DateTime::from_ymdhms(1970, 1, 2, 0, 0, 0) .unwrap() - .set_offset(3660) - .unwrap(); + .set_offset(Offset::from_seconds(3660).unwrap()); assert_eq!( "1970-01-02T01:01:00+01:01", with_offset.format_rfc3339(Precision::Seconds) ); - let with_offset = with_offset.set_offset(-3660).unwrap(); + let with_offset = with_offset.set_offset(Offset::from_seconds(-3660).unwrap()); assert_eq!( "1970-01-01T22:59:00-01:01", with_offset.format_rfc3339(Precision::Seconds) @@ -476,257 +475,111 @@ mod datetime_tests { fn add_sub_date() { let date_time = DateTime::from_ymd(1970, 1, 1).unwrap(); - let modified = date_time.add_days(123).unwrap(); + let modified = date_time.add_days(123); assert_eq!(10627200, modified.timestamp()); - let modified = date_time.add_months(11).unwrap(); + let modified = date_time.add_months(11); assert_eq!("1970-12-01", modified.format("yyyy-MM-dd")); - let modified = date_time.add_months(12).unwrap(); + let modified = date_time.add_months(12); assert_eq!("1971-01-01", modified.format("yyyy-MM-dd")); - let modified = date_time.add_months(14).unwrap(); + let modified = date_time.add_months(14); assert_eq!("1971-03-01", modified.format("yyyy-MM-dd")); // Leap year cases - let modified = date_time.add_days(30).unwrap(); + let modified = date_time.add_days(30); assert_eq!("1970-01-31", modified.format("yyyy-MM-dd")); - let modified = modified.add_months(1).unwrap(); + let modified = modified.add_months(1); assert_eq!("1970-02-28", modified.format("yyyy-MM-dd")); - let modified = modified.add_years(2).unwrap(); + let modified = modified.add_years(2); assert_eq!("1972-02-28", modified.format("yyyy-MM-dd")); - let modified = date_time.add_years(2).unwrap().add_days(30).unwrap(); + let modified = date_time.add_years(2).add_days(30); assert_eq!("1972-01-31", modified.format("yyyy-MM-dd")); - let modified = modified.add_months(1).unwrap(); + let modified = modified.add_months(1); assert_eq!("1972-02-29", modified.format("yyyy-MM-dd")); let date_time = DateTime::from_ymd(1971, 1, 1).unwrap(); - let modified = date_time.sub_months(1).unwrap(); + let modified = date_time.sub_months(1); assert_eq!("1970-12-01", modified.format("yyyy-MM-dd")); let date_time = DateTime::from_ymd(1972, 3, 31).unwrap(); - let modified = date_time.sub_months(1).unwrap(); + let modified = date_time.sub_months(1); assert_eq!("1972-02-29", modified.format("yyyy-MM-dd")); - let modified = modified.sub_months(1).unwrap(); + let modified = modified.sub_months(1); assert_eq!("1972-01-29", modified.format("yyyy-MM-dd")); - let date_time = DateTime::from_ymd(5_879_611, 7, 12).unwrap(); - assert!(date_time.add_days(1).is_err()); - let date_time = DateTime::from_ymd(5_879_611, 6, 13).unwrap(); - assert!(date_time.add_months(1).is_err()); - let date_time = DateTime::from_ymd(5_879_610, 7, 13).unwrap(); - assert!(date_time.add_years(1).is_err()); - - let date_time = DateTime::from_ymd(-5_879_611, 6, 23).unwrap(); - assert!(date_time.sub_days(1).is_err()); - let date_time = DateTime::from_ymd(-5_879_611, 7, 22).unwrap(); - assert!(date_time.sub_months(1).is_err()); - let date_time = DateTime::from_ymd(-5_879_610, 6, 22).unwrap(); - assert!(date_time.sub_years(1).is_err()); - let date_time = DateTime::from_ymd(1, 1, 1).unwrap(); - assert_eq!( - "-0001-12-31", - date_time.sub_days(1).unwrap().format("yyyy-MM-dd") - ); + assert_eq!("-0001-12-31", date_time.sub_days(1).format("yyyy-MM-dd")); - assert_eq!( - "-0001-12-01", - date_time.sub_months(1).unwrap().format("yyyy-MM-dd") - ); + assert_eq!("-0001-12-01", date_time.sub_months(1).format("yyyy-MM-dd")); - assert_eq!( - "-0001-01-01", - date_time.sub_years(1).unwrap().format("yyyy-MM-dd") - ); + assert_eq!("-0001-01-01", date_time.sub_years(1).format("yyyy-MM-dd")); let date_time = DateTime::from_ymd(-1, 12, 31).unwrap(); - assert_eq!( - "-0001-12-30", - date_time.sub_days(1).unwrap().format("yyyy-MM-dd") - ); + assert_eq!("-0001-12-30", date_time.sub_days(1).format("yyyy-MM-dd")); - assert_eq!( - "-0001-11-30", - date_time.sub_months(1).unwrap().format("yyyy-MM-dd") - ); + assert_eq!("-0001-11-30", date_time.sub_months(1).format("yyyy-MM-dd")); - assert_eq!( - "-0002-12-31", - date_time.sub_years(1).unwrap().format("yyyy-MM-dd") - ); + assert_eq!("-0002-12-31", date_time.sub_years(1).format("yyyy-MM-dd")); let date_time = DateTime::from_ymd(1, 1, 1).unwrap(); - assert_eq!( - "0001-01-02", - date_time.add_days(1).unwrap().format("yyyy-MM-dd") - ); + assert_eq!("0001-01-02", date_time.add_days(1).format("yyyy-MM-dd")); - assert_eq!( - "0001-02-01", - date_time.add_months(1).unwrap().format("yyyy-MM-dd") - ); + assert_eq!("0001-02-01", date_time.add_months(1).format("yyyy-MM-dd")); - assert_eq!( - "0002-01-01", - date_time.add_years(1).unwrap().format("yyyy-MM-dd") - ); + assert_eq!("0002-01-01", date_time.add_years(1).format("yyyy-MM-dd")); let date_time = DateTime::from_ymd(-1, 12, 31).unwrap(); - assert_eq!( - "0001-01-01", - date_time.add_days(1).unwrap().format("yyyy-MM-dd") - ); + assert_eq!("0001-01-01", date_time.add_days(1).format("yyyy-MM-dd")); - assert_eq!( - "0001-01-31", - date_time.add_months(1).unwrap().format("yyyy-MM-dd") - ); + assert_eq!("0001-01-31", date_time.add_months(1).format("yyyy-MM-dd")); - assert_eq!( - "0001-12-31", - date_time.add_years(1).unwrap().format("yyyy-MM-dd") - ); + assert_eq!("0001-12-31", date_time.add_years(1).format("yyyy-MM-dd")); let date_time = DateTime::from_ymd(2020, 2, 29).unwrap(); - assert_eq!( - "2021-02-28", - date_time.add_years(1).unwrap().format("yyyy-MM-dd") - ); - assert_eq!( - "2019-02-28", - date_time.sub_years(1).unwrap().format("yyyy-MM-dd") - ); + assert_eq!("2021-02-28", date_time.add_years(1).format("yyyy-MM-dd")); + assert_eq!("2019-02-28", date_time.sub_years(1).format("yyyy-MM-dd")); let date_time = DateTime::from_ymd(1, 1, 1).unwrap(); - assert_eq!( - "0001-02-01", - date_time.add_months(1).unwrap().format("yyyy-MM-dd") - ); + assert_eq!("0001-02-01", date_time.add_months(1).format("yyyy-MM-dd")); } #[test] fn add_sub_time() { - let date_time = DateTime::from_ymd(5_879_611, 7, 12).unwrap(); - assert!(date_time.add_days(1).is_err()); - let date_time = DateTime::from_ymd(5_879_611, 6, 13).unwrap(); - assert!(date_time.add_months(1).is_err()); - let date_time = DateTime::from_ymd(5_879_610, 7, 13).unwrap(); - assert!(date_time.add_years(1).is_err()); - - let date_time = DateTime::from_ymd(-5_879_611, 6, 23).unwrap(); - assert!(date_time.sub_days(1).is_err()); - let date_time = DateTime::from_ymd(-5_879_611, 7, 22).unwrap(); - assert!(date_time.sub_months(1).is_err()); - let date_time = DateTime::from_ymd(-5_879_610, 6, 22).unwrap(); - assert!(date_time.sub_years(1).is_err()); - - let date_time = DateTime::from_ymdhms(5_879_611, 7, 12, 23, 0, 0).unwrap(); - assert!(date_time.add_hours(1).is_err()); - let date_time = DateTime::from_ymdhms(5_879_611, 7, 12, 23, 59, 0).unwrap(); - assert!(date_time.add_minutes(1).is_err()); - let date_time = DateTime::from_ymdhms(5_879_611, 7, 12, 23, 59, 59).unwrap(); - assert!(date_time.add_seconds(1).is_err()); - let date_time = DateTime::from_ymdhms(5_879_611, 7, 12, 23, 59, 59) - .unwrap() - .set_nano(999_000_000) - .unwrap(); - assert!(date_time.add_millis(1).is_err()); - let date_time = DateTime::from_ymdhms(5_879_611, 7, 12, 23, 59, 59) - .unwrap() - .set_nano(999_999_000) - .unwrap(); - - assert!(date_time.add_micros(1).is_err()); - let date_time = DateTime::from_ymdhms(5_879_611, 7, 12, 23, 59, 59) - .unwrap() - .set_nano(999_999_999) - .unwrap(); - assert!(date_time.add_nanos(1).is_err()); - - let date_time = DateTime::from_ymdhms(-5_879_611, 6, 23, 0, 59, 59).unwrap(); - assert!(date_time.sub_hours(1).is_err()); - let date_time = DateTime::from_ymdhms(-5_879_611, 6, 23, 0, 0, 59).unwrap(); - assert!(date_time.sub_minutes(1).is_err()); - let date_time = DateTime::from_ymdhms(-5_879_611, 6, 23, 0, 0, 0).unwrap(); - assert!(date_time.sub_seconds(1).is_err()); - let date_time = DateTime::from_ymdhms(-5_879_611, 6, 23, 0, 0, 0) - .unwrap() - .set_nano(999_999) - .unwrap(); - assert!(date_time.sub_millis(1).is_err()); - let date_time = DateTime::from_ymdhms(-5_879_611, 6, 23, 0, 0, 0) - .unwrap() - .set_nano(999) - .unwrap(); - - assert!(date_time.sub_micros(1).is_err()); - let date_time = DateTime::from_ymdhms(-5_879_611, 6, 23, 0, 0, 0) - .unwrap() - .set_nano(0) - .unwrap(); - assert!(date_time.sub_nanos(1).is_err()); - let date_time = DateTime::from_ymd(1, 1, 1).unwrap(); assert_eq!( "-0001-12-31T23:59:59.999999999Z", - date_time - .sub_nanos(1) - .unwrap() - .format_rfc3339(Precision::Nanos) + date_time.sub_nanos(1).format_rfc3339(Precision::Nanos) ); assert_eq!( "-0001-12-31T23:59:59.999999000Z", - date_time - .sub_micros(1) - .unwrap() - .format_rfc3339(Precision::Nanos) + date_time.sub_micros(1).format_rfc3339(Precision::Nanos) ); assert_eq!( "-0001-12-31T23:59:59.999000000Z", - date_time - .sub_millis(1) - .unwrap() - .format_rfc3339(Precision::Nanos) + date_time.sub_millis(1).format_rfc3339(Precision::Nanos) ); assert_eq!( "-0001-12-31T23:59:59.000000000Z", - date_time - .sub_seconds(1) - .unwrap() - .format_rfc3339(Precision::Nanos) + date_time.sub_seconds(1).format_rfc3339(Precision::Nanos) ); assert_eq!( "-0001-12-31T23:59:00.000000000Z", - date_time - .sub_minutes(1) - .unwrap() - .format_rfc3339(Precision::Nanos) + date_time.sub_minutes(1).format_rfc3339(Precision::Nanos) ); assert_eq!( "-0001-12-31T23:00:00.000000000Z", - date_time - .sub_hours(1) - .unwrap() - .format_rfc3339(Precision::Nanos) + date_time.sub_hours(1).format_rfc3339(Precision::Nanos) ); assert_eq!( "-0001-12-31T00:00:00.000000000Z", - date_time - .sub_days(1) - .unwrap() - .format_rfc3339(Precision::Nanos) + date_time.sub_days(1).format_rfc3339(Precision::Nanos) ); assert_eq!( "-0001-12-01T00:00:00.000000000Z", - date_time - .sub_months(1) - .unwrap() - .format_rfc3339(Precision::Nanos) + date_time.sub_months(1).format_rfc3339(Precision::Nanos) ); assert_eq!( "-0001-01-01T00:00:00.000000000Z", - date_time - .sub_years(1) - .unwrap() - .format_rfc3339(Precision::Nanos) + date_time.sub_years(1).format_rfc3339(Precision::Nanos) ); let date_time = DateTime::from_ymdhms(-1, 12, 31, 23, 59, 59) @@ -735,69 +588,186 @@ mod datetime_tests { .unwrap(); assert_eq!( "0001-01-01T00:00:00.000000000Z", - date_time - .add_nanos(1) - .unwrap() - .format_rfc3339(Precision::Nanos) + date_time.add_nanos(1).format_rfc3339(Precision::Nanos) ); assert_eq!( "0001-01-01T00:00:00.000000999Z", - date_time - .add_micros(1) - .unwrap() - .format_rfc3339(Precision::Nanos) + date_time.add_micros(1).format_rfc3339(Precision::Nanos) ); assert_eq!( "0001-01-01T00:00:00.000999999Z", - date_time - .add_millis(1) - .unwrap() - .format_rfc3339(Precision::Nanos) + date_time.add_millis(1).format_rfc3339(Precision::Nanos) ); assert_eq!( "0001-01-01T00:00:00.999999999Z", - date_time - .add_seconds(1) - .unwrap() - .format_rfc3339(Precision::Nanos) + date_time.add_seconds(1).format_rfc3339(Precision::Nanos) ); assert_eq!( "0001-01-01T00:00:59.999999999Z", - date_time - .add_minutes(1) - .unwrap() - .format_rfc3339(Precision::Nanos) + date_time.add_minutes(1).format_rfc3339(Precision::Nanos) ); assert_eq!( "0001-01-01T00:59:59.999999999Z", - date_time - .add_hours(1) - .unwrap() - .format_rfc3339(Precision::Nanos) + date_time.add_hours(1).format_rfc3339(Precision::Nanos) ); assert_eq!( "0001-01-01T23:59:59.999999999Z", - date_time - .add_days(1) - .unwrap() - .format_rfc3339(Precision::Nanos) + date_time.add_days(1).format_rfc3339(Precision::Nanos) ); assert_eq!( "0001-01-31T23:59:59.999999999Z", - date_time - .add_months(1) - .unwrap() - .format_rfc3339(Precision::Nanos) + date_time.add_months(1).format_rfc3339(Precision::Nanos) ); assert_eq!( "0001-12-31T23:59:59.999999999Z", - date_time - .add_years(1) - .unwrap() - .format_rfc3339(Precision::Nanos) + date_time.add_years(1).format_rfc3339(Precision::Nanos) ); } + #[test] + #[should_panic] + fn add_overflow_days() { + let date_time = DateTime::from_ymd(5_879_611, 7, 12).unwrap(); + date_time.add_days(1); + } + + #[test] + #[should_panic] + fn add_overflow_months() { + let date_time = DateTime::from_ymd(5_879_611, 6, 13).unwrap(); + date_time.add_months(1); + } + + #[test] + #[should_panic] + fn add_overflow_years() { + let date_time = DateTime::from_ymd(5_879_610, 7, 13).unwrap(); + date_time.add_years(1); + } + + #[test] + #[should_panic] + fn sub_overflow_days() { + let date_time = DateTime::from_ymd(-5_879_611, 6, 23).unwrap(); + date_time.sub_days(1); + } + + #[test] + #[should_panic] + fn sub_overflow_months() { + let date_time = DateTime::from_ymd(-5_879_611, 7, 22).unwrap(); + date_time.sub_months(1); + } + + #[test] + #[should_panic] + fn sub_overflow_years() { + let date_time = DateTime::from_ymd(-5_879_610, 6, 22).unwrap(); + date_time.sub_years(1); + } + + #[test] + #[should_panic] + fn add_overflow_hours() { + let date_time = DateTime::from_ymdhms(5_879_611, 7, 12, 23, 0, 0).unwrap(); + date_time.add_hours(1); + } + + #[test] + #[should_panic] + fn add_overflow_minutes() { + let date_time = DateTime::from_ymdhms(5_879_611, 7, 12, 23, 59, 0).unwrap(); + date_time.add_minutes(1); + } + + #[test] + #[should_panic] + fn add_overflow_seconds() { + let date_time = DateTime::from_ymdhms(5_879_611, 7, 12, 23, 59, 59).unwrap(); + date_time.add_seconds(1); + } + + #[test] + #[should_panic] + fn add_overflow_millis() { + let date_time = DateTime::from_ymdhms(5_879_611, 7, 12, 23, 59, 59) + .unwrap() + .set_nano(999_000_000) + .unwrap(); + date_time.add_millis(1); + } + + #[test] + #[should_panic] + fn add_overflow_micros() { + let date_time = DateTime::from_ymdhms(5_879_611, 7, 12, 23, 59, 59) + .unwrap() + .set_nano(999_999_000) + .unwrap(); + date_time.add_micros(1); + } + + #[test] + #[should_panic] + fn add_overflow_nanos() { + let date_time = DateTime::from_ymdhms(5_879_611, 7, 12, 23, 59, 59) + .unwrap() + .set_nano(999_999_999) + .unwrap(); + date_time.add_nanos(1); + } + + #[test] + #[should_panic] + fn sub_underflow_hours() { + let date_time = DateTime::from_ymdhms(-5_879_611, 6, 23, 0, 59, 59).unwrap(); + date_time.sub_hours(1); + } + + #[test] + #[should_panic] + fn sub_underflow_minutes() { + let date_time = DateTime::from_ymdhms(-5_879_611, 6, 23, 0, 0, 59).unwrap(); + date_time.sub_minutes(1); + } + + #[test] + #[should_panic] + fn sub_underflow_seconds() { + let date_time = DateTime::from_ymdhms(-5_879_611, 6, 23, 0, 0, 0).unwrap(); + date_time.sub_seconds(1); + } + + #[test] + #[should_panic] + fn sub_underflow_millis() { + let date_time = DateTime::from_ymdhms(-5_879_611, 6, 23, 0, 0, 0) + .unwrap() + .set_nano(999_999) + .unwrap(); + date_time.sub_millis(1); + } + + #[test] + #[should_panic] + fn sub_underflow_micros() { + let date_time = DateTime::from_ymdhms(-5_879_611, 6, 23, 0, 0, 0) + .unwrap() + .set_nano(999) + .unwrap(); + date_time.sub_micros(1); + } + + #[test] + #[should_panic] + fn sub_underflow_nanos() { + let date_time = DateTime::from_ymdhms(-5_879_611, 6, 23, 0, 0, 0) + .unwrap() + .set_nano(0) + .unwrap(); + date_time.sub_nanos(1); + } + #[test] fn set() { let date_time = DateTime::from_ymdhms(2000, 5, 10, 12, 32, 1) diff --git a/tests/format.rs b/tests/format.rs index 01bc61e..a229d07 100644 --- a/tests/format.rs +++ b/tests/format.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod format_tests { - use astrolabe::{Date, DateTime, OffsetUtilities, Time}; + use astrolabe::{Date, DateTime, Offset, OffsetUtilities, Time}; #[test] fn era() { @@ -508,7 +508,9 @@ mod format_tests { assert_eq!("00:00:00 +00:00", time.format("HH:mm:ss xxxxx")); assert_eq!("00:00:00 +00:00", time.format("HH:mm:ss xxxxxx")); - let time = Time::from_hms(0, 0, 0).unwrap().set_offset(-3661).unwrap(); + let time = Time::from_hms(0, 0, 0) + .unwrap() + .set_offset(Offset::from_seconds(-3661).unwrap()); assert_eq!("22:58:59 -0101", time.format("HH:mm:ss X")); assert_eq!("22:58:59 -0101", time.format("HH:mm:ss XX")); assert_eq!("22:58:59 -01:01", time.format("HH:mm:ss XXX")); @@ -522,7 +524,9 @@ mod format_tests { assert_eq!("22:58:59 -01:01:01", time.format("HH:mm:ss xxxxx")); assert_eq!("22:58:59 -01:01", time.format("HH:mm:ss xxxxxx")); - let time = Time::from_hms(0, 0, 0).unwrap().set_offset(3661).unwrap(); + let time = Time::from_hms(0, 0, 0) + .unwrap() + .set_offset(Offset::from_seconds(3661).unwrap()); assert_eq!("01:01:01 +0101", time.format("HH:mm:ss X")); assert_eq!("01:01:01 +0101", time.format("HH:mm:ss XX")); assert_eq!("01:01:01 +01:01", time.format("HH:mm:ss XXX")); diff --git a/tests/offset.rs b/tests/offset.rs index b78e74d..4fcd976 100644 --- a/tests/offset.rs +++ b/tests/offset.rs @@ -1,160 +1,153 @@ #[cfg(test)] mod offset_tests { - use astrolabe::{DateTime, DateUtilities, OffsetUtilities, Time, TimeUtilities}; + use astrolabe::{DateTime, DateUtilities, Offset, OffsetUtilities, Time, TimeUtilities}; + + #[test] + fn offset() { + assert!(Offset::from_hms(0, 0, 0).is_ok()); + assert!(Offset::from_hms(0, 0, 59).is_ok()); + assert!(Offset::from_hms(0, 59, 59).is_ok()); + assert!(Offset::from_hms(23, 59, 59).is_ok()); + assert!(Offset::from_hms(24, 59, 59).is_err()); + assert!(Offset::from_hms(23, 60, 59).is_err()); + assert!(Offset::from_hms(23, 59, 60).is_err()); + + assert!(Offset::from_hms(-23, 0, 0).is_ok()); + assert!(Offset::from_hms(-23, 59, 59).is_ok()); + assert!(Offset::from_hms(-24, 59, 59).is_err()); + assert!(Offset::from_hms(-23, 60, 59).is_err()); + assert!(Offset::from_hms(-23, 59, 60).is_err()); + + assert!(Offset::from_seconds(0).is_ok()); + assert!(Offset::from_seconds(-86_399).is_ok()); + assert!(Offset::from_seconds(86_399).is_ok()); + assert!(Offset::from_seconds(-86_400).is_err()); + assert!(Offset::from_seconds(86_400).is_err()); + } #[test] fn set_offset_hms() { let time = Time::from_hms(0, 0, 0).unwrap(); - assert_eq!(0, time.get_offset()); - assert_eq!((0, 0, 0), time.get_offset_hms()); - let time = time.set_offset_hms(1, 0, 0).unwrap(); - assert_eq!(3600, time.get_offset()); - assert_eq!((1, 0, 0), time.get_offset_hms()); - let time = time.set_offset_hms(-1, 0, 0).unwrap(); - assert_eq!(-3600, time.get_offset()); - assert_eq!((-1, 0, 0), time.get_offset_hms()); - let time = time.set_offset_hms(23, 59, 59).unwrap(); - assert_eq!(86399, time.get_offset()); - assert_eq!((23, 59, 59), time.get_offset_hms()); - let time = time.set_offset_hms(-23, 59, 59).unwrap(); - assert_eq!(-86399, time.get_offset()); - assert_eq!((-23, 59, 59), time.get_offset_hms()); - - assert!(time.set_offset_hms(24, 0, 0).is_err()); - assert!(time.set_offset_hms(-24, 0, 0).is_err()); + assert_eq!(0, time.get_offset().resolve()); + assert_eq!((0, 0, 0), time.get_offset().resolve_hms()); + let time = time.set_offset(Offset::from_hms(1, 0, 0).unwrap()); + assert_eq!(3600, time.get_offset().resolve()); + assert_eq!((1, 0, 0), time.get_offset().resolve_hms()); + let time = time.set_offset(Offset::from_hms(-1, 0, 0).unwrap()); + assert_eq!(-3600, time.get_offset().resolve()); + assert_eq!((-1, 0, 0), time.get_offset().resolve_hms()); + let time = time.set_offset(Offset::from_hms(23, 59, 59).unwrap()); + assert_eq!(86399, time.get_offset().resolve()); + assert_eq!((23, 59, 59), time.get_offset().resolve_hms()); + let time = time.set_offset(Offset::from_hms(-23, 59, 59).unwrap()); + assert_eq!(-86399, time.get_offset().resolve()); + assert_eq!((-23, 59, 59), time.get_offset().resolve_hms()); let date_time = DateTime::from_hms(0, 0, 0).unwrap(); - assert_eq!(0, date_time.get_offset()); - assert_eq!((0, 0, 0), date_time.get_offset_hms()); - let date_time = date_time.set_offset_hms(1, 0, 0).unwrap(); - assert_eq!(3600, date_time.get_offset()); - assert_eq!((1, 0, 0), date_time.get_offset_hms()); - let date_time = date_time.set_offset_hms(-1, 0, 0).unwrap(); - assert_eq!(-3600, date_time.get_offset()); - assert_eq!((-1, 0, 0), date_time.get_offset_hms()); - let date_time = date_time.set_offset_hms(23, 59, 59).unwrap(); - assert_eq!(86399, date_time.get_offset()); - assert_eq!((23, 59, 59), date_time.get_offset_hms()); - let date_time = date_time.set_offset_hms(-23, 59, 59).unwrap(); - assert_eq!(-86399, date_time.get_offset()); - assert_eq!((-23, 59, 59), date_time.get_offset_hms()); - - assert!(date_time.set_offset_hms(24, 0, 0).is_err()); - assert!(date_time.set_offset_hms(-24, 0, 0).is_err()); - - let date_time = DateTime::from_ymdhms(5_879_611, 7, 12, 23, 0, 0).unwrap(); - assert!(date_time.set_offset_hms(0, 59, 59).is_ok()); - assert!(date_time.set_offset_hms(1, 0, 0).is_err()); - let date_time = DateTime::from_ymdhms(-5_879_611, 6, 23, 1, 0, 0).unwrap(); - assert!(date_time.set_offset_hms(-1, 0, 0).is_ok()); - assert!(date_time.set_offset_hms(-1, 0, 1).is_err()); + assert_eq!(0, date_time.get_offset().resolve()); + assert_eq!((0, 0, 0), date_time.get_offset().resolve_hms()); + let date_time = date_time.set_offset(Offset::from_hms(1, 0, 0).unwrap()); + assert_eq!(3600, date_time.get_offset().resolve()); + assert_eq!((1, 0, 0), date_time.get_offset().resolve_hms()); + let date_time = date_time.set_offset(Offset::from_hms(-1, 0, 0).unwrap()); + assert_eq!(-3600, date_time.get_offset().resolve()); + assert_eq!((-1, 0, 0), date_time.get_offset().resolve_hms()); + let date_time = date_time.set_offset(Offset::from_hms(23, 59, 59).unwrap()); + assert_eq!(86399, date_time.get_offset().resolve()); + assert_eq!((23, 59, 59), date_time.get_offset().resolve_hms()); + let date_time = date_time.set_offset(Offset::from_hms(-23, 59, 59).unwrap()); + assert_eq!(-86399, date_time.get_offset().resolve()); + assert_eq!((-23, 59, 59), date_time.get_offset().resolve_hms()); } #[test] fn set_offset() { let time = Time::from_hms(0, 0, 0).unwrap(); - assert_eq!(0, time.get_offset()); - let time = time.set_offset(3600).unwrap(); - assert_eq!(3600, time.get_offset()); - let time = time.set_offset(-3600).unwrap(); - assert_eq!(-3600, time.get_offset()); - let time = time.set_offset(86399).unwrap(); - assert_eq!(86399, time.get_offset()); - let time = time.set_offset(-86399).unwrap(); - assert_eq!(-86399, time.get_offset()); - - assert!(time.set_offset(86400).is_err()); - assert!(time.set_offset(-86400).is_err()); + assert_eq!(0, time.get_offset().resolve()); + let time = time.set_offset(Offset::from_seconds(3600).unwrap()); + assert_eq!(3600, time.get_offset().resolve()); + let time = time.set_offset(Offset::from_seconds(-3600).unwrap()); + assert_eq!(-3600, time.get_offset().resolve()); + let time = time.set_offset(Offset::from_seconds(86399).unwrap()); + assert_eq!(86399, time.get_offset().resolve()); + let time = time.set_offset(Offset::from_seconds(-86399).unwrap()); + assert_eq!(-86399, time.get_offset().resolve()); + + assert!(Offset::from_seconds(86400).is_err()); + assert!(Offset::from_seconds(-86400).is_err()); let date_time = DateTime::from_hms(0, 0, 0).unwrap(); - assert_eq!(0, date_time.get_offset()); - let date_time = date_time.set_offset(3600).unwrap(); - assert_eq!(3600, date_time.get_offset()); - let date_time = date_time.set_offset(-3600).unwrap(); - assert_eq!(-3600, date_time.get_offset()); - let date_time = date_time.set_offset(86399).unwrap(); - assert_eq!(86399, date_time.get_offset()); - let date_time = date_time.set_offset(-86399).unwrap(); - assert_eq!(-86399, date_time.get_offset()); - - assert!(date_time.set_offset(86400).is_err()); - assert!(date_time.set_offset(-86400).is_err()); - - let date_time = DateTime::from_ymdhms(5_879_611, 7, 12, 23, 0, 0).unwrap(); - assert!(date_time.set_offset(3599).is_ok()); - assert!(date_time.set_offset(3600).is_err()); - let date_time = DateTime::from_ymdhms(-5_879_611, 6, 23, 1, 0, 0).unwrap(); - assert!(date_time.set_offset(-3600).is_ok()); - assert!(date_time.set_offset(-3601).is_err()); + assert_eq!(0, date_time.get_offset().resolve()); + let date_time = date_time.set_offset(Offset::from_seconds(3600).unwrap()); + assert_eq!(3600, date_time.get_offset().resolve()); + let date_time = date_time.set_offset(Offset::from_seconds(-3600).unwrap()); + assert_eq!(-3600, date_time.get_offset().resolve()); + let date_time = date_time.set_offset(Offset::from_seconds(86399).unwrap()); + assert_eq!(86399, date_time.get_offset().resolve()); + let date_time = date_time.set_offset(Offset::from_seconds(-86399).unwrap()); + assert_eq!(-86399, date_time.get_offset().resolve()); + + assert!(Offset::from_seconds(86400).is_err()); + assert!(Offset::from_seconds(-86400).is_err()); } #[test] fn as_offset_hms() { + assert!(Offset::from_hms(24, 0, 0).is_err()); + assert!(Offset::from_hms(-24, 0, 0).is_err()); + let time = Time::from_hms(0, 0, 0).unwrap(); - assert_eq!(0, time.get_offset()); - let time = time.as_offset_hms(1, 0, 0).unwrap(); - assert_eq!(3600, time.get_offset()); - let time = time.as_offset_hms(-1, 0, 0).unwrap(); - assert_eq!(-3600, time.get_offset()); - let time = time.as_offset_hms(23, 59, 59).unwrap(); - assert_eq!(86399, time.get_offset()); - let time = time.as_offset_hms(-23, 59, 59).unwrap(); - assert_eq!(-86399, time.get_offset()); - - assert!(time.as_offset_hms(24, 0, 0).is_err()); - assert!(time.as_offset_hms(-24, 0, 0).is_err()); + assert_eq!(0, time.get_offset().resolve()); + let time = time.as_offset(Offset::from_hms(1, 0, 0).unwrap()); + assert_eq!(3600, time.get_offset().resolve()); + let time = time.as_offset(Offset::from_hms(-1, 0, 0).unwrap()); + assert_eq!(-3600, time.get_offset().resolve()); + let time = time.as_offset(Offset::from_hms(23, 59, 59).unwrap()); + assert_eq!(86399, time.get_offset().resolve()); + let time = time.as_offset(Offset::from_hms(-23, 59, 59).unwrap()); + assert_eq!(-86399, time.get_offset().resolve()); let date_time = DateTime::from_hms(0, 0, 0).unwrap(); - assert_eq!(0, date_time.get_offset()); - let date_time = date_time.as_offset_hms(1, 0, 0).unwrap(); - assert_eq!(3600, date_time.get_offset()); - let date_time = date_time.as_offset_hms(-1, 0, 0).unwrap(); - assert_eq!(-3600, date_time.get_offset()); - let date_time = date_time.as_offset_hms(23, 59, 59).unwrap(); - assert_eq!(86399, date_time.get_offset()); - let date_time = date_time.as_offset_hms(-23, 59, 59).unwrap(); - assert_eq!(-86399, date_time.get_offset()); - - assert!(date_time.as_offset_hms(24, 0, 0).is_err()); - assert!(date_time.as_offset_hms(-24, 0, 0).is_err()); - - let date_time = DateTime::from_ymdhms(5_879_611, 7, 12, 23, 0, 0).unwrap(); - assert!(date_time.as_offset_hms(-0, 59, 59).is_ok()); - assert!(date_time.as_offset_hms(-1, 0, 0).is_err()); - let date_time = DateTime::from_ymdhms(-5_879_611, 6, 23, 1, 0, 0).unwrap(); - assert!(date_time.as_offset_hms(1, 0, 0).is_ok()); - assert!(date_time.as_offset_hms(1, 0, 1).is_err()); + assert_eq!(0, date_time.get_offset().resolve()); + let date_time = date_time.as_offset(Offset::from_hms(1, 0, 0).unwrap()); + assert_eq!(3600, date_time.get_offset().resolve()); + let date_time = date_time.as_offset(Offset::from_hms(-1, 0, 0).unwrap()); + assert_eq!(-3600, date_time.get_offset().resolve()); + let date_time = date_time.as_offset(Offset::from_hms(23, 59, 59).unwrap()); + assert_eq!(86399, date_time.get_offset().resolve()); + let date_time = date_time.as_offset(Offset::from_hms(-23, 59, 59).unwrap()); + assert_eq!(-86399, date_time.get_offset().resolve()); } #[test] fn as_offset() { let time = Time::from_hms(0, 0, 0).unwrap(); - assert_eq!(0, time.get_offset()); - let time = time.as_offset(3600).unwrap(); - assert_eq!(3600, time.get_offset()); - let time = time.as_offset(-3600).unwrap(); - assert_eq!(-3600, time.get_offset()); - let time = time.as_offset(86399).unwrap(); - assert_eq!(86399, time.get_offset()); - let time = time.as_offset(-86399).unwrap(); - assert_eq!(-86399, time.get_offset()); - - assert!(time.as_offset(86400).is_err()); - assert!(time.as_offset(-86400).is_err()); + assert_eq!(0, time.get_offset().resolve()); + let time = time.as_offset(Offset::from_seconds(3600).unwrap()); + assert_eq!(3600, time.get_offset().resolve()); + let time = time.as_offset(Offset::from_seconds(-3600).unwrap()); + assert_eq!(-3600, time.get_offset().resolve()); + let time = time.as_offset(Offset::from_seconds(86399).unwrap()); + assert_eq!(86399, time.get_offset().resolve()); + let time = time.as_offset(Offset::from_seconds(-86399).unwrap()); + assert_eq!(-86399, time.get_offset().resolve()); + + assert!(Offset::from_seconds(86400).is_err()); + assert!(Offset::from_seconds(-86400).is_err()); let date_time = DateTime::from_hms(0, 0, 0).unwrap(); - assert_eq!(0, date_time.get_offset()); - let date_time = date_time.as_offset(3600).unwrap(); - assert_eq!(3600, date_time.get_offset()); - let date_time = date_time.as_offset(-3600).unwrap(); - assert_eq!(-3600, date_time.get_offset()); - let date_time = date_time.as_offset(86399).unwrap(); - assert_eq!(86399, date_time.get_offset()); - let date_time = date_time.as_offset(-86399).unwrap(); - assert_eq!(-86399, date_time.get_offset()); - - assert!(date_time.as_offset(86400).is_err()); - assert!(date_time.as_offset(-86400).is_err()); + assert_eq!(0, date_time.get_offset().resolve()); + let date_time = date_time.as_offset(Offset::from_seconds(3600).unwrap()); + assert_eq!(3600, date_time.get_offset().resolve()); + let date_time = date_time.as_offset(Offset::from_seconds(-3600).unwrap()); + assert_eq!(-3600, date_time.get_offset().resolve()); + let date_time = date_time.as_offset(Offset::from_seconds(86399).unwrap()); + assert_eq!(86399, date_time.get_offset().resolve()); + let date_time = date_time.as_offset(Offset::from_seconds(-86399).unwrap()); + assert_eq!(-86399, date_time.get_offset().resolve()); + + assert!(Offset::from_seconds(86400).is_err()); + assert!(Offset::from_seconds(-86400).is_err()); } #[test] @@ -163,11 +156,11 @@ mod offset_tests { assert_eq!(0, time.hour()); assert_eq!(0, time.minute()); assert_eq!(0, time.second()); - let time = time.set_offset(3661).unwrap(); + let time = time.set_offset(Offset::from_seconds(3661).unwrap()); assert_eq!(1, time.hour()); assert_eq!(1, time.minute()); assert_eq!(1, time.second()); - let time = time.set_offset(-3661).unwrap(); + let time = time.set_offset(Offset::from_seconds(-3661).unwrap()); assert_eq!(22, time.hour()); assert_eq!(58, time.minute()); assert_eq!(59, time.second()); @@ -176,41 +169,38 @@ mod offset_tests { assert_eq!(0, time.hour()); assert_eq!(0, time.minute()); assert_eq!(0, time.second()); - let time = time.set_offset(3661).unwrap(); + let time = time.set_offset(Offset::from_seconds(3661).unwrap()); assert_eq!(1, time.hour()); assert_eq!(1, time.minute()); assert_eq!(1, time.second()); - let time = time.set_offset(-3661).unwrap(); + let time = time.set_offset(Offset::from_seconds(-3661).unwrap()); assert_eq!(22, time.hour()); assert_eq!(58, time.minute()); assert_eq!(59, time.second()); - let date_time = DateTime::from_ymdhms(5_879_611, 7, 12, 23, 0, 0).unwrap(); - assert!(date_time.as_offset(-3599).is_ok()); - assert!(date_time.as_offset(-3600).is_err()); - let date_time = DateTime::from_ymdhms(-5_879_611, 6, 23, 1, 0, 0).unwrap(); - assert!(date_time.as_offset(3600).is_ok()); - assert!(date_time.as_offset(3601).is_err()); + let date_time = DateTime::now().set_offset(Offset::Local); + assert_eq!(Offset::Local, date_time.get_offset()); + date_time.get_offset().resolve(); } #[test] fn apply() { - let time = Time::from_hms(0, 0, 0).unwrap().set_offset(3661).unwrap(); - assert_eq!(3661, time.add_hours(1).unwrap().get_offset()); + let time = Time::from_hms(0, 0, 0) + .unwrap() + .set_offset(Offset::from_seconds(3661).unwrap()); + assert_eq!(3661, time.add_hours(1).get_offset().resolve()); let time = DateTime::from_hms(0, 0, 0) .unwrap() - .set_offset(3661) - .unwrap(); - assert_eq!(3661, time.add_years(1).unwrap().get_offset()); - assert_eq!(3661, time.add_hours(1).unwrap().get_offset()); + .set_offset(Offset::from_seconds(3661).unwrap()); + assert_eq!(3661, time.add_years(1).get_offset().resolve()); + assert_eq!(3661, time.add_hours(1).get_offset().resolve()); } #[test] fn set_get() { let date_time = DateTime::from_ymdhms(1970, 1, 2, 0, 0, 0) .unwrap() - .set_offset(-3661) - .unwrap(); + .set_offset(Offset::from_seconds(-3661).unwrap()); assert_eq!(22, date_time.hour()); assert_eq!(1, date_time.day()); let date_time = date_time.set_hour(5).unwrap(); @@ -243,8 +233,7 @@ mod offset_tests { let date_time = DateTime::from_ymdhms(1970, 1, 2, 0, 0, 0) .unwrap() - .set_offset(3600) - .unwrap(); + .set_offset(Offset::from_seconds(3600).unwrap()); assert_eq!(1, date_time.hour()); assert_eq!(2, date_time.day()); let date_time = date_time.set_hour(5).unwrap(); @@ -277,8 +266,7 @@ mod offset_tests { let date_time = DateTime::from_ymdhms(1970, 1, 2, 0, 0, 0) .unwrap() - .set_offset(82800) - .unwrap(); + .set_offset(Offset::from_seconds(82800).unwrap()); assert_eq!(23, date_time.hour()); assert_eq!(2, date_time.day()); let date_time = date_time.set_hour(5).unwrap(); @@ -311,8 +299,7 @@ mod offset_tests { let date_time = DateTime::from_ymdhms(1970, 1, 2, 0, 0, 0) .unwrap() - .set_offset(-82800) - .unwrap(); + .set_offset(Offset::from_seconds(-82800).unwrap()); assert_eq!(1, date_time.hour()); assert_eq!(1, date_time.day()); let date_time = date_time.set_hour(5).unwrap(); @@ -345,8 +332,7 @@ mod offset_tests { let date_time = DateTime::from_ymdhms(1970, 1, 2, 0, 0, 0) .unwrap() - .set_offset(-43200) - .unwrap(); + .set_offset(Offset::from_seconds(-43200).unwrap()); assert_eq!(12, date_time.hour()); assert_eq!(1, date_time.day()); let date_time = date_time.set_hour(5).unwrap(); @@ -380,22 +366,51 @@ mod offset_tests { #[test] fn format() { - let time = Time::from_hms(1, 1, 1).unwrap().set_offset(3661).unwrap(); + let time = Time::from_hms(1, 1, 1) + .unwrap() + .set_offset(Offset::from_seconds(3661).unwrap()); assert_eq!("02:02:02", time.format("HH:mm:ss")); - let time = Time::from_hms(0, 0, 0).unwrap().set_offset(-3661).unwrap(); + let time = Time::from_hms(0, 0, 0) + .unwrap() + .set_offset(Offset::from_seconds(-3661).unwrap()); assert_eq!("22:58:59", time.format("HH:mm:ss")); let date_time = DateTime::from_hms(1, 1, 1) .unwrap() - .set_offset(3661) - .unwrap(); + .set_offset(Offset::from_seconds(3661).unwrap()); assert_eq!("02:02:02", date_time.format("HH:mm:ss")); let date_time = DateTime::from_hms(0, 0, 0) .unwrap() - .set_offset(-3661) - .unwrap(); + .set_offset(Offset::from_seconds(-3661).unwrap()); assert_eq!("22:58:59", date_time.format("HH:mm:ss")); } + + #[test] + fn local() { + let offset = Offset::Local; + offset.resolve(); + } + + #[test] + fn clone() { + let offset = Offset::Local; + #[allow(clippy::clone_on_copy)] + let _ = offset.clone(); + } + + #[test] + #[should_panic] + fn overflow() { + let date_time = DateTime::from_ymdhms(5_879_611, 7, 12, 23, 59, 59).unwrap(); + date_time.set_offset(Offset::Fixed(1)); + } + + #[test] + #[should_panic] + fn underflow() { + let date_time = DateTime::from_ymdhms(-5_879_611, 6, 23, 0, 0, 0).unwrap(); + date_time.set_offset(Offset::Fixed(-1)); + } } diff --git a/tests/time.rs b/tests/time.rs index eeec095..dbd6041 100644 --- a/tests/time.rs +++ b/tests/time.rs @@ -7,7 +7,10 @@ mod time_tests { #[test] fn debug() { let time = Time::default(); - assert_eq!("Time { nanoseconds: 0, offset: 0 }", format!("{:?}", time)); + assert_eq!( + "Time { nanoseconds: 0, offset: Fixed(0) }", + format!("{:?}", time) + ); } #[test] @@ -46,7 +49,7 @@ mod time_tests { #[test] fn ord() { let time = Time::default(); - let time_2 = time.add_hours(1).unwrap(); + let time_2 = time.add_hours(1); assert!(time < time_2); assert_eq!(std::cmp::Ordering::Less, time.cmp(&time_2)); } @@ -54,6 +57,7 @@ mod time_tests { #[test] fn now() { let _ = Time::now(); + let _ = Time::now_local(); } #[test] @@ -160,14 +164,14 @@ mod time_tests { #[test] fn add_sub() { - let mut time = Time::default().add_hours(1).unwrap(); + let mut time = Time::default().add_hours(1); - time = time.add_hours(1).unwrap(); - time = time.add_minutes(2).unwrap(); - time = time.add_seconds(3).unwrap(); - time = time.add_millis(5).unwrap(); - time = time.add_micros(6).unwrap(); - time = time.add_nanos(7).unwrap(); + time = time.add_hours(1); + time = time.add_minutes(2); + time = time.add_seconds(3); + time = time.add_millis(5); + time = time.add_micros(6); + time = time.add_nanos(7); assert_eq!(2, time.hour()); assert_eq!(2, time.minute()); @@ -176,41 +180,57 @@ mod time_tests { assert_eq!(5_006, time.micro()); assert_eq!(5_006_007, time.nano()); - time = time.sub_hours(2).unwrap(); - time = time.sub_minutes(2).unwrap(); - time = time.sub_seconds(3).unwrap(); - time = time.sub_millis(5).unwrap(); - time = time.sub_micros(6).unwrap(); - time = time.sub_nanos(7).unwrap(); + time = time.sub_hours(2); + time = time.sub_minutes(2); + time = time.sub_seconds(3); + time = time.sub_millis(5); + time = time.sub_micros(6); + time = time.sub_nanos(7); assert_eq!(0, time.as_nanos()); - assert!(time.sub_nanos(1).is_err()); - assert!(time.add_hours(24).is_err()); + assert_eq!(86_399_999_999_999, time.sub_nanos(1).as_nanos()); + assert_eq!(0, time.add_hours(24).as_nanos()); + assert_eq!(23 * 3600 * 1_000_000_000, time.add_hours(23).as_nanos()); let time = Time::from_hms(23, 59, 59) .unwrap() .set_nano(999_999_999) .unwrap(); - assert!(time.add_hours(1).is_err()); - assert!(time.add_minutes(1).is_err()); - assert!(time.add_seconds(1).is_err()); - assert!(time.add_millis(1).is_err()); - assert!(time.add_micros(1).is_err()); - assert!(time.add_nanos(1).is_err()); + assert_eq!(3600 * 1_000_000_000 - 1, time.add_hours(1).as_nanos()); + assert_eq!(60 * 1_000_000_000 - 1, time.add_minutes(1).as_nanos()); + assert_eq!(1_000_000_000 - 1, time.add_seconds(1).as_nanos()); + assert_eq!(1_000_000 - 1, time.add_millis(1).as_nanos()); + assert_eq!(1_000 - 1, time.add_micros(1).as_nanos()); + assert_eq!(0, time.add_nanos(1).as_nanos()); let time = Time::default(); - assert!(time.sub_hours(1).is_err()); - assert!(time.sub_minutes(1).is_err()); - assert!(time.sub_seconds(1).is_err()); - assert!(time.sub_millis(1).is_err()); - assert!(time.sub_micros(1).is_err()); - assert!(time.sub_nanos(1).is_err()); + assert_eq!( + 86_399_999_999_999 - 3600 * 1_000_000_000 + 1, + time.sub_hours(1).as_nanos() + ); + assert_eq!( + 86_399_999_999_999 - 60 * 1_000_000_000 + 1, + time.sub_minutes(1).as_nanos() + ); + assert_eq!( + 86_399_999_999_999 - 1_000_000_000 + 1, + time.sub_seconds(1).as_nanos() + ); + assert_eq!( + 86_399_999_999_999 - 1_000_000 + 1, + time.sub_millis(1).as_nanos() + ); + assert_eq!( + 86_399_999_999_999 - 1_000 + 1, + time.sub_micros(1).as_nanos() + ); + assert_eq!(86_399_999_999_999, time.sub_nanos(1).as_nanos()); } #[test] fn set() { - let mut time = Time::default().add_nanos(u32::MAX).unwrap(); + let mut time = Time::default().add_nanos(u32::MAX); time = time.set_hour(1).unwrap(); time = time.set_minute(2).unwrap();