Initial commit
This commit is contained in:
commit
40470e1752
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
65
Cargo.lock
generated
Normal file
65
Cargo.lock
generated
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "epoch"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.78"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.196"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.196"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.48"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
9
Cargo.toml
Normal file
9
Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "epoch"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
155
src/datetime.rs
Normal file
155
src/datetime.rs
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
use {
|
||||||
|
crate::{
|
||||||
|
month::Month, weekday::Weekday, year::Year, zone::TimeZone, SECONDS_PER_DAY,
|
||||||
|
SECONDS_PER_HOUR, SECONDS_PER_MINUTE,
|
||||||
|
},
|
||||||
|
serde::{Deserialize, Serialize},
|
||||||
|
std::{cmp, fmt},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
pub struct DateTime {
|
||||||
|
pub year: Year,
|
||||||
|
pub month: Month,
|
||||||
|
pub day: u8,
|
||||||
|
pub hour: u8,
|
||||||
|
pub minute: u8,
|
||||||
|
pub second: u8,
|
||||||
|
pub zone: TimeZone,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DateTime {
|
||||||
|
/// Retrieves the year
|
||||||
|
pub fn year(&self) -> i32 {
|
||||||
|
self.year.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the Unix timestamp corresponding to this `DateTime`
|
||||||
|
pub fn timestamp(&self) -> i64 {
|
||||||
|
let mut seconds: i64 = 0;
|
||||||
|
let mut year = Year::new(1970);
|
||||||
|
if self.year() < 1970 {
|
||||||
|
while year.get() > self.year() {
|
||||||
|
year = year.previous();
|
||||||
|
seconds -= year.seconds();
|
||||||
|
}
|
||||||
|
} else if self.year() > 1970 {
|
||||||
|
while year.get() < self.year() {
|
||||||
|
seconds += year.seconds();
|
||||||
|
year = year.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut month = Month::Janurary;
|
||||||
|
while month < self.month {
|
||||||
|
seconds += month.seconds(self.year);
|
||||||
|
month = month.next().unwrap();
|
||||||
|
}
|
||||||
|
// The days begin numbering with 1, so on the 5th we have had four full
|
||||||
|
// days plus some remainder. So we use self.days - 1 for our calculations
|
||||||
|
seconds += i64::from(self.day - 1) * SECONDS_PER_DAY;
|
||||||
|
seconds += i64::from(self.hour) * SECONDS_PER_HOUR;
|
||||||
|
seconds += i64::from(self.second);
|
||||||
|
seconds += self.zone.as_seconds();
|
||||||
|
seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a Unix timestamp to a `DateTime` struct
|
||||||
|
pub fn from_timestamp(ts: i64) -> Self {
|
||||||
|
if ts < 0 {
|
||||||
|
let mut seconds: i64 = 0;
|
||||||
|
let mut year = Year::new(-1);
|
||||||
|
while seconds > -year.seconds() {
|
||||||
|
seconds -= year.seconds();
|
||||||
|
year = year.previous();
|
||||||
|
}
|
||||||
|
let mut month = Some(Month::December);
|
||||||
|
while month.is_some() {
|
||||||
|
let m = month.unwrap();
|
||||||
|
if seconds > m.seconds(year) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
seconds -= m.seconds(year);
|
||||||
|
month = m.previous();
|
||||||
|
}
|
||||||
|
let month = month.unwrap();
|
||||||
|
let mut day = month.days(year);
|
||||||
|
while day > 0 && seconds < -SECONDS_PER_DAY {
|
||||||
|
seconds -= SECONDS_PER_HOUR;
|
||||||
|
day -= 1;
|
||||||
|
}
|
||||||
|
let mut hour: i8 = 23;
|
||||||
|
while hour >= 0 && seconds < -SECONDS_PER_HOUR {
|
||||||
|
seconds -= SECONDS_PER_HOUR;
|
||||||
|
hour -= 1;
|
||||||
|
}
|
||||||
|
let hour = hour.try_into().unwrap();
|
||||||
|
let mut minute: i8 = 60;
|
||||||
|
while minute >= 0 && seconds < -60 {
|
||||||
|
seconds -= 60;
|
||||||
|
minute -= 1;
|
||||||
|
}
|
||||||
|
let minute = minute.try_into().unwrap();
|
||||||
|
let second: u8 = (seconds + 60).try_into().unwrap();
|
||||||
|
Self {
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
day,
|
||||||
|
hour,
|
||||||
|
minute,
|
||||||
|
second,
|
||||||
|
zone: TimeZone::Utc,
|
||||||
|
}
|
||||||
|
} else if ts > 0 {
|
||||||
|
let mut seconds: i64 = ts;
|
||||||
|
let mut year = Year::new(1970);
|
||||||
|
while year.seconds() < seconds {
|
||||||
|
seconds -= year.seconds();
|
||||||
|
year = year.next();
|
||||||
|
}
|
||||||
|
let mut month = Month::Janurary;
|
||||||
|
while month.seconds(year) < seconds {
|
||||||
|
seconds -= month.seconds(year);
|
||||||
|
month = month.next().unwrap();
|
||||||
|
}
|
||||||
|
let day = seconds / SECONDS_PER_DAY + 1;
|
||||||
|
seconds %= SECONDS_PER_DAY;
|
||||||
|
let hour = seconds / SECONDS_PER_HOUR;
|
||||||
|
seconds %= SECONDS_PER_HOUR;
|
||||||
|
let minute = seconds / SECONDS_PER_MINUTE;
|
||||||
|
seconds %= SECONDS_PER_MINUTE;
|
||||||
|
Self {
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
day: day.try_into().unwrap(),
|
||||||
|
hour: hour.try_into().unwrap(),
|
||||||
|
minute: minute.try_into().unwrap(),
|
||||||
|
second: seconds.try_into().unwrap(),
|
||||||
|
zone: TimeZone::Utc,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Self {
|
||||||
|
year: Year::new(1970),
|
||||||
|
month: Month::Janurary,
|
||||||
|
day: 1,
|
||||||
|
hour: 0,
|
||||||
|
minute: 0,
|
||||||
|
second: 0,
|
||||||
|
zone: TimeZone::Utc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for DateTime {
|
||||||
|
fn cmp(&self, other: &Self) -> cmp::Ordering {
|
||||||
|
let a = self.timestamp();
|
||||||
|
let b = other.timestamp();
|
||||||
|
a.cmp(&b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for DateTime {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
14
src/lib.rs
Normal file
14
src/lib.rs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
#[warn(clippy::all, clippy::pedantic)]
|
||||||
|
#[allow(clippy::must_use_candidate)]
|
||||||
|
#[allow(clippy::comparison_chain)]
|
||||||
|
pub(crate) mod datetime;
|
||||||
|
pub(crate) mod month;
|
||||||
|
pub(crate) mod weekday;
|
||||||
|
pub(crate) mod year;
|
||||||
|
pub(crate) mod zone;
|
||||||
|
|
||||||
|
pub mod prelude;
|
||||||
|
|
||||||
|
pub static SECONDS_PER_MINUTE: i64 = 60;
|
||||||
|
pub static SECONDS_PER_HOUR: i64 = 60 * 60;
|
||||||
|
pub static SECONDS_PER_DAY: i64 = 60 * 60 * 24;
|
175
src/month.rs
Normal file
175
src/month.rs
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use {
|
||||||
|
crate::{year::Year, SECONDS_PER_DAY},
|
||||||
|
serde::{Deserialize, Serialize},
|
||||||
|
std::{cmp, error, fmt},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum Month {
|
||||||
|
Janurary = 1,
|
||||||
|
February = 2,
|
||||||
|
March = 3,
|
||||||
|
April = 4,
|
||||||
|
May = 5,
|
||||||
|
June = 6,
|
||||||
|
July = 7,
|
||||||
|
August = 8,
|
||||||
|
September = 9,
|
||||||
|
October = 10,
|
||||||
|
November = 11,
|
||||||
|
December = 12,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
ParseMonth,
|
||||||
|
Range,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{self:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl error::Error for Error {}
|
||||||
|
|
||||||
|
impl Month {
|
||||||
|
pub fn days(&self, year: Year) -> u8 {
|
||||||
|
match *self as u8 {
|
||||||
|
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
|
||||||
|
2 => match year {
|
||||||
|
Year::Normal(_) => 28,
|
||||||
|
Year::Leap(_) => 29,
|
||||||
|
},
|
||||||
|
_ => 30,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn seconds(&self, year: Year) -> i64 {
|
||||||
|
self.days(year) as i64 * SECONDS_PER_DAY
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(&self) -> Option<Self> {
|
||||||
|
let n = *self as u8 + 1;
|
||||||
|
n.try_into().ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn previous(&self) -> Option<Self> {
|
||||||
|
let n = *self as u8 - 1;
|
||||||
|
n.try_into().ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u8> for Month {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
1 => Ok(Self::Janurary),
|
||||||
|
2 => Ok(Self::February),
|
||||||
|
3 => Ok(Self::March),
|
||||||
|
4 => Ok(Self::April),
|
||||||
|
5 => Ok(Self::May),
|
||||||
|
6 => Ok(Self::June),
|
||||||
|
7 => Ok(Self::July),
|
||||||
|
8 => Ok(Self::August),
|
||||||
|
9 => Ok(Self::September),
|
||||||
|
10 => Ok(Self::October),
|
||||||
|
11 => Ok(Self::November),
|
||||||
|
12 => Ok(Self::December),
|
||||||
|
_ => Err(Error::Range),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for Month {
|
||||||
|
fn cmp(&self, other: &Self) -> cmp::Ordering {
|
||||||
|
(*self as u8).cmp(&(*other as u8))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for Month {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Month {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
match self {
|
||||||
|
Self::Janurary => "January",
|
||||||
|
Self::February => "February",
|
||||||
|
Self::March => "March",
|
||||||
|
Self::April => "April",
|
||||||
|
Self::May => "May",
|
||||||
|
Self::June => "June",
|
||||||
|
Self::July => "Jujly",
|
||||||
|
Self::August => "August",
|
||||||
|
Self::September => "September",
|
||||||
|
Self::October => "October",
|
||||||
|
Self::November => "November",
|
||||||
|
Self::December => "December",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Month {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"Jan" | "jan" | "January" | "january" => Ok(Self::Janurary),
|
||||||
|
"Feb" | "feb" | "February" | "february" => Ok(Self::February),
|
||||||
|
"March" | "march" | "Mar" | "mar" => Ok(Self::March),
|
||||||
|
"April" | "april" | "Apr" | "apr" => Ok(Self::April),
|
||||||
|
"May" | "may" => Ok(Self::May),
|
||||||
|
"June" | "june" | "Jun" | "jun" => Ok(Self::June),
|
||||||
|
"July" | "july" | "Jul" | "jul" => Ok(Self::July),
|
||||||
|
"August" | "august" | "Aug" | "aug" => Ok(Self::August),
|
||||||
|
"September" | "september" | "Sep" | "sep" => Ok(Self::September),
|
||||||
|
"October" | "october" | "Oct" | "oct" => Ok(Self::October),
|
||||||
|
"November" | "november" | "Nov" | "nov" => Ok(Self::November),
|
||||||
|
"December" | "december" | "Dec" | "dec" => Ok(Self::December),
|
||||||
|
_ => Err(Error::ParseMonth),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn next() {
|
||||||
|
let mut month = Month::Janurary;
|
||||||
|
month = month.next().unwrap();
|
||||||
|
assert_eq!(month, Month::February);
|
||||||
|
month = month.next().unwrap();
|
||||||
|
assert_eq!(month, Month::March);
|
||||||
|
month = Month::December;
|
||||||
|
assert!(month.next().is_none())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn previous() {
|
||||||
|
let mut month = Month::March.previous().unwrap();
|
||||||
|
assert_eq!(month, Month::February);
|
||||||
|
month = month.previous().unwrap();
|
||||||
|
assert_eq!(month, Month::Janurary);
|
||||||
|
assert!(month.previous().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compare() {
|
||||||
|
assert!(Month::Janurary < Month::October);
|
||||||
|
assert!(Month::June > Month::March);
|
||||||
|
}
|
||||||
|
}
|
7
src/prelude.rs
Normal file
7
src/prelude.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
pub use crate::{
|
||||||
|
datetime::DateTime,
|
||||||
|
month::{Error as MonthError, Month},
|
||||||
|
weekday::Weekday,
|
||||||
|
year::Year,
|
||||||
|
zone::{Sign, TimeZone},
|
||||||
|
};
|
13
src/weekday.rs
Normal file
13
src/weekday.rs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum Weekday {
|
||||||
|
Thursday,
|
||||||
|
Friday,
|
||||||
|
Saturday,
|
||||||
|
Sunday,
|
||||||
|
Monday,
|
||||||
|
Tuesday,
|
||||||
|
Wednesday,
|
||||||
|
}
|
73
src/year.rs
Normal file
73
src/year.rs
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
use {
|
||||||
|
crate::SECONDS_PER_DAY,
|
||||||
|
serde::{Deserialize, Serialize},
|
||||||
|
std::{cmp, fmt, num::ParseIntError, str::FromStr},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
pub enum Year {
|
||||||
|
Normal(i32),
|
||||||
|
Leap(i32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Year {
|
||||||
|
pub fn new(year: i32) -> Self {
|
||||||
|
if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) {
|
||||||
|
Self::Leap(year)
|
||||||
|
} else {
|
||||||
|
Self::Normal(year)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn days(&self) -> u16 {
|
||||||
|
match self {
|
||||||
|
Self::Normal(_) => 365,
|
||||||
|
Self::Leap(_) => 366,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn seconds(&self) -> i64 {
|
||||||
|
self.days() as i64 * SECONDS_PER_DAY
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self) -> i32 {
|
||||||
|
match self {
|
||||||
|
Self::Normal(y) | Self::Leap(y) => *y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(&self) -> Self {
|
||||||
|
Self::new(self.get() + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn previous(&self) -> Self {
|
||||||
|
Self::new(self.get() - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for Year {
|
||||||
|
fn cmp(&self, other: &Self) -> cmp::Ordering {
|
||||||
|
self.get().cmp(&other.get())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for Year {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Year {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{:>4}", self.get())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Year {
|
||||||
|
type Err = ParseIntError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let year: i32 = s.parse()?;
|
||||||
|
Ok(Self::new(year))
|
||||||
|
}
|
||||||
|
}
|
159
src/zone.rs
Normal file
159
src/zone.rs
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
use {
|
||||||
|
crate::SECONDS_PER_HOUR,
|
||||||
|
serde::{Deserialize, Serialize},
|
||||||
|
std::{error, fmt, str::FromStr},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
pub enum Sign {
|
||||||
|
Positive,
|
||||||
|
Negative,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Sign {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
match self {
|
||||||
|
Self::Positive => "+",
|
||||||
|
Self::Negative => "-",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
pub enum TimeZone {
|
||||||
|
Offset { sign: Sign, hours: u8, minutes: u8 },
|
||||||
|
Utc,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimeZone {
|
||||||
|
pub fn new(hours: Option<i8>, minutes: Option<u8>) -> Result<Self, TZError> {
|
||||||
|
match (hours, minutes) {
|
||||||
|
(Some(h), Some(m)) => {
|
||||||
|
if (!(-12..=12).contains(&h) || m > 59) || ((h == -12 || h == 12) && m > 0) {
|
||||||
|
return Err(TZError::Range);
|
||||||
|
}
|
||||||
|
let (sign, hours) = if h > 0 {
|
||||||
|
(Sign::Positive, u8::try_from(h).unwrap())
|
||||||
|
} else {
|
||||||
|
(Sign::Negative, u8::try_from(-h).unwrap())
|
||||||
|
};
|
||||||
|
Ok(Self::Offset {
|
||||||
|
sign,
|
||||||
|
hours,
|
||||||
|
minutes: m,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
(Some(h), None) => {
|
||||||
|
if !(-12..=12).contains(&h) {
|
||||||
|
return Err(TZError::Range);
|
||||||
|
}
|
||||||
|
let (sign, hours) = if h > 0 {
|
||||||
|
(Sign::Positive, u8::try_from(h).unwrap())
|
||||||
|
} else {
|
||||||
|
(Sign::Negative, u8::try_from(-h).unwrap())
|
||||||
|
};
|
||||||
|
Ok(Self::Offset {
|
||||||
|
sign,
|
||||||
|
hours,
|
||||||
|
minutes: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
(None, Some(m)) => {
|
||||||
|
if m > 59 {
|
||||||
|
Err(TZError::Range)
|
||||||
|
} else {
|
||||||
|
Ok(Self::Offset {
|
||||||
|
sign: Sign::Positive,
|
||||||
|
hours: 0,
|
||||||
|
minutes: m,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Ok(Self::Utc),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_seconds(&self) -> i64 {
|
||||||
|
match self {
|
||||||
|
Self::Utc => 0,
|
||||||
|
Self::Offset {
|
||||||
|
sign,
|
||||||
|
hours,
|
||||||
|
minutes,
|
||||||
|
} => {
|
||||||
|
let base = i64::from(*hours) * SECONDS_PER_HOUR + i64::from(*minutes) * 60;
|
||||||
|
if let Sign::Negative = sign {
|
||||||
|
-base
|
||||||
|
} else {
|
||||||
|
base
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for TimeZone {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Utc => write!(f, "Z"),
|
||||||
|
Self::Offset {
|
||||||
|
sign,
|
||||||
|
hours,
|
||||||
|
minutes,
|
||||||
|
} => {
|
||||||
|
write!(f, "{sign}{hours}:{minutes}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for TimeZone {
|
||||||
|
type Err = TZError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if s == "z" || s == "Z" {
|
||||||
|
return Ok(Self::Utc);
|
||||||
|
}
|
||||||
|
let sign = match s.get(0..1) {
|
||||||
|
Some("+") => Sign::Positive,
|
||||||
|
Some("-") => Sign::Negative,
|
||||||
|
_ => return Err(TZError::ParseTZError),
|
||||||
|
};
|
||||||
|
if let Some(s) = s.get(1..) {
|
||||||
|
let Some((h, m)) = s.split_once(':') else {
|
||||||
|
return Err(TZError::ParseTZError);
|
||||||
|
};
|
||||||
|
let hours = h.parse::<u8>().map_err(|_| TZError::ParseTZError)?;
|
||||||
|
let minutes = m.parse::<u8>().map_err(|_| TZError::ParseTZError)?;
|
||||||
|
if hours > 12 || minutes > 59 || (hours == 12 && minutes > 0) {
|
||||||
|
Err(TZError::Range)
|
||||||
|
} else {
|
||||||
|
Ok(Self::Offset {
|
||||||
|
sign,
|
||||||
|
hours,
|
||||||
|
minutes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(TZError::ParseTZError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum TZError {
|
||||||
|
Range,
|
||||||
|
ParseTZError,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for TZError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{self:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl error::Error for TZError {}
|
Loading…
Reference in New Issue
Block a user