use chrono::{Datelike, TimeZone, Utc};
use crate::conversions::{from_unix_ms, to_date_time};
use crate::types::{BrightDateError, BrightDateValue};
fn date_to_bd(year: i32, month: u32, day: u32, hour: u32, min: u32, sec: u32) -> BrightDateValue {
let dt = Utc
.with_ymd_and_hms(year, month, day, hour, min, sec)
.single()
.expect("invalid calendar date");
from_unix_ms(dt.timestamp_millis() as f64).unwrap_or(f64::NAN)
}
pub fn start_of_year(year: i32) -> BrightDateValue {
date_to_bd(year, 1, 1, 0, 0, 0)
}
pub fn end_of_year(year: i32) -> BrightDateValue {
date_to_bd(year, 12, 31, 23, 59, 59)
}
pub fn start_of_month(year: i32, month: u32) -> BrightDateValue {
date_to_bd(year, month, 1, 0, 0, 0)
}
pub fn end_of_month(year: i32, month: u32) -> BrightDateValue {
let (next_year, next_month) = if month == 12 {
(year + 1, 1)
} else {
(year, month + 1)
};
let start_next = date_to_bd(next_year, next_month, 1, 0, 0, 0);
start_next - 1.0 / 86_400.0
}
pub fn get_year(bd: BrightDateValue) -> i32 {
to_date_time(bd).year()
}
pub fn get_month(bd: BrightDateValue) -> u32 {
to_date_time(bd).month()
}
pub fn get_day_of_month(bd: BrightDateValue) -> u32 {
to_date_time(bd).day()
}
pub fn get_day_of_week(bd: BrightDateValue) -> u32 {
let wd = to_date_time(bd).weekday();
wd.num_days_from_sunday()
}
pub fn get_day_of_year(bd: BrightDateValue) -> u32 {
to_date_time(bd).ordinal()
}
pub fn is_leap_year(bd: BrightDateValue) -> bool {
let year = get_year(bd);
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
pub fn days_in_year(bd: BrightDateValue) -> u32 {
if is_leap_year(bd) { 366 } else { 365 }
}
pub fn days_in_month(bd: BrightDateValue) -> u32 {
let dt = to_date_time(bd);
let year = dt.year();
let month = dt.month();
let (next_year, next_month) = if month == 12 {
(year + 1, 1u32)
} else {
(year, month + 1)
};
Utc.with_ymd_and_hms(next_year, next_month, 1, 0, 0, 0)
.single()
.map(|d| {
let prev = d - chrono::Duration::days(1);
prev.day()
})
.unwrap_or(30)
}
pub fn from_calendar(
year: i32,
month: u32,
day: u32,
hour: u32,
minute: u32,
second: u32,
) -> Result<BrightDateValue, BrightDateError> {
let dt = Utc
.with_ymd_and_hms(year, month, day, hour, minute, second)
.single()
.ok_or_else(|| {
BrightDateError::InvalidInput(format!(
"invalid calendar date: {year:04}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02}"
))
})?;
from_unix_ms(dt.timestamp_millis() as f64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn start_of_year_2000() {
let bd = start_of_year(2000);
let dt = to_date_time(bd);
assert_eq!(dt.year(), 2000);
assert_eq!(dt.month(), 1);
assert_eq!(dt.day(), 1);
}
#[test]
fn end_of_year_before_next() {
let end_2024 = end_of_year(2024);
let start_2025 = start_of_year(2025);
assert!(end_2024 < start_2025);
}
#[test]
fn month_boundaries_2000_02() {
let start = start_of_month(2000, 2);
let end = end_of_month(2000, 2);
let dt_start = to_date_time(start);
let dt_end = to_date_time(end);
assert_eq!(dt_start.month(), 2);
assert_eq!(dt_end.month(), 2);
assert!(start < end);
}
#[test]
fn leap_year_detection() {
let bd_2000 = start_of_year(2000);
let bd_1900 = start_of_year(1900);
assert!(is_leap_year(bd_2000));
assert!(!is_leap_year(bd_1900));
}
#[test]
fn days_in_month_feb_leap() {
let bd = start_of_month(2000, 2);
assert_eq!(days_in_month(bd), 29);
}
#[test]
fn from_calendar_roundtrip() {
let bd = from_calendar(2024, 6, 15, 12, 0, 0).unwrap();
let dt = to_date_time(bd);
assert_eq!(dt.year(), 2024);
assert_eq!(dt.month(), 6);
assert_eq!(dt.day(), 15);
}
#[test]
fn day_of_week_j2000() {
let bd = 0.0_f64;
assert_eq!(get_day_of_week(bd), 6);
}
}