use numrs2::prelude::*;
use numrs2::types::datetime::array_ops;
use numrs2::types::datetime::business_days;
fn day(s: &str) -> DateTime64 {
DateTime64::from_iso_string(s, DateTimeUnit::Day).expect("should parse test date")
}
#[test]
fn test_busday_offset_roll_forward_snaps_saturday_to_monday() {
let result = business_days::busday_offset(&day("2026-08-15"), 0, Some("forward"), None)
.expect("should roll forward");
assert_eq!(result.value(), day("2026-08-17").value());
let alias = business_days::busday_offset(&day("2026-08-15"), 0, Some("following"), None)
.expect("should roll via alias");
assert_eq!(alias.value(), day("2026-08-17").value());
}
#[test]
fn test_busday_offset_roll_backward_snaps_saturday_to_friday() {
let result = business_days::busday_offset(&day("2026-08-15"), 0, Some("backward"), None)
.expect("should roll backward");
assert_eq!(result.value(), day("2026-08-14").value());
let alias = business_days::busday_offset(&day("2026-08-15"), 0, Some("preceding"), None)
.expect("should roll via alias");
assert_eq!(alias.value(), day("2026-08-14").value());
}
#[test]
fn test_busday_offset_modifiedfollowing_crosses_month_rolls_backward() {
let result =
business_days::busday_offset(&day("2026-10-31"), 0, Some("modifiedfollowing"), None)
.expect("should roll modifiedfollowing");
assert_eq!(result.value(), day("2026-10-30").value());
}
#[test]
fn test_busday_offset_modifiedfollowing_without_crossing_behaves_like_forward() {
let result =
business_days::busday_offset(&day("2026-08-15"), 0, Some("modifiedfollowing"), None)
.expect("should roll modifiedfollowing");
assert_eq!(result.value(), day("2026-08-17").value());
}
#[test]
fn test_busday_offset_modifiedpreceding_crosses_month_rolls_forward() {
let result =
business_days::busday_offset(&day("2026-11-01"), 0, Some("modifiedpreceding"), None)
.expect("should roll modifiedpreceding");
assert_eq!(result.value(), day("2026-11-02").value());
}
#[test]
fn test_busday_offset_modifiedpreceding_without_crossing_behaves_like_backward() {
let result =
business_days::busday_offset(&day("2026-08-15"), 0, Some("modifiedpreceding"), None)
.expect("should roll modifiedpreceding");
assert_eq!(result.value(), day("2026-08-14").value());
}
#[test]
fn test_busday_offset_roll_raise_errors_on_non_business_day() {
assert!(business_days::busday_offset(&day("2026-08-15"), 0, None, None).is_err());
assert!(business_days::busday_offset(&day("2026-08-15"), 0, Some("raise"), None).is_err());
assert!(business_days::busday_offset(&day("2026-08-17"), 0, Some("raise"), None).is_ok());
}
#[test]
fn test_busday_offset_roll_nat_returns_nat_sentinel() {
let result = business_days::busday_offset(&day("2026-08-15"), 0, Some("nat"), None)
.expect("roll='nat' must not error");
assert!(result.is_nat());
let formatted = datetime_as_string(&result, None, None).expect("NaT should format, not error");
assert_eq!(formatted, "NaT");
let valid = business_days::busday_offset(&day("2026-08-17"), 0, Some("nat"), None)
.expect("Monday is already valid");
assert!(!valid.is_nat());
}
#[test]
fn test_busday_offset_hops_over_weekends() {
let forward = business_days::busday_offset(&day("2026-08-14"), 1, Some("raise"), None)
.expect("should offset");
assert_eq!(forward.value(), day("2026-08-17").value());
let backward = business_days::busday_offset(&day("2026-08-17"), -1, Some("raise"), None)
.expect("should offset");
assert_eq!(backward.value(), day("2026-08-14").value());
let five = business_days::busday_offset(&day("2026-08-17"), 5, Some("raise"), None)
.expect("should offset");
assert_eq!(five.value(), day("2026-08-24").value());
let zero = business_days::busday_offset(&day("2026-08-17"), 0, Some("raise"), None)
.expect("should offset");
assert_eq!(zero.value(), day("2026-08-17").value());
}
#[test]
fn test_busday_offset_holidays_affect_both_rolling_and_stepping() {
let holidays = [day("2026-08-21")];
let rolled =
business_days::busday_offset(&day("2026-08-21"), 0, Some("forward"), Some(&holidays))
.expect("should roll past the holiday");
assert_eq!(rolled.value(), day("2026-08-24").value());
let tuesday_holiday = [day("2026-08-18")];
let stepped =
business_days::busday_offset(&day("2026-08-17"), 2, Some("raise"), Some(&tuesday_holiday))
.expect("should step past the holiday");
assert_eq!(stepped.value(), day("2026-08-20").value());
}
#[test]
fn test_busday_offset_unknown_roll_errors() {
assert!(business_days::busday_offset(&day("2026-08-17"), 0, Some("sideways"), None).is_err());
}
#[test]
fn test_busday_offset_array_applies_roll_per_element() {
let dts = Array::from_vec(vec![
day("2026-08-15"),
day("2026-08-15"),
day("2026-08-17"),
]);
let offsets = Array::from_vec(vec![0i32, 0i32, 1i32]);
let forward =
array_ops::busday_offset_array(&dts, &offsets, Some("forward")).expect("should not error");
let forward_vals: Vec<i64> = forward.to_vec().iter().map(|d| d.value()).collect();
assert_eq!(
forward_vals,
vec![
day("2026-08-17").value(), day("2026-08-17").value(), day("2026-08-18").value(), ]
);
let backward = array_ops::busday_offset_array(
&Array::from_vec(vec![day("2026-08-15")]),
&Array::from_vec(vec![0i32]),
Some("backward"),
)
.expect("should not error");
assert_eq!(backward.to_vec()[0].value(), day("2026-08-14").value());
}
#[test]
fn test_busday_offset_array_length_mismatch_errors() {
let dts = Array::from_vec(vec![day("2026-08-17")]);
let offsets = Array::from_vec(vec![0i32, 1i32]);
assert!(array_ops::busday_offset_array(&dts, &offsets, Some("raise")).is_err());
}
#[test]
fn test_busday_count_monday_to_friday_same_week_is_four() {
let count = business_days::busday_count(&day("2026-08-17"), &day("2026-08-21"), None)
.expect("should count");
assert_eq!(count, 4);
}
#[test]
fn test_busday_count_over_a_weekend() {
let count = business_days::busday_count(&day("2026-08-14"), &day("2026-08-18"), None)
.expect("should count");
assert_eq!(count, 2);
}
#[test]
fn test_busday_count_reversed_is_negative() {
let forward = business_days::busday_count(&day("2026-08-17"), &day("2026-08-21"), None)
.expect("should count");
let reversed = business_days::busday_count(&day("2026-08-21"), &day("2026-08-17"), None)
.expect("should count reversed");
assert_eq!(reversed, -forward);
assert_eq!(reversed, -4);
}
#[test]
fn test_busday_count_with_holiday_excluded() {
let holidays = [day("2026-08-19")];
let count =
business_days::busday_count(&day("2026-08-17"), &day("2026-08-21"), Some(&holidays))
.expect("should count excluding holiday");
assert_eq!(count, 3);
let weekend_holiday = [day("2026-08-15")]; let count_weekend_holiday = business_days::busday_count(
&day("2026-08-17"),
&day("2026-08-21"),
Some(&weekend_holiday),
)
.expect("should count");
assert_eq!(count_weekend_holiday, 4);
}
#[test]
fn test_busday_count_nat_endpoint_errors() {
let nat = DateTime64::nat(DateTimeUnit::Day);
assert!(business_days::busday_count(&nat, &day("2026-08-21"), None).is_err());
assert!(business_days::busday_count(&day("2026-08-17"), &nat, None).is_err());
}
#[test]
fn test_busday_count_array_matches_scalar_per_element() {
let begins = Array::from_vec(vec![day("2026-08-17"), day("2026-08-21")]);
let ends = Array::from_vec(vec![day("2026-08-21"), day("2026-08-17")]);
let counts = array_ops::busday_count_array(&begins, &ends).expect("should count array");
assert_eq!(counts.to_vec(), vec![4i64, -4i64]);
}
#[test]
fn test_datetime_as_string_naive_default_matches_explicit_naive() {
let dt = datetime64("2023-06-15T08:09:10", Some("s")).expect("should parse");
let via_none = datetime_as_string(&dt, None, None).expect("naive via None");
let via_explicit = datetime_as_string(&dt, None, Some("naive")).expect("naive explicit");
assert_eq!(via_none, via_explicit);
assert_eq!(via_none, "2023-06-15T08:09:10");
}
#[test]
fn test_datetime_as_string_utc_vs_naive_differ_only_by_suffix() {
let dt = datetime64("2023-06-15T08:09:10", Some("s")).expect("should parse");
let naive = datetime_as_string(&dt, None, None).expect("naive");
let utc = datetime_as_string(&dt, None, Some("UTC")).expect("utc");
assert_eq!(format!("{naive}Z"), utc);
}
#[test]
fn test_datetime_as_string_local_matches_independently_computed_chrono_offset() {
use chrono::{DateTime as ChronoDateTime, Local, Utc};
let dt = datetime64("2023-06-15T12:00:00", Some("s")).expect("should parse");
let s = datetime_as_string(&dt, None, Some("local")).expect("should format local");
let utc_dt: ChronoDateTime<Utc> = ChronoDateTime::from_timestamp(
DateTime64::from_iso_string("2023-06-15T12:00:00", DateTimeUnit::Second)
.expect("should parse reference instant")
.value(),
0,
)
.expect("valid timestamp");
let local_dt = utc_dt.with_timezone(&Local);
let offset_seconds = local_dt.offset().local_minus_utc();
let sign = if offset_seconds >= 0 { '+' } else { '-' };
let abs_secs = offset_seconds.unsigned_abs();
let expected_suffix = format!("{sign}{:02}{:02}", abs_secs / 3600, (abs_secs % 3600) / 60);
assert!(
s.ends_with(&expected_suffix),
"expected suffix '{expected_suffix}' at the end of '{s}'"
);
assert!(
!s.contains('Z'),
"local-formatted string must not contain Z: {s}"
);
}
#[test]
fn test_datetime_as_string_unknown_timezone_is_an_error() {
let dt = datetime64("2023-01-01", Some("D")).expect("should parse");
assert!(datetime_as_string(&dt, None, Some("not-a-timezone")).is_err());
}