11use anyhow:: Result ;
22use chrono:: NaiveDate ;
3- use std:: collections:: HashMap ;
3+ use std:: collections:: { HashMap , HashSet } ;
44
55use crate :: entry:: { Activity , ActivityType } ;
66use 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 {
0 commit comments