use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Tenor(u32);
impl Tenor {
pub const ON: Tenor = Tenor(1);
pub const W1: Tenor = Tenor(7);
pub const M1: Tenor = Tenor(30);
pub const M2: Tenor = Tenor(60);
pub const M3: Tenor = Tenor(91);
pub const M6: Tenor = Tenor(182);
pub const Y1: Tenor = Tenor(365);
pub const Y2: Tenor = Tenor(730);
pub const Y3: Tenor = Tenor(1095);
pub const Y5: Tenor = Tenor(1826);
pub const Y7: Tenor = Tenor(2555);
pub const Y10: Tenor = Tenor(3650);
pub const Y20: Tenor = Tenor(7300);
pub const Y30: Tenor = Tenor(10950);
#[inline]
pub const fn days(d: u32) -> Self {
Self(d)
}
#[inline]
pub const fn weeks(w: u32) -> Self {
Self(w * 7)
}
#[inline]
pub const fn months(m: u32) -> Self {
Self(m * 30)
}
#[inline]
pub const fn years(y: u32) -> Self {
Self(y * 365)
}
#[inline]
pub const fn as_days(self) -> u32 {
self.0
}
}
impl From<u32> for Tenor {
#[inline]
fn from(days: u32) -> Self {
Self(days)
}
}
impl fmt::Display for Tenor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
1 => write!(f, "ON"),
7 => write!(f, "1W"),
30 => write!(f, "1M"),
60 => write!(f, "2M"),
91 => write!(f, "3M"),
182 => write!(f, "6M"),
365 => write!(f, "1Y"),
730 => write!(f, "2Y"),
1095 => write!(f, "3Y"),
1826 => write!(f, "5Y"),
2555 => write!(f, "7Y"),
3650 => write!(f, "10Y"),
7300 => write!(f, "20Y"),
10950 => write!(f, "30Y"),
d => {
if d % 365 == 0 {
write!(f, "{}Y", d / 365)
} else if d % 30 == 0 {
write!(f, "{}M", d / 30)
} else if d % 7 == 0 {
write!(f, "{}W", d / 7)
} else {
write!(f, "{}D", d)
}
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TenorParseError(String);
impl fmt::Display for TenorParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"invalid tenor '{}': expected <n>D, <n>W, <n>M, or <n>Y (e.g. 10Y, 3M, 45D)",
self.0
)
}
}
impl std::error::Error for TenorParseError {}
impl FromStr for Tenor {
type Err = TenorParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
if s.is_empty() {
return Err(TenorParseError(s.to_string()));
}
if s.eq_ignore_ascii_case("ON") {
return Ok(Tenor::ON);
}
let bytes = s.as_bytes();
let suffix = *bytes.last().unwrap(); let prefix = &s[..s.len() - 1];
if prefix.is_empty() {
return Err(TenorParseError(s.to_string()));
}
let n: u32 = prefix.parse().map_err(|_| TenorParseError(s.to_string()))?;
match suffix.to_ascii_uppercase() {
b'D' => Ok(Tenor::days(n)),
b'W' => Ok(Tenor::weeks(n)),
b'M' => Ok(Tenor::months(n)),
b'Y' => Ok(Tenor::years(n)),
_ => Err(TenorParseError(s.to_string())),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constant_on() {
assert_eq!(Tenor::ON.as_days(), 1);
}
#[test]
fn constant_w1() {
assert_eq!(Tenor::W1.as_days(), 7);
}
#[test]
fn constant_m3() {
assert_eq!(Tenor::M3.as_days(), 91);
}
#[test]
fn constant_y10() {
assert_eq!(Tenor::Y10.as_days(), 3650);
}
#[test]
fn constant_y30() {
assert_eq!(Tenor::Y30.as_days(), 10950);
}
#[test]
fn constructor_days() {
assert_eq!(Tenor::days(45).as_days(), 45);
}
#[test]
fn constructor_weeks() {
assert_eq!(Tenor::weeks(2).as_days(), 14);
}
#[test]
fn constructor_months() {
assert_eq!(Tenor::months(3).as_days(), 90);
assert_ne!(Tenor::months(3), Tenor::M3);
}
#[test]
fn constructor_years() {
assert_eq!(Tenor::years(10).as_days(), 3650);
assert_eq!(Tenor::years(10), Tenor::Y10);
}
#[test]
fn from_u32() {
let t: Tenor = 3650_u32.into();
assert_eq!(t, Tenor::Y10);
}
#[test]
fn display_named_constants() {
assert_eq!(Tenor::ON.to_string(), "ON");
assert_eq!(Tenor::W1.to_string(), "1W");
assert_eq!(Tenor::M3.to_string(), "3M");
assert_eq!(Tenor::Y10.to_string(), "10Y");
assert_eq!(Tenor::Y30.to_string(), "30Y");
}
#[test]
fn display_ad_hoc_days() {
assert_eq!(Tenor::days(45).to_string(), "45D");
}
#[test]
fn display_ad_hoc_weeks() {
assert_eq!(Tenor::days(14).to_string(), "2W");
}
#[test]
fn display_ad_hoc_months() {
assert_eq!(Tenor::days(120).to_string(), "4M");
}
#[test]
fn display_ad_hoc_years() {
assert_eq!(Tenor::days(1460).to_string(), "4Y");
}
#[test]
fn parse_years() {
assert_eq!("10Y".parse::<Tenor>().unwrap(), Tenor::Y10);
assert_eq!("1Y".parse::<Tenor>().unwrap(), Tenor::Y1);
}
#[test]
fn parse_months() {
assert_eq!("6M".parse::<Tenor>().unwrap(), Tenor::months(6));
}
#[test]
fn parse_days() {
assert_eq!("45D".parse::<Tenor>().unwrap(), Tenor::days(45));
}
#[test]
fn parse_weeks() {
assert_eq!("2W".parse::<Tenor>().unwrap(), Tenor::weeks(2));
}
#[test]
fn parse_overnight() {
assert_eq!("ON".parse::<Tenor>().unwrap(), Tenor::ON);
assert_eq!("on".parse::<Tenor>().unwrap(), Tenor::ON);
}
#[test]
fn parse_case_insensitive() {
assert_eq!("10y".parse::<Tenor>().unwrap(), Tenor::Y10);
assert_eq!("3m".parse::<Tenor>().unwrap(), Tenor::months(3));
assert_eq!("45d".parse::<Tenor>().unwrap(), Tenor::days(45));
assert_eq!("2w".parse::<Tenor>().unwrap(), Tenor::weeks(2));
}
#[test]
fn parse_invalid_empty() {
assert!("".parse::<Tenor>().is_err());
}
#[test]
fn parse_invalid_no_suffix() {
assert!("365".parse::<Tenor>().is_err());
}
#[test]
fn parse_invalid_no_number() {
assert!("Y".parse::<Tenor>().is_err());
}
#[test]
fn parse_invalid_unknown_suffix() {
assert!("10Q".parse::<Tenor>().is_err());
}
#[test]
fn round_trip_named_constants() {
for tenor in &[
Tenor::ON, Tenor::W1, Tenor::M1, Tenor::M2, Tenor::Y1, Tenor::Y2, Tenor::Y3, Tenor::Y7, Tenor::Y10, Tenor::Y20, Tenor::Y30, ] {
let s = tenor.to_string();
let parsed: Tenor = s
.parse()
.unwrap_or_else(|_| panic!("round-trip failed for {s}"));
assert_eq!(parsed, *tenor, "round-trip mismatch for {s}");
}
}
#[test]
fn treasury_knot_display_asymmetry() {
assert_eq!(Tenor::M3.to_string(), "3M");
assert_ne!("3M".parse::<Tenor>().unwrap(), Tenor::M3);
assert_eq!(Tenor::M6.to_string(), "6M");
assert_ne!("6M".parse::<Tenor>().unwrap(), Tenor::M6);
assert_eq!(Tenor::Y5.to_string(), "5Y");
assert_ne!("5Y".parse::<Tenor>().unwrap(), Tenor::Y5);
}
#[test]
fn round_trip_ad_hoc() {
let t = Tenor::days(45);
let s = t.to_string(); let parsed: Tenor = s.parse().unwrap();
assert_eq!(parsed, t);
}
#[test]
fn ordering() {
assert!(Tenor::M1 < Tenor::M3);
assert!(Tenor::Y1 < Tenor::Y10);
assert!(Tenor::Y10 < Tenor::Y30);
}
}