Skip to content

Commit 94a55a6

Browse files
author
Per-Gunnar Eriksson
committed
Final fixes reporting
1 parent 644370f commit 94a55a6

File tree

3 files changed

+118
-45
lines changed

3 files changed

+118
-45
lines changed

src/commands/report.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,21 @@ pub fn execute(
5252
// Convert entries to activities
5353
let mut activities = storage::entries_to_activities(&filtered_entries, Some(range.start_date), Some(range.end_date));
5454

55-
// For today-only reports, ensure we only show activities that occurred today
55+
// For today-only reports, ensure we show all activities that have a start or end time today
5656
if is_today_only {
5757
activities.retain(|activity| {
58-
activity.end.date_naive() == now.date_naive()
58+
let today = now.date_naive();
59+
let activity_date = activity.end.date_naive();
60+
61+
// For current day reports, show activities that end today
62+
activity_date == today
5963
});
6064
} else {
61-
// For other date ranges, ensure activities fall within the specified range
65+
// For date range reports, show all activities that occur within the range
6266
activities.retain(|activity| {
6367
let activity_date = activity.end.date_naive();
68+
69+
// Include activities where the end date falls within the range
6470
activity_date >= range.start_date && activity_date <= range.end_date
6571
});
6672
}
@@ -108,6 +114,7 @@ pub fn execute(
108114

109115
Ok(())
110116
}
117+
111118
/// Parse date range from various command line arguments
112119
fn parse_date_range(
113120
date: Option<&str>,

src/report/mod.rs

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use anyhow::Result;
22
use chrono::NaiveDate;
3-
use std::collections::HashMap;
3+
use std::collections::{HashMap, HashSet};
44

55
use crate::entry::{Activity, ActivityType};
66
use crate::util;
@@ -217,9 +217,9 @@ fn format_projects(projects: &HashMap<String, (chrono::Duration, Vec<String>)>)
217217
output
218218
}
219219

220-
fn group_by_activity(activities: &[Activity]) -> HashMap<ActivityType, Vec<(String, String, chrono::Duration)>> {
221-
let mut grouped: HashMap<ActivityType, Vec<(String, String, chrono::Duration)>> = HashMap::new();
222-
let mut seen_tasks: HashMap<(ActivityType, String, String), chrono::Duration> = HashMap::new();
220+
fn group_by_activity(activities: &[Activity]) -> HashMap<ActivityType, Vec<(String, String, chrono::Duration, NaiveDate)>> {
221+
let mut grouped: HashMap<ActivityType, Vec<(String, String, chrono::Duration, NaiveDate)>> = HashMap::new();
222+
let mut seen_tasks: HashMap<(ActivityType, String, String, NaiveDate), chrono::Duration> = HashMap::new();
223223

224224
// First, calculate total durations for each unique activity
225225
for activity in activities {
@@ -228,26 +228,34 @@ fn group_by_activity(activities: &[Activity]) -> HashMap<ActivityType, Vec<(Stri
228228
}
229229

230230
let project = activity.project.clone().unwrap_or_default();
231-
let key = (activity.activity_type.clone(), project.clone(), activity.task.clone());
231+
let activity_date = activity.end.date_naive();
232+
let key = (activity.activity_type.clone(), project.clone(), activity.task.clone(), activity_date);
232233

233234
let duration = seen_tasks.entry(key).or_insert(chrono::Duration::zero());
234235
*duration = *duration + activity.duration;
235236
}
236237

237238
// Then, populate the groups
238-
for ((activity_type, project, task), duration) in seen_tasks {
239+
for ((activity_type, project, task, date), duration) in seen_tasks {
239240
if !grouped.contains_key(&activity_type) {
240241
grouped.insert(activity_type.clone(), Vec::new());
241242
}
242243

243244
if let Some(group) = grouped.get_mut(&activity_type) {
244-
group.push((project, task, duration));
245+
group.push((project, task, duration, date));
245246
}
246247
}
247248

248-
// Sort each group by project and task
249+
// Sort each group by date, then by project and task
249250
for group in grouped.values_mut() {
250251
group.sort_by(|a, b| {
252+
// First sort by date
253+
let date_cmp = a.3.cmp(&b.3);
254+
if date_cmp != std::cmp::Ordering::Equal {
255+
return date_cmp;
256+
}
257+
258+
// Then sort by project and task
251259
let a_key = format!("{}{}", a.0.to_lowercase(), a.1.to_lowercase());
252260
let b_key = format!("{}{}", b.0.to_lowercase(), b.1.to_lowercase());
253261
a_key.cmp(&b_key)
@@ -257,12 +265,35 @@ fn group_by_activity(activities: &[Activity]) -> HashMap<ActivityType, Vec<(Stri
257265
grouped
258266
}
259267

260-
fn format_activity_groups(groups: &HashMap<ActivityType, Vec<(String, String, chrono::Duration)>>) -> String {
268+
fn format_activity_groups(groups: &HashMap<ActivityType, Vec<(String, String, chrono::Duration, NaiveDate)>>) -> String {
261269
let mut output = String::new();
270+
let mut saw_multi_days = false;
271+
272+
// Check if we have activities from multiple days
273+
let mut unique_dates = HashSet::new();
274+
for activities in groups.values() {
275+
for (_, _, _, date) in activities {
276+
unique_dates.insert(*date);
277+
}
278+
}
279+
280+
saw_multi_days = unique_dates.len() > 1;
262281

263282
// Format work activities
264283
if let Some(work_activities) = groups.get(&ActivityType::Work) {
265-
for (project, task, duration) in work_activities {
284+
let mut current_date: Option<NaiveDate> = None;
285+
286+
for (project, task, duration, date) in work_activities {
287+
// Add date header if date changes and we have multiple days
288+
if saw_multi_days && current_date.map_or(true, |d| d != *date) {
289+
if current_date.is_some() {
290+
output.push('\n');
291+
}
292+
293+
current_date = Some(*date);
294+
output.push_str(&format!("{}:\n", date.format("%Y-%m-%d")));
295+
}
296+
266297
let project_str = if project.is_empty() {
267298
String::new()
268299
} else {
@@ -280,7 +311,19 @@ fn format_activity_groups(groups: &HashMap<ActivityType, Vec<(String, String, ch
280311

281312
// Format break activities
282313
if let Some(break_activities) = groups.get(&ActivityType::Break) {
283-
for (project, task, duration) in break_activities {
314+
let mut current_date: Option<NaiveDate> = None;
315+
316+
for (project, task, duration, date) in break_activities {
317+
// Add date header if date changes and we have multiple days
318+
if saw_multi_days && current_date.map_or(true, |d| d != *date) {
319+
if current_date.is_some() {
320+
output.push('\n');
321+
}
322+
323+
current_date = Some(*date);
324+
output.push_str(&format!("{}:\n", date.format("%Y-%m-%d")));
325+
}
326+
284327
let project_str = if project.is_empty() {
285328
String::new()
286329
} else {

src/storage.rs

Lines changed: 54 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use anyhow::{Context, Result};
2-
use chrono::{DateTime, Local, NaiveDate};
2+
use chrono::{DateTime, Local, NaiveDate, TimeZone};
33
use std::fs::{self, File, OpenOptions};
44
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write};
55
use std::path::Path;
@@ -144,38 +144,61 @@ pub fn entries_to_activities(entries: &[Entry], start_date: Option<NaiveDate>, e
144144
let start_time = entries[i].datetime;
145145
let end_time = entries[i+1].datetime;
146146

147-
// Apply date filtering if specified
148-
// Only include activities that occur within the date range
149-
if let Some(start_date) = start_date {
150-
if end_time.date_naive() < start_date {
151-
continue; // Skip activities that end before the start date
152-
}
153-
}
154-
155-
if let Some(end_date) = end_date {
156-
if start_time.date_naive() > end_date {
157-
continue; // Skip activities that start after the end date
158-
}
159-
}
160-
161-
// Only include activities where the end time is within the date range
162-
// This ensures only activities that complete within the requested range are shown
163-
if let (Some(start_date), Some(end_date)) = (start_date, end_date) {
164-
let activity_date = end_time.date_naive();
165-
if activity_date < start_date || activity_date > end_date {
166-
continue;
147+
// If the times span multiple days, split them into separate daily activities
148+
if start_time.date_naive() != end_time.date_naive() {
149+
// Create an activity for each day in the range
150+
let mut current_date = start_time.date_naive();
151+
let end_date = end_time.date_naive();
152+
153+
while current_date <= end_date {
154+
// Define the start and end of the activity for this specific day
155+
let day_start = if current_date == start_time.date_naive() {
156+
start_time
157+
} else {
158+
// Start of the day (midnight)
159+
Local.from_utc_datetime(&current_date.and_hms_opt(0, 0, 0).unwrap())
160+
};
161+
162+
let day_end = if current_date == end_date {
163+
end_time
164+
} else {
165+
// End of the day (23:59:59)
166+
Local.from_utc_datetime(&current_date.and_hms_opt(23, 59, 59).unwrap())
167+
};
168+
169+
let activity = Activity::new(
170+
entries[i+1].name.clone(),
171+
day_start,
172+
day_end,
173+
false,
174+
entries[i+1].comment.clone(),
175+
);
176+
177+
activities.push(activity);
178+
179+
// Move to the next day
180+
current_date = current_date.succ_opt().unwrap();
167181
}
182+
} else {
183+
// Regular single-day activity
184+
let activity = Activity::new(
185+
entries[i+1].name.clone(),
186+
start_time,
187+
end_time,
188+
false,
189+
entries[i+1].comment.clone(),
190+
);
191+
192+
activities.push(activity);
168193
}
169-
170-
let activity = Activity::new(
171-
entries[i+1].name.clone(),
172-
start_time,
173-
end_time,
174-
false,
175-
entries[i+1].comment.clone(),
176-
);
177-
178-
activities.push(activity);
194+
}
195+
196+
// Apply date filtering if specified
197+
if let (Some(start), Some(end)) = (start_date, end_date) {
198+
activities.retain(|activity| {
199+
let activity_date = activity.end.date_naive();
200+
activity_date >= start && activity_date <= end
201+
});
179202
}
180203

181204
activities

0 commit comments

Comments
 (0)