pragma-common 0.7.0

Pragma Common types for Rust
Documentation
use std::fmt;

use chrono::Datelike;

use super::{FuturesContractBuilder, FuturesContractParseError};

const MAX_MONTHS_IN_PAST: i32 = 0;
const MAX_MONTHS_IN_FUTURE: i32 = 12;
const MAX_FUTURES_ROOT_LEN: usize = 16;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
    feature = "borsh",
    derive(borsh::BorshSerialize, borsh::BorshDeserialize)
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub enum Contract {
    Futures(FuturesContract),
}

impl Contract {
    pub fn from_raw_symbol(symbol: &str) -> Result<Self, FuturesContractParseError> {
        FuturesContractBuilder::raw(symbol)
            .build()
            .map(Contract::Futures)
    }

    pub fn from_cme_symbol(symbol: &str) -> Result<Self, FuturesContractParseError> {
        Self::from_raw_symbol(symbol)
    }

    pub fn from_activ_symbol(symbol: &str) -> Result<Self, FuturesContractParseError> {
        FuturesContractBuilder::activ(symbol).map(Contract::Futures)
    }

    pub fn raw_symbol(&self) -> String {
        match self {
            Self::Futures(contract) => contract.raw_symbol(),
        }
    }

    pub fn contract_month_yyyymm(&self) -> String {
        match self {
            Self::Futures(contract) => contract.contract_month_yyyymm(),
        }
    }

    pub fn validate_against_date(
        &self,
        date: chrono::NaiveDate,
    ) -> Result<(), FuturesContractParseError> {
        match self {
            Self::Futures(contract) => contract.validate_against_date(date),
        }
    }
}

impl fmt::Display for Contract {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.raw_symbol())
    }
}

impl std::str::FromStr for Contract {
    type Err = FuturesContractParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::from_raw_symbol(s)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
    feature = "borsh",
    derive(borsh::BorshSerialize, borsh::BorshDeserialize)
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct FuturesContract {
    #[cfg_attr(feature = "utoipa", schema(value_type = String))]
    pub root: FuturesRoot,
    pub month: FuturesMonth,
    pub year: u16,
}

impl FuturesContract {
    pub fn raw_symbol(&self) -> String {
        format!(
            "{}{}{:02}",
            self.root.cme_code(),
            self.month.code(),
            self.year % 100
        )
    }

    pub fn contract_month_yyyymm(&self) -> String {
        format!("{}{:02}", self.year, self.month.number())
    }

    pub fn validate_against_date(
        &self,
        date: chrono::NaiveDate,
    ) -> Result<(), FuturesContractParseError> {
        let contract_month_index = contract_month_index(self.year, self.month);
        let reference_month_index = date_month_index(date);
        let months_from_reference = contract_month_index - reference_month_index;

        if months_from_reference < -MAX_MONTHS_IN_PAST {
            return Err(FuturesContractParseError::TooFarInPast {
                contract: self.raw_symbol(),
                reference_date: date.to_string(),
            });
        }

        if months_from_reference > MAX_MONTHS_IN_FUTURE {
            return Err(FuturesContractParseError::TooFarInFuture {
                contract: self.raw_symbol(),
                reference_date: date.to_string(),
            });
        }

        Ok(())
    }
}

impl fmt::Display for FuturesContract {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.raw_symbol())
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(
    feature = "borsh",
    derive(borsh::BorshSerialize, borsh::BorshDeserialize)
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct FuturesRoot {
    len: u8,
    bytes: [u8; MAX_FUTURES_ROOT_LEN],
}

impl FuturesRoot {
    pub fn new(root: &str) -> Result<Self, FuturesContractParseError> {
        let normalized = root.trim();
        if normalized.is_empty()
            || normalized.len() > MAX_FUTURES_ROOT_LEN
            || !normalized.bytes().all(|byte| byte.is_ascii_alphanumeric())
        {
            return Err(FuturesContractParseError::InvalidRoot(root.to_string()));
        }

        let mut bytes = [0; MAX_FUTURES_ROOT_LEN];
        for (output, input) in bytes.iter_mut().zip(normalized.bytes()) {
            *output = input.to_ascii_uppercase();
        }

        Ok(Self {
            len: u8::try_from(normalized.len()).expect("MAX_FUTURES_ROOT_LEN must fit in u8"),
            bytes,
        })
    }

    pub fn cme_code(&self) -> &str {
        std::str::from_utf8(&self.bytes[..usize::from(self.len)])
            .expect("FuturesRoot is validated as ASCII")
    }
}

impl fmt::Display for FuturesRoot {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.cme_code())
    }
}

impl std::str::FromStr for FuturesRoot {
    type Err = FuturesContractParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::new(s)
    }
}

#[cfg(feature = "serde")]
impl serde::Serialize for FuturesRoot {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(self.cme_code())
    }
}

#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for FuturesRoot {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let root = <String as serde::Deserialize>::deserialize(deserializer)?;
        Self::new(&root).map_err(serde::de::Error::custom)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
    feature = "borsh",
    derive(borsh::BorshSerialize, borsh::BorshDeserialize)
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub enum FuturesMonth {
    January,
    February,
    March,
    April,
    May,
    June,
    July,
    August,
    September,
    October,
    November,
    December,
}

impl FuturesMonth {
    pub fn code(self) -> char {
        match self {
            Self::January => 'F',
            Self::February => 'G',
            Self::March => 'H',
            Self::April => 'J',
            Self::May => 'K',
            Self::June => 'M',
            Self::July => 'N',
            Self::August => 'Q',
            Self::September => 'U',
            Self::October => 'V',
            Self::November => 'X',
            Self::December => 'Z',
        }
    }

    pub fn from_code(code: char) -> Option<Self> {
        match code.to_ascii_uppercase() {
            'F' => Some(Self::January),
            'G' => Some(Self::February),
            'H' => Some(Self::March),
            'J' => Some(Self::April),
            'K' => Some(Self::May),
            'M' => Some(Self::June),
            'N' => Some(Self::July),
            'Q' => Some(Self::August),
            'U' => Some(Self::September),
            'V' => Some(Self::October),
            'X' => Some(Self::November),
            'Z' => Some(Self::December),
            _ => None,
        }
    }

    pub const fn number(self) -> u8 {
        match self {
            Self::January => 1,
            Self::February => 2,
            Self::March => 3,
            Self::April => 4,
            Self::May => 5,
            Self::June => 6,
            Self::July => 7,
            Self::August => 8,
            Self::September => 9,
            Self::October => 10,
            Self::November => 11,
            Self::December => 12,
        }
    }
}

impl fmt::Display for FuturesMonth {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.code())
    }
}

fn contract_month_index(year: u16, month: FuturesMonth) -> i32 {
    i32::from(year) * 12 + i32::from(month.number())
}

fn date_month_index(date: chrono::NaiveDate) -> i32 {
    date.year() * 12 + i32::try_from(date.month()).expect("chrono month must fit in i32")
}