content: --- * -*- 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
  