use crate::errors::QlResult;
use crate::time::date::{Date, Month, Year};
use crate::time::weekday::Weekday;
use crate::types::Integer;
const ALL_MONTH_CODES: &[u8] = b"FGHJKMNQUVXZ";
const MAIN_CYCLE_CODES: &[u8] = b"HMUZ";
pub fn is_asx_date(date: Date, main_cycle: bool) -> bool {
if date.weekday() != Weekday::Friday {
return false;
}
let d = date.day_of_month();
if !(8..=14).contains(&d) {
return false;
}
if !main_cycle {
return true;
}
matches!(
date.month(),
Month::March | Month::June | Month::September | Month::December
)
}
pub fn is_asx_code(code: &str, main_cycle: bool) -> bool {
let bytes = code.as_bytes();
if bytes.len() != 2 {
return false;
}
if !bytes[1].is_ascii_digit() {
return false;
}
let valid = if main_cycle {
MAIN_CYCLE_CODES
} else {
ALL_MONTH_CODES
};
valid.contains(&bytes[0].to_ascii_uppercase())
}
pub fn code(asx_date: Date) -> QlResult<String> {
crate::require!(
is_asx_date(asx_date, false),
"{asx_date} is not an ASX date"
);
let month_code = ALL_MONTH_CODES[(asx_date.month().ordinal() - 1) as usize] as char;
Ok(format!("{month_code}{}", asx_date.year() % 10))
}
pub fn date(asx_code: &str, reference_date: Date) -> QlResult<Date> {
crate::require!(
is_asx_code(asx_code, false),
"{asx_code} is not a valid ASX code"
);
let bytes = asx_code.as_bytes();
let m = match ALL_MONTH_CODES
.iter()
.position(|&c| c == bytes[0].to_ascii_uppercase())
{
Some(index) => Month::from_ordinal(index as Integer + 1),
None => crate::fail!("invalid ASX month letter. code: {asx_code}"),
};
let mut y = (bytes[1] - b'0') as Year;
if y == 0 && reference_date.year() <= 1909 {
y += 10;
}
y += reference_date.year() - reference_date.year() % 10;
let result = next_date(Date::new(1, m, y), false);
if result >= reference_date {
Ok(result)
} else {
crate::require!(
y + 10 <= Date::max_date().year(),
"no ASX date matching {asx_code} on or after {reference_date} \
within the supported date range"
);
Ok(next_date(Date::new(1, m, y + 10), false))
}
}
pub fn next_date(date: Date, main_cycle: bool) -> Date {
let mut y = date.year();
let mut m = date.month().ordinal();
let offset: Integer = if main_cycle { 3 } else { 1 };
let mut skip_months = offset - m % offset;
if skip_months != offset || date.day_of_month() > 14 {
skip_months += m;
if skip_months <= 12 {
m = skip_months;
} else {
m = skip_months - 12;
y += 1;
}
}
let result = Date::nth_weekday(2, Weekday::Friday, Month::from_ordinal(m), y);
if result <= date {
next_date(Date::new(15, Month::from_ordinal(m), y), main_cycle)
} else {
result
}
}
pub fn next_date_from_code(
asx_code: &str,
main_cycle: bool,
reference_date: Date,
) -> QlResult<Date> {
let asx_date = date(asx_code, reference_date)?;
Ok(next_date(asx_date + 1, main_cycle))
}
pub fn next_code(date: Date, main_cycle: bool) -> QlResult<String> {
code(next_date(date, main_cycle))
}
pub fn next_code_from_code(
asx_code: &str,
main_cycle: bool,
reference_date: Date,
) -> QlResult<String> {
code(next_date_from_code(asx_code, main_cycle, reference_date)?)
}
#[cfg(test)]
mod tests {
use super::*;
const ASX_CODES: [&str; 120] = [
"F0", "G0", "H0", "J0", "K0", "M0", "N0", "Q0", "U0", "V0", "X0", "Z0", "F1", "G1", "H1",
"J1", "K1", "M1", "N1", "Q1", "U1", "V1", "X1", "Z1", "F2", "G2", "H2", "J2", "K2", "M2",
"N2", "Q2", "U2", "V2", "X2", "Z2", "F3", "G3", "H3", "J3", "K3", "M3", "N3", "Q3", "U3",
"V3", "X3", "Z3", "F4", "G4", "H4", "J4", "K4", "M4", "N4", "Q4", "U4", "V4", "X4", "Z4",
"F5", "G5", "H5", "J5", "K5", "M5", "N5", "Q5", "U5", "V5", "X5", "Z5", "F6", "G6", "H6",
"J6", "K6", "M6", "N6", "Q6", "U6", "V6", "X6", "Z6", "F7", "G7", "H7", "J7", "K7", "M7",
"N7", "Q7", "U7", "V7", "X7", "Z7", "F8", "G8", "H8", "J8", "K8", "M8", "N8", "Q8", "U8",
"V8", "X8", "Z8", "F9", "G9", "H9", "J9", "K9", "M9", "N9", "Q9", "U9", "V9", "X9", "Z9",
];
#[test]
fn asx_dates_from_2000_to_2040() {
let last = Date::new(1, Month::January, 2040);
let mut counter = Date::new(1, Month::January, 2000);
while counter <= last {
let asx = next_date(counter, false);
assert!(asx > counter, "{asx} is not greater than {counter}",);
assert!(
is_asx_date(asx, false),
"{asx} is not an ASX date (calculated from {counter})",
);
assert!(
asx <= next_date(counter, true),
"{asx} is not less than or equal to the next future in the main cycle {}",
next_date(counter, true),
);
assert_eq!(
date(&code(asx).unwrap(), counter).unwrap(),
asx,
"{} at calendar day {counter} is not the ASX code matching {asx}",
code(asx).unwrap(),
);
for asx_code in &ASX_CODES {
assert!(
date(asx_code, counter).unwrap() >= counter,
"{} is wrong for {asx_code} at reference date {counter}",
date(asx_code, counter).unwrap(),
);
}
counter += 1;
}
}
#[test]
fn asx_specific_dates() {
let date_2024 = Date::new(12, Month::January, 2024);
assert_eq!(date_2024.weekday(), Weekday::Friday);
assert!(is_asx_date(date_2024, false));
assert!(!is_asx_date(date_2024, true));
assert_eq!(
next_date_from_code("F2", false, Date::new(1, Month::January, 2000)).unwrap(),
Date::new(8, Month::February, 2002),
);
assert_eq!(
next_date_from_code("K3", true, Date::new(1, Month::January, 2014)).unwrap(),
Date::new(9, Month::June, 2023),
);
assert_eq!(
next_code(Date::new(1, Month::January, 2024), false).unwrap(),
"F4"
);
assert_eq!(
next_code(Date::new(15, Month::January, 2024), false).unwrap(),
"G4"
);
assert_eq!(
next_code(Date::new(15, Month::January, 2024), true).unwrap(),
"H4"
);
assert_eq!(
next_code_from_code("F4", false, Date::new(1, Month::January, 2020)).unwrap(),
"G4"
);
assert_eq!(
next_code_from_code("Z4", true, Date::new(1, Month::January, 2020)).unwrap(),
"H5"
);
}
#[test]
fn date_is_rejected_beyond_the_supported_range() {
let reference = Date::new(31, Month::December, 2199);
assert!(date("F9", reference).is_err());
}
}