trimsec 2.2.1

Calculate time saved on using speed multipliers.
Documentation
use chrono::{Datelike, TimeZone};

use crate::errors::TTimeError;

pub struct TimeConfig {
    pub duration: f64,
    pub multiplier: f64,
    pub splits: i64,
}

impl TimeConfig {
    pub fn new(duration: &str, multiplier_user: &str) -> Result<TimeConfig, TTimeError> {
        let duration_tuple = parse_duration(duration)?;
        let multiplier_value = parse_multiplier(multiplier_user)?;

        Ok(TimeConfig {
            duration: duration_tuple.0,
            multiplier: multiplier_value,
            splits: duration_tuple.1,
        })
    }

    pub fn trim(&self) -> Result<(f64, f64, i64), TTimeError> {
        let old_duration = self.duration;
        let multiplier = self.multiplier;

        let new_duration = old_duration / multiplier;
        let saved_time = old_duration - new_duration;

        Ok((new_duration, saved_time, self.splits))
    }
}

fn parse_multiplier(multiplier_user: &str) -> Result<f64, TTimeError> {
    let multiplier = if let Some(stripped) = multiplier_user.strip_suffix('x') {
        stripped
    } else {
        multiplier_user
    };

    let multiplier_value: f64 = multiplier
        .parse()
        .map_err(|_| TTimeError::InvalidMultiplierFormat)?;

    if !(1.0..100.0).contains(&multiplier_value) {
        Err(TTimeError::MultiplierOutOfRange)
    } else {
        Ok(multiplier_value)
    }
}

#[must_use]
pub fn parse_time(time: f64) -> String {
    let mut time_string = String::new();

    let days = time as u64 / 86400;
    let hours = (time as u64 % 86400) / 3600;
    let minutes = (time as u64 % 3600) / 60;
    let seconds = time as u64 % 60;

    for (i, time) in [days, hours, minutes, seconds].iter().enumerate() {
        if *time != 0 {
            time_string.push_str(&format!(
                "{}{}",
                time,
                match i {
                    0 => "d",
                    1 => "h",
                    2 => "m",
                    3 => "s",
                    _ => "",
                }
            ));
        }
    }

    time_string
}

pub fn parse_duration(duration: &str) -> Result<(f64, i64), TTimeError> {
    let mut total_seconds = 0f64;
    let mut splits = 0;

    for part in duration.split('+') {
        let mut current_number = String::new();
        let mut part_seconds = 0f64;

        for c in part.chars() {
            if c.is_ascii_digit() || c == '.' {
                current_number.push(c);
            } else if c.is_whitespace() {
                continue;
            } else {
                let number: f64 = current_number
                    .parse()
                    .map_err(|_| TTimeError::NegativeDuration)?;
                current_number.clear();
                part_seconds += match c {
                    's' => number,
                    'm' => number * 60.0,
                    'h' => number * 3600.0,
                    'd' => number * 86400.0,
                    _ => return Err(TTimeError::InvalidTimeUnit),
                };
            }
        }

        if !current_number.is_empty() {
            return Err(TTimeError::InvalidDurationFormat);
        }

        total_seconds += part_seconds;
        splits += 1;
    }

    Ok((total_seconds, splits))
}

#[must_use]
pub fn calculate_remaining(trimmed_dur: f64) -> f64 {
    let now = chrono::Local::now();
    let end_of_day = chrono::Local
        .with_ymd_and_hms(now.year(), now.month(), now.day(), 23, 59, 59)
        .unwrap();
    let duration = end_of_day.signed_duration_since(now).num_seconds() as f64;

    if duration > trimmed_dur {
        duration - trimmed_dur
    } else {
        0.0
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_duration() {
        assert_eq!(parse_duration("1s").unwrap(), (1.0, 1));
        assert_eq!(parse_duration("1m").unwrap(), (60.0, 1));
        assert_eq!(parse_duration("1h").unwrap(), (3600.0, 1));
        assert_eq!(parse_duration("1d").unwrap(), (86400.0, 1));
        assert_eq!(parse_duration("1d1h1m1s").unwrap(), (90061.0, 1));
        assert_eq!(parse_duration("1h+1m+1s").unwrap(), (3661.0, 3));
        assert_eq!(parse_duration("1.5h").unwrap(), (5400.0, 1));
        assert_eq!(parse_duration("1.5h+30m").unwrap(), (7200.0, 2));
        assert!(parse_duration("1x").is_err());
        assert!(parse_duration("1").is_err());
    }

    #[test]
    fn test_parse_time() {
        assert_eq!(parse_time(1.0), "1s");
        assert_eq!(parse_time(60.0), "1m");
        assert_eq!(parse_time(3600.0), "1h");
        assert_eq!(parse_time(86400.0), "1d");
        assert_eq!(parse_time(90061.0), "1d1h1m1s");
    }

    #[test]
    fn test_config_new() {
        assert!(TimeConfig::new("1s", "2x").is_ok());
        assert!(TimeConfig::new("1m", "1.5x").is_ok());
        assert!(TimeConfig::new("1h", "1.25x").is_ok());
        assert!(TimeConfig::new("1d", "2x").is_ok());
        assert!(TimeConfig::new("1d1h1m1s", "2x").is_ok());
        assert!(TimeConfig::new("1.5h", "2x").is_ok());
        assert!(TimeConfig::new("1.5h+30m", "2x").is_ok());
        assert!(TimeConfig::new("1x", "2x").is_err());
        assert!(TimeConfig::new("1", "2x").is_err());
    }

    #[test]
    fn test_trim() {
        let config = TimeConfig::new("1d", "2x").unwrap();
        assert_eq!(config.trim().unwrap(), (43200.0, 43200.0, 1));
    }

    #[test]
    fn test_run() {
        let config = TimeConfig::new("1d", "2x").unwrap();
        let result = config.trim().unwrap();
        assert_eq!(parse_time(result.0), "12h");
        assert_eq!(parse_time(result.1), "12h");
        assert_eq!(result.2, 1);
    }
}