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:
kingchenc
2026-05-22 21:49:21 +02:00
parent 2b3a1b7384
commit d5ff0a9df6
2 changed files with 101 additions and 0 deletions
+97
View File
@@ -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();