greed 0.6.0

A rust tool to automate trades 📈
use crate::date::NaiveDateTimeConvert;
use crate::error::GreedError;
use chrono::{DateTime, Duration, NaiveDate, NaiveDateTime, Utc};
use std::ops::Range;

#[derive(Debug, Default, PartialEq)]
pub struct HistoryRange {
    pub from: Option<String>,
    pub to: Option<String>,
    pub last_hour: bool,
    pub hours: Option<i64>,
    pub days: Option<i64>,
}

impl HistoryRange {
    pub fn resolve(&self, now: DateTime<Utc>) -> Result<Range<DateTime<Utc>>, GreedError> {
        if self.last_hour {
            return Ok((now - Duration::hours(1))..now);
        }
        if let Some(hours) = self.hours {
            return Ok((now - Duration::hours(hours))..now);
        }
        if let Some(days) = self.days {
            return Ok((now - Duration::days(days))..now);
        }
        if let Some(from) = &self.from {
            let start = parse_date_time(from)?;
            let end = match &self.to {
                Some(to) => parse_date_time(to)?,
                None => now,
            };
            return Ok(start..end);
        }
        Ok((now - Duration::days(1))..now)
    }
}

fn parse_date_time(value: &str) -> Result<DateTime<Utc>, GreedError> {
    if let Ok(naive) = NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M") {
        return Ok(naive.to_utc());
    }
    if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%m-%d") {
        let naive = date.and_hms_opt(0, 0, 0).expect("midnight is always valid");
        return Ok(naive.to_utc());
    }
    Err(GreedError::new(&format!(
        "unable to parse '{value}' as a date/time; expected format '%Y-%m-%d %H:%M' or '%Y-%m-%d'"
    )))
}

#[cfg(test)]
mod test {
    use super::*;
    use chrono::TimeZone;

    fn now() -> DateTime<Utc> {
        Utc.with_ymd_and_hms(2026, 10, 17, 12, 0, 0).unwrap()
    }

    fn range_args() -> HistoryRange {
        HistoryRange::default()
    }

    #[test]
    fn resolve_defaults_to_last_day() {
        let range = range_args().resolve(now()).expect("resolve should succeed");
        assert_eq!(range, (now() - Duration::days(1))..now());
    }

    #[test]
    fn resolve_last_hour() {
        let args = HistoryRange {
            last_hour: true,
            ..range_args()
        };
        let range = args.resolve(now()).expect("resolve should succeed");
        assert_eq!(range, (now() - Duration::hours(1))..now());
    }

    #[test]
    fn resolve_hours() {
        let args = HistoryRange {
            hours: Some(3),
            ..range_args()
        };
        let range = args.resolve(now()).expect("resolve should succeed");
        assert_eq!(range, (now() - Duration::hours(3))..now());
    }

    #[test]
    fn resolve_days() {
        let args = HistoryRange {
            days: Some(5),
            ..range_args()
        };
        let range = args.resolve(now()).expect("resolve should succeed");
        assert_eq!(range, (now() - Duration::days(5))..now());
    }

    #[test]
    fn resolve_explicit_from_only_defaults_end_to_now() {
        let args = HistoryRange {
            from: Some("2026-10-01".to_string()),
            ..range_args()
        };
        let range = args.resolve(now()).expect("resolve should succeed");
        let expected_start = Utc.with_ymd_and_hms(2026, 10, 1, 0, 0, 0).unwrap();
        assert_eq!(range, expected_start..now());
    }

    #[test]
    fn resolve_explicit_from_and_to() {
        let args = HistoryRange {
            from: Some("2026-10-01".to_string()),
            to: Some("2026-10-02 14:30".to_string()),
            ..range_args()
        };
        let range = args.resolve(now()).expect("resolve should succeed");
        let expected_start = Utc.with_ymd_and_hms(2026, 10, 1, 0, 0, 0).unwrap();
        let expected_end = Utc.with_ymd_and_hms(2026, 10, 2, 14, 30, 0).unwrap();
        assert_eq!(range, expected_start..expected_end);
    }

    #[test]
    fn resolve_rejects_unparseable_from() {
        let args = HistoryRange {
            from: Some("not-a-date".to_string()),
            ..range_args()
        };
        assert!(args.resolve(now()).is_err());
    }

    #[test]
    fn parse_date_time_accepts_date_and_time() {
        let parsed = parse_date_time("2026-10-01 08:15").expect("should parse");
        assert_eq!(parsed, Utc.with_ymd_and_hms(2026, 10, 1, 8, 15, 0).unwrap());
    }

    #[test]
    fn parse_date_time_accepts_date_only_as_midnight() {
        let parsed = parse_date_time("2026-10-01").expect("should parse");
        assert_eq!(parsed, Utc.with_ymd_and_hms(2026, 10, 1, 0, 0, 0).unwrap());
    }

    #[test]
    fn parse_date_time_rejects_garbage() {
        assert!(parse_date_time("garbage").is_err());
    }
}