#[derive(Default, Debug, Clone, PartialEq, Copy)]
pub struct DateTime {
pub year: i32,
pub month: i32,
pub day: i32,
pub hour: i32,
pub minute: i32,
pub second: f64,
pub timezone: Timezone,
}
#[derive(Debug, Clone, PartialEq)]
pub enum DateError {
DateTooEarly,
InvalidDayOfYear,
DateNotUTC,
InvalidCalendarDate,
InvalidTimeOfDay,
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub enum Timezone {
#[default]
UTC,
UT1,
}
pub fn validate_datetime(datetime: &DateTime) -> Result<(), DateError> {
if !(1..=12).contains(&datetime.month)
|| datetime.day < 1
|| datetime.day > days_in_month(datetime.year, datetime.month)
{
return Err(DateError::InvalidCalendarDate);
}
if !(0..=23).contains(&datetime.hour)
|| !(0..=59).contains(&datetime.minute)
|| !(0.0..61.0).contains(&datetime.second)
{
return Err(DateError::InvalidTimeOfDay);
}
Ok(())
}
fn is_leap_year(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
fn days_in_month(year: i32, month: i32) -> i32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 if is_leap_year(year) => 29,
2 => 28,
_ => 0,
}
}
pub fn utc2jday(utc_datetime: &DateTime) -> Result<(f64, f64), DateError> {
let (mjd, mjdfrac) = utc2mjday(utc_datetime)?;
let mut jd: f64 = mjd + 2400000.5;
let mut jdfrac: f64 = mjdfrac;
if !(0.0..1.0).contains(&jdfrac) {
jd += jdfrac.floor();
jdfrac = jdfrac - jdfrac.floor();
}
Ok((jd, jdfrac))
}
pub fn utc2mjday(utc_datetime: &DateTime) -> Result<(f64, f64), DateError> {
if utc_datetime.timezone != Timezone::UTC {
return Err(DateError::DateNotUTC);
}
validate_datetime(utc_datetime)?;
if utc_datetime.year < 1582
|| (utc_datetime.year == 1582 && utc_datetime.month < 10)
|| (utc_datetime.year == 1582 && utc_datetime.month == 10 && utc_datetime.day < 10)
{
return Err(DateError::DateTooEarly);
}
let year = utc_datetime.year as f64;
let month = utc_datetime.month as f64;
let day = utc_datetime.day as f64;
let hour = utc_datetime.hour as f64;
let minute = utc_datetime.minute as f64;
let second = utc_datetime.second;
let year_leap: f64;
let month_leap: f64;
if month <= 2. {
year_leap = year - 1.;
month_leap = month + 12.;
} else {
year_leap = year;
month_leap = month;
}
let b_leap: f64 =
(year_leap / 400.).floor() - (year_leap / 100.).floor() + (year_leap / 4.).floor();
let mut mjd = 365. * year_leap - 679004. + b_leap + (30.6001 * (month_leap + 1.)).floor() + day;
let mut mjdfrac = (second + minute * 60. + hour * 3600.) / 86400.;
if !(0.0..1.0).contains(&mjdfrac) {
mjd += mjdfrac.floor();
mjdfrac = mjdfrac - mjdfrac.floor();
}
Ok((mjd, mjdfrac))
}
pub fn dayofyr2utc(year: i32, dayofyr: f64) -> Result<DateTime, DateError> {
if !dayofyr.is_finite() || dayofyr < 1.0 {
return Err(DateError::InvalidDayOfYear);
}
let is_leap = is_leap_year(year);
let days_per_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let max_days = if is_leap { 366 } else { 365 };
let day_int = dayofyr.floor() as i32;
if day_int > max_days {
return Err(DateError::InvalidDayOfYear);
}
let day_frac = dayofyr - day_int as f64;
let mut day_count = 0;
let mut month = 1;
let mut day = 1;
for (idx, &days_in_month) in days_per_month.iter().enumerate() {
let days_this_month = if idx == 1 && is_leap {
29 } else {
days_in_month
};
if day_int <= day_count + days_this_month {
month = (idx + 1) as i32;
day = day_int - day_count;
break;
}
day_count += days_this_month;
}
let total_seconds = day_frac * 86400.0;
let hour = (total_seconds / 3600.0).floor() as i32;
let remaining_seconds = total_seconds - (hour as f64 * 3600.0);
let minute = (remaining_seconds / 60.0).floor() as i32;
let second = remaining_seconds - (minute as f64 * 60.0);
let mut final_second = second;
let mut final_minute = minute;
let mut final_hour = hour;
let mut final_day = day;
let mut final_month = month;
let mut final_year = year;
if final_second >= 60.0 {
final_second -= 60.0;
final_minute += 1;
}
if final_minute >= 60 {
final_minute -= 60;
final_hour += 1;
}
if final_hour >= 24 {
final_hour -= 24;
final_day += 1;
}
let days_in_current_month = if final_month == 2 && is_leap {
29 } else {
days_per_month[(final_month - 1) as usize]
};
if final_day > days_in_current_month {
final_day -= days_in_current_month;
final_month += 1;
}
if final_month > 12 {
final_month -= 12;
final_year += 1;
}
let datetime = DateTime {
year: final_year,
month: final_month,
day: final_day,
hour: final_hour,
minute: final_minute,
second: final_second,
timezone: Timezone::UTC,
};
Ok(datetime)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_datetime() {
let valid = DateTime {
year: 2024,
month: 2,
day: 29,
hour: 23,
minute: 59,
second: 60.5,
timezone: Timezone::UTC,
};
assert_eq!(validate_datetime(&valid), Ok(()));
let cases = [
(
DateTime {
year: 1900,
..valid
},
Err(DateError::InvalidCalendarDate),
),
(
DateTime {
year: 2000,
..valid
},
Ok(()),
),
(
DateTime { month: 0, ..valid },
Err(DateError::InvalidCalendarDate),
),
(
DateTime { month: 13, ..valid },
Err(DateError::InvalidCalendarDate),
),
(
DateTime {
month: 4,
day: 31,
..valid
},
Err(DateError::InvalidCalendarDate),
),
(
DateTime { day: 0, ..valid },
Err(DateError::InvalidCalendarDate),
),
(
DateTime { hour: -1, ..valid },
Err(DateError::InvalidTimeOfDay),
),
(
DateTime { hour: 24, ..valid },
Err(DateError::InvalidTimeOfDay),
),
(
DateTime {
minute: 60,
..valid
},
Err(DateError::InvalidTimeOfDay),
),
(
DateTime {
second: -0.1,
..valid
},
Err(DateError::InvalidTimeOfDay),
),
(
DateTime {
second: 61.0,
..valid
},
Err(DateError::InvalidTimeOfDay),
),
(
DateTime {
second: f64::NAN,
..valid
},
Err(DateError::InvalidTimeOfDay),
),
];
for (datetime, expected) in cases {
assert_eq!(validate_datetime(&datetime), expected, "{datetime:?}");
}
assert_eq!(
utc2jday(&DateTime { month: 13, ..valid }),
Err(DateError::InvalidCalendarDate)
);
assert_eq!(
dayofyr2utc(2024, f64::NAN),
Err(DateError::InvalidDayOfYear)
);
assert_eq!(
dayofyr2utc(2024, f64::INFINITY),
Err(DateError::InvalidDayOfYear)
);
}
#[test]
fn test_utc2jday() {
let datetime1 = DateTime {
year: 1959,
month: 3,
day: 25,
hour: 12,
minute: 34,
second: 49.123,
timezone: Timezone::UTC,
};
let (jd1, jdfrac1) = utc2jday(&datetime1).unwrap();
let jd1_total = jd1 + jdfrac1;
let jd1_expect = 2_436_653.024_179_664_4;
assert!(
(jd1_total - jd1_expect).abs() < 1e-8,
"Julian Date Test failed: expected {}, got {}",
jd1_expect,
jd1_total
);
let datetime2 = DateTime {
year: 2026,
month: 1,
day: 23,
hour: 0,
minute: 10,
second: 32.999,
timezone: Timezone::UTC,
};
let (jd2, jdfrac2) = utc2jday(&datetime2).unwrap();
let jd2_total = jd2 + jdfrac2;
let jd2_expect = 2_461_063.507_326_377;
assert!(
(jd2_total - jd2_expect).abs() < 1e-8,
"Julian Date Test failed: expected {}, got {}",
jd2_expect,
jd2_total
);
let datetime3 = DateTime {
year: 2000,
month: 1,
day: 1,
hour: 0,
minute: 0,
second: 0.0,
timezone: Timezone::UTC,
};
let (jd3, jdfrac3) = utc2jday(&datetime3).unwrap();
let jd3_total = jd3 + jdfrac3;
let jd3_expect = 2451544.5;
assert!(
(jd3_total - jd3_expect).abs() < 1e-8,
"Julian Date Test failed: expected {}, got {}",
jd3_expect,
jd3_total
);
let datetime4 = DateTime {
year: 1582,
month: 10,
day: 9,
hour: 12,
minute: 0,
second: 0.0,
timezone: Timezone::UTC,
};
let result = utc2jday(&datetime4);
assert!(
result.is_err(),
"Should return error for date before Oct 10, 1582"
);
assert_eq!(result.unwrap_err(), DateError::DateTooEarly);
let datetime5 = DateTime {
year: 1990,
month: 10,
day: 9,
hour: 12,
minute: 0,
second: 0.0,
timezone: Timezone::UT1,
};
let result = utc2jday(&datetime5);
assert!(result.is_err(), "Should return error for non-UTC date");
assert_eq!(result.unwrap_err(), DateError::DateNotUTC);
}
#[test]
fn test_utc2mjday() {
let datetime1 = DateTime {
year: 1997,
month: 4,
day: 2,
hour: 16,
minute: 12,
second: 35.505,
timezone: Timezone::UTC,
};
let (mjd1, mjdfrac1) = utc2mjday(&datetime1).unwrap();
let mjd1_total = mjd1 + mjdfrac1;
let mjd1_expect = 50_540.675_410_937_5;
assert!(
(mjd1_total - mjd1_expect).abs() < 1e-8,
"Modified Julian Date Test failed: expected {}, got {}",
mjd1_expect,
mjd1_total
);
let datetime2 = DateTime {
year: 2013,
month: 8,
day: 12,
hour: 2,
minute: 49,
second: 57.623,
timezone: Timezone::UTC,
};
let (mjd2, mjdfrac2) = utc2mjday(&datetime2).unwrap();
let mjd2_total = mjd2 + mjdfrac2;
let mjd2_expect = 56_516.118_028_043_97;
assert!(
(mjd2_total - mjd2_expect).abs() < 1e-8,
"Modified Julian Date Test failed: expected {}, got {}",
mjd2_expect,
mjd2_total
);
let datetime3 = DateTime {
year: 2000,
month: 1,
day: 1,
hour: 0,
minute: 0,
second: 0.0,
timezone: Timezone::UTC,
};
let (mjd3, mjdfrac3) = utc2mjday(&datetime3).unwrap();
let mjd3_total = mjd3 + mjdfrac3;
let mjd3_expect = 51544.0;
assert!(
(mjd3_total - mjd3_expect).abs() < 1e-8,
"Modified Julian Date Test failed: expected {}, got {}",
mjd3_expect,
mjd3_total
);
let datetime4 = DateTime {
year: 1582,
month: 10,
day: 9,
hour: 12,
minute: 0,
second: 0.0,
timezone: Timezone::UTC,
};
let result = utc2mjday(&datetime4);
assert!(
result.is_err(),
"Should return error for date before Oct 10, 1582"
);
assert_eq!(result.unwrap_err(), DateError::DateTooEarly);
let datetime5 = DateTime {
year: 1990,
month: 10,
day: 9,
hour: 12,
minute: 0,
second: 0.0,
timezone: Timezone::UT1,
};
let result = utc2mjday(&datetime5);
assert!(result.is_err(), "Should return error for non-UTC date");
assert_eq!(result.unwrap_err(), DateError::DateNotUTC);
}
#[test]
fn test_dayofyr_rounding() {
let datetime1 = dayofyr2utc(1959, 100.5).unwrap();
assert_eq!(
datetime1.year, 1959,
"Day 100.5 of 1959: year should be 1959, got {}",
datetime1.year
);
assert_eq!(
datetime1.month, 4,
"Day 100.5 of 1959: month should be 4 (April), got {}",
datetime1.month
);
assert_eq!(
datetime1.day, 10,
"Day 100.5 of 1959: day should be 10, got {}",
datetime1.day
);
assert_eq!(
datetime1.hour, 12,
"Day 100.5 of 1959: hour should be 12, got {}",
datetime1.hour
);
assert_eq!(
datetime1.minute, 0,
"Day 100.5 of 1959: minute should be 0, got {}",
datetime1.minute
);
assert!(
(datetime1.second - 0.0).abs() < 1e-6,
"Day 100.5 of 1959: second should be 0.0, got {}",
datetime1.second
);
let datetime2 = dayofyr2utc(2024, 200.75).unwrap();
assert_eq!(
datetime2.year, 2024,
"Day 200.75 of 2024: year should be 2024, got {}",
datetime2.year
);
assert_eq!(
datetime2.month, 7,
"Day 200.75 of 2024: month should be 7 (July), got {}",
datetime2.month
);
assert_eq!(
datetime2.day, 18,
"Day 200.75 of 2024: day should be 18, got {}",
datetime2.day
);
assert_eq!(
datetime2.hour, 18,
"Day 200.75 of 2024: hour should be 18, got {}",
datetime2.hour
);
assert_eq!(
datetime2.minute, 0,
"Day 200.75 of 2024: minute should be 0, got {}",
datetime2.minute
);
assert!(
(datetime2.second - 0.0).abs() < 1e-6,
"Day 200.75 of 2024: second should be 0.0, got {}",
datetime2.second
);
let datetime3 = dayofyr2utc(2023, 365.9999999999).unwrap();
assert_eq!(
datetime3.year, 2023,
"Day 365.9999999999 of 2023: year should be 2023, got {}",
datetime3.year
);
assert_eq!(
datetime3.month, 12,
"Day 365.9999999999 of 2023: month should be 12 (December), got {}",
datetime3.month
);
assert_eq!(
datetime3.day, 31,
"Day 365.9999999999 of 2023: day should be 31, got {}",
datetime3.day
);
assert_eq!(
datetime3.hour, 23,
"Day 365.9999999999 of 2023: hour should be 23, got {}",
datetime3.hour
);
assert_eq!(
datetime3.minute, 59,
"Day 365.9999999999 of 2023: minute should be 59, got {}",
datetime3.minute
);
assert!(
datetime3.second >= 59.0 && datetime3.second < 60.0,
"Day 365.9999999999 of 2023: second should be between 59.0 and 60.0, got {}",
datetime3.second
);
let datetime4 = dayofyr2utc(2024, 366.0).unwrap();
assert_eq!(
datetime4.year, 2024,
"Day 366 of 2024: year should be 2024, got {}",
datetime4.year
);
assert_eq!(
datetime4.month, 12,
"Day 366 of 2024: month should be 12 (December), got {}",
datetime4.month
);
assert_eq!(
datetime4.day, 31,
"Day 366 of 2024: day should be 31, got {}",
datetime4.day
);
assert_eq!(
datetime4.hour, 0,
"Day 366 of 2024: hour should be 0, got {}",
datetime4.hour
);
assert_eq!(
datetime4.minute, 0,
"Day 366 of 2024: minute should be 0, got {}",
datetime4.minute
);
assert!(
(datetime4.second - 0.0).abs() < 1e-6,
"Day 366 of 2024: second should be 0.0, got {}",
datetime4.second
);
let datetime5 = dayofyr2utc(2024, 60.5).unwrap();
assert_eq!(
datetime5.year, 2024,
"Day 60.5 of 2024: year should be 2024, got {}",
datetime5.year
);
assert_eq!(
datetime5.month, 2,
"Day 60.5 of 2024: month should be 2 (February), got {}",
datetime5.month
);
assert_eq!(
datetime5.day, 29,
"Day 60.5 of 2024: day should be 29 (leap day), got {}",
datetime5.day
);
assert_eq!(
datetime5.hour, 12,
"Day 60.5 of 2024: hour should be 12, got {}",
datetime5.hour
);
assert_eq!(
datetime5.minute, 0,
"Day 60.5 of 2024: minute should be 0, got {}",
datetime5.minute
);
assert!(
(datetime5.second - 0.0).abs() < 1e-6,
"Day 60.5 of 2024: second should be 0.0, got {}",
datetime5.second
);
let result = dayofyr2utc(2023, 366.1);
assert!(
result.is_err(),
"Day 366.1 of 2023 (non-leap year): should return error for day of year exceeding 365, got Ok({:?})",
result
);
assert_eq!(
result.unwrap_err(),
DateError::InvalidDayOfYear,
"Day 366.1 of 2023: error should be InvalidDayOfYear"
);
let result = dayofyr2utc(2024, 367.0);
assert!(
result.is_err(),
"Day 367 of 2024 (leap year): should return error for day of year exceeding 366, got Ok({:?})",
result
);
assert_eq!(
result.unwrap_err(),
DateError::InvalidDayOfYear,
"Day 367 of 2024: error should be InvalidDayOfYear"
);
let result = dayofyr2utc(2024, 0.5);
assert!(
result.is_err(),
"Day 0.5 of 2024: should return error for day of year less than 1, got Ok({:?})",
result
);
assert_eq!(
result.unwrap_err(),
DateError::InvalidDayOfYear,
"Day 0.5 of 2024: error should be InvalidDayOfYear"
);
let result = dayofyr2utc(2024, 0.0);
assert!(
result.is_err(),
"Day 0.0 of 2024: should return error for day of year equal to 0, got Ok({:?})",
result
);
assert_eq!(
result.unwrap_err(),
DateError::InvalidDayOfYear,
"Day 0.0 of 2024: error should be InvalidDayOfYear"
);
}
}