use super::validate::{validate_date, validate_doy};
use crate::{
errors::{
out_of_range::{create_conditional_oor, create_simple_oor},
AstrolabeError,
},
util::leap::{is_leap_year, leap_years},
};
pub(crate) fn days_to_date(days: i32) -> (i32, u32, u32) {
const LEAPOCH: i64 = 730_179;
const DAYS_PER_400Y: i64 = 365 * 400 + 97;
const DAYS_PER_100Y: i64 = 365 * 100 + 24;
const DAYS_PER_4Y: i64 = 365 * 4 + 1;
const MONTH_DAYS: [i64; 12] = [31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 29];
let days = days as i64 - LEAPOCH;
let mut qc_cycles = days / DAYS_PER_400Y;
let mut remdays = days % DAYS_PER_400Y;
if remdays.is_negative() {
remdays += DAYS_PER_400Y;
qc_cycles -= 1;
}
let mut c_cycles = remdays / DAYS_PER_100Y;
if c_cycles == 4 {
c_cycles -= 1;
}
remdays -= c_cycles * DAYS_PER_100Y;
let q_cycles = remdays / DAYS_PER_4Y;
remdays -= q_cycles * DAYS_PER_4Y;
let mut remyears = remdays / 365;
if remyears == 4 {
remyears -= 1;
}
let mut year = 2000 + remyears + 4 * q_cycles + 100 * c_cycles + 400 * qc_cycles;
remdays -= remyears * 365;
let mut mon = 0;
for mdays in MONTH_DAYS.iter() {
mon += 1;
if remdays < *mdays {
break;
}
remdays -= *mdays;
}
let mday = remdays + 1;
let mon = if mon + 2 > 12 {
year += 1;
mon - 10
} else {
mon + 2
};
if year < 1 {
year -= 1;
}
(year as i32, mon as u32, mday as u32)
}
pub(crate) fn date_to_days(year: i32, month: u32, day: u32) -> Result<i32, AstrolabeError> {
validate_date(year, month, day)?;
let leap_years = leap_years(year);
let (mut doy, mdays) = year_month_to_doy(year, month)?;
if day > mdays || day == 0 {
return Err(create_conditional_oor(
"day",
1,
mdays as i128,
day as i128,
format!("because month is {}", month),
));
}
doy += day - 1;
Ok(if year.is_negative() {
doy = if is_leap_year(year) { 366 } else { 365 } - doy;
(year + 1) * 365 - leap_years as i32 - doy as i32
} else {
(year.abs() - 1) * 365 + leap_years as i32 + doy as i32
})
}
pub(crate) fn year_doy_to_days(
year: i32,
mut doy: u32,
ignore_leap: bool,
) -> Result<i32, AstrolabeError> {
validate_doy(year, doy)?;
let leap_years = leap_years(year);
doy -= 1;
if ignore_leap && is_leap_year(year) && doy >= 60 {
doy += 1;
}
Ok(if year.is_negative() {
doy = if is_leap_year(year) { 366 } else { 365 } - doy;
(year + 1) * 365 - leap_years as i32 - doy as i32
} else {
(year.abs() - 1) * 365 + leap_years as i32 + doy as i32
})
}
pub(crate) fn year_month_to_doy(year: i32, month: u32) -> Result<(u32, u32), AstrolabeError> {
let is_leap_year = is_leap_year(year);
if is_leap_year {
Ok(match month {
1 => (0, 31),
2 => (31, 29),
3 => (60, 31),
4 => (91, 30),
5 => (121, 31),
6 => (152, 30),
7 => (182, 31),
8 => (213, 31),
9 => (244, 30),
10 => (274, 31),
11 => (305, 30),
12 => (335, 31),
_ => return Err(create_simple_oor("month", 1, 12, month as i128)),
})
} else {
Ok(match month {
1 => (0, 31),
2 => (31, 28),
3 => (59, 31),
4 => (90, 30),
5 => (120, 31),
6 => (151, 30),
7 => (181, 31),
8 => (212, 31),
9 => (243, 30),
10 => (273, 31),
11 => (304, 30),
12 => (334, 31),
_ => return Err(create_simple_oor("month", 1, 12, month as i128)),
})
}
}
pub(crate) fn days_to_doy(days: i32) -> u32 {
let (year, month, day) = days_to_date(days);
let (doy, _) = year_month_to_doy(year, month).unwrap();
doy + day
}
pub(crate) fn days_to_wday(days: i32, monday_first: bool) -> u32 {
(days.unsigned_abs() % 7 + if monday_first { 0 } else { 1 }) % 7
}
pub(crate) fn weekdays_in_month(year: i32, month: u32, weekday: u8) -> Vec<u32> {
let (_, days) = year_month_to_doy(year, month).unwrap();
let start_days = date_to_days(year, month, 1).unwrap();
let mut weekday_index = 0;
for index in 0..=6 {
if days_to_wday(start_days + index as i32, false) == weekday as u32 {
weekday_index = index;
break;
}
}
let mut weekdays = Vec::new();
for index in 0..=5 {
let day = weekday_index + 1 + index * 7;
if day <= days {
weekdays.push(day);
} else {
break;
}
}
weekdays
}
pub(crate) fn days_to_wyear(days: i32) -> u32 {
let (year, month, day) = days_to_date(days);
let month = month as i32;
let day = day as i32;
let a = if month <= 2 { year - 1 } else { year };
let b = a / 4 - a / 100 + a / 400;
let c = (a - 1) / 4 - (a - 1) / 100 + (a - 1) / 400;
let s = b - c;
let e = if month <= 2 { 0 } else { s + 1 };
let f = if month <= 2 {
day - 1 + 31 * (month - 1)
} else {
day + (153 * (month - 3) + 2) / 5 + 58 + s
};
let g = (a + b) % 7;
let d = (f + g - e) % 7;
let n = f + 3 - d;
match n {
n if n.is_negative() => (53 - (g - s) / 5) as u32,
n if n > 364 + s => 1,
_ => (n / 7 + 1) as u32,
}
}
pub(crate) fn years_between(
first_days: i32,
first_nanos: u64,
second_days: i32,
second_nanos: u64,
) -> i32 {
let first_year = days_to_date(first_days).0;
let first_doy = days_to_doy(first_days);
let second_year = days_to_date(second_days).0;
let second_doy = days_to_doy(second_days);
let mut years_between = first_year - second_year;
if first_year >= 1 && second_year < 1 {
years_between -= 1
} else if first_year < 1 && second_year >= 1 {
years_between += 1
};
let extra_year = if years_between == 0 {
0
} else if first_year > second_year
&& (first_doy < second_doy || (first_doy == second_doy && first_nanos < second_nanos))
{
-1
} else if first_year < second_year
&& (first_doy > second_doy || (first_doy == second_doy && first_nanos > second_nanos))
{
1
} else {
0
};
years_between + extra_year
}
pub(crate) fn months_between(
first_days: i32,
first_nanos: u64,
second_days: i32,
second_nanos: u64,
) -> i32 {
let (first_year, first_month, first_day) = days_to_date(first_days);
let (second_year, second_month, second_day) = days_to_date(second_days);
let mut years_between = first_year - second_year;
if first_year >= 1 && second_year < 1 {
years_between -= 1
} else if first_year < 1 && second_year >= 1 {
years_between += 1
};
let months_between = years_between * 12 + first_month as i32 - second_month as i32;
let extra_month = if months_between == 0 {
0
} else if first_year > second_year
&& (first_day < second_day || (first_day == second_day && first_nanos < second_nanos))
{
-1
} else if first_year < second_year
&& (first_day > second_day || (first_day == second_day && first_nanos > second_nanos))
{
1
} else {
0
};
months_between + extra_month
}