pub mod constants;
use constants::*;
use std::time::{SystemTime, UNIX_EPOCH};
use std::{fmt, ops::Add, ops::Sub};
#[derive(Debug, Clone, Copy)]
pub struct DateTime {
pub year: u64,
pub month: u64,
pub day: u64,
pub hour: u64,
pub minute: u64,
pub second: u64,
pub timezone: TimeZone,
}
impl DateTime {
pub fn new(
year: u64,
month: u64,
day: u64,
hour: u64,
minute: u64,
second: u64,
timezone: TimeZone,
) -> Result<Self, String> {
if month < 1 || month > 12 {
return Err("Invalid month".to_string());
}
if day < 1 || day > days_in_month(month, year) {
return Err("Invalid day".to_string());
}
if hour > 23 {
return Err("Invalid hour".to_string());
}
if minute > 59 {
return Err("Invalid minute".to_string());
}
if second > 59 {
return Err("Invalid second".to_string());
}
let mut total_seconds = 0;
for y in 1970..year {
total_seconds += if is_leap_year(y) { 366 } else { 365 } * SECONDS_IN_DAY;
}
for m in 1..month {
total_seconds += days_in_month(m, year) as i64 * SECONDS_IN_DAY;
}
total_seconds += (day - 1) as i64 * SECONDS_IN_DAY;
total_seconds += hour as i64 * SECONDS_IN_HOUR;
total_seconds += minute as i64 * SECONDS_IN_MINUTE;
total_seconds += second as i64;
total_seconds -= timezone.offset_in_seconds();
let utc_datetime = Self::from_unix_seconds(total_seconds, timezone)?;
Ok(utc_datetime)
}
pub fn calculate_total_seconds(
year: u64,
month: u64,
day: u64,
hour: u64,
minute: u64,
second: u64,
) -> Result<i64, String> {
if year < 1970 {
return Err("Year must be 1970 or later".to_string());
}
let mut total_seconds: i64 = 0;
for y in 1970..year {
total_seconds += if is_leap_year(y) {
SECONDS_IN_LEAPYEAR
} else {
SECONDS_IN_YEAR
};
}
for m in 1..month {
total_seconds += days_in_month(m, year) as i64 * SECONDS_IN_DAY;
}
total_seconds += (day as i64 - 1) * SECONDS_IN_DAY;
total_seconds += hour as i64 * SECONDS_IN_HOUR;
total_seconds += minute as i64 * SECONDS_IN_MINUTE;
total_seconds += second as i64;
Ok(total_seconds)
}
pub fn strftime(&self, format: &str) -> String {
let mut result = format.to_string();
result = result.replace("%Y", &format!("{:04}", self.year));
result = result.replace("%m", &format!("{:02}", self.month));
result = result.replace("%d", &format!("{:02}", self.day));
result = result.replace("%H", &format!("{:02}", self.hour));
result = result.replace("%M", &format!("{:02}", self.minute));
result = result.replace("%S", &format!("{:02}", self.second));
result
}
pub fn to_unix_seconds(&self) -> i64 {
let mut total_seconds: i64 = 0;
for year in 1970..self.year {
total_seconds += if is_leap_year(year) { 366 } else { 365 } * SECONDS_IN_DAY;
}
for month in 1..self.month {
total_seconds += days_in_month(month, self.year) as i64 * SECONDS_IN_DAY;
}
total_seconds += (self.day - 1) as i64 * SECONDS_IN_DAY;
total_seconds += self.hour as i64 * SECONDS_IN_HOUR;
total_seconds += self.minute as i64 * SECONDS_IN_MINUTE;
total_seconds += self.second as i64;
total_seconds - self.timezone.offset_in_seconds()
}
pub fn from_unix_seconds(unix_seconds: i64, timezone: TimeZone) -> Result<Self, String> {
let adjusted_seconds = unix_seconds + timezone.offset_in_seconds();
let mut remaining_seconds = adjusted_seconds;
if remaining_seconds < 0 {
return Err("Unix seconds cannot represent a date before 1970-01-01".to_string());
}
let mut year = 1970;
while remaining_seconds >= (if is_leap_year(year) { 366 } else { 365 }) * SECONDS_IN_DAY {
remaining_seconds -= (if is_leap_year(year) { 366 } else { 365 }) * SECONDS_IN_DAY;
year += 1;
}
let mut month = 1;
while remaining_seconds >= days_in_month(month, year) as i64 * SECONDS_IN_DAY {
remaining_seconds -= days_in_month(month, year) as i64 * SECONDS_IN_DAY;
month += 1;
}
let day = (remaining_seconds / SECONDS_IN_DAY) as u64 + 1;
remaining_seconds %= SECONDS_IN_DAY;
let hour = (remaining_seconds / 3600) as u64;
remaining_seconds %= 3600;
let minute = (remaining_seconds / 60) as u64;
let second = (remaining_seconds % 60) as u64;
Ok(Self {
year,
month,
day,
hour,
minute,
second,
timezone,
})
}
pub fn add_timedelta(&self, delta: TimeDelta) -> Result<Self, String> {
let current_unix = self.to_unix_seconds(); let delta_seconds = compute_total_seconds(
delta.weeks,
delta.days,
delta.hours,
delta.minutes,
delta.seconds,
);
let timezone = self.timezone.clone();
let new_unix = current_unix + delta_seconds; if new_unix < 0 {
return Err(
"Resulting DateTime is before Unix epoch (1970-01-01 00:00:00 UTC)".to_string(),
);
}
DateTime::from_unix_seconds(new_unix, timezone) }
pub fn sub_timedelta(&self, delta: TimeDelta) -> Result<Self, String> {
let current_unix = self.to_unix_seconds(); let delta_seconds = compute_total_seconds(
delta.weeks,
delta.days,
delta.hours,
delta.minutes,
delta.seconds,
);
let new_unix = current_unix - delta_seconds; let timezone = self.timezone.clone();
if new_unix < 0 {
return Err(
"Resulting DateTime is before Unix epoch (1970-01-01 00:00:00 UTC)".to_string(),
);
}
DateTime::from_unix_seconds(new_unix, timezone) }
}
impl fmt::Display for DateTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
self.year, self.month, self.day, self.hour, self.minute, self.second
)
}
}
impl Add<TimeDelta> for DateTime {
type Output = Result<DateTime, String>;
fn add(self, delta: TimeDelta) -> Self::Output {
DateTime::add_timedelta(&self, delta)
}
}
impl Sub<TimeDelta> for DateTime {
type Output = Result<DateTime, String>;
fn sub(self, delta: TimeDelta) -> Self::Output {
DateTime::sub_timedelta(&self, delta)
}
}
impl PartialEq for DateTime {
fn eq(&self, other: &Self) -> bool {
self.year == other.year
&& self.month == other.month
&& self.day == other.day
&& self.hour == other.hour
&& self.minute == other.minute
&& self.second == other.second
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TimeDelta {
pub weeks: i64,
pub days: i64,
pub hours: i64,
pub minutes: i64,
pub seconds: i64,
}
impl std::fmt::Display for TimeDelta {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut components = Vec::new();
if self.weeks != 0 {
components.push(format!(
"{} week{}",
self.weeks,
if self.weeks.abs() == 1 { "" } else { "s" }
));
}
if self.days != 0 {
components.push(format!(
"{} day{}",
self.days,
if self.days.abs() == 1 { "" } else { "s" }
));
}
if self.hours != 0 {
components.push(format!(
"{} hour{}",
self.hours,
if self.hours.abs() == 1 { "" } else { "s" }
));
}
if self.minutes != 0 {
components.push(format!(
"{} minute{}",
self.minutes,
if self.minutes.abs() == 1 { "" } else { "s" }
));
}
if self.seconds != 0 || components.is_empty() {
components.push(format!(
"{} second{}",
self.seconds,
if self.seconds.abs() == 1 { "" } else { "s" }
));
}
write!(f, "{}", components.join(", "))
}
}
impl Default for TimeDelta {
fn default() -> Self {
Self {
weeks: 0,
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum TimeZone {
UTC,
KST, EST, PST, JST, IST, CET, AST, CST, MST, AKST, HST, BST, WET, EET, SAST, EAT, AEST, ACST, AWST, CSTAsia, SGT, HKT, }
impl TimeZone {
pub const fn offset_in_seconds(&self) -> i64 {
match self {
TimeZone::UTC => OFFSET_UTC,
TimeZone::KST => OFFSET_KST,
TimeZone::EST => OFFSET_EST,
TimeZone::PST => OFFSET_PST,
TimeZone::JST => OFFSET_JST,
TimeZone::IST => OFFSET_IST,
TimeZone::CET => OFFSET_CET,
TimeZone::AST => OFFSET_AST,
TimeZone::CST => OFFSET_CST,
TimeZone::MST => OFFSET_MST,
TimeZone::AKST => OFFSET_AKST,
TimeZone::HST => OFFSET_HST,
TimeZone::BST => OFFSET_BST,
TimeZone::WET => OFFSET_WET,
TimeZone::EET => OFFSET_EET,
TimeZone::SAST => OFFSET_SAST,
TimeZone::EAT => OFFSET_EAT,
TimeZone::AEST => OFFSET_AEST,
TimeZone::ACST => OFFSET_ACST,
TimeZone::AWST => OFFSET_AWST,
TimeZone::CSTAsia => OFFSET_CST_ASIA,
TimeZone::SGT => OFFSET_SGT,
TimeZone::HKT => OFFSET_HKT,
}
}
}
pub fn now(timezone: TimeZone) -> Result<DateTime, String> {
let now = SystemTime::now();
let duration_since_epoch = now.duration_since(UNIX_EPOCH).expect("Time went backwards");
let total_seconds = duration_since_epoch.as_secs();
let adjusted_seconds = adjust_second_with_timezone(total_seconds, timezone);
calculate_date_since_epoch(adjusted_seconds as i64, timezone)
}
pub const fn is_leap_year(year: u64) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
pub fn days_in_month(month: u64, year: u64) -> u64 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, 4 | 6 | 9 | 11 => 30, 2 => {
if is_leap_year(year) {
29
} else {
28
}
}
_ => 0, }
}
pub const fn compute_total_seconds(
weeks: i64,
days: i64,
hours: i64,
minutes: i64,
seconds: i64,
) -> i64 {
weeks * SECONDS_IN_WEEK as i64
+ days * SECONDS_IN_DAY as i64
+ hours * SECONDS_IN_HOUR as i64
+ minutes * SECONDS_IN_MINUTE as i64
+ seconds
}
pub const fn adjust_second_with_timezone(total_seconds: u64, timezone: TimeZone) -> u64 {
let timezone_offset = timezone.offset_in_seconds();
let adjusted_seconds = (total_seconds as i64 + timezone_offset) as u64;
adjusted_seconds
}
pub fn calculate_date_since_epoch(
adjusted_seconds: i64,
timezone: TimeZone,
) -> Result<DateTime, String> {
let mut days = adjusted_seconds as u64 / SECONDS_IN_DAY as u64;
let remainder_seconds = adjusted_seconds % SECONDS_IN_DAY;
let hour = (remainder_seconds / SECONDS_IN_HOUR) as u64;
let remainder_seconds = remainder_seconds % SECONDS_IN_HOUR;
let minute = (remainder_seconds / SECONDS_IN_MINUTE) as u64;
let second = (remainder_seconds % SECONDS_IN_MINUTE) as u64;
let mut year = 1970;
while days >= if is_leap_year(year) { 366 } else { 365 } {
days -= if is_leap_year(year) { 366 } else { 365 };
year += 1;
}
let mut month = 1;
while days >= days_in_month(month, year) {
days -= days_in_month(month, year);
month += 1;
}
let day = days as u64 + 1;
DateTime::new(year, month, day, hour, minute, second, timezone)
}