Files
skyshift/src/time/range.rs
T
skkeyeandClaude Opus 5.5 7bbc1ee0fa Add the time module: clock, 12h/24h text and typed times
- 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]>
2026-09-24 02:59:21 -04:00

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));
}
}