fix:timeclock: sessions can begin at a same-named session's end time

Now we process entries in a more careful order: time, then parse
order, like journal format. This fixes the original issue and another
one mentioned at #2417.
This commit is contained in:
Simon Michael 2025-08-31 12:01:04 +01:00
parent 5a3e34cc55
commit e6dbe5d231
2 changed files with 41 additions and 6 deletions

View File

@ -57,12 +57,12 @@ instance Read TimeclockCode where
data Session = Session
{ in' :: TimeclockEntry,
out :: TimeclockEntry
}
} deriving Show
data Sessions = Sessions
{ completed :: [Session],
active :: [TimeclockEntry]
}
} deriving Show
-- | Find the relevant clockin in the actives list that should be paired with this clockout.
-- If there is a session that has the same account name, then use that.
@ -124,23 +124,28 @@ pairClockEntries (entry : rest) actives sessions
else entry : actives
-- | Convert time log entries to journal transactions, allowing multiple clocked-in sessions at once.
-- This is the new, default behaviour.
-- Entries are processed in time order, then (for entries with the same time) in parse order.
-- When there is no clockout, one is added with the provided current time.
-- Sessions crossing midnight are split into days to give accurate per-day totals.
-- If any entries cannot be paired as expected, an error is raised.
-- This is the new, default behaviour.
timeclockEntriesToTransactions :: LocalTime -> [TimeclockEntry] -> [Transaction]
timeclockEntriesToTransactions now entries = transactions
where
-- XXX should they be date sorted ? or processed in the order written ?
sessions = pairClockEntries (sortBy (\e1 e2 -> compare (tldatetime e1) (tldatetime e2)) entries) [] []
sessions = dbg6 "sessions" $ pairClockEntries (sortTimeClockEntries entries) [] []
transactionsFromSession s = entryFromTimeclockInOut (in' s) (out s)
-- If any "in" sessions are in the future, then set their out time to the initial time
outtime te = max now (tldatetime te)
createout te = TimeclockEntry (tlsourcepos te) Out (outtime te) (tlaccount te) "" "" []
outs = map createout (active sessions)
stillopen = pairClockEntries ((active sessions) <> outs) [] []
stillopen = dbg6 "stillopen" $ pairClockEntries ((active sessions) <> outs) [] []
transactions = map transactionsFromSession $ sortBy (\s1 s2 -> compare (in' s1) (in' s2)) (completed sessions ++ completed stillopen)
-- | Sort timeclock entries first by date and time (with time zone ignored as usual), then by file position.
-- Ie, sort by time, but preserve the parse order of entries with the same time.
sortTimeClockEntries :: [TimeclockEntry] -> [TimeclockEntry]
sortTimeClockEntries = sortBy (\e1 e2 -> compare (tldatetime e1, tlsourcepos e1) (tldatetime e2, tlsourcepos e2))
-- | Convert time log entries to journal transactions, allowing only one clocked-in session at a time.
-- Entries must be a strict alternation of in and out, beginning with in.
-- When there is no clockout, one is added with the provided current time.

View File

@ -160,7 +160,37 @@ $ hledger --old-timeclock -f timeclock:- print
>=
# ** 12. A session can begin at another session's end time (#2417).
<
i 2025-01-01 12:00:00 a
o 2025-01-01 13:00:00
i 2025-01-01 13:00:00 b
o 2025-01-01 14:00:00
$ hledger -f timeclock:- print
2025-01-01 * 12:00-13:00
(a) 1.00h
2025-01-01 * 13:00-14:00
(b) 1.00h
>=
# ** 13. Even if that session has the same account name (#2417).
<
i 2024-01-01 13:00:00 a
o 2024-01-01 14:00:00
i 2024-01-01 14:00:00 a
o 2024-01-01 15:00:00
$ hledger -f timeclock:- print
2024-01-01 * 13:00-14:00
(a) 1.00h
2024-01-01 * 14:00-15:00
(a) 1.00h
>=
# ** OLD: