diff --git a/src/cli/commands/edit.rs b/src/cli/commands/edit.rs index a8761f3..89a0240 100644 --- a/src/cli/commands/edit.rs +++ b/src/cli/commands/edit.rs @@ -24,9 +24,7 @@ fn all_files() -> Result, StreamdError> { let content = fs::read_to_string(path)?; let stream_file = parse_markdown_file(&file_name, &content); - if let Ok(shard) = - localize_stream_file(&stream_file, &TaskConfiguration, chrono_tz::UTC) - { + if let Ok(shard) = localize_stream_file(&stream_file, &TaskConfiguration) { shards.push(shard); } } diff --git a/src/cli/commands/timesheet.rs b/src/cli/commands/timesheet.rs index a1c4a37..67ab956 100644 --- a/src/cli/commands/timesheet.rs +++ b/src/cli/commands/timesheet.rs @@ -4,8 +4,6 @@ use std::path::Path; use chrono::Datelike; use chrono::NaiveDate; -use chrono::Utc; -use chrono_tz::Tz; use walkdir::WalkDir; use crate::config::Settings; @@ -21,7 +19,7 @@ use crate::timesheet::{ DayType, DayWarning, MonthReport, TimesheetReport, }; -fn load_all_shards(base_folder: &Path, tz: Tz) -> Result, StreamdError> { +fn load_all_shards(base_folder: &Path) -> Result, StreamdError> { let mut shards = Vec::new(); for entry in WalkDir::new(base_folder) @@ -35,8 +33,7 @@ fn load_all_shards(base_folder: &Path, tz: Tz) -> Result, St let content = fs::read_to_string(path)?; let stream_file = parse_markdown_file(&file_name, &content); - if let Ok(shard) = localize_stream_file(&stream_file, &BasicTimesheetConfiguration, tz) - { + if let Ok(shard) = localize_stream_file(&stream_file, &BasicTimesheetConfiguration) { shards.push(shard); } } @@ -323,13 +320,6 @@ pub fn run(decimal: bool, debug: bool) -> Result<(), StreamdError> { // Load repository configuration let repo_config = load_repository_config(base_folder)?; - // Parse timezone from config, defaulting to UTC - let tz: Tz = repo_config - .timezone - .as_deref() - .and_then(|s| s.parse().ok()) - .unwrap_or(chrono_tz::UTC); - // Check if timesheet is configured let timesheet_config = match repo_config.timesheet { Some(config) => config, @@ -347,14 +337,12 @@ pub fn run(decimal: bool, debug: bool) -> Result<(), StreamdError> { } }; - let now = Utc::now(); - // Load all markdown files and extract timesheets - let all_shards = load_all_shards(base_folder, tz)?; - let timesheets = extract_timesheets(&all_shards, now, tz)?; + let all_shards = load_all_shards(base_folder)?; + let timesheets = extract_timesheets(&all_shards)?; // Generate the report - let report = generate_report(×heets, ×heet_config, now, tz)?; + let report = generate_report(×heets, ×heet_config)?; if report.months.is_empty() { println!("No timesheet data found for the configured periods."); diff --git a/src/cli/commands/todo.rs b/src/cli/commands/todo.rs index 17bfba2..fe8e870 100644 --- a/src/cli/commands/todo.rs +++ b/src/cli/commands/todo.rs @@ -26,9 +26,7 @@ fn all_files() -> Result, StreamdError> { let content = fs::read_to_string(path)?; let stream_file = parse_markdown_file(&file_name, &content); - if let Ok(shard) = - localize_stream_file(&stream_file, &TaskConfiguration, chrono_tz::UTC) - { + if let Ok(shard) = localize_stream_file(&stream_file, &TaskConfiguration) { shards.push(shard); } } diff --git a/src/localize/datetime.rs b/src/localize/datetime.rs index 3ff90a2..87a2718 100644 --- a/src/localize/datetime.rs +++ b/src/localize/datetime.rs @@ -1,5 +1,4 @@ -use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc}; -use chrono_tz::Tz; +use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc}; use once_cell::sync::Lazy; use regex::Regex; use std::path::Path; @@ -18,25 +17,15 @@ static DATE_MARKER_REGEX: Lazy = Lazy::new(|| Regex::new(r"^\d{8}$").unwr /// Regex for validating time marker format (6 digits). static TIME_MARKER_REGEX: Lazy = Lazy::new(|| Regex::new(r"^\d{6}$").unwrap()); -/// Convert a NaiveDateTime to UTC via the given timezone. -/// Falls back to the earliest local interpretation for ambiguous DST times. -fn naive_to_utc(dt: NaiveDateTime, tz: Tz) -> Option> { - tz.from_local_datetime(&dt) - .single() - .or_else(|| tz.from_local_datetime(&dt).earliest()) - .map(|dt| dt.with_timezone(&Utc)) -} - /// Extract a datetime from a file name in the format YYYYMMDD-HHMMSS. /// /// The time component is optional and can be 4-6 digits (HHMM, HHMMS, or HHMMSS). -/// The datetime is interpreted in the given timezone. /// /// # Examples -/// - "20230101-123456 Some Text.md" -> DateTime for 2023-01-01 12:34:56 in tz -/// - "20230101 Some Text.md" -> DateTime for 2023-01-01 00:00:00 in tz +/// - "20230101-123456 Some Text.md" -> DateTime for 2023-01-01 12:34:56 +/// - "20230101 Some Text.md" -> DateTime for 2023-01-01 00:00:00 /// - "invalid-file-name.md" -> None -pub fn extract_datetime_from_file_name(file_name: &str, tz: Tz) -> Option> { +pub fn extract_datetime_from_file_name(file_name: &str) -> Option> { let base_name = Path::new(file_name) .file_name() .and_then(|s| s.to_str()) @@ -59,23 +48,20 @@ pub fn extract_datetime_from_file_name(file_name: &str, tz: Tz) -> Option Option { - if !DATETIME_MARKER_REGEX.is_match(marker) { - return None; - } - NaiveDateTime::parse_from_str(marker, "%Y%m%d%H%M%S").ok() + .map(|dt| dt.and_utc()) } /// Extract a datetime from a marker string in the exact format: YYYYMMDDHHMMSS. /// -/// The datetime is interpreted in the given timezone. /// Returns the parsed datetime if the format matches and values are valid. -pub fn extract_datetime_from_marker(marker: &str, tz: Tz) -> Option> { - parse_naive_datetime_from_marker(marker).and_then(|dt| naive_to_utc(dt, tz)) +pub fn extract_datetime_from_marker(marker: &str) -> Option> { + if !DATETIME_MARKER_REGEX.is_match(marker) { + return None; + } + + NaiveDateTime::parse_from_str(marker, "%Y%m%d%H%M%S") + .ok() + .map(|dt| dt.and_utc()) } /// Extract a date from a marker string in the exact format: YYYYMMDD. @@ -104,7 +90,6 @@ pub fn extract_time_from_marker(marker: &str) -> Option { /// /// The function processes markers in reverse order, allowing later markers to override /// earlier ones. It combines date-only and time-only markers when both are present. -/// All naive datetimes (from markers and the inherited fallback) are interpreted in `tz`. /// /// Rules: /// - If a full datetime marker (14 digits) is found, it sets both date and time @@ -114,7 +99,6 @@ pub fn extract_time_from_marker(marker: &str) -> Option { pub fn extract_datetime_from_marker_list( markers: &[String], inherited_datetime: DateTime, - tz: Tz, ) -> DateTime { let mut shard_time: Option = None; let mut shard_date: Option = None; @@ -127,39 +111,34 @@ pub fn extract_datetime_from_marker_list( if let Some(date) = extract_date_from_marker(marker) { shard_date = Some(date); } - if let Some(naive_dt) = parse_naive_datetime_from_marker(marker) { - shard_date = Some(naive_dt.date()); - shard_time = Some(naive_dt.time()); + if let Some(datetime) = extract_datetime_from_marker(marker) { + shard_date = Some(datetime.naive_utc().date()); + shard_time = Some(datetime.naive_utc().time()); } } - // Interpret the inherited datetime in the configured timezone for fallback values - let inherited_local = inherited_datetime.with_timezone(&tz).naive_local(); - // Combine date and time, applying defaults as needed - let final_date = shard_date.unwrap_or_else(|| inherited_local.date()); + let final_date = shard_date.unwrap_or_else(|| inherited_datetime.naive_utc().date()); let final_time = match (shard_date, shard_time) { // If we have a date but no time, use midnight (Some(_), None) => NaiveTime::from_hms_opt(0, 0, 0).unwrap(), // Otherwise use the shard time or inherit - _ => shard_time.unwrap_or_else(|| inherited_local.time()), + _ => shard_time.unwrap_or_else(|| inherited_datetime.naive_utc().time()), }; - let naive = NaiveDateTime::new(final_date, final_time); - naive_to_utc(naive, tz).unwrap_or_else(|| inherited_datetime) + NaiveDateTime::new(final_date, final_time).and_utc() } #[cfg(test)] mod tests { use super::*; use chrono::TimeZone; - use chrono_tz::UTC; #[test] fn test_extract_date_from_file_name_valid() { let file_name = "20230101-123456 Some Text.md"; assert_eq!( - extract_datetime_from_file_name(file_name, UTC), + extract_datetime_from_file_name(file_name), Some(Utc.with_ymd_and_hms(2023, 1, 1, 12, 34, 56).unwrap()) ); } @@ -167,14 +146,14 @@ mod tests { #[test] fn test_extract_date_from_file_name_invalid() { let file_name = "invalid-file-name.md"; - assert_eq!(extract_datetime_from_file_name(file_name, UTC), None); + assert_eq!(extract_datetime_from_file_name(file_name), None); } #[test] fn test_extract_date_from_file_name_without_time() { let file_name = "20230101 Some Text.md"; assert_eq!( - extract_datetime_from_file_name(file_name, UTC), + extract_datetime_from_file_name(file_name), Some(Utc.with_ymd_and_hms(2023, 1, 1, 0, 0, 0).unwrap()) ); } @@ -183,7 +162,7 @@ mod tests { fn test_extract_date_from_file_name_short_time() { let file_name = "20230101-1234 Some Text.md"; assert_eq!( - extract_datetime_from_file_name(file_name, UTC), + extract_datetime_from_file_name(file_name), Some(Utc.with_ymd_and_hms(2023, 1, 1, 12, 34, 0).unwrap()) ); } @@ -191,61 +170,41 @@ mod tests { #[test] fn test_extract_date_from_file_name_empty_string() { let file_name = ""; - assert_eq!(extract_datetime_from_file_name(file_name, UTC), None); + assert_eq!(extract_datetime_from_file_name(file_name), None); } #[test] fn test_extract_date_from_file_name_with_full_path() { let file_name = "/path/to/20230101-123456 Some Text.md"; assert_eq!( - extract_datetime_from_file_name(file_name, UTC), + extract_datetime_from_file_name(file_name), Some(Utc.with_ymd_and_hms(2023, 1, 1, 12, 34, 56).unwrap()) ); } - #[test] - fn test_extract_date_from_file_name_with_timezone_offset() { - // Europe/Berlin is UTC+1 in January (CET) - let file_name = "20230101-120000 Some Text.md"; - assert_eq!( - extract_datetime_from_file_name(file_name, chrono_tz::Europe::Berlin), - Some(Utc.with_ymd_and_hms(2023, 1, 1, 11, 0, 0).unwrap()) - ); - } - #[test] fn test_extract_datetime_from_marker_valid() { let marker = "20250101150000"; assert_eq!( - extract_datetime_from_marker(marker, UTC), + extract_datetime_from_marker(marker), Some(Utc.with_ymd_and_hms(2025, 1, 1, 15, 0, 0).unwrap()) ); } - #[test] - fn test_extract_datetime_from_marker_with_timezone_offset() { - // Europe/Berlin is UTC+1 in January (CET) - let marker = "20250101150000"; - assert_eq!( - extract_datetime_from_marker(marker, chrono_tz::Europe::Berlin), - Some(Utc.with_ymd_and_hms(2025, 1, 1, 14, 0, 0).unwrap()) - ); - } - #[test] fn test_extract_datetime_from_marker_invalid_format() { - assert_eq!(extract_datetime_from_marker("2025010115000", UTC), None); // too short - assert_eq!(extract_datetime_from_marker("202501011500000", UTC), None); // too long - assert_eq!(extract_datetime_from_marker("2025-01-01T150000", UTC), None); // separators - assert_eq!(extract_datetime_from_marker("2025010115000a", UTC), None); // non-digit - assert_eq!(extract_datetime_from_marker("", UTC), None); + assert_eq!(extract_datetime_from_marker("2025010115000"), None); // too short + assert_eq!(extract_datetime_from_marker("202501011500000"), None); // too long + assert_eq!(extract_datetime_from_marker("2025-01-01T150000"), None); // separators + assert_eq!(extract_datetime_from_marker("2025010115000a"), None); // non-digit + assert_eq!(extract_datetime_from_marker(""), None); } #[test] fn test_extract_datetime_from_marker_invalid_values() { - assert_eq!(extract_datetime_from_marker("20250230120000", UTC), None); // Feb 30 - assert_eq!(extract_datetime_from_marker("20250101126000", UTC), None); // minute 60 - assert_eq!(extract_datetime_from_marker("20250101240000", UTC), None); // hour 24 + assert_eq!(extract_datetime_from_marker("20250230120000"), None); // Feb 30 + assert_eq!(extract_datetime_from_marker("20250101126000"), None); // minute 60 + assert_eq!(extract_datetime_from_marker("20250101240000"), None); // hour 24 } #[test] @@ -301,10 +260,7 @@ mod tests { #[test] fn test_no_markers_inherits_datetime() { let inherited = Utc.with_ymd_and_hms(2025, 1, 2, 3, 4, 5).unwrap(); - assert_eq!( - extract_datetime_from_marker_list(&[], inherited, UTC), - inherited - ); + assert_eq!(extract_datetime_from_marker_list(&[], inherited), inherited); } #[test] @@ -317,7 +273,7 @@ mod tests { "1234567".to_string(), ]; assert_eq!( - extract_datetime_from_marker_list(&markers, inherited, UTC), + extract_datetime_from_marker_list(&markers, inherited), inherited ); } @@ -327,7 +283,7 @@ mod tests { let inherited = Utc.with_ymd_and_hms(2025, 6, 7, 8, 9, 10).unwrap(); let markers = vec!["20250101".to_string()]; assert_eq!( - extract_datetime_from_marker_list(&markers, inherited, UTC), + extract_datetime_from_marker_list(&markers, inherited), Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap() ); } @@ -337,7 +293,7 @@ mod tests { let inherited = Utc.with_ymd_and_hms(2025, 6, 7, 8, 9, 10).unwrap(); let markers = vec!["150000".to_string()]; assert_eq!( - extract_datetime_from_marker_list(&markers, inherited, UTC), + extract_datetime_from_marker_list(&markers, inherited), Utc.with_ymd_and_hms(2025, 6, 7, 15, 0, 0).unwrap() ); } @@ -347,7 +303,7 @@ mod tests { let inherited = Utc.with_ymd_and_hms(2025, 6, 7, 8, 9, 10).unwrap(); let markers = vec!["20250101150000".to_string()]; assert_eq!( - extract_datetime_from_marker_list(&markers, inherited, UTC), + extract_datetime_from_marker_list(&markers, inherited), Utc.with_ymd_and_hms(2025, 1, 1, 15, 0, 0).unwrap() ); } @@ -357,7 +313,7 @@ mod tests { let inherited = Utc.with_ymd_and_hms(2025, 6, 7, 8, 9, 10).unwrap(); let markers = vec!["20250101".to_string(), "150000".to_string()]; assert_eq!( - extract_datetime_from_marker_list(&markers, inherited, UTC), + extract_datetime_from_marker_list(&markers, inherited), Utc.with_ymd_and_hms(2025, 1, 1, 15, 0, 0).unwrap() ); } @@ -372,7 +328,7 @@ mod tests { "160000".to_string(), ]; assert_eq!( - extract_datetime_from_marker_list(&markers, inherited, UTC), + extract_datetime_from_marker_list(&markers, inherited), Utc.with_ymd_and_hms(2025, 1, 1, 15, 0, 0).unwrap() ); } @@ -387,7 +343,7 @@ mod tests { ]; // The first date (20250101) and first time (150000) should win over the later combined datetime assert_eq!( - extract_datetime_from_marker_list(&markers, inherited, UTC), + extract_datetime_from_marker_list(&markers, inherited), Utc.with_ymd_and_hms(2025, 1, 1, 15, 0, 0).unwrap() ); } @@ -402,19 +358,8 @@ mod tests { "150000".to_string(), // valid ]; assert_eq!( - extract_datetime_from_marker_list(&markers, inherited, UTC), + extract_datetime_from_marker_list(&markers, inherited), Utc.with_ymd_and_hms(2025, 1, 1, 15, 0, 0).unwrap() ); } - - #[test] - fn test_marker_list_with_timezone_offset() { - // Europe/Berlin is UTC+2 in summer (CEST) - let inherited = Utc.with_ymd_and_hms(2025, 6, 7, 8, 9, 10).unwrap(); - let markers = vec!["150000".to_string()]; - assert_eq!( - extract_datetime_from_marker_list(&markers, inherited, chrono_tz::Europe::Berlin), - Utc.with_ymd_and_hms(2025, 6, 7, 13, 0, 0).unwrap() - ); - } } diff --git a/src/localize/shard.rs b/src/localize/shard.rs index 0badda5..279b8c6 100644 --- a/src/localize/shard.rs +++ b/src/localize/shard.rs @@ -1,5 +1,4 @@ use chrono::{DateTime, Utc}; -use chrono_tz::Tz; use indexmap::{IndexMap, IndexSet}; use crate::error::StreamdError; @@ -18,13 +17,12 @@ pub fn localize_shard( config: &RepositoryConfiguration, propagated: &IndexMap, moment: DateTime, - tz: Tz, ) -> LocalizedShard { let mut position = propagated.clone(); let mut private_position: IndexMap = IndexMap::new(); // Extract datetime from markers - let adjusted_moment = extract_datetime_from_marker_list(&shard.markers, moment, tz); + let adjusted_moment = extract_datetime_from_marker_list(&shard.markers, moment); // Convert markers to a set for if_with checking let marker_set: IndexSet = shard.markers.iter().cloned().collect(); @@ -66,7 +64,7 @@ pub fn localize_shard( let children: Vec = shard .children .iter() - .map(|child| localize_shard(child, config, &position, adjusted_moment, tz)) + .map(|child| localize_shard(child, config, &position, adjusted_moment)) .collect(); // Merge private position into final position @@ -86,13 +84,11 @@ pub fn localize_shard( /// Localize an entire stream file. /// /// Extracts the datetime from the file name and localizes the root shard. -/// Timestamps in the file name and markers are interpreted in `tz`. pub fn localize_stream_file( stream_file: &StreamFile, config: &RepositoryConfiguration, - tz: Tz, ) -> Result { - let shard_date = extract_datetime_from_file_name(&stream_file.file_name, tz) + let shard_date = extract_datetime_from_file_name(&stream_file.file_name) .ok_or_else(|| StreamdError::DateExtractionError(stream_file.file_name.clone()))?; let shard = stream_file @@ -103,13 +99,7 @@ pub fn localize_stream_file( let mut initial_location = IndexMap::new(); initial_location.insert("file".to_string(), stream_file.file_name.clone()); - Ok(localize_shard( - shard, - config, - &initial_location, - shard_date, - tz, - )) + Ok(localize_shard(shard, config, &initial_location, shard_date)) } #[cfg(test)] @@ -117,7 +107,6 @@ mod tests { use super::*; use crate::models::{Dimension, Marker, MarkerPlacement}; use chrono::TimeZone; - use chrono_tz::UTC; fn make_config() -> RepositoryConfiguration { RepositoryConfiguration::new() @@ -160,7 +149,7 @@ mod tests { let stream_file = StreamFile::new("20250622-121000 Test File.md") .with_shard(Shard::new(1, 1).with_markers(vec!["Streamd".to_string()])); - let result = localize_stream_file(&stream_file, &config, UTC).unwrap(); + let result = localize_stream_file(&stream_file, &config).unwrap(); assert_eq!( result.moment, @@ -181,7 +170,7 @@ mod tests { Shard::new(1, 1).with_markers(vec!["Timesheet".to_string(), "Streamd".to_string()]), ); - let result = localize_stream_file(&stream_file, &config, UTC).unwrap(); + let result = localize_stream_file(&stream_file, &config).unwrap(); assert_eq!( result.moment, @@ -213,7 +202,7 @@ mod tests { let stream_file = StreamFile::new("20260131-210000 Test File.md") .with_shard(Shard::new(1, 1).with_markers(vec!["A".to_string(), "B".to_string()])); - let result = localize_stream_file(&stream_file, &config, UTC).unwrap(); + let result = localize_stream_file(&stream_file, &config).unwrap(); assert_eq!(result.location.get("project"), Some(&"b".to_string())); } @@ -237,7 +226,7 @@ mod tests { let stream_file = StreamFile::new("20260131-210000 Test File.md") .with_shard(Shard::new(1, 1).with_markers(vec!["A".to_string(), "B".to_string()])); - let result = localize_stream_file(&stream_file, &config, UTC).unwrap(); + let result = localize_stream_file(&stream_file, &config).unwrap(); assert_eq!(result.location.get("project"), Some(&"a".to_string())); } @@ -261,7 +250,7 @@ mod tests { let stream_file = StreamFile::new("20260131-210000 Test File.md") .with_shard(Shard::new(1, 1).with_markers(vec!["A".to_string(), "B".to_string()])); - let result = localize_stream_file(&stream_file, &config, UTC).unwrap(); + let result = localize_stream_file(&stream_file, &config).unwrap(); assert_eq!(result.location.get("label"), Some(&"b".to_string())); } @@ -286,7 +275,7 @@ mod tests { let stream_file = StreamFile::new("20260131-210000 Test File.md") .with_shard(Shard::new(1, 1).with_markers(vec!["A".to_string(), "B".to_string()])); - let result = localize_stream_file(&stream_file, &config, UTC).unwrap(); + let result = localize_stream_file(&stream_file, &config).unwrap(); assert_eq!(result.location.get("label"), Some(&"a".to_string())); } diff --git a/src/timesheet/extract.rs b/src/timesheet/extract.rs index 0a6c8ff..017434f 100644 --- a/src/timesheet/extract.rs +++ b/src/timesheet/extract.rs @@ -1,5 +1,4 @@ use chrono::{DateTime, Utc}; -use chrono_tz::Tz; use itertools::Itertools; use crate::error::StreamdError; @@ -36,11 +35,7 @@ fn shards_to_timesheet_points(shards: &[LocalizedShard]) -> Vec } /// Aggregate timesheet points for a single day into a Timesheet. -fn aggregate_timecard_day( - points: &[TimesheetPoint], - now: DateTime, - tz: Tz, -) -> Result, StreamdError> { +fn aggregate_timecard_day(points: &[TimesheetPoint]) -> Result, StreamdError> { if points.is_empty() { return Ok(None); } @@ -51,23 +46,23 @@ fn aggregate_timecard_day( pts }; - let card_date = sorted_points[0].moment.with_timezone(&tz).date_naive(); + let card_date = sorted_points[0].moment.date_naive(); let mut is_sick_leave = false; let mut special_day_type: Option = None; // State machine: starting in "break" mode (not working) let mut last_is_break = true; - let mut last_time = sorted_points[0].moment.with_timezone(&tz).time(); + let mut last_time = sorted_points[0].moment.time(); let mut timecards: Vec = Vec::new(); for point in &sorted_points { - if point.moment.with_timezone(&tz).date_naive() != card_date { + if point.moment.date_naive() != card_date { return Err(StreamdError::TimesheetError( "Dates of all given timesheet days should be consistent".to_string(), )); } - let point_time = point.moment.with_timezone(&tz).time(); + let point_time = point.moment.time(); match point.point_type { TimesheetPointType::Holiday => { @@ -118,16 +113,10 @@ fn aggregate_timecard_day( // Check that we ended in break mode if !last_is_break { - let now_local = now.with_timezone(&tz); - if card_date == now_local.date_naive() { - // No closing break yet for today — artificially close at now - timecards.push(Timecard::new(last_time, now_local.time())); - } else { - return Err(StreamdError::TimesheetError(format!( - "Last Timecard of {} is not a break!", - card_date - ))); - } + return Err(StreamdError::TimesheetError(format!( + "Last Timecard of {} is not a break!", + card_date + ))); } // Only return a timesheet if there's meaningful data @@ -144,24 +133,17 @@ fn aggregate_timecard_day( } /// Aggregate timesheet points into timesheets, grouped by day. -fn aggregate_timecards( - points: &[TimesheetPoint], - now: DateTime, - tz: Tz, -) -> Result, StreamdError> { +fn aggregate_timecards(points: &[TimesheetPoint]) -> Result, StreamdError> { let mut timesheets = Vec::new(); // Sort points by moment to ensure proper grouping let mut sorted_points = points.to_vec(); sorted_points.sort_by_key(|p| p.moment); - // Group by local date in the configured timezone - for (_date, group) in &sorted_points - .iter() - .chunk_by(|p| p.moment.with_timezone(&tz).date_naive()) - { + // Group by date + for (_date, group) in &sorted_points.iter().chunk_by(|p| p.moment.date_naive()) { let day_points: Vec<_> = group.cloned().collect(); - if let Some(timesheet) = aggregate_timecard_day(&day_points, now, tz)? { + if let Some(timesheet) = aggregate_timecard_day(&day_points)? { timesheets.push(timesheet); } } @@ -170,13 +152,9 @@ fn aggregate_timecards( } /// Extract timesheets from localized shards. -pub fn extract_timesheets( - shards: &[LocalizedShard], - now: DateTime, - tz: Tz, -) -> Result, StreamdError> { +pub fn extract_timesheets(shards: &[LocalizedShard]) -> Result, StreamdError> { let points = shards_to_timesheet_points(shards); - aggregate_timecards(&points, now, tz) + aggregate_timecards(&points) } #[cfg(test)] @@ -185,13 +163,6 @@ mod tests { use chrono::{NaiveTime, TimeZone}; use indexmap::IndexMap; - use chrono_tz::UTC; - - /// A fixed "now" in the past, so tests never match today. - fn past_now() -> DateTime { - Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap() - } - fn point(at: DateTime, point_type: TimesheetPointType) -> LocalizedShard { let mut location = IndexMap::new(); location.insert( @@ -227,7 +198,7 @@ mod tests { ), ]; - let result = extract_timesheets(&shards, past_now(), UTC).unwrap(); + let result = extract_timesheets(&shards).unwrap(); assert_eq!(result.len(), 1); assert_eq!(result[0].date, day.date_naive()); @@ -280,7 +251,7 @@ mod tests { ), ]; - let result = extract_timesheets(&shards, past_now(), UTC).unwrap(); + let result = extract_timesheets(&shards).unwrap(); assert_eq!(result.len(), 1); assert_eq!(result[0].timecards.len(), 3); @@ -331,7 +302,7 @@ mod tests { ), ]; - let result = extract_timesheets(&shards, past_now(), UTC).unwrap(); + let result = extract_timesheets(&shards).unwrap(); assert_eq!(result.len(), 1); assert_eq!(result[0].timecards.len(), 3); @@ -365,7 +336,7 @@ mod tests { ), ]; - let result = extract_timesheets(&shards, past_now(), UTC).unwrap(); + let result = extract_timesheets(&shards).unwrap(); assert_eq!(result.len(), 2); assert_eq!(result[0].date, day1.date_naive()); @@ -388,7 +359,7 @@ mod tests { ), ]; - let result = extract_timesheets(&shards, past_now(), UTC).unwrap(); + let result = extract_timesheets(&shards).unwrap(); assert_eq!(result.len(), 1); assert_eq!(result[0].special_day_type, Some(SpecialDayType::Vacation)); @@ -411,7 +382,7 @@ mod tests { ), ]; - let result = extract_timesheets(&shards, past_now(), UTC).unwrap(); + let result = extract_timesheets(&shards).unwrap(); assert_eq!(result.len(), 1); assert_eq!(result[0].special_day_type, Some(SpecialDayType::Holiday)); @@ -433,7 +404,7 @@ mod tests { ), ]; - let result = extract_timesheets(&shards, past_now(), UTC).unwrap(); + let result = extract_timesheets(&shards).unwrap(); assert_eq!(result.len(), 1); assert_eq!(result[0].special_day_type, Some(SpecialDayType::Undertime)); @@ -460,7 +431,7 @@ mod tests { ), ]; - let result = extract_timesheets(&shards, past_now(), UTC).unwrap(); + let result = extract_timesheets(&shards).unwrap(); assert_eq!(result.len(), 1); assert!(result[0].is_sick_leave); @@ -483,7 +454,7 @@ mod tests { ), ]; - let result = extract_timesheets(&shards, past_now(), UTC).unwrap(); + let result = extract_timesheets(&shards).unwrap(); assert_eq!(result.len(), 1); assert!(result[0].is_sick_leave); @@ -492,7 +463,7 @@ mod tests { #[test] fn test_empty_input() { - let result = extract_timesheets(&[], past_now(), UTC).unwrap(); + let result = extract_timesheets(&[]).unwrap(); assert!(result.is_empty()); } @@ -512,7 +483,7 @@ mod tests { ), ]; - let result = extract_timesheets(&shards, past_now(), UTC); + let result = extract_timesheets(&shards); assert!(result.is_err()); let err = result.unwrap_err(); @@ -540,7 +511,7 @@ mod tests { ), ]; - let result = extract_timesheets(&shards, past_now(), UTC); + let result = extract_timesheets(&shards); assert!(result.is_err()); let err = result.unwrap_err(); @@ -563,7 +534,7 @@ mod tests { ), ]; - let result = extract_timesheets(&shards, past_now(), UTC).unwrap(); + let result = extract_timesheets(&shards).unwrap(); assert!(result.is_empty()); } diff --git a/src/timesheet/generator.rs b/src/timesheet/generator.rs index ede7898..e6cde40 100644 --- a/src/timesheet/generator.rs +++ b/src/timesheet/generator.rs @@ -2,8 +2,7 @@ use std::collections::HashMap; use std::fs; use std::path::Path; -use chrono::{DateTime, Datelike, NaiveDate, Utc, Weekday}; -use chrono_tz::Tz; +use chrono::{Datelike, NaiveDate, Weekday}; use crate::error::StreamdError; use crate::models::{SpecialDayType, Timesheet}; @@ -116,8 +115,6 @@ fn calculate_actual_minutes( pub fn generate_report( timesheets: &[Timesheet], config: &TimesheetConfig, - now: DateTime, - tz: Tz, ) -> Result { if config.periods.is_empty() { return Ok(TimesheetReport::new()); @@ -131,8 +128,8 @@ pub fn generate_report( let earliest_period_start = config.periods.iter().map(|p| p.start).min().unwrap(); let latest_period_end = config.periods.iter().map(|p| p.end).max().unwrap(); - // Limit to today in the configured timezone - let today = now.with_timezone(&tz).date_naive(); + // Limit to today + let today = chrono::Local::now().date_naive(); let end_date = latest_period_end.min(today); // Group by month and generate reports @@ -263,13 +260,7 @@ mod tests { use super::*; use crate::models::Timecard; use crate::timesheet::Period; - use chrono::{NaiveTime, TimeZone}; - use chrono_tz::UTC; - - /// A "now" well past all test dates so report limits aren't hit. - fn future_now() -> DateTime { - Utc.with_ymd_and_hms(2030, 1, 1, 0, 0, 0).unwrap() - } + use chrono::NaiveTime; fn date(year: i32, month: u32, day: u32) -> NaiveDate { NaiveDate::from_ymd_opt(year, month, day).unwrap() @@ -446,7 +437,7 @@ mod tests { #[test] fn test_generate_report_empty_config() { let config = TimesheetConfig { periods: vec![] }; - let report = generate_report(&[], &config, future_now(), UTC).unwrap(); + let report = generate_report(&[], &config).unwrap(); assert!(report.months.is_empty()); } @@ -456,7 +447,7 @@ mod tests { let timesheets = vec![make_timesheet(date(2026, 3, 2), vec![(9, 0, 17, 0)])]; let config = make_config(date(2026, 3, 2), date(2026, 3, 2), 40.0); - let report = generate_report(×heets, &config, future_now(), UTC).unwrap(); + let report = generate_report(×heets, &config).unwrap(); assert_eq!(report.months.len(), 1); assert_eq!(report.months[0].days.len(), 1); @@ -475,7 +466,7 @@ mod tests { // March 2 is Monday, March 3 is Tuesday let config = make_config(date(2026, 3, 2), date(2026, 3, 3), 40.0); - let report = generate_report(×heets, &config, future_now(), UTC).unwrap(); + let report = generate_report(×heets, &config).unwrap(); assert_eq!(report.months[0].days.len(), 2); @@ -493,7 +484,7 @@ mod tests { let timesheets = vec![make_timesheet(date(2026, 3, 2), vec![(9, 0, 17, 0)])]; let config = make_config(date(2026, 3, 2), date(2026, 3, 8), 40.0); - let report = generate_report(×heets, &config, future_now(), UTC).unwrap(); + let report = generate_report(×heets, &config).unwrap(); // Should only include Mon-Fri (5 days), not Sat-Sun let days = &report.months[0].days; @@ -512,7 +503,7 @@ mod tests { ]; let config = make_config(date(2026, 3, 2), date(2026, 3, 8), 40.0); - let report = generate_report(×heets, &config, future_now(), UTC).unwrap(); + let report = generate_report(×heets, &config).unwrap(); // Should include Saturday let has_saturday = report.months[0] @@ -535,7 +526,7 @@ mod tests { }; let config = make_config(date(2026, 3, 2), date(2026, 3, 2), 40.0); - let report = generate_report(&[ts], &config, future_now(), UTC).unwrap(); + let report = generate_report(&[ts], &config).unwrap(); assert!(report.has_warnings()); assert!(report.months[0].days[0].has_warnings()); @@ -550,7 +541,7 @@ mod tests { ]; let config = make_config(date(2026, 3, 2), date(2026, 3, 3), 40.0); - let report = generate_report(×heets, &config, future_now(), UTC).unwrap(); + let report = generate_report(×heets, &config).unwrap(); // Balance should be +120 min (+2h: 18h actual - 16h expected) assert_eq!(report.cumulative_balance, 120);