pragma-common 0.7.0

Pragma Common types for Rust
Documentation
mod builder;
mod errors;
#[cfg(feature = "proto")]
mod proto;
mod types;

pub use builder::FuturesContractBuilder;
pub use errors::FuturesContractParseError;
pub use types::{Contract, FuturesContract, FuturesMonth, FuturesRoot};

#[cfg(test)]
mod tests {
    use chrono::{Datelike, NaiveDate};
    use rstest::rstest;

    use super::*;

    #[rstest]
    #[case("plj6", "PL", FuturesMonth::April, 2026, "PLJ26")]
    #[case("NGK26", "NG", FuturesMonth::May, 2026, "NGK26")]
    #[case("RTYM6", "RTY", FuturesMonth::June, 2026, "RTYM26")]
    #[case("6EZ6", "6E", FuturesMonth::December, 2026, "6EZ26")]
    #[case("ABCJ6", "ABC", FuturesMonth::April, 2026, "ABCJ26")]
    fn parses_supported_raw_contracts(
        #[case] raw: &str,
        #[case] root: &str,
        #[case] month: FuturesMonth,
        #[case] year: u16,
        #[case] display: &str,
    ) {
        let contract = FuturesContractBuilder::raw(raw).build().unwrap();

        assert_eq!(contract.root, FuturesRoot::new(root).unwrap());
        assert_eq!(contract.month, month);
        assert_eq!(contract.year, year);
        assert_eq!(contract.to_string(), display);
    }

    #[test]
    fn one_digit_year_normalizes_to_current_or_next_decade() {
        let contract = FuturesContractBuilder::raw("CLZ5").build().unwrap();
        let current_year =
            u16::try_from(chrono::Utc::now().date_naive().year()).expect("year fits in u16");

        assert_eq!(contract.year % 10, 5);
        assert!(contract.year >= current_year);
        assert!(contract.year < current_year + 10);
    }

    #[rstest]
    #[case("GC", FuturesContractParseError::MissingYear)]
    #[case("6", FuturesContractParseError::MissingMonthCode)]
    #[case("GCA6", FuturesContractParseError::InvalidMonthCode('A'))]
    #[case("P.J6", FuturesContractParseError::InvalidRoot("P.".to_string()))]
    #[case("PLA6", FuturesContractParseError::InvalidMonthCode('A'))]
    #[case("PLJ260", FuturesContractParseError::InvalidYear("260".to_string()))]
    fn rejects_invalid_contracts(#[case] raw: &str, #[case] expected: FuturesContractParseError) {
        let error = FuturesContractBuilder::raw(raw).build().unwrap_err();

        match (error, expected) {
            (
                FuturesContractParseError::InvalidYear(actual),
                FuturesContractParseError::InvalidYear(expected),
            ) => assert_eq!(actual, expected),
            (
                FuturesContractParseError::InvalidRoot(actual),
                FuturesContractParseError::InvalidRoot(expected),
            ) => assert_eq!(actual, expected),
            (actual, expected) => assert_eq!(actual, expected),
        }
    }

    #[test]
    fn parses_activ_contracts() {
        let contract = FuturesContractBuilder::activ("GC/26M").unwrap();

        assert_eq!(contract.root, FuturesRoot::new("GC").unwrap());
        assert_eq!(contract.month, FuturesMonth::June);
        assert_eq!(contract.year, 2026);
        assert_eq!(contract.raw_symbol(), "GCM26");
    }

    #[test]
    fn activ_parser_rejects_invalid_month_code() {
        let error = FuturesContractBuilder::activ("GC/26A").unwrap_err();

        assert_eq!(error, FuturesContractParseError::InvalidMonthCode('A'));
    }

    #[test]
    fn contract_enum_exposes_raw_symbol() {
        let contract = Contract::from_raw_symbol("CLK6").unwrap();

        assert_eq!(contract.raw_symbol(), "CLK26");
        assert_eq!(contract.to_string(), "CLK26");
    }

    #[test]
    fn contract_exposes_contract_month_yyyymm() {
        let contract = Contract::from_raw_symbol("CLM6").unwrap();

        assert_eq!(contract.contract_month_yyyymm(), "202606");
    }

    #[test]
    fn contract_validation_accepts_reasonable_reference_date() {
        let contract = FuturesContractBuilder::raw("PLJ6").build().unwrap();
        let reference_date = NaiveDate::from_ymd_opt(2026, 3, 24).unwrap();

        assert_eq!(contract.validate_against_date(reference_date), Ok(()));
    }

    #[test]
    fn contract_validation_rejects_contracts_too_far_in_past() {
        let contract = FuturesContractBuilder::raw("PLG26").build().unwrap();
        let reference_date = NaiveDate::from_ymd_opt(2026, 3, 24).unwrap();

        assert_eq!(
            contract.validate_against_date(reference_date),
            Err(FuturesContractParseError::TooFarInPast {
                contract: "PLG26".to_string(),
                reference_date: reference_date.to_string(),
            })
        );
    }

    #[test]
    fn contract_validation_rejects_contracts_too_far_in_future() {
        let contract = FuturesContractBuilder::raw("PLJ27").build().unwrap();
        let reference_date = NaiveDate::from_ymd_opt(2026, 3, 24).unwrap();

        assert_eq!(
            contract.validate_against_date(reference_date),
            Err(FuturesContractParseError::TooFarInFuture {
                contract: "PLJ27".to_string(),
                reference_date: reference_date.to_string(),
            })
        );
    }
}