alpaca-time 0.24.9

New York time and US trading-calendar semantics for the alpaca-rust workspace
Documentation
use chrono::{Datelike, Duration, NaiveDate, Weekday};

use crate::calendar::{add_trading_days, is_trading_date_naive};
use crate::clock::parse_naive_date;
use crate::error::{TimeError, TimeResult};
use crate::types::DateRange;

fn format_date(date: NaiveDate) -> String {
    date.format("%Y-%m-%d").to_string()
}

fn parse_weekday_code(weekday: &str) -> TimeResult<Weekday> {
    match weekday {
        "mon" => Ok(Weekday::Mon),
        "tue" => Ok(Weekday::Tue),
        "wed" => Ok(Weekday::Wed),
        "thu" => Ok(Weekday::Thu),
        "fri" => Ok(Weekday::Fri),
        "sat" => Ok(Weekday::Sat),
        "sun" => Ok(Weekday::Sun),
        _ => Err(TimeError::new(
            "invalid_weekday",
            format!("invalid weekday: {weekday}"),
        )),
    }
}

pub fn add_days(date: &str, days: i32) -> TimeResult<String> {
    let date = parse_naive_date(date)?;
    Ok(format_date(date + Duration::days(days as i64)))
}

pub fn dates(start_date: &str, end_date: &str) -> TimeResult<Vec<String>> {
    let start = parse_naive_date(start_date)?;
    let end = parse_naive_date(end_date)?;

    if start > end {
        return Err(TimeError::new(
            "invalid_date_range",
            format!("start_date must be <= end_date: {start_date} > {end_date}"),
        ));
    }

    let mut values = Vec::new();
    let mut current = start;
    while current <= end {
        values.push(format_date(current));
        current = current
            .succ_opt()
            .ok_or_else(|| TimeError::new("date_overflow", "date overflow while building list"))?;
    }

    Ok(values)
}

pub fn trading_dates(start_date: &str, end_date: &str) -> TimeResult<Vec<String>> {
    Ok(dates(start_date, end_date)?
        .into_iter()
        .filter(|date| {
            parse_naive_date(date)
                .map(is_trading_date_naive)
                .unwrap_or(false)
        })
        .collect())
}

pub fn nth_weekday(year: i32, month: u32, weekday: &str, nth: u32) -> TimeResult<String> {
    if !(1..=12).contains(&month) || nth == 0 {
        return Err(TimeError::new(
            "invalid_weekday_selection",
            format!("invalid nth weekday input: year={year}, month={month}, nth={nth}"),
        ));
    }

    let target_weekday = parse_weekday_code(weekday)?;
    let mut current = NaiveDate::from_ymd_opt(year, month, 1).ok_or_else(|| {
        TimeError::new(
            "invalid_weekday_selection",
            format!("invalid nth weekday input: year={year}, month={month}, nth={nth}"),
        )
    })?;

    while current.weekday() != target_weekday {
        current = current.succ_opt().ok_or_else(|| {
            TimeError::new("date_overflow", "date overflow while selecting weekday")
        })?;
    }

    current += Duration::days(((nth - 1) * 7) as i64);
    if current.month() != month {
        return Err(TimeError::new(
            "invalid_weekday_selection",
            format!("weekday {weekday} #{nth} does not exist in {year}-{month:02}"),
        ));
    }

    Ok(format_date(current))
}

pub fn is_last_trading_date_of_week(date: &str) -> bool {
    let Ok(date) = parse_naive_date(date) else {
        return false;
    };
    if !is_trading_date_naive(date) {
        return false;
    }

    let Ok(next_trading) =
        add_trading_days(&format_date(date), 1).and_then(|value| parse_naive_date(&value))
    else {
        return false;
    };
    let current_week_start = date - Duration::days(date.weekday().num_days_from_monday() as i64);
    let next_week_start =
        next_trading - Duration::days(next_trading.weekday().num_days_from_monday() as i64);
    current_week_start != next_week_start
}

pub fn weekly_last_trading_dates(start_date: &str, end_date: &str) -> TimeResult<Vec<String>> {
    let mut values = Vec::new();
    for date in trading_dates(start_date, end_date)? {
        if is_last_trading_date_of_week(&date) {
            values.push(date);
        }
    }
    Ok(values)
}

pub fn last_trading_date_of_week(date: &str) -> TimeResult<String> {
    let week = calendar_week_range(date)?;
    weekly_last_trading_dates(&week.start_date, &week.end_date)?
        .into_iter()
        .next()
        .ok_or_else(|| {
            TimeError::new(
                "missing_last_trading_date_of_week",
                format!("no trading day found for week containing {date}"),
            )
        })
}

pub fn calendar_week_range(date: &str) -> TimeResult<DateRange> {
    let date = parse_naive_date(date)?;
    let start = date - Duration::days(date.weekday().num_days_from_monday() as i64);
    let end = start + Duration::days(6);
    Ok(DateRange {
        start_date: format_date(start),
        end_date: format_date(end),
    })
}

pub fn iso_week_range(year: i32, week: u32) -> TimeResult<DateRange> {
    let start = NaiveDate::from_isoywd_opt(year, week, Weekday::Mon).ok_or_else(|| {
        TimeError::new(
            "invalid_iso_week",
            format!("invalid iso week: year={year}, week={week}"),
        )
    })?;
    let end = start + Duration::days(6);
    Ok(DateRange {
        start_date: format_date(start),
        end_date: format_date(end),
    })
}