- clock: the system clock, with a time zone override (name or fixed offset) and a manual adjustment; Start/Pause record whole minutes so displayed times and totals always add up. - format: every time, date and duration shown in the app goes through Formatter (12h/24h, 7h 45m / 7:45 / 7.75); parse_time reads both 12h and 24h input (22:30, 2230, 22h30, 10:30pm, 10p). - resolve: turns a typed "01:30" into the moment meant, so an end time before the start is the next day (night shifts). - range: date ranges (week, month, last N days). Co-Authored-By: Claude Opus 5.5 <[email protected]>
90 lines
2.9 KiB
Rust
90 lines
2.9 KiB
Rust
//! Ranges of dates: a week, a month, the last 30 days...
|
|
|
|
use jiff::ToSpan;
|
|
use jiff::civil::{Date, Weekday};
|
|
|
|
/// Every date from `first` to `last`, both included.
|
|
///
|
|
/// `first <= last` always holds: [`DateRange::new`] puts the two dates in
|
|
/// order, so there is no such thing as an "inverted" range.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct DateRange {
|
|
first: Date,
|
|
last: Date,
|
|
}
|
|
|
|
impl DateRange {
|
|
/// The dates between `a` and `b` (in either order), both included.
|
|
pub fn new(a: Date, b: Date) -> Self {
|
|
let (first, last) = if a <= b { (a, b) } else { (b, a) };
|
|
DateRange { first, last }
|
|
}
|
|
|
|
/// The week that contains `date`, starting on `week_start`.
|
|
pub fn week_of(date: Date, week_start: Weekday) -> Self {
|
|
let first = date.saturating_sub(date.weekday().since(week_start).days());
|
|
DateRange::new(first, first.saturating_add(6.days()))
|
|
}
|
|
|
|
/// The calendar month that contains `date`.
|
|
pub fn month_of(date: Date) -> Self {
|
|
DateRange::new(date.first_of_month(), date.last_of_month())
|
|
}
|
|
|
|
/// The `count` days ending on `last`, so `last_days(today, 7)` is the
|
|
/// past week including today.
|
|
pub fn last_days(last: Date, count: i32) -> Self {
|
|
DateRange::new(last.saturating_sub((count.max(1) - 1).days()), last)
|
|
}
|
|
|
|
pub fn first(self) -> Date {
|
|
self.first
|
|
}
|
|
|
|
pub fn last(self) -> Date {
|
|
self.last
|
|
}
|
|
|
|
pub fn contains(self, date: Date) -> bool {
|
|
self.first <= date && date <= self.last
|
|
}
|
|
|
|
/// How many dates the range holds (a single day counts as 1).
|
|
pub fn day_count(self) -> i32 {
|
|
let hours = self.last.duration_since(self.first).as_hours();
|
|
(hours / 24) as i32 + 1
|
|
}
|
|
|
|
/// The range of the same length that ends the day before this one starts.
|
|
pub fn previous(self) -> Self {
|
|
let last = self.first.saturating_sub(1.day());
|
|
DateRange::last_days(last, self.day_count())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use jiff::civil::date;
|
|
|
|
#[test]
|
|
fn weeks_start_on_the_chosen_day() {
|
|
// 2026-09-24 is a Thursday.
|
|
let thursday = date(2026, 9, 24);
|
|
let monday_week = DateRange::week_of(thursday, Weekday::Monday);
|
|
assert_eq!((monday_week.first(), monday_week.last()), (date(2026, 9, 21), date(2026, 9, 27)));
|
|
let sunday_week = DateRange::week_of(thursday, Weekday::Sunday);
|
|
assert_eq!(sunday_week.first(), date(2026, 9, 20));
|
|
assert_eq!(sunday_week.day_count(), 7);
|
|
}
|
|
|
|
#[test]
|
|
fn counts_and_previous() {
|
|
let range = DateRange::last_days(date(2026, 9, 30), 30);
|
|
assert_eq!(range.first(), date(2026, 9, 1));
|
|
assert_eq!(range.day_count(), 30);
|
|
assert_eq!(range.previous().last(), date(2026, 8, 31));
|
|
assert_eq!(DateRange::new(date(2026, 9, 5), date(2026, 9, 1)).first(), date(2026, 9, 1));
|
|
}
|
|
}
|