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")
|
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.
|
/// Bucket size.
|
||||||
pub const fn bucket(self) -> i64 {
|
pub const fn bucket(self) -> i64 {
|
||||||
self.bucket
|
self.bucket
|
||||||
@@ -267,6 +328,42 @@ mod tests {
|
|||||||
assert!(Timeframe::new(-1).is_err());
|
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]
|
#[test]
|
||||||
fn floors_to_bucket_boundary() {
|
fn floors_to_bucket_boundary() {
|
||||||
let tf = Timeframe::new(100).unwrap();
|
let tf = Timeframe::new(100).unwrap();
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ The reader is defensive about real-world files:
|
|||||||
`TickAggregator` rolls a stream of trade `Tick`s up into `Candle`s of an
|
`TickAggregator` rolls a stream of trade `Tick`s up into `Candle`s of an
|
||||||
arbitrary timeframe. The timeframe's bucket size is in the same unit as the
|
arbitrary timeframe. The timeframe's bucket size is in the same unit as the
|
||||||
tick timestamps (milliseconds for Binance, seconds for daily bars, …).
|
tick timestamps (milliseconds for Binance, seconds for daily bars, …).
|
||||||
|
Build a `Timeframe` with `Timeframe::new` (a raw bucket size), the
|
||||||
|
`millis` / `seconds` / `one_minute_ms` shortcuts, or the `minutes` / `hours` /
|
||||||
|
`days` constructors — each of the last three builds on **seconds**, so
|
||||||
|
`Timeframe::minutes(5)` is a 300-second bucket.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use wickra_data::aggregator::{TickAggregator, Timeframe};
|
use wickra_data::aggregator::{TickAggregator, Timeframe};
|
||||||
|
|||||||
Reference in New Issue
Block a user