1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250
|
--- * -*- outline-regexp:"--- \\*"; -*-
--- ** doc
-- 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.
What exactly is this format ? It was introduced in timeclock.el (<http://www.emacswiki.org/emacs/TimeClock>).
The old specification in timeclock.el 2.6 was:
@
A timeclock contains data in the form of a single entry per line.
Each entry has the form:
CODE YYYY/MM/DD HH:MM:SS [COMMENT]
CODE is one of: b, h, i, o or O. COMMENT is optional when the code is
i, o or O. The meanings of the codes are:
b Set the current time balance, or \"time debt\". Useful when
archiving old log data, when a debt must be carried forward.
The COMMENT here is the number of seconds of debt.
h Set the required working time for the given day. This must
be the first entry for that day. The COMMENT in this case is
the number of hours in this workday. Floating point amounts
are allowed.
i Clock in. The COMMENT in this case should be the name of the
project worked on.
o Clock out. COMMENT is unnecessary, but can be used to provide
a description of how the period went, for example.
O Final clock out. Whatever project was being worked on, it is
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 OverloadedStrings #-}
--- ** exports
module Hledger.Read.TimeclockReader (
-- * Reader
reader,
-- * Misc other exports
timeclockfilep,
)
where
--- ** imports
import Control.Monad
import Control.Monad.Except (ExceptT, liftEither)
import Control.Monad.State.Strict
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Text.Megaparsec hiding (parse)
import Hledger.Data
-- XXX too much reuse ?
import Hledger.Read.Common
import Hledger.Utils
import Data.Text as T (strip)
import Data.Functor ((<&>))
--- ** doctest setup
-- $setup
-- >>> :set -XOverloadedStrings
--- ** reader
reader :: MonadIO m => Reader m
reader = Reader
{rFormat = Timeclock
,rExtensions = ["timeclock"]
,rReadFn = handleReadFnToTextReadFn parse
,rParser = timeclockfilep
}
-- | Parse and post-process a "Journal" from timeclock.el's timeclock
-- format, saving the provided file path and the current time, or give an
-- error.
parse :: InputOpts -> FilePath -> Text -> ExceptT String IO Journal
parse iopts fp t = initialiseAndParseJournal (timeclockfilep iopts) iopts fp t
>>= liftEither . journalApplyAliases (aliasesFromOpts iopts)
>>= journalFinalise iopts fp t
--- ** parsers
-- timeclockfilepspecial :: InputOpts -> JournalParser m ParsedJournal
-- timeclockfilepspecial args =
-- timeclockfilep args
timeclockfilep :: MonadIO m => InputOpts -> JournalParser m ParsedJournal
timeclockfilep iopts = do
many timeclockitemp
eof
j@Journal{jparsetimeclockentries=es} <- get
-- Convert timeclock entries in this journal to transactions, closing any unfinished sessions.
-- Doing this here rather than in journalFinalise means timeclock sessions can't span file boundaries,
-- but it simplifies code above.
now <- liftIO getCurrentLocalTime
-- journalFinalise expects the transactions in reverse order, so reverse the output in either case
let
j' = if _oldtimeclock iopts
then
-- timeclockToTransactionsOld expects the entries to be in normal order,
-- but they have been parsed in reverse order, so reverse them before calling
j{jtxns = reverse $ timeclockToTransactionsOld now $ reverse es, jparsetimeclockentries = []}
else
-- We don't need to reverse these transactions
-- since they are sorted inside of timeclockToTransactions
j{jtxns = reverse $ timeclockToTransactions now es, jparsetimeclockentries = []}
return j'
where
-- As all ledger line types can be distinguished by the first
-- character, excepting transactions versus empty (blank or
-- comment-only) lines, can use choice w/o try
timeclockitemp = choice [
void (lift emptyorcommentlinep)
,entryp >>= \e -> modify' (\j -> j{jparsetimeclockentries = e : jparsetimeclockentries j})
] <?> "timeclock entry, comment line, or empty line"
where entryp = if _oldtimeclock iopts then oldtimeclockentryp else timeclockentryp
-- | Parse a timeclock entry (loose pre-1.50 format).
oldtimeclockentryp :: JournalParser m TimeclockEntry
oldtimeclockentryp = do
pos <- getSourcePos
code <- oneOf ("bhioO" :: [Char])
lift skipNonNewlineSpaces1
datetime <- datetimep
account <- fmap (fromMaybe "") $ optional $ lift skipNonNewlineSpaces1 >> modifiedaccountnamep True
description <- fmap (maybe "" T.strip) $ optional $ lift $ skipNonNewlineSpaces1 >> descriptionp
(comment, tags) <- lift transactioncommentp
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
|