wickra-data: add minutes/hours/days Timeframe constructors
Timeframe gained new/millis/seconds/one_minute_ms; add minutes, hours and days alongside them. Each builds on seconds (minutes(5) -> a 300-second bucket), consistent with Timeframe::seconds, and guards the multiplication with checked_mul so an oversized n yields Error::InvalidTimeframe instead of an overflow panic. A non-positive n is rejected by Timeframe::new. Each method carries a runnable doctest, and unit tests cover the known bucket sizes, non-positive rejection and overflow rejection.
This commit is contained in:
@@ -42,6 +42,67 @@ impl Timeframe {
|
||||
Self::new(60_000).expect("60_000 > 0")
|
||||
}
|
||||
|
||||
/// Convenience: build a timeframe of `n` whole minutes, measured in
|
||||
/// seconds — consistent with [`Timeframe::seconds`].
|
||||
///
|
||||
/// `minutes(5)` yields a bucket of `300`, for use with second-resolution
|
||||
/// timestamps. For millisecond timestamps (Binance) multiply yourself or
|
||||
/// use [`Timeframe::millis`].
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`Error::InvalidTimeframe`] if `n` is not positive or if
|
||||
/// `n * 60` overflows `i64`.
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_data::aggregator::Timeframe;
|
||||
/// assert_eq!(Timeframe::minutes(5)?.bucket(), 300);
|
||||
/// # Ok::<(), wickra_data::Error>(())
|
||||
/// ```
|
||||
pub fn minutes(n: i64) -> Result<Self> {
|
||||
let bucket = n
|
||||
.checked_mul(60)
|
||||
.ok_or_else(|| Error::InvalidTimeframe(format!("{n} minutes overflows i64 seconds")))?;
|
||||
Self::new(bucket)
|
||||
}
|
||||
|
||||
/// Convenience: build a timeframe of `n` whole hours, measured in seconds
|
||||
/// (`hours(2)` → a bucket of `7_200`).
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`Error::InvalidTimeframe`] if `n` is not positive or if
|
||||
/// `n * 3_600` overflows `i64`.
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_data::aggregator::Timeframe;
|
||||
/// assert_eq!(Timeframe::hours(2)?.bucket(), 7_200);
|
||||
/// # Ok::<(), wickra_data::Error>(())
|
||||
/// ```
|
||||
pub fn hours(n: i64) -> Result<Self> {
|
||||
let bucket = n
|
||||
.checked_mul(3_600)
|
||||
.ok_or_else(|| Error::InvalidTimeframe(format!("{n} hours overflows i64 seconds")))?;
|
||||
Self::new(bucket)
|
||||
}
|
||||
|
||||
/// Convenience: build a timeframe of `n` whole days, measured in seconds
|
||||
/// (`days(1)` → a bucket of `86_400`).
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`Error::InvalidTimeframe`] if `n` is not positive or if
|
||||
/// `n * 86_400` overflows `i64`.
|
||||
///
|
||||
/// ```
|
||||
/// use wickra_data::aggregator::Timeframe;
|
||||
/// assert_eq!(Timeframe::days(1)?.bucket(), 86_400);
|
||||
/// # Ok::<(), wickra_data::Error>(())
|
||||
/// ```
|
||||
pub fn days(n: i64) -> Result<Self> {
|
||||
let bucket = n
|
||||
.checked_mul(86_400)
|
||||
.ok_or_else(|| Error::InvalidTimeframe(format!("{n} days overflows i64 seconds")))?;
|
||||
Self::new(bucket)
|
||||
}
|
||||
|
||||
/// Bucket size.
|
||||
pub const fn bucket(self) -> i64 {
|
||||
self.bucket
|
||||
@@ -267,6 +328,42 @@ mod tests {
|
||||
assert!(Timeframe::new(-1).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minute_hour_day_constructors_compute_seconds() {
|
||||
assert_eq!(Timeframe::minutes(1).unwrap().bucket(), 60);
|
||||
assert_eq!(Timeframe::minutes(5).unwrap().bucket(), 300);
|
||||
assert_eq!(Timeframe::hours(1).unwrap().bucket(), 3_600);
|
||||
assert_eq!(Timeframe::hours(4).unwrap().bucket(), 14_400);
|
||||
assert_eq!(Timeframe::days(1).unwrap().bucket(), 86_400);
|
||||
assert_eq!(Timeframe::days(7).unwrap().bucket(), 604_800);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minute_hour_day_constructors_reject_non_positive() {
|
||||
for n in [0, -1, -60] {
|
||||
assert!(Timeframe::minutes(n).is_err());
|
||||
assert!(Timeframe::hours(n).is_err());
|
||||
assert!(Timeframe::days(n).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minute_hour_day_constructors_reject_overflow() {
|
||||
// `n * unit` overflows i64 long before `new`'s sign check runs.
|
||||
assert!(matches!(
|
||||
Timeframe::minutes(i64::MAX),
|
||||
Err(Error::InvalidTimeframe(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
Timeframe::hours(i64::MAX),
|
||||
Err(Error::InvalidTimeframe(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
Timeframe::days(i64::MAX),
|
||||
Err(Error::InvalidTimeframe(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn floors_to_bucket_boundary() {
|
||||
let tf = Timeframe::new(100).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user