use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Timeframe {
pub value: u32,
pub unit: TimeframeUnit,
}
impl PartialOrd for Timeframe {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Timeframe {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.to_seconds().cmp(&other.to_seconds())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum TimeframeUnit {
Second,
Minute,
Hour,
Day,
Week,
Month,
Year,
}
impl Timeframe {
pub fn new(value: u32, unit: TimeframeUnit) -> Self {
Self { value, unit }
}
pub fn parse(s: &str) -> Option<Self> {
if s.is_empty() {
return None;
}
let digit_end = s.find(|c: char| !c.is_numeric())?;
if digit_end == 0 {
return None;
}
let (num_str, unit_str) = s.split_at(digit_end);
let value: u32 = num_str.parse().ok()?;
let unit = match unit_str.to_lowercase().as_str() {
"s" | "sec" | "second" | "seconds" => TimeframeUnit::Second,
"m" | "min" | "minute" | "minutes" => TimeframeUnit::Minute,
"h" | "hr" | "hour" | "hours" => TimeframeUnit::Hour,
"d" | "day" | "days" => TimeframeUnit::Day,
"w" | "week" | "weeks" => TimeframeUnit::Week,
"mo" | "mon" | "month" | "months" => TimeframeUnit::Month,
_ => return None,
};
Some(Self { value, unit })
}
pub fn to_seconds(&self) -> u64 {
let multiplier = match self.unit {
TimeframeUnit::Second => 1,
TimeframeUnit::Minute => 60,
TimeframeUnit::Hour => 3600,
TimeframeUnit::Day => 86400,
TimeframeUnit::Week => 604800,
TimeframeUnit::Month => 2592000, TimeframeUnit::Year => 31536000, };
self.value as u64 * multiplier
}
pub fn to_millis(&self) -> u64 {
self.to_seconds() * 1000
}
pub fn can_aggregate_from(&self, base: &Timeframe) -> bool {
let self_seconds = self.to_seconds();
let base_seconds = base.to_seconds();
self_seconds > base_seconds && self_seconds.is_multiple_of(base_seconds)
}
pub fn aggregation_factor(&self, base: &Timeframe) -> Option<usize> {
if !self.can_aggregate_from(base) {
return None;
}
Some((self.to_seconds() / base.to_seconds()) as usize)
}
pub fn align_timestamp(&self, timestamp: i64) -> i64 {
let interval = self.to_seconds() as i64;
(timestamp / interval) * interval
}
pub fn next_aligned_timestamp(&self, timestamp: i64) -> i64 {
self.align_timestamp(timestamp) + self.to_seconds() as i64
}
pub fn m1() -> Self {
Self::new(1, TimeframeUnit::Minute)
}
pub fn m5() -> Self {
Self::new(5, TimeframeUnit::Minute)
}
pub fn m15() -> Self {
Self::new(15, TimeframeUnit::Minute)
}
pub fn m30() -> Self {
Self::new(30, TimeframeUnit::Minute)
}
pub fn h1() -> Self {
Self::new(1, TimeframeUnit::Hour)
}
pub fn h4() -> Self {
Self::new(4, TimeframeUnit::Hour)
}
pub fn d1() -> Self {
Self::new(1, TimeframeUnit::Day)
}
pub fn w1() -> Self {
Self::new(1, TimeframeUnit::Week)
}
pub fn is_intraday(&self) -> bool {
matches!(
self.unit,
TimeframeUnit::Second | TimeframeUnit::Minute | TimeframeUnit::Hour
)
}
}
impl fmt::Display for Timeframe {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let unit_str = match self.unit {
TimeframeUnit::Second => "s",
TimeframeUnit::Minute => "m",
TimeframeUnit::Hour => "h",
TimeframeUnit::Day => "d",
TimeframeUnit::Week => "w",
TimeframeUnit::Month => "M",
TimeframeUnit::Year => "y",
};
write!(f, "{}{}", self.value, unit_str)
}
}
impl std::str::FromStr for Timeframe {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s).ok_or_else(|| format!("Invalid timeframe: {}", s))
}
}
impl Default for Timeframe {
fn default() -> Self {
Self::d1()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_timeframe_parsing() {
assert_eq!(
Timeframe::parse("5m"),
Some(Timeframe::new(5, TimeframeUnit::Minute))
);
assert_eq!(
Timeframe::parse("1h"),
Some(Timeframe::new(1, TimeframeUnit::Hour))
);
assert_eq!(
Timeframe::parse("4h"),
Some(Timeframe::new(4, TimeframeUnit::Hour))
);
assert_eq!(
Timeframe::parse("1d"),
Some(Timeframe::new(1, TimeframeUnit::Day))
);
}
#[test]
fn test_aggregation() {
let m1 = Timeframe::m1();
let m5 = Timeframe::m5();
let h1 = Timeframe::h1();
assert!(m5.can_aggregate_from(&m1));
assert!(h1.can_aggregate_from(&m1));
assert!(h1.can_aggregate_from(&m5));
assert_eq!(m5.aggregation_factor(&m1), Some(5));
assert_eq!(h1.aggregation_factor(&m1), Some(60));
assert_eq!(h1.aggregation_factor(&m5), Some(12));
}
#[test]
fn test_timestamp_alignment() {
let m5 = Timeframe::m5();
let ts = 1704111165; let aligned = m5.align_timestamp(ts);
assert_eq!(aligned % 300, 0); }
}