imp:timeclock: syntax is more robust and featureful

The default timeclock parser (ie when not using --old-timeclock) has
the following changes, related to issues such as
[#2141], [#2365], [#2400], [#2417]:

- semicolon now always starts a comment; timeclock account names can't include semicolons
  (though journal account names still can)
- clock-in and clock-out entries now have different syntax
- clock-ins now require an account name
- clock-outs now can have a comment and tags
- the doc has been rewritten, and now mentions the --old-timeclock flag

- lib: accountnamep and modifiedaccountnamep now take a flag to allow semicolons or not
This commit is contained in:
Simon Michael 2025-08-31 09:16:38 +01:00
parent 0d0f2697de
commit 5a3e34cc55
10 changed files with 258 additions and 128 deletions

View File

@ -7,6 +7,7 @@ converted to 'Transactions' and queried like a ledger.
-} -}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StandaloneDeriving #-}
module Hledger.Data.Timeclock ( module Hledger.Data.Timeclock (
timeclockEntriesToTransactions timeclockEntriesToTransactions
@ -31,6 +32,10 @@ import Hledger.Data.Dates
import Hledger.Data.Amount import Hledger.Data.Amount
import Hledger.Data.Posting import Hledger.Data.Posting
-- detailed output for debugging
-- deriving instance Show TimeclockEntry
-- compact output
instance Show TimeclockEntry where instance Show TimeclockEntry where
show t = printf "%s %s %s %s" (show $ tlcode t) (show $ tldatetime t) (tlaccount t) (tldescription t) show t = printf "%s %s %s %s" (show $ tlcode t) (show $ tldatetime t) (tlaccount t) (tldescription t)
@ -122,10 +127,11 @@ pairClockEntries (entry : rest) actives sessions
-- When there is no clockout, one is added with the provided current time. -- 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. -- 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. -- If any entries cannot be paired as expected, an error is raised.
-- This is the default behaviour. -- This is the new, default behaviour.
timeclockEntriesToTransactions :: LocalTime -> [TimeclockEntry] -> [Transaction] timeclockEntriesToTransactions :: LocalTime -> [TimeclockEntry] -> [Transaction]
timeclockEntriesToTransactions now entries = transactions timeclockEntriesToTransactions now entries = transactions
where 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 = pairClockEntries (sortBy (\e1 e2 -> compare (tldatetime e1) (tldatetime e2)) entries) [] []
transactionsFromSession s = entryFromTimeclockInOut (in' s) (out s) transactionsFromSession s = entryFromTimeclockInOut (in' s) (out s)
-- If any "in" sessions are in the future, then set their out time to the initial time -- If any "in" sessions are in the future, then set their out time to the initial time
@ -140,7 +146,7 @@ timeclockEntriesToTransactions now entries = transactions
-- When there is no clockout, one is added with the provided current time. -- 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. -- Sessions crossing midnight are split into days to give accurate per-day totals.
-- If entries are not in the expected in/out order, an error is raised. -- If entries are not in the expected in/out order, an error is raised.
-- This is the legacy behaviour, enabled by --old-timeclock. -- This is the old, legacy behaviour, enabled by --old-timeclock.
timeclockEntriesToTransactionsSingle :: LocalTime -> [TimeclockEntry] -> [Transaction] timeclockEntriesToTransactionsSingle :: LocalTime -> [TimeclockEntry] -> [Transaction]
timeclockEntriesToTransactionsSingle _ [] = [] timeclockEntriesToTransactionsSingle _ [] = []
timeclockEntriesToTransactionsSingle now [i] timeclockEntriesToTransactionsSingle now [i]
@ -213,8 +219,8 @@ entryFromTimeclockInOut i o
tstatus = Cleared, tstatus = Cleared,
tcode = "", tcode = "",
tdescription = desc, tdescription = desc,
tcomment = tlcomment i, tcomment = tlcomment i <> tlcomment o,
ttags = tltags i, ttags = tltags i ++ tltags o,
tpostings = ps, tpostings = ps,
tprecedingcomment="" tprecedingcomment=""
} }

View File

@ -74,6 +74,7 @@ module Hledger.Read.Common (
-- ** account names -- ** account names
modifiedaccountnamep, modifiedaccountnamep,
accountnamep, accountnamep,
accountnamenosemicolonp,
-- ** account aliases -- ** account aliases
accountaliasp, accountaliasp,
@ -691,16 +692,18 @@ yearorintp = do
--- *** account names --- *** account names
-- | Parse an account name (plus one following space if present), -- | Parse an account name plus one following space if present (see accountnamep);
-- then apply any parent account prefix and/or account aliases currently in effect, -- then apply any parent account prefix and/or account aliases currently in effect,
-- in that order. (Ie first add the parent account prefix, then rewrite with aliases). -- in that order. Ie first add the parent account prefix, then rewrite with aliases.
-- This calls error if any account alias with an invalid regular expression exists. -- This calls error if any account alias with an invalid regular expression exists.
modifiedaccountnamep :: JournalParser m AccountName -- The flag says whether account names may include semicolons; currently account names
modifiedaccountnamep = do -- in journal format may, but account names in timeclock/timedot formats may not.
modifiedaccountnamep :: Bool -> JournalParser m AccountName
modifiedaccountnamep allowsemicolon = do
parent <- getParentAccount parent <- getParentAccount
als <- getAccountAliases als <- getAccountAliases
-- off1 <- getOffset -- off1 <- getOffset
a <- lift accountnamep a <- lift $ if allowsemicolon then accountnamep else accountnamenosemicolonp
-- off2 <- getOffset -- off2 <- getOffset
-- XXX or accountNameApplyAliasesMemo ? doesn't seem to make a difference (retest that function) -- XXX or accountNameApplyAliasesMemo ? doesn't seem to make a difference (retest that function)
case accountNameApplyAliases als $ joinAccountNames parent a of case accountNameApplyAliases als $ joinAccountNames parent a of
@ -715,15 +718,17 @@ modifiedaccountnamep = do
-- | Parse an account name, plus one following space if present. -- | Parse an account name, plus one following space if present.
-- Account names have one or more parts separated by the account separator character, -- Account names have one or more parts separated by the account separator character,
-- and are terminated by two or more spaces (or end of input). -- and are terminated by two or more spaces (or end of input).
-- Each part is at least one character long, may have single spaces inside it, -- Each part is at least one character long, may have single spaces inside it, and starts with a non-whitespace.
-- and starts with a non-whitespace. -- (We should have required them to start with an alphanumeric, but didn't.)
-- Note, this means "{account}", "%^!" and ";comment" are all accepted -- Note, this means account names can contain all kinds of punctuation, including ; which usually starts a following comment.
-- (parent parsers usually prevent/consume the last). -- Parent parsers usually remove the following comment before using this parser.
-- It should have required parts to start with an alphanumeric;
-- for now it remains as-is for backwards compatibility.
accountnamep :: TextParser m AccountName accountnamep :: TextParser m AccountName
accountnamep = singlespacedtext1p accountnamep = singlespacedtext1p
-- Like accountnamep, but stops parsing if it reaches a semicolon.
accountnamenosemicolonp :: TextParser m AccountName
accountnamenosemicolonp = singlespacednoncommenttext1p
-- | Parse a single line of possibly empty text enclosed in double quotes. -- | Parse a single line of possibly empty text enclosed in double quotes.
doublequotedtextp :: TextParser m Text doublequotedtextp :: TextParser m Text
doublequotedtextp = between (char '"') (char '"') $ doublequotedtextp = between (char '"') (char '"') $
@ -1374,10 +1379,10 @@ followingcommentp = fst <$> followingcommentpWith (void $ takeWhileP Nothing (/=
-- using the provided line parser to parse each line. -- using the provided line parser to parse each line.
-- This returns the comment text, and the combined results from the line parser. -- This returns the comment text, and the combined results from the line parser.
-- --
-- Following comments begin with a semicolon and extend to the end of the line. -- Following comments are a 1-or-more-lines comment,
-- They can optionally be continued on the next lines, -- beginning with a semicolon possibly preceded by whitespace on the current line,
-- where each next line begins with an indent and another semicolon. -- or with an indented semicolon on the next line.
-- (This parser expects to see these semicolons and indents.) -- Additional lines also must begin with an indented semicolon.
-- --
-- Like Ledger, we sometimes allow data to be embedded in comments. -- Like Ledger, we sometimes allow data to be embedded in comments.
-- account directive comments and transaction comments can contain tags, -- account directive comments and transaction comments can contain tags,
@ -1441,10 +1446,11 @@ commentlinetagsp = do
-- | Parse a transaction comment and extract its tags. -- | Parse a transaction comment and extract its tags.
-- --
-- The first line of a transaction may be followed by comments, which -- The first line of a transaction may be followed a 1-or-more-lines comment,
-- begin with semicolons and extend to the end of the line. Transaction -- beginning with a semicolon possibly preceded by whitespace on the current line,
-- comments may span multiple lines, but comment lines below the -- or with an indented semicolon on the next line. Additional lines also must
-- transaction must be preceded by leading whitespace. -- begin with an indented semicolon.
-- See also followingcommentpWith.
-- --
-- 2000/1/1 ; a transaction comment starting on the same line ... -- 2000/1/1 ; a transaction comment starting on the same line ...
-- ; extending to the next line -- ; extending to the next line

View File

@ -517,7 +517,7 @@ accountdirectivep = do
-- the account name, possibly modified by preceding alias or apply account directives -- the account name, possibly modified by preceding alias or apply account directives
acct <- (notFollowedBy (char '(' <|> char '[') <?> "account name without brackets") >> acct <- (notFollowedBy (char '(' <|> char '[') <?> "account name without brackets") >>
modifiedaccountnamep modifiedaccountnamep True
-- maybe a comment, on this and/or following lines -- maybe a comment, on this and/or following lines
(cmt, tags) <- lift transactioncommentp (cmt, tags) <- lift transactioncommentp
@ -959,7 +959,7 @@ postingphelper isPostingRule mTransactionYear = do
lift skipNonNewlineSpaces1 lift skipNonNewlineSpaces1
status <- lift statusp status <- lift statusp
lift skipNonNewlineSpaces lift skipNonNewlineSpaces
account <- modifiedaccountnamep account <- modifiedaccountnamep True
return (status, account) return (status, account)
let (ptype, account') = (accountNamePostingType account, textUnbracket account) let (ptype, account') = (accountNamePostingType account, textUnbracket account)
lift skipNonNewlineSpaces lift skipNonNewlineSpaces

View File

@ -1,17 +1,14 @@
--- * -*- outline-regexp:"--- \\*"; -*- --- * -*- outline-regexp:"--- \\*"; -*-
--- ** doc --- ** doc
-- In Emacs, use TAB on lines beginning with "-- *" to collapse/expand sections. -- In Emacs, use TAB on lines beginning with "-- *" to collapse/expand sections.
-- Keep relevant parts synced with manual:
{-| {-|
A reader for the timeclock file format generated by timeclock.el A reader for the timeclock file format.
(<http://www.emacswiki.org/emacs/TimeClock>). Example:
@ What exactly is this format ? It was introduced in timeclock.el (<http://www.emacswiki.org/emacs/TimeClock>).
i 2007\/03\/10 12:26:00 hledger The old specification in timeclock.el 2.6 was:
o 2007\/03\/10 17:26:02
@
From timeclock.el 2.6:
@ @
A timeclock contains data in the form of a single entry per line. A timeclock contains data in the form of a single entry per line.
@ -41,6 +38,92 @@ i, o or O. The meanings of the codes are:
now finished. Useful for creating summary reports. now finished. Useful for creating summary reports.
@ @
Ledger's timeclock format is different, and hledger's timeclock format is different again.
For example: in a clock-in entry, after the time,
- timeclock.el's timeclock has 0-1 fields: [COMMENT]
- Ledger's timeclock has 0-2 fields: [ACCOUNT[ PAYEE]]
- hledger's timeclock has 1-3 fields: ACCOUNT[ DESCRIPTION[;COMMENT]]
hledger's timeclock format is:
@
# Comment lines like these, and blank lines, are ignored:
# comment line
; comment line
* comment line
# Lines beginning with b, h, or capital O are also ignored, for compatibility:
b SIMPLEDATE HH:MM[:SS][+-ZZZZ][ TEXT]
h SIMPLEDATE HH:MM[:SS][+-ZZZZ][ TEXT]
O SIMPLEDATE HH:MM[:SS][+-ZZZZ][ TEXT]
# Lines beginning with i or o are are clock-in / clock-out entries:
i SIMPLEDATE HH:MM[:SS][+-ZZZZ] ACCOUNT[ DESCRIPTION][;COMMENT]]
o SIMPLEDATE HH:MM[:SS][+-ZZZZ][ ACCOUNT][;COMMENT]
@
The date is a hledger [simple date](#simple-dates) (YYYY-MM-DD or similar).
The time parts must use two digits.
The seconds are optional.
A + or - four-digit time zone is accepted for compatibility, but currently ignored; times are always interpreted as a local time.
In clock-in entries (`i`), the account name is required.
A transaction description, separated from the account name by 2+ spaces, is optional.
A transaction comment, beginning with `;`, is also optional.
In clock-out entries (`o`) have no description, but can have a comment if you wish.
A clock-in and clock-out pair form a "transaction" posting some number of hours to an account - also known as a session.
Eg:
```timeclock
i 2015/03/30 09:00:00 session1
o 2015/03/30 10:00:00
```
```cli
$ hledger -f a.timeclock print
2015-03-30 * 09:00-10:00
(session1) 1.00h
```
Clock-ins and clock-outs are matched by their account/session name.
If a clock-outs does not specify a name, the most recent unclosed clock-in is closed.
Also, sessions spanning more than one day are automatically split at day boundaries.
Eg, the following time log:
```timeclock
i 2015/03/30 09:00:00 some account optional description after 2 spaces ; optional comment, tags:
o 2015/03/30 09:20:00
i 2015/03/31 22:21:45 another:account
o 2015/04/01 02:00:34
i 2015/04/02 12:00:00 another:account ; this demonstrates multple sessions being clocked in
i 2015/04/02 13:00:00 some account
o 2015/04/02 14:00:00
o 2015/04/02 15:00:00 another:account
```
generates these transactions:
```cli
$ hledger -f t.timeclock print
2015-03-30 * optional description after 2 spaces ; optional comment, tags:
(some account) 0.33h
2015-03-31 * 22:21-23:59
(another:account) 1.64h
2015-04-01 * 00:00-02:00
(another:account) 2.01h
2015-04-02 * 12:00-15:00 ; this demonstrates multiple sessions being clocked in
(another:account) 3.00h
2015-04-02 * 13:00-14:00
(some account) 1.00h
```
-} -}
--- ** language --- ** language
@ -68,6 +151,7 @@ import Hledger.Data
import Hledger.Read.Common import Hledger.Read.Common
import Hledger.Utils import Hledger.Utils
import Data.Text as T (strip) import Data.Text as T (strip)
import Data.Functor ((<&>))
--- ** doctest setup --- ** doctest setup
-- $setup -- $setup
@ -121,17 +205,43 @@ timeclockfilep iopts = do many timeclockitemp
-- comment-only) lines, can use choice w/o try -- comment-only) lines, can use choice w/o try
timeclockitemp = choice [ timeclockitemp = choice [
void (lift emptyorcommentlinep) void (lift emptyorcommentlinep)
, timeclockentryp >>= \e -> modify' (\j -> j{jparsetimeclockentries = e : jparsetimeclockentries j}) , entryp >>= \e -> modify' (\j -> j{jparsetimeclockentries = e : jparsetimeclockentries j})
] <?> "timeclock entry, comment line, or empty line" ] <?> "timeclock entry, comment line, or empty line"
where entryp = if _oldtimeclock iopts then oldtimeclockentryp else timeclockentryp
-- | Parse a timeclock entry. -- | Parse a timeclock entry (loose pre-1.50 format).
timeclockentryp :: JournalParser m TimeclockEntry oldtimeclockentryp :: JournalParser m TimeclockEntry
timeclockentryp = do oldtimeclockentryp = do
pos <- getSourcePos pos <- getSourcePos
code <- oneOf ("bhioO" :: [Char]) code <- oneOf ("bhioO" :: [Char])
lift skipNonNewlineSpaces1 lift skipNonNewlineSpaces1
datetime <- datetimep datetime <- datetimep
account <- fmap (fromMaybe "") $ optional $ lift skipNonNewlineSpaces1 >> modifiedaccountnamep account <- fmap (fromMaybe "") $ optional $ lift skipNonNewlineSpaces1 >> modifiedaccountnamep True
description <- fmap (maybe "" T.strip) $ optional $ lift $ skipNonNewlineSpaces1 >> descriptionp description <- fmap (maybe "" T.strip) $ optional $ lift $ skipNonNewlineSpaces1 >> descriptionp
(comment, tags) <- lift transactioncommentp (comment, tags) <- lift transactioncommentp
return $ TimeclockEntry pos (read [code]) datetime account description comment tags return $ TimeclockEntry pos (read [code]) datetime account description comment tags
-- | Parse a timeclock entry (more robust post-1.50 format).
timeclockentryp :: JournalParser m TimeclockEntry
timeclockentryp = do
pos <- getSourcePos
code <- oneOf ("iobhO" :: [Char])
lift skipNonNewlineSpaces1
datetime <- datetimep
(account, description) <- case code of
'i' -> do
lift skipNonNewlineSpaces1
a <- modifiedaccountnamep False
d <- optional (lift $ skipNonNewlineSpaces1 >> descriptionp) <&> maybe "" T.strip
return (a, d)
'o' -> do
-- Notice the try needed here to avoid a parse error if there's trailing spaces.
-- Unlike descriptionp above, modifiedaccountnamep requires nonempty text.
-- And when a parser in an optional fails after consuming input, optional doesn't backtrack,
-- it propagates the failure.
a <- optional (try $ lift skipNonNewlineSpaces1 >> modifiedaccountnamep False) <&> fromMaybe ""
return (a, "")
_ -> return ("", "")
lift skipNonNewlineSpaces
(comment, tags) <- lift $ optional transactioncommentp <&> fromMaybe ("",[])
return $ TimeclockEntry pos (read [code]) datetime account description comment tags

View File

@ -176,7 +176,7 @@ timedotentryp = do
dp "timedotentryp" dp "timedotentryp"
notFollowedBy datelinep notFollowedBy datelinep
lift $ optional $ choice [orgheadingprefixp, skipNonNewlineSpaces1] lift $ optional $ choice [orgheadingprefixp, skipNonNewlineSpaces1]
a <- modifiedaccountnamep a <- modifiedaccountnamep False
lift skipNonNewlineSpaces lift skipNonNewlineSpaces
taggedhours <- lift durationsp taggedhours <- lift durationsp
(comment0, tags0) <- (comment0, tags0) <-

View File

@ -4715,18 +4715,62 @@ $ hledger -f paypal-custom.csv print
# Timeclock # Timeclock
The time logging format of timeclock.el, as read by hledger. hledger can read time logs in the timeclock time logging format
of [timeclock.el](http://www.emacswiki.org/emacs/TimeClock).
As with [Ledger](http://ledger-cli.org/3.0/doc/ledger3.html#Time-Keeping),
hledger's timeclock format is a subset/variant of timeclock.el's.
hledger can read time logs in timeclock format. Note, hledger's timeclock format was made more robust in hledger 1.43 and 1.50.
[As with Ledger](http://ledger-cli.org/3.0/doc/ledger3.html#Time-Keeping), If your old time logs are rejected, you should adapt them to modern hledger;
these are (a subset of) but for now you can also restore the pre-1.43 behaviour with the `--old-timeclock` flag.
[timeclock.el](http://www.emacswiki.org/emacs/TimeClock)'s format,
containing clock-in and clock-out entries as in the example below. Here the timeclock format in hledger 1.50+:
The date is a [simple date](#simple-dates).
The time format is HH:MM[:SS][+-ZZZZ]. Seconds and timezone are optional. ```timeclock
The timezone, if present, must be four digits and is ignored # Comment lines like these, and blank lines, are ignored:
(currently the time is always interpreted as a local time). # comment line
Lines beginning with `#` or `;` or `*`, and blank lines, are ignored. ; comment line
* comment line
# Lines beginning with b, h, or capital O are also ignored, for compatibility:
b SIMPLEDATE HH:MM[:SS][+-ZZZZ][ TEXT]
h SIMPLEDATE HH:MM[:SS][+-ZZZZ][ TEXT]
O SIMPLEDATE HH:MM[:SS][+-ZZZZ][ TEXT]
# Lines beginning with i or o are are clock-in / clock-out entries:
i SIMPLEDATE HH:MM[:SS][+-ZZZZ] ACCOUNT[ DESCRIPTION][;COMMENT]]
o SIMPLEDATE HH:MM[:SS][+-ZZZZ][ ACCOUNT][;COMMENT]
```
The date is a hledger [simple date](#simple-dates) (YYYY-MM-DD or similar).
The time parts must use two digits.
The seconds are optional.
A + or - four-digit time zone is accepted for compatibility, but currently ignored; times are always interpreted as a local time.
In clock-in entries (`i`), the account name is required.
A transaction description, separated from the account name by 2+ spaces, is optional.
A transaction comment, beginning with `;`, is also optional.
(Indented following comment lines are also allowed, as in journal format.)
In clock-out entries (`o`) have no description, but can have a comment if you wish.
A clock-in and clock-out pair form a "transaction" posting some number of hours to an account - also known as a session.
Eg:
```timeclock
i 2015/03/30 09:00:00 session1
o 2015/03/30 10:00:00
```
```cli
$ hledger -f a.timeclock print
2015-03-30 * 09:00-10:00
(session1) 1.00h
```
Clock-ins and clock-outs are matched by their account/session name.
If a clock-outs does not specify a name, the most recent unclosed clock-in is closed.
Also, sessions spanning more than one day are automatically split at day boundaries.
Eg, the following time log:
```timeclock ```timeclock
i 2015/03/30 09:00:00 some account optional description after 2 spaces ; optional comment, tags: i 2015/03/30 09:00:00 some account optional description after 2 spaces ; optional comment, tags:
@ -4739,13 +4783,7 @@ o 2015/04/02 14:00:00
o 2015/04/02 15:00:00 another:account o 2015/04/02 15:00:00 another:account
``` ```
hledger treats each clock-in/clock-out pair as a transaction posting generates these transactions:
some number of hours to an account. Entries are paired by the account
name if the same name is given for a clock-in/clock-out pair. If no
name is given for a clock-out, then it is paired with the most recent
clock-in entry. If the session spans more than one day, it is split into
several transactions, one for each day. For the above time log,
`hledger print` generates these journal entries:
```cli ```cli
$ hledger -f t.timeclock print $ hledger -f t.timeclock print

View File

@ -1,5 +1,5 @@
#!/usr/bin/env -S hledger check -f #!/usr/bin/env -S hledger check -f
# Clockout time before previous clockin. # Clockout time before previous clockin.
i 2022/01/01 00:01:00 i 2022/01/01 00:01:00 a
o 2022/01/01 00:00:00 o 2022/01/01 00:00:00

View File

@ -1,8 +1,11 @@
$$$ hledger check -f tcorderedactions.timeclock $$$ hledger check -f tcorderedactions.timeclock
>>>2 /Error: .*tcorderedactions.timeclock:8:1: >>>2 /Error: .*tcorderedactions.timeclock:8:1:
8 \| i 2022-01-01 00:01:00 8 \| i 2022-01-01 00:01:00 a
\| \^ \| \^
Encountered clockin entry for session "" that is already active. Encountered clockin entry for session "a" that is already active.
/ /
>>>= 1 >>>= 1

View File

@ -4,5 +4,5 @@
# two clockouts without intervening clockin, # two clockouts without intervening clockin,
# or an initial clockout with no preceding clockin. # or an initial clockout with no preceding clockin.
i 2022/01/01 00:00:00 i 2022/01/01 00:00:00 a
i 2022/01/01 00:01:00 i 2022/01/01 00:01:00 a

View File

@ -1,104 +1,92 @@
# * timeclock input # * timeclock input
# ** 1. a timeclock session is parsed as a similarly-named transaction with one virtual posting. # ** 1. A timeclock session is parsed as a similarly-named transaction with one virtual posting.
# "session" is a synonym for "account" here. # "session" is a synonym for "account" here.
# After the account name there can be a description, with 2+ spaces between them.
< <
i 2009/1/1 08:00:00
o 2009/1/1 09:00:00
i 2009/1/2 08:00:00 account name i 2009/1/2 08:00:00 account name
o 2009/1/2 09:00:00 o 2009/1/2 09:00:00
i 2009/1/3 08:00:00 some:account name and a description i 2009/1/3 08:00:00 some:account name a description
o 2009/1/3 09:00:00 o 2009/1/3 09:00:00
$ hledger -f timeclock:- print $ hledger -f timeclock:- print
>
2009-01-01 * 08:00-09:00
() 1.00h
2009-01-02 * 08:00-09:00 2009-01-02 * 08:00-09:00
(account name) 1.00h (account name) 1.00h
2009-01-03 * and a description 2009-01-03 * a description
(some:account name) 1.00h (some:account name) 1.00h
>= >=
# ** 2. Command-line account aliases are applied. # ** 2. Command-line account aliases are applied.
$ hledger -ftimeclock:- print --alias '/account/=FOO' $ hledger -ftimeclock:- print --alias '/account/=FOO'
2009-01-01 * 08:00-09:00
() 1.00h
2009-01-02 * 08:00-09:00 2009-01-02 * 08:00-09:00
(FOO name) 1.00h (FOO name) 1.00h
2009-01-03 * and a description 2009-01-03 * a description
(some:FOO name) 1.00h (some:FOO name) 1.00h
>= >=
# ** 3. For session with no clock-out, an implicit clock-out at report time is assumed. # ** 3. For session with no clock-out, an implicit clock-out at report time is assumed.
< <
i 2020/1/1 08:00 i 2020/1/1 08:00 a
$ hledger -f timeclock:- balance $ hledger -f timeclock:- balance
> /./ > /./
>= >=
# ** 4. A time log not starting with a clock-in is an error. # ** 4. A time log not starting with a clock-in is an error.
< <
o 2020/1/1 08:00 o 2020/1/1 08:00 a
$ hledger -f timeclock:- balance $ hledger -f timeclock:- balance
>2 /Could not find previous clockin to match this clockout./ >2 /Could not find previous clockin to match this clockout./
>= !0 >= !0
# ** 5. Two consecutive anonymous clock-ins is an error. (?) # ** 5. Timeclock amounts are always rounded to two decimal places (#1527).
<
i 2020/1/1 08:00
i 2020/1/1 09:00
$ hledger -f timeclock:- balance
>2 /Encountered clockin entry for session "" that is already active./
>= !0
# ** 6. Timeclock amounts are always rounded to two decimal places (#1527).
< <
i 2020-01-30 08:38:35 acct i 2020-01-30 08:38:35 acct
o 2020-01-30 09:03:35 o 2020-01-30 09:03:35
$ hledger -f timeclock:- print $ hledger -f timeclock:- print
2020-01-30 * 08:38-09:03 2020-01-30 * 08:38-09:03
(acct) 0.42h (acct) 0.42h
>= >=
# ** 7. Comments and tags are supported on the clock-in. Double space is required # ** 6. Comments and tags are supported on both clock-in and clock-out.
# between account name and description or comment, but not between description and comment. # Semicolon starts a comment immediately, spaces before it are not required before it.
< <
i 2023-05-01 08:00:00 acct 1 description ; a comment with tag: i 2023-05-01 08:00:00 session1 ; clock-in comment with tag:
o 2023-05-01 09:00:00 o 2023-05-01 09:00:00; clock-out comment, foo:
i 2023-05-02 08:00:00 acct 2 ; another comment i 2023-05-02 08:00:00 session2
o 2023-05-02 09:00:00 o 2023-05-02 09:00:00
$ hledger -f timeclock:- print tag:tag $ hledger -f timeclock:- print tag:tag
2023-05-01 * description ; a comment with tag: 2023-05-01 * 08:00-09:00 ; clock-in comment with tag:
(acct 1) 1.00h ; clock-out comment, foo:
(session1) 1.00h
>= >=
# ** 8. TODO Comments on clock-outs are ignored / added to posting / added to transaction. # ** 7. Clock-in entries require an account name.
# XXX
< <
i 2025-01-01 09:00:00 session1 i 2025-01-01 09:00:00
o 2025-01-01 10:00:00 ; clock-out comment o 2025-01-01 10:00:00
$
#$ hledger -f timeclock:- print
# ** 9. Multiple sessions can be simultaneously clocked in. Clockouts can be named and in any order. $ hledger -f timeclock:- print
>2 /unexpected newline/
>= !0
# ** 8. Multiple sessions can be clocked in simultaneously. Clockouts can specify the session they are closing.
< <
i 2025-01-01 09:00:00 session1 9 to 5 session i 2025-01-01 09:00:00 session1 9 to 5 session
i 2025-01-01 12:00:00 session2 12 to 2 session, overlapping i 2025-01-01 12:00:00 session2 12 to 2 session, overlapping
o 2025-01-01 14:00:00 session2 o 2025-01-01 14:00:00 session2
o 2025-01-01 17:00:00 session1 o 2025-01-01 17:00:00 session1
$ hledger -f timeclock:- print $ hledger -f timeclock:- print
>
2025-01-01 * 9 to 5 session 2025-01-01 * 9 to 5 session
(session1) 8.00h (session1) 8.00h
@ -107,7 +95,7 @@ $ hledger -f timeclock:- print
>= >=
# ** 10. Unnamed clockouts apply to the most recently clocked-in session. # ** 9. Clockouts without a name apply to the most recently clocked-in session.
< <
i 2025-01-01 09:00:00 session1 start 9-12 i 2025-01-01 09:00:00 session1 start 9-12
i 2025-01-01 10:00:00 session2 start 10-5 i 2025-01-01 10:00:00 session2 start 10-5
@ -115,7 +103,7 @@ i 2025-01-01 11:00:00 session3 start 11-1
o 2025-01-01 13:00:00 o 2025-01-01 13:00:00
o 2025-01-01 12:00:00 session1 o 2025-01-01 12:00:00 session1
o 2025-01-01 17:00:00 o 2025-01-01 17:00:00
$
$ hledger -f timeclock:- print $ hledger -f timeclock:- print
2025-01-01 * start 9-12 2025-01-01 * start 9-12
(session1) 3.00h (session1) 3.00h
@ -128,14 +116,14 @@ $ hledger -f timeclock:- print
>= >=
# ** 11. Multiple active sessions can span multiple days. # ** 10. Multiple active sessions can span multiple days.
< <
i 2025-03-11 19:00:00 multi:1 i 2025-03-11 19:00:00 multi:1
i 2025-03-11 20:00:00 multi:2 i 2025-03-11 20:00:00 multi:2
o 2025-03-12 08:00:00 o 2025-03-12 08:00:00
o 2025-03-12 09:00:00 o 2025-03-12 09:00:00
$ hledger -f timeclock:- print $ hledger -f timeclock:- print
>
2025-03-11 * 19:00-23:59 2025-03-11 * 19:00-23:59
(multi:1) 5.00h (multi:1) 5.00h
@ -150,7 +138,7 @@ $ hledger -f timeclock:- print
>= >=
# ** 12. The --old-timeclock flag reverts to the old behavior. # ** 11. The --old-timeclock flag reverts to the old syntax and behavior.
< <
i 2009/1/1 08:00:00 i 2009/1/1 08:00:00
o 2009/1/1 09:00:00 stuff on checkout record is ignored o 2009/1/1 09:00:00 stuff on checkout record is ignored
@ -161,7 +149,6 @@ i 2009/1/3 08:00:00 some:account name and a description
o 2009/1/3 09:00:00 o 2009/1/3 09:00:00
$ hledger --old-timeclock -f timeclock:- print $ hledger --old-timeclock -f timeclock:- print
>
2009-01-01 * 08:00-09:00 2009-01-01 * 08:00-09:00
() 1.00h () 1.00h
@ -173,26 +160,6 @@ $ hledger --old-timeclock -f timeclock:- print
>= >=
# ** 13. TODO Overlapping sessions can have the same name (#2417).
<
i 2024-04-10 13:00:00 test
o 2024-04-10 14:00:00
i 2024-04-10 13:00:00 test
o 2024-04-10 15:00:00
$
# $ hledger -f timeclock:- print
# >=
# ** 14. TODO A start time can be the same as another session's end time (#2417).
<
i 2024-04-10 13:00:00 test
o 2024-04-10 14:00:00
i 2024-04-10 14:00:00 test
o 2024-04-10 15:00:00
$
# $ hledger -f timeclock:- print
# >=
# ** OLD: # ** OLD: