use std::{cmp::Ordering, fmt, str::FromStr};
#[derive(Debug, Clone)]
pub struct Interval {
pub value: String,
months: i32,
days: i32,
microseconds: i64,
}
impl Interval {
pub fn new(value: String) -> Self {
let (months, days, microseconds) = Self::parse_interval(&value);
Interval { value, months, days, microseconds }
}
fn parse_interval(s: &str) -> (i32, i32, i64) {
let mut months = 0;
let mut days = 0;
let mut microseconds = 0i64;
let parts: Vec<&str> = s.split_whitespace().collect();
if parts.is_empty() {
return (0, 0, 0);
}
if let Some(to_pos) = parts.iter().position(|&p| p.eq_ignore_ascii_case("TO")) {
if to_pos >= 2 {
let value_part = parts[0];
let from_unit = parts[to_pos - 1];
let to_unit = parts[to_pos + 1];
if from_unit.eq_ignore_ascii_case("YEAR") && to_unit.eq_ignore_ascii_case("MONTH") {
if let Some(dash_pos) = value_part.find('-') {
let years: i32 = value_part[..dash_pos].parse().unwrap_or(0);
let month_part: i32 = value_part[dash_pos + 1..].parse().unwrap_or(0);
months = years * 12 + month_part;
} else {
let years: i32 = value_part.parse().unwrap_or(0);
months = years * 12;
}
}
else if from_unit.eq_ignore_ascii_case("DAY") {
if let Some(space_pos) = value_part.find(' ') {
days = value_part[..space_pos].parse().unwrap_or(0);
let time_part = value_part[space_pos + 1..].trim();
microseconds = Self::parse_time_to_microseconds(time_part);
} else {
days = value_part.parse().unwrap_or(0);
}
}
else if from_unit.eq_ignore_ascii_case("HOUR")
|| from_unit.eq_ignore_ascii_case("MINUTE")
|| from_unit.eq_ignore_ascii_case("SECOND")
{
microseconds = Self::parse_time_to_microseconds(value_part);
}
}
} else {
if parts.len() >= 2 {
let value_part = parts[0];
let unit = parts[1];
match unit.to_uppercase().as_str() {
"YEAR" | "YEARS" => {
let years: i32 = value_part.parse().unwrap_or(0);
months = years * 12;
}
"MONTH" | "MONTHS" => {
months = value_part.parse().unwrap_or(0);
}
"DAY" | "DAYS" => {
days = value_part.parse().unwrap_or(0);
}
"HOUR" | "HOURS" => {
let hours: i64 = value_part.parse().unwrap_or(0);
microseconds = hours * 3600 * 1_000_000;
}
"MINUTE" | "MINUTES" => {
let minutes: i64 = value_part.parse().unwrap_or(0);
microseconds = minutes * 60 * 1_000_000;
}
"SECOND" | "SECONDS" => {
microseconds = Self::parse_seconds_to_microseconds(value_part);
}
_ => {}
}
}
}
(months, days, microseconds)
}
fn parse_time_to_microseconds(s: &str) -> i64 {
let parts: Vec<&str> = s.split(':').collect();
let mut total_microseconds = 0i64;
if !parts.is_empty() {
if let Ok(hours) = parts[0].parse::<i64>() {
total_microseconds += hours * 3600 * 1_000_000;
}
}
if parts.len() > 1 {
if let Ok(minutes) = parts[1].parse::<i64>() {
total_microseconds += minutes * 60 * 1_000_000;
}
}
if parts.len() > 2 {
total_microseconds += Self::parse_seconds_to_microseconds(parts[2]);
}
total_microseconds
}
fn parse_seconds_to_microseconds(s: &str) -> i64 {
if let Some(dot_pos) = s.find('.') {
let whole: i64 = s[..dot_pos].parse().unwrap_or(0);
let frac_str = &s[dot_pos + 1..];
let frac_str_padded = format!("{:0<6}", frac_str);
let frac: i64 = frac_str_padded[..6].parse().unwrap_or(0);
whole * 1_000_000 + frac
} else {
s.parse::<i64>().unwrap_or(0) * 1_000_000
}
}
fn cmp_value(&self) -> i128 {
let total_days = (self.months as i64) * 30 + (self.days as i64);
let days_in_microseconds = total_days as i128 * 86_400_000_000i128;
days_in_microseconds + (self.microseconds as i128)
}
}
impl FromStr for Interval {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Interval::new(s.to_string()))
}
}
impl fmt::Display for Interval {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.value)
}
}
impl PartialOrd for Interval {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Interval {
fn cmp(&self, other: &Self) -> Ordering {
self.cmp_value().cmp(&other.cmp_value())
}
}
impl PartialEq for Interval {
fn eq(&self, other: &Self) -> bool {
self.months == other.months
&& self.days == other.days
&& self.microseconds == other.microseconds
}
}
impl Eq for Interval {}
impl std::hash::Hash for Interval {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.months.hash(state);
self.days.hash(state);
self.microseconds.hash(state);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_interval_comparison_simple_years() {
let i1 = Interval::new("1 YEAR".to_string());
let i2 = Interval::new("2 YEAR".to_string());
assert!(i1 < i2, "1 YEAR should be less than 2 YEAR");
}
#[test]
fn test_interval_comparison_simple_months() {
let i1 = Interval::new("1 MONTH".to_string());
let i2 = Interval::new("6 MONTH".to_string());
assert!(i1 < i2, "1 MONTH should be less than 6 MONTH");
}
#[test]
fn test_interval_comparison_simple_days() {
let i1 = Interval::new("1 DAY".to_string());
let i2 = Interval::new("30 DAY".to_string());
assert!(i1 < i2, "1 DAY should be less than 30 DAY");
}
#[test]
fn test_interval_comparison_month_vs_days() {
let i1 = Interval::new("1 MONTH".to_string());
let i2 = Interval::new("30 DAY".to_string());
assert_ne!(i1, i2, "1 MONTH and 30 DAY have different representations");
assert_eq!(
i1.cmp(&i2),
Ordering::Equal,
"1 MONTH should compare equal to 30 DAY (approximation)"
);
}
#[test]
fn test_interval_comparison_year_vs_months() {
let i1 = Interval::new("1 YEAR".to_string());
let i2 = Interval::new("12 MONTH".to_string());
assert_eq!(i1, i2, "1 YEAR should equal 12 MONTH");
}
#[test]
fn test_interval_comparison_year_vs_days() {
let i1 = Interval::new("1 YEAR".to_string());
let i2 = Interval::new("360 DAY".to_string());
assert_ne!(i1, i2, "1 YEAR and 360 DAY have different representations");
assert_eq!(
i1.cmp(&i2),
Ordering::Equal,
"1 YEAR should compare equal to 360 DAY (approximation)"
);
}
#[test]
fn test_interval_comparison_hours() {
let i1 = Interval::new("1 HOUR".to_string());
let i2 = Interval::new("2 HOUR".to_string());
assert!(i1 < i2, "1 HOUR should be less than 2 HOUR");
}
#[test]
fn test_interval_comparison_minutes() {
let i1 = Interval::new("30 MINUTE".to_string());
let i2 = Interval::new("90 MINUTE".to_string());
assert!(i1 < i2, "30 MINUTE should be less than 90 MINUTE");
}
#[test]
fn test_interval_comparison_seconds() {
let i1 = Interval::new("45 SECOND".to_string());
let i2 = Interval::new("90 SECOND".to_string());
assert!(i1 < i2, "45 SECOND should be less than 90 SECOND");
}
#[test]
fn test_interval_comparison_year_to_month() {
let i1 = Interval::new("1-6 YEAR TO MONTH".to_string());
let i2 = Interval::new("18 MONTH".to_string());
assert_eq!(i1, i2, "1-6 YEAR TO MONTH should equal 18 MONTH");
}
#[test]
fn test_interval_comparison_year_to_month_ordering() {
let i1 = Interval::new("1-0 YEAR TO MONTH".to_string());
let i2 = Interval::new("1-6 YEAR TO MONTH".to_string());
let i3 = Interval::new("2-0 YEAR TO MONTH".to_string());
assert!(i1 < i2, "1-0 should be less than 1-6");
assert!(i2 < i3, "1-6 should be less than 2-0");
}
#[test]
fn test_interval_comparison_cross_type_month_vs_day() {
let i1 = Interval::new("1 MONTH".to_string());
let i2 = Interval::new("31 DAY".to_string());
assert!(i1 < i2, "1 MONTH should be less than 31 DAY");
}
#[test]
fn test_interval_comparison_cross_type_month_vs_day_greater() {
let i1 = Interval::new("1 MONTH".to_string());
let i2 = Interval::new("29 DAY".to_string());
assert!(i1 > i2, "1 MONTH should be greater than 29 DAY");
}
#[test]
fn test_interval_comparison_equality() {
let i1 = Interval::new("5 YEAR".to_string());
let i2 = Interval::new("5 YEAR".to_string());
assert_eq!(i1, i2, "Same intervals should be equal");
}
#[test]
fn test_interval_parsing_zero() {
let i = Interval::new("0 DAY".to_string());
assert_eq!(i.days, 0);
assert_eq!(i.months, 0);
assert_eq!(i.microseconds, 0);
}
#[test]
fn test_interval_ordering_total() {
let mut intervals = [
Interval::new("2 YEAR".to_string()),
Interval::new("1 MONTH".to_string()),
Interval::new("1 DAY".to_string()),
Interval::new("1 YEAR".to_string()),
Interval::new("30 DAY".to_string()),
];
intervals.sort();
assert_eq!(intervals[0].value, "1 DAY");
assert!(
(intervals[1].value == "1 MONTH" && intervals[2].value == "30 DAY")
|| (intervals[1].value == "30 DAY" && intervals[2].value == "1 MONTH")
);
assert_eq!(intervals[3].value, "1 YEAR");
assert_eq!(intervals[4].value, "2 YEAR");
}
#[test]
fn test_interval_cmp_value_calculation() {
let i1 = Interval::new("1 YEAR".to_string());
let expected = 360i128 * 86_400_000_000i128;
assert_eq!(i1.cmp_value(), expected);
let i2 = Interval::new("1 DAY".to_string());
let expected = 86_400_000_000i128;
assert_eq!(i2.cmp_value(), expected);
let i3 = Interval::new("1 HOUR".to_string());
let expected = 3_600_000_000i128;
assert_eq!(i3.cmp_value(), expected);
}
#[test]
fn test_interval_from_str() {
let i: Interval = "5 YEAR".parse().unwrap();
assert_eq!(i.value, "5 YEAR");
assert_eq!(i.months, 60); }
#[test]
fn test_interval_display() {
let i = Interval::new("5 YEAR".to_string());
assert_eq!(format!("{}", i), "5 YEAR");
}
#[test]
fn test_interval_parsing_fractional_seconds() {
let i = Interval::new("1.5 SECOND".to_string());
assert_eq!(i.microseconds, 1_500_000); }
#[test]
fn test_interval_comparison_with_fractional_seconds() {
let i1 = Interval::new("1.5 SECOND".to_string());
let i2 = Interval::new("2 SECOND".to_string());
assert!(i1 < i2, "1.5 SECOND should be less than 2 SECOND");
}
#[test]
fn test_interval_hash_consistency() {
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
fn calculate_hash<T: Hash>(t: &T) -> u64 {
let mut s = DefaultHasher::new();
t.hash(&mut s);
s.finish()
}
let i1 = Interval::new("1 YEAR".to_string());
let i2 = Interval::new("12 MONTH".to_string());
assert_eq!(i1, i2, "1 YEAR should equal 12 MONTH");
assert_eq!(
calculate_hash(&i1),
calculate_hash(&i2),
"Equal intervals must have equal hash values"
);
let i3 = Interval::new("1-6 YEAR TO MONTH".to_string());
let i4 = Interval::new("18 MONTH".to_string());
assert_eq!(i3, i4, "1-6 YEAR TO MONTH should equal 18 MONTH");
assert_eq!(
calculate_hash(&i3),
calculate_hash(&i4),
"Equal intervals must have equal hash values"
);
let i5 = Interval::new("1 YEAR".to_string());
let i6 = Interval::new("2 YEAR".to_string());
assert_ne!(i5, i6, "1 YEAR should not equal 2 YEAR");
}
}