use date_tuple::DateTuple;
use date_utils;
use regex::Regex;
use std::cmp::Ordering;
use std::convert::From;
use std::fmt;
use std::str::FromStr;
const MONTH_STRINGS: [&str; 12] = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
pub type Month = MonthTuple;
#[cfg_attr(
feature = "serde_support",
derive(serde::Serialize, serde::Deserialize)
)]
#[derive(PartialEq, Eq, Debug, Copy, Clone, Hash)]
pub struct MonthTuple {
y: u16,
m: u8,
}
impl MonthTuple {
pub fn new(y: u16, m: u8) -> Result<MonthTuple, String> {
if (1..=12).contains(&m) {
if y <= 9999 {
Ok(MonthTuple { y, m })
} else {
Err(format!(
"Invalid year in MonthTuple: {:?}\nYear must be <= 9999.",
MonthTuple { y, m }
))
}
} else {
Err(format!(
"Invalid month in MonthTuple: {:?}\nMonth must be between 1 and 12; Note that months are ONE-BASED since version 2.0.0.",
MonthTuple { y, m }
))
}
}
pub fn this_month() -> MonthTuple {
date_utils::now_as_monthtuple()
}
pub fn get_year(self) -> u16 {
self.y
}
pub fn get_month(self) -> u8 {
self.m
}
pub fn next_month(self) -> MonthTuple {
if self.y == 9999 && self.m == 12 {
return self;
}
if self.m == 12 {
MonthTuple {
y: self.y + 1,
m: 1,
}
} else {
MonthTuple {
y: self.y,
m: self.m + 1,
}
}
}
pub fn previous_month(self) -> MonthTuple {
if self.y == 0 && self.m == 1 {
return self;
}
if self.m == 1 {
MonthTuple {
y: self.y - 1,
m: 12,
}
} else {
MonthTuple {
y: self.y,
m: self.m - 1,
}
}
}
pub fn add_months(&mut self, months: u32) {
for _ in 0..months {
*self = self.next_month();
}
}
pub fn subtract_months(&mut self, months: u32) {
for _ in 0..months {
*self = self.previous_month();
}
}
pub fn add_years(&mut self, years: u16) {
let mut new_years = self.y + years;
if new_years > 9999 {
new_years = 9999;
}
self.y = new_years;
}
pub fn subtract_years(&mut self, years: u16) {
let mut new_years = i32::from(self.y) - i32::from(years);
if new_years < 0 {
new_years = 0;
}
self.y = new_years as u16;
}
pub fn to_readable_string(self) -> String {
match MONTH_STRINGS.iter().nth(self.m as usize - 1) {
Some(s) => return format!("{} {:04}", s, self.y),
None => panic!("Invalid MonthTuple: {:?}", self),
}
}
}
impl fmt::Display for MonthTuple {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:04}-{:02}", self.y, self.m)
}
}
impl FromStr for MonthTuple {
type Err = String;
fn from_str(s: &str) -> Result<MonthTuple, Self::Err> {
lazy_static! {
static ref VALID_FORMAT: Regex = Regex::new(r"^\d{4}-\d{2}$").unwrap();
static ref LEGACY_FORMAT: Regex = Regex::new(r"^\d{6}$").unwrap();
}
if VALID_FORMAT.is_match(s) {
match MonthTuple::new(
u16::from_str(&s[0..4]).unwrap(),
u8::from_str(&s[5..7]).unwrap(),
) {
Ok(m) => Ok(m),
Err(e) => Err(format!("Invalid month passed to from_str: {}", e)),
}
} else if LEGACY_FORMAT.is_match(s) {
let (s1, s2) = s.split_at(4);
match MonthTuple::new(u16::from_str(s1).unwrap(), u8::from_str(s2).unwrap()) {
Ok(m) => Ok(m),
Err(e) => Err(format!("Invalid month passed to from_str: {}", e)),
}
} else {
Err(format!(
"Invalid str formatting of MonthTuple: {}\nExpects a string formatted like 2018-11",
s
))
}
}
}
impl PartialOrd for MonthTuple {
fn partial_cmp(&self, other: &MonthTuple) -> Option<Ordering> {
if self.y == other.y {
self.m.partial_cmp(&other.m)
} else {
self.y.partial_cmp(&other.y)
}
}
}
#[cfg_attr(tarpaulin, skip)]
impl Ord for MonthTuple {
fn cmp(&self, other: &MonthTuple) -> Ordering {
if self.y == other.y {
self.m.cmp(&other.m)
} else {
self.y.cmp(&other.y)
}
}
}
impl From<DateTuple> for MonthTuple {
fn from(date: DateTuple) -> Self {
MonthTuple {
y: date.get_year(),
m: date.get_month(),
}
}
}
#[cfg(test)]
mod tests {
use super::MonthTuple;
use date_tuple::DateTuple;
#[test]
fn test_next_month() {
let tuple1 = MonthTuple::new(2000, 5).unwrap();
let tuple2 = MonthTuple::new(2000, 12).unwrap();
let tuple3 = MonthTuple::new(9999, 12).unwrap();
assert_eq!(MonthTuple { y: 2000, m: 6 }, tuple1.next_month());
assert_eq!(MonthTuple { y: 2001, m: 1 }, tuple2.next_month());
assert_eq!(tuple3, tuple3.next_month());
}
#[test]
fn test_previous_month() {
let tuple1 = MonthTuple::new(2000, 5).unwrap();
let tuple2 = MonthTuple::new(2000, 1).unwrap();
let tuple3 = MonthTuple::new(0, 1).unwrap();
assert_eq!(MonthTuple { y: 2000, m: 4 }, tuple1.previous_month());
assert_eq!(MonthTuple { y: 1999, m: 12 }, tuple2.previous_month());
assert_eq!(tuple3, tuple3.previous_month());
}
#[test]
#[should_panic]
fn test_to_readable_string_panic() {
let tuple = MonthTuple { y: 2000, m: 13 };
tuple.to_readable_string();
}
#[test]
fn test_from_date() {
let date = DateTuple::new(2000, 5, 10).unwrap();
assert_eq!(MonthTuple { y: 2000, m: 5 }, MonthTuple::from(date));
}
}