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]>
This commit is contained in:
1 parent
f646da0a8e
commit
7bbc1ee0fa
6 files changed
+693
No files matched your search
@@ -0,0 +1,89 @@
|
||||
//! 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));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user