Initial commit

This commit is contained in:
Nathan Fisher 2024-01-29 10:58:27 -05:00
commit 40470e1752
10 changed files with 671 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

65
Cargo.lock generated Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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 {}