chore: make datetime pure

This commit is contained in:
Konstantin Fickel 2026-04-07 13:13:55 +02:00
parent 2ab2b6a52b
commit e8dc2013bc
Signed by: kfickel
GPG key ID: A793722F9933C1A5
2 changed files with 43 additions and 23 deletions

View file

@ -4,6 +4,7 @@ use std::path::Path;
use chrono::Datelike; use chrono::Datelike;
use chrono::NaiveDate; use chrono::NaiveDate;
use chrono::Utc;
use walkdir::WalkDir; use walkdir::WalkDir;
use crate::config::Settings; use crate::config::Settings;
@ -339,7 +340,7 @@ pub fn run(decimal: bool, debug: bool) -> Result<(), StreamdError> {
// Load all markdown files and extract timesheets // Load all markdown files and extract timesheets
let all_shards = load_all_shards(base_folder)?; let all_shards = load_all_shards(base_folder)?;
let timesheets = extract_timesheets(&all_shards)?; let timesheets = extract_timesheets(&all_shards, Utc::now())?;
// Generate the report // Generate the report
let report = generate_report(&timesheets, &timesheet_config)?; let report = generate_report(&timesheets, &timesheet_config)?;

View file

@ -35,7 +35,10 @@ fn shards_to_timesheet_points(shards: &[LocalizedShard]) -> Vec<TimesheetPoint>
} }
/// Aggregate timesheet points for a single day into a Timesheet. /// Aggregate timesheet points for a single day into a Timesheet.
fn aggregate_timecard_day(points: &[TimesheetPoint]) -> Result<Option<Timesheet>, StreamdError> { fn aggregate_timecard_day(
points: &[TimesheetPoint],
now: DateTime<Utc>,
) -> Result<Option<Timesheet>, StreamdError> {
if points.is_empty() { if points.is_empty() {
return Ok(None); return Ok(None);
} }
@ -113,10 +116,15 @@ fn aggregate_timecard_day(points: &[TimesheetPoint]) -> Result<Option<Timesheet>
// Check that we ended in break mode // Check that we ended in break mode
if !last_is_break { if !last_is_break {
return Err(StreamdError::TimesheetError(format!( if card_date == now.date_naive() {
"Last Timecard of {} is not a break!", // No closing break yet for today — artificially close at now
card_date timecards.push(Timecard::new(last_time, now.time()));
))); } else {
return Err(StreamdError::TimesheetError(format!(
"Last Timecard of {} is not a break!",
card_date
)));
}
} }
// Only return a timesheet if there's meaningful data // Only return a timesheet if there's meaningful data
@ -133,7 +141,10 @@ fn aggregate_timecard_day(points: &[TimesheetPoint]) -> Result<Option<Timesheet>
} }
/// Aggregate timesheet points into timesheets, grouped by day. /// Aggregate timesheet points into timesheets, grouped by day.
fn aggregate_timecards(points: &[TimesheetPoint]) -> Result<Vec<Timesheet>, StreamdError> { fn aggregate_timecards(
points: &[TimesheetPoint],
now: DateTime<Utc>,
) -> Result<Vec<Timesheet>, StreamdError> {
let mut timesheets = Vec::new(); let mut timesheets = Vec::new();
// Sort points by moment to ensure proper grouping // Sort points by moment to ensure proper grouping
@ -143,7 +154,7 @@ fn aggregate_timecards(points: &[TimesheetPoint]) -> Result<Vec<Timesheet>, Stre
// Group by date // Group by date
for (_date, group) in &sorted_points.iter().chunk_by(|p| p.moment.date_naive()) { for (_date, group) in &sorted_points.iter().chunk_by(|p| p.moment.date_naive()) {
let day_points: Vec<_> = group.cloned().collect(); let day_points: Vec<_> = group.cloned().collect();
if let Some(timesheet) = aggregate_timecard_day(&day_points)? { if let Some(timesheet) = aggregate_timecard_day(&day_points, now)? {
timesheets.push(timesheet); timesheets.push(timesheet);
} }
} }
@ -152,9 +163,12 @@ fn aggregate_timecards(points: &[TimesheetPoint]) -> Result<Vec<Timesheet>, Stre
} }
/// Extract timesheets from localized shards. /// Extract timesheets from localized shards.
pub fn extract_timesheets(shards: &[LocalizedShard]) -> Result<Vec<Timesheet>, StreamdError> { pub fn extract_timesheets(
shards: &[LocalizedShard],
now: DateTime<Utc>,
) -> Result<Vec<Timesheet>, StreamdError> {
let points = shards_to_timesheet_points(shards); let points = shards_to_timesheet_points(shards);
aggregate_timecards(&points) aggregate_timecards(&points, now)
} }
#[cfg(test)] #[cfg(test)]
@ -163,6 +177,11 @@ mod tests {
use chrono::{NaiveTime, TimeZone}; use chrono::{NaiveTime, TimeZone};
use indexmap::IndexMap; use indexmap::IndexMap;
/// A fixed "now" in the past, so tests never match today.
fn past_now() -> DateTime<Utc> {
Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap()
}
fn point(at: DateTime<Utc>, point_type: TimesheetPointType) -> LocalizedShard { fn point(at: DateTime<Utc>, point_type: TimesheetPointType) -> LocalizedShard {
let mut location = IndexMap::new(); let mut location = IndexMap::new();
location.insert( location.insert(
@ -198,7 +217,7 @@ mod tests {
), ),
]; ];
let result = extract_timesheets(&shards).unwrap(); let result = extract_timesheets(&shards, past_now()).unwrap();
assert_eq!(result.len(), 1); assert_eq!(result.len(), 1);
assert_eq!(result[0].date, day.date_naive()); assert_eq!(result[0].date, day.date_naive());
@ -251,7 +270,7 @@ mod tests {
), ),
]; ];
let result = extract_timesheets(&shards).unwrap(); let result = extract_timesheets(&shards, past_now()).unwrap();
assert_eq!(result.len(), 1); assert_eq!(result.len(), 1);
assert_eq!(result[0].timecards.len(), 3); assert_eq!(result[0].timecards.len(), 3);
@ -302,7 +321,7 @@ mod tests {
), ),
]; ];
let result = extract_timesheets(&shards).unwrap(); let result = extract_timesheets(&shards, past_now()).unwrap();
assert_eq!(result.len(), 1); assert_eq!(result.len(), 1);
assert_eq!(result[0].timecards.len(), 3); assert_eq!(result[0].timecards.len(), 3);
@ -336,7 +355,7 @@ mod tests {
), ),
]; ];
let result = extract_timesheets(&shards).unwrap(); let result = extract_timesheets(&shards, past_now()).unwrap();
assert_eq!(result.len(), 2); assert_eq!(result.len(), 2);
assert_eq!(result[0].date, day1.date_naive()); assert_eq!(result[0].date, day1.date_naive());
@ -359,7 +378,7 @@ mod tests {
), ),
]; ];
let result = extract_timesheets(&shards).unwrap(); let result = extract_timesheets(&shards, past_now()).unwrap();
assert_eq!(result.len(), 1); assert_eq!(result.len(), 1);
assert_eq!(result[0].special_day_type, Some(SpecialDayType::Vacation)); assert_eq!(result[0].special_day_type, Some(SpecialDayType::Vacation));
@ -382,7 +401,7 @@ mod tests {
), ),
]; ];
let result = extract_timesheets(&shards).unwrap(); let result = extract_timesheets(&shards, past_now()).unwrap();
assert_eq!(result.len(), 1); assert_eq!(result.len(), 1);
assert_eq!(result[0].special_day_type, Some(SpecialDayType::Holiday)); assert_eq!(result[0].special_day_type, Some(SpecialDayType::Holiday));
@ -404,7 +423,7 @@ mod tests {
), ),
]; ];
let result = extract_timesheets(&shards).unwrap(); let result = extract_timesheets(&shards, past_now()).unwrap();
assert_eq!(result.len(), 1); assert_eq!(result.len(), 1);
assert_eq!(result[0].special_day_type, Some(SpecialDayType::Undertime)); assert_eq!(result[0].special_day_type, Some(SpecialDayType::Undertime));
@ -431,7 +450,7 @@ mod tests {
), ),
]; ];
let result = extract_timesheets(&shards).unwrap(); let result = extract_timesheets(&shards, past_now()).unwrap();
assert_eq!(result.len(), 1); assert_eq!(result.len(), 1);
assert!(result[0].is_sick_leave); assert!(result[0].is_sick_leave);
@ -454,7 +473,7 @@ mod tests {
), ),
]; ];
let result = extract_timesheets(&shards).unwrap(); let result = extract_timesheets(&shards, past_now()).unwrap();
assert_eq!(result.len(), 1); assert_eq!(result.len(), 1);
assert!(result[0].is_sick_leave); assert!(result[0].is_sick_leave);
@ -463,7 +482,7 @@ mod tests {
#[test] #[test]
fn test_empty_input() { fn test_empty_input() {
let result = extract_timesheets(&[]).unwrap(); let result = extract_timesheets(&[], past_now()).unwrap();
assert!(result.is_empty()); assert!(result.is_empty());
} }
@ -483,7 +502,7 @@ mod tests {
), ),
]; ];
let result = extract_timesheets(&shards); let result = extract_timesheets(&shards, past_now());
assert!(result.is_err()); assert!(result.is_err());
let err = result.unwrap_err(); let err = result.unwrap_err();
@ -511,7 +530,7 @@ mod tests {
), ),
]; ];
let result = extract_timesheets(&shards); let result = extract_timesheets(&shards, past_now());
assert!(result.is_err()); assert!(result.is_err());
let err = result.unwrap_err(); let err = result.unwrap_err();
@ -534,7 +553,7 @@ mod tests {
), ),
]; ];
let result = extract_timesheets(&shards).unwrap(); let result = extract_timesheets(&shards, past_now()).unwrap();
assert!(result.is_empty()); assert!(result.is_empty());
} }