use core::panic;
use std::{
cmp::Ordering,
fmt::Display,
ops::{Add, Div, Mul, Rem, Sub},
str::FromStr,
sync::LazyLock,
};
use anyhow::{Error, Result};
use num_traits::{Bounded, FromPrimitive, One, ToPrimitive, Zero};
use rand::{Rng as _, rng};
use regex::Regex;
use crate::{
error::Error::InvalidMonthOfYear,
realtime::cv::{ConstrainedValue, ConstrainedValueParser},
};
pub(crate) static MONTH_RANGE_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^(\+?\d+)\.\.(\+?\d+)$").expect("invalid month range regex"));
pub(crate) static MONTH_REPETITION_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^(\+?\d+)(\.\.(\+?\d+))?/(\+?\d+)$").expect("invalid month repetition regex")
});
pub type Month = ConstrainedValue<MonthOfYear>;
impl Month {
pub(crate) fn first() -> Self {
Month::Specific(vec![MonthOfYear(1)])
}
pub(crate) fn quarterly() -> Self {
Month::Specific(vec![
MonthOfYear(1),
MonthOfYear(4),
MonthOfYear(7),
MonthOfYear(10),
])
}
pub(crate) fn semiannually() -> Self {
Month::Specific(vec![MonthOfYear(1), MonthOfYear(7)])
}
}
impl Default for Month {
fn default() -> Self {
Month::All
}
}
impl TryFrom<&str> for Month {
type Error = Error;
fn try_from(s: &str) -> Result<Self> {
Month::parse(s)
}
}
impl FromStr for Month {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Month::try_from(s)
}
}
impl ConstrainedValueParser<'_, MonthOfYear> for Month {
fn invalid(s: &str) -> Error {
InvalidMonthOfYear(s.to_string()).into()
}
fn allow_rand() -> bool {
true
}
fn all() -> Self {
Month::All
}
fn rand() -> Self {
let rand_month = rng()
.random_range(u8::from(MonthOfYear::min_value())..=u8::from(MonthOfYear::max_value()));
Month::Specific(vec![MonthOfYear(rand_month)])
}
fn repetition_regex() -> Regex {
MONTH_REPETITION_RE.clone()
}
fn range_regex() -> Regex {
MONTH_RANGE_RE.clone()
}
fn rep(start: MonthOfYear, end: Option<MonthOfYear>, rep: u8) -> Self {
Month::Repetition { start, end, rep }
}
fn range(first: MonthOfYear, second: MonthOfYear) -> Self {
Month::Range(first, second)
}
fn specific(values: Vec<MonthOfYear>) -> Self {
Month::Specific(values)
}
}
impl Display for Month {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Month::All => write!(f, "*"),
Month::Specific(values) => {
let mut first = true;
for v in values {
if !first {
write!(f, ",")?;
}
write!(f, "{}", u8::from(*v))?;
first = false;
}
Ok(())
}
Month::Range(start, end) => write!(f, "{}..{}", u8::from(*start), u8::from(*end)),
Month::Repetition { start, end, rep } => {
if let Some(end) = end {
write!(f, "{}..{}/{}", u8::from(*start), u8::from(*end), rep)
} else {
write!(f, "{}/{}", u8::from(*start), rep)
}
}
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct MonthOfYear(pub(crate) u8);
impl Bounded for MonthOfYear {
fn min_value() -> Self {
MonthOfYear(1)
}
fn max_value() -> Self {
MonthOfYear(12)
}
}
impl ToPrimitive for MonthOfYear {
fn to_i64(&self) -> Option<i64> {
Some(<i64 as From<u8>>::from(self.0))
}
fn to_u64(&self) -> Option<u64> {
Some(<u64 as From<u8>>::from(self.0))
}
}
impl FromPrimitive for MonthOfYear {
fn from_i64(n: i64) -> Option<Self> {
if (1..=12).contains(&n) {
Some(MonthOfYear(u8::try_from(n).ok()?))
} else {
None
}
}
fn from_u64(n: u64) -> Option<Self> {
if (1..=12).contains(&n) {
Some(MonthOfYear(u8::try_from(n).ok()?))
} else {
None
}
}
}
impl Zero for MonthOfYear {
fn zero() -> Self {
MonthOfYear(1)
}
fn is_zero(&self) -> bool {
*self == MonthOfYear::zero()
}
}
impl One for MonthOfYear {
fn one() -> Self {
MonthOfYear(2)
}
}
impl Add for MonthOfYear {
type Output = MonthOfYear;
fn add(self, rhs: Self) -> Self::Output {
if self.is_zero() {
rhs
} else if rhs.is_zero() {
self
} else {
let new = MonthOfYear(self.0 + rhs.0 - 1);
if new > MonthOfYear::max_value() {
panic!("MonthOfYear addition overflowed");
} else {
new
}
}
}
}
impl Sub for MonthOfYear {
type Output = MonthOfYear;
fn sub(self, rhs: Self) -> Self::Output {
match rhs.0.cmp(&self.0) {
Ordering::Greater => panic!("MonthOfYear subtraction underflowed"),
Ordering::Equal => MonthOfYear::zero(),
Ordering::Less => MonthOfYear(self.0 - rhs.0 + 1),
}
}
}
impl Mul for MonthOfYear {
type Output = MonthOfYear;
fn mul(self, _rhs: Self) -> Self::Output {
panic!("MonthOfYear multiplication is not supported");
}
}
impl Div for MonthOfYear {
type Output = MonthOfYear;
fn div(self, _rhs: Self) -> Self::Output {
panic!("MonthOfYear division is not supported");
}
}
impl Rem for MonthOfYear {
type Output = MonthOfYear;
fn rem(self, rhs: Self) -> Self::Output {
MonthOfYear(((self.0 - 1) % rhs.0) + 1)
}
}
impl FromStr for MonthOfYear {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
let value = s
.parse::<u8>()
.map_err(|_| InvalidMonthOfYear(s.to_string()))?;
if (1..=12).contains(&value) {
Ok(MonthOfYear(value))
} else {
Err(InvalidMonthOfYear(s.to_string()).into())
}
}
}
impl From<MonthOfYear> for u8 {
fn from(month: MonthOfYear) -> u8 {
month.0
}
}
#[cfg(test)]
pub(crate) mod tests {
use std::{cmp::Ordering, fmt::Write as _, sync::LazyLock};
use anyhow::Result;
use num_traits::{Bounded, FromPrimitive as _, One as _, ToPrimitive as _, Zero as _};
use proptest::{
prelude::{any, proptest},
prop_assume, prop_compose,
};
use rand::{Rng as _, rng};
use regex::Regex;
use crate::realtime::cv::ConstrainedValueMatcher as _;
use super::{MONTH_RANGE_RE, MONTH_REPETITION_RE, Month, MonthOfYear};
pub(crate) static VALID_MONTH_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^\+?(1[0-2]|0?[1-9])$").unwrap());
prop_compose! {
pub(crate) fn month_strategy()(num in any::<u8>(), sign in any::<bool>(), zero_pad in any::<bool>()) -> (String, u8) {
let month = (num % 12) + 1;
let month_str = if sign && zero_pad {
format!("+{month:02}")
} else if sign {
format!("+{month}")
} else if zero_pad{
format!("{month:02}")
} else {
month.to_string()
};
(month_str, month)
}
}
prop_compose! {
fn arb_valid_range()(first in month_strategy(), second in month_strategy()) -> (String, u8, u8) {
let (first_str, first_val) = first;
let (second_str, second_val) = second;
if first_val <= second_val {
(format!("{first_str}..{second_str}"), first_val, second_val)
} else {
(format!("{second_str}..{first_str}"), second_val, first_val)
}
}
}
prop_compose! {
fn arb_valid_repetition()(s in arb_valid_range(), rep in any::<u8>(), sign in any::<bool>()) -> (String, u8, u8, u8) {
let (mut prefix, min, max) = s;
let rep = if rep == 0 { 1 } else { rep };
let rep_str = if sign {
format!("+{rep}")
} else {
rep.to_string()
};
write!(prefix, "/{rep_str}").unwrap();
(prefix, min, max, rep)
}
}
prop_compose! {
fn arb_valid_repetition_no_end()(first in month_strategy(), rep in any::<u8>()) -> String {
let (mut first_str, _) = first;
let rep = if rep == 0 { 1 } else { rep };
write!(first_str, "/{rep}").unwrap();
first_str
}
}
prop_compose! {
pub fn invalid_month_strategy()(num in any::<u8>()) -> String {
let month = if num > 0 && num <= 12 {
num + 12
} else {
num
};
month.to_string()
}
}
prop_compose! {
fn arb_invalid_range()(first in month_strategy(), second in month_strategy()) -> String {
let (_, first_val) = first;
let (_, second_val) = second;
let new_first = if first_val == second_val {
first_val - 1
} else {
first_val
};
match first_val.cmp(&second_val) {
Ordering::Less | Ordering::Equal => format!("{second_val}..{new_first}"),
Ordering::Greater => format!("{new_first}..{second_val}"),
}
}
}
prop_compose! {
fn arb_invalid_repetition()(s in arb_invalid_range(), rep in any::<u8>()) -> String {
let mut prefix = s;
write!(prefix, "/{rep}").unwrap();
prefix
}
}
prop_compose! {
fn arb_invalid_repetition_zero_rep()(s in arb_valid_range()) -> String {
let (mut prefix, _, _) = s;
write!(prefix, "/0").unwrap();
prefix
}
}
proptest! {
#[test]
fn random_input_errors(s in "\\PC*") {
prop_assume!(!VALID_MONTH_RE.is_match(s.as_str()));
prop_assume!(!MONTH_REPETITION_RE.is_match(s.as_str()));
prop_assume!(!MONTH_RANGE_RE.is_match(s.as_str()));
prop_assume!(s.as_str() != "*");
prop_assume!(s.as_str() != "R");
assert!(Month::try_from(s.as_str()).is_err());
assert!(s.parse::<Month>().is_err());
}
#[test]
fn invalid_month_errors(s in invalid_month_strategy()) {
let month_res = Month::try_from(s.as_str());
assert!(month_res.is_err());
let month_res = s.parse::<Month>();
assert!(month_res.is_err());
}
#[test]
fn arb_invalid_range_errors(s in arb_invalid_range()) {
assert!(Month::try_from(s.as_str()).is_err());
assert!(s.parse::<Month>().is_err());
}
#[test]
fn arb_invalid_repetition_zero_rep_errors(s in arb_invalid_repetition_zero_rep()) {
assert!(Month::try_from(s.as_str()).is_err());
assert!(s.parse::<Month>().is_err());
}
}
proptest! {
#[test]
fn arb_valid_month(value in month_strategy()) {
let (month_str, _) = value;
let month_res = Month::try_from(month_str.as_str());
assert!(month_res.is_ok());
let month_res = month_str.parse::<Month>();
assert!(month_res.is_ok());
}
#[test]
fn arb_valid_month_range(s in arb_valid_range()) {
let (s, _, _) = s;
assert!(Month::try_from(s.as_str()).is_ok());
assert!(s.parse::<Month>().is_ok());
}
#[test]
fn arb_valid_month_repetition(s in arb_valid_repetition()) {
let (prefix, _, _, _) = s;
assert!(Month::try_from(prefix.as_str()).is_ok());
assert!(prefix.parse::<Month>().is_ok());
}
#[test]
fn arb_valid_month_repetition_no_end(s in arb_valid_repetition_no_end()) {
assert!(Month::try_from(s.as_str()).is_ok());
assert!(s.parse::<Month>().is_ok());
}
#[test]
fn any_valid_range_matches(s in arb_valid_range()) {
let (range_str, min, max) = s;
prop_assume!(min != max);
match Month::try_from(range_str.as_str()) {
Err(e) => panic!("valid range '{range_str}' failed to parse: {e}"),
Ok(cv_range) => for _ in 0..256 {
let in_range = rng().random_range(min..=max);
assert!(cv_range.matches(MonthOfYear(in_range)), "day {in_range} should match range '{range_str}'");
if min > u8::from(MonthOfYear::min_value()) {
let below = rng().random_range(u8::from(MonthOfYear::min_value())..min);
assert!(!cv_range.matches(MonthOfYear(below)), "day {below} should not match range '{range_str}'");
}
if max + 1 < u8::from(MonthOfYear::max_value()) {
let above = rng().random_range((max + 1)..=u8::from(MonthOfYear::max_value()));
assert!(!cv_range.matches(MonthOfYear(above)), "day {above} should not match range '{range_str}'");
}
},
}
}
}
#[test]
fn empty_string_errors() {
assert!(Month::try_from("").is_err());
assert!("".parse::<Month>().is_err());
}
#[test]
fn all() -> Result<()> {
assert_eq!(Month::All, Month::try_from("*")?);
assert_eq!(Month::All, "*".parse::<Month>()?);
Ok(())
}
#[test]
fn rand_works() {
assert!(Month::try_from("R").is_ok());
assert!("R".parse::<Month>().is_ok());
}
#[test]
#[should_panic(expected = "MonthOfYear addition overflowed")]
fn add_panics_properly() {
let month1 = MonthOfYear(6);
let month2 = MonthOfYear(8);
let _ = month1 + month2;
}
#[test]
#[should_panic(expected = "MonthOfYear subtraction underflowed")]
fn sub_panics_properly() {
let month1 = MonthOfYear(5);
let month2 = MonthOfYear(8);
let _ = month1 - month2;
}
#[test]
#[should_panic(expected = "MonthOfYear multiplication is not supported")]
fn mul_panics_properly() {
let month1 = MonthOfYear(5);
let month2 = MonthOfYear(8);
let _ = month1 * month2;
}
#[test]
#[should_panic(expected = "MonthOfYear division is not supported")]
fn div_panics_properly() {
let month1 = MonthOfYear(5);
let month2 = MonthOfYear(8);
let _ = month1 / month2;
}
#[test]
fn invalid_input_errors() {
assert!("0".parse::<MonthOfYear>().is_err());
}
#[test]
fn sub_works() {
let month = MonthOfYear::zero();
let month1 = MonthOfYear(10);
let month2 = MonthOfYear(3);
let result = month1 - month2;
assert_eq!(MonthOfYear(10), month1 - month);
assert_eq!(result.0, 8);
}
#[test]
fn add_works() {
let month = MonthOfYear::zero();
let month1 = MonthOfYear::one();
let month2 = MonthOfYear(5);
assert_eq!(MonthOfYear(5), month + month2);
assert_eq!(MonthOfYear(5), month2 + month);
assert_eq!(MonthOfYear(6), month1 + month2);
}
#[test]
fn rem_works() {
let month = MonthOfYear::zero();
let month1 = MonthOfYear::one();
let month2 = MonthOfYear(3);
assert_eq!(MonthOfYear(1), month % month1);
assert_eq!(MonthOfYear(2), month1 % month1);
assert_eq!(MonthOfYear(1), month2 % month1);
}
#[test]
fn from_i64_works() -> Result<()> {
for i in 1..=12 {
let month_opt = MonthOfYear::from_i64(i);
assert!(month_opt.is_some());
let month = month_opt.unwrap();
assert_eq!(u8::try_from(i)?, month.0);
}
assert!(MonthOfYear::from_i64(0).is_none());
assert!(MonthOfYear::from_i64(13).is_none());
Ok(())
}
#[test]
fn from_u64_works() {
for i in 1..=12 {
let month_opt = MonthOfYear::from_u64(u64::from(i));
assert!(month_opt.is_some());
let month = month_opt.unwrap();
assert_eq!(i, month.0);
}
assert!(MonthOfYear::from_u64(0).is_none());
assert!(MonthOfYear::from_u64(13).is_none());
}
#[test]
fn to_i64_works() {
for i in 1..=12 {
let month = MonthOfYear(i);
let month_i64_opt = month.to_i64();
assert!(month_i64_opt.is_some());
let month_i64 = month_i64_opt.unwrap();
assert_eq!(i64::from(i), month_i64);
}
}
#[test]
fn to_u64_works() {
for i in 1..=12 {
let month = MonthOfYear(i);
let month_u64_opt = month.to_u64();
assert!(month_u64_opt.is_some());
let month_u64 = month_u64_opt.unwrap();
assert_eq!(u64::from(i), month_u64);
}
}
#[test]
fn u8_from_works() {
for i in 1..=12 {
let month = MonthOfYear(i);
let month_u8 = u8::from(month);
assert_eq!(i, month_u8);
}
}
#[test]
fn matches() {
let month = "1..12/2".parse::<Month>().unwrap();
assert!(month.matches(MonthOfYear(1)));
assert!(!month.matches(MonthOfYear(2)));
assert!(month.matches(MonthOfYear(3)));
assert!(!month.matches(MonthOfYear(4)));
assert!(month.matches(MonthOfYear(5)));
assert!(!month.matches(MonthOfYear(6)));
assert!(month.matches(MonthOfYear(7)));
assert!(!month.matches(MonthOfYear(8)));
assert!(month.matches(MonthOfYear(9)));
assert!(!month.matches(MonthOfYear(10)));
assert!(month.matches(MonthOfYear(11)));
assert!(!month.matches(MonthOfYear(12)));
let month = "1..12/3".parse::<Month>().unwrap();
assert!(month.matches(MonthOfYear(1)));
assert!(!month.matches(MonthOfYear(2)));
assert!(!month.matches(MonthOfYear(3)));
assert!(month.matches(MonthOfYear(4)));
assert!(!month.matches(MonthOfYear(5)));
assert!(!month.matches(MonthOfYear(6)));
assert!(month.matches(MonthOfYear(7)));
assert!(!month.matches(MonthOfYear(8)));
assert!(!month.matches(MonthOfYear(9)));
assert!(month.matches(MonthOfYear(10)));
assert!(!month.matches(MonthOfYear(11)));
assert!(!month.matches(MonthOfYear(12)));
let month = "1..12/4".parse::<Month>().unwrap();
assert!(month.matches(MonthOfYear(1)));
assert!(!month.matches(MonthOfYear(2)));
assert!(!month.matches(MonthOfYear(3)));
assert!(!month.matches(MonthOfYear(4)));
assert!(month.matches(MonthOfYear(5)));
assert!(!month.matches(MonthOfYear(6)));
assert!(!month.matches(MonthOfYear(7)));
assert!(!month.matches(MonthOfYear(8)));
assert!(month.matches(MonthOfYear(9)));
assert!(!month.matches(MonthOfYear(10)));
assert!(!month.matches(MonthOfYear(11)));
assert!(!month.matches(MonthOfYear(12)));
let month = "2..10/3".parse::<Month>().unwrap();
assert!(!month.matches(MonthOfYear(1)));
assert!(month.matches(MonthOfYear(2)));
assert!(!month.matches(MonthOfYear(3)));
assert!(!month.matches(MonthOfYear(4)));
assert!(month.matches(MonthOfYear(5)));
assert!(!month.matches(MonthOfYear(6)));
assert!(!month.matches(MonthOfYear(7)));
assert!(month.matches(MonthOfYear(8)));
assert!(!month.matches(MonthOfYear(9)));
assert!(!month.matches(MonthOfYear(10)));
assert!(!month.matches(MonthOfYear(11)));
assert!(!month.matches(MonthOfYear(12)));
}
#[test]
fn default_works() {
let default_month = Month::default();
assert_eq!(Month::All, default_month);
}
#[test]
fn first_works() {
let first_month = Month::first();
assert!(first_month.matches(MonthOfYear(1)));
for month in 2..=12 {
assert!(!first_month.matches(MonthOfYear(month)));
}
}
#[test]
fn quarterly_works() {
let quarterly = Month::quarterly();
assert!(quarterly.matches(MonthOfYear(1)));
assert!(!quarterly.matches(MonthOfYear(2)));
assert!(!quarterly.matches(MonthOfYear(3)));
assert!(quarterly.matches(MonthOfYear(4)));
assert!(!quarterly.matches(MonthOfYear(5)));
assert!(!quarterly.matches(MonthOfYear(6)));
assert!(quarterly.matches(MonthOfYear(7)));
assert!(!quarterly.matches(MonthOfYear(8)));
assert!(!quarterly.matches(MonthOfYear(9)));
assert!(quarterly.matches(MonthOfYear(10)));
assert!(!quarterly.matches(MonthOfYear(11)));
assert!(!quarterly.matches(MonthOfYear(12)));
}
#[test]
fn semiannually_works() {
let semiannual = Month::semiannually();
assert!(semiannual.matches(MonthOfYear(1)));
assert!(!semiannual.matches(MonthOfYear(2)));
assert!(!semiannual.matches(MonthOfYear(3)));
assert!(!semiannual.matches(MonthOfYear(4)));
assert!(!semiannual.matches(MonthOfYear(5)));
assert!(!semiannual.matches(MonthOfYear(6)));
assert!(semiannual.matches(MonthOfYear(7)));
assert!(!semiannual.matches(MonthOfYear(8)));
assert!(!semiannual.matches(MonthOfYear(9)));
assert!(!semiannual.matches(MonthOfYear(10)));
assert!(!semiannual.matches(MonthOfYear(11)));
assert!(!semiannual.matches(MonthOfYear(12)));
}
#[test]
fn display_works() {
let month = Month::All;
assert_eq!("*", month.to_string());
let month = Month::Specific(vec![MonthOfYear(1), MonthOfYear(3), MonthOfYear(12)]);
assert_eq!("1,3,12", month.to_string());
let month = Month::Range(MonthOfYear(2), MonthOfYear(8));
assert_eq!("2..8", month.to_string());
let month = Month::Repetition {
start: MonthOfYear(1),
end: Some(MonthOfYear(12)),
rep: 3,
};
assert_eq!("1..12/3", month.to_string());
let month = Month::Repetition {
start: MonthOfYear(4),
end: None,
rep: 2,
};
assert_eq!("4/2", month.to_string());
}
}