fix:timeclock: --old-timeclock also affects included files [#2417]

This required changing the Reader type and passing InputOpts down to
journal's include directive parser.
This commit is contained in:
Simon Michael 2025-09-01 05:49:12 +01:00
parent 8bd9d92981
commit efe1d11edb
7 changed files with 66 additions and 48 deletions

View File

@ -172,27 +172,25 @@ import Hledger.Read.InputOptions
-- main types; a few more below -- main types; a few more below
-- | A hledger journal reader is a triple of storage format name, a -- | A hledger journal reader is a storage format name,
-- detector of that format, and a parser from that format to Journal. -- a list of file extensions assumed to be in this format,
-- The type variable m appears here so that rParserr can hold a -- and an IO action that reads data in this format, returning a Journal.
-- journal parser, which depends on it. --
-- The journal parser used by the latter is also stored separately for direct use
-- by the journal reader's includedirectivep to parse included files.
-- The type variable m is needed for this parser.
-- Lately it requires an InputOpts, basically to support --old-timeclock.
data Reader m = Reader { data Reader m = Reader {
-- The canonical name of the format handled by this reader. "journal", "timedot", "csv" etc.
-- The canonical name of the format handled by this reader rFormat :: StorageFormat
rFormat :: StorageFormat -- The file extensions recognised as containing this format.
,rExtensions :: [String]
-- The file extensions recognised as containing this format -- An IO action for reading this format, producing a journal or an error message.
,rExtensions :: [String] -- It accepts input options, a file path to show in error messages, and a handle to read data from.
,rReadFn :: InputOpts -> FilePath -> Handle -> ExceptT String IO Journal
-- The entry point for reading this format, accepting input options, file -- The megaparsec parser called by the above, provided separately for parsing included files.
-- path for error messages and file contents via the handle, producing an exception-raising IO ,rParser :: MonadIO m => InputOpts -> ErroringJournalParser m ParsedJournal
-- action that produces a journal or error message. }
,rReadFn :: InputOpts -> FilePath -> Handle -> ExceptT String IO Journal
-- The actual megaparsec parser called by the above, in case
-- another parser (includedirectivep) wants to use it directly.
,rParser :: MonadIO m => ErroringJournalParser m ParsedJournal
}
instance Show (Reader m) where show r = show (rFormat r) ++ " reader" instance Show (Reader m) where show r = show (rFormat r) ++ " reader"

View File

@ -46,7 +46,7 @@ reader sep = Reader
{rFormat = Sep sep {rFormat = Sep sep
,rExtensions = [show sep] ,rExtensions = [show sep]
,rReadFn = parse sep ,rReadFn = parse sep
,rParser = fail "sorry, CSV files can't be included yet" -- PARTIAL: ,rParser = const $ fail "sorry, CSV files can't be included yet" -- PARTIAL:
-- This unnecessarily shows the CSV file's first line in the error message, -- This unnecessarily shows the CSV file's first line in the error message,
-- but gives a more useful message than just calling error'. -- but gives a more useful message than just calling error'.
-- XXX Note every call to error' in Hledger.Read.* is potentially a similar problem - -- XXX Note every call to error' in Hledger.Read.* is potentially a similar problem -

View File

@ -220,7 +220,7 @@ parse iopts f = parseAndFinaliseJournal journalp' iopts f
journalp' = do journalp' = do
-- reverse parsed aliases to ensure that they are applied in order given on commandline -- reverse parsed aliases to ensure that they are applied in order given on commandline
mapM_ addAccountAlias (reverse $ aliasesFromOpts iopts) mapM_ addAccountAlias (reverse $ aliasesFromOpts iopts)
journalp journalp iopts
--- ** parsers --- ** parsers
--- *** journal --- *** journal
@ -228,23 +228,23 @@ parse iopts f = parseAndFinaliseJournal journalp' iopts f
-- | A journal parser. Accumulates and returns a "ParsedJournal", -- | A journal parser. Accumulates and returns a "ParsedJournal",
-- which should be finalised/validated before use. -- which should be finalised/validated before use.
-- --
-- >>> rejp (journalp <* eof) "2015/1/1\n a 0\n" -- >>> rejp (journalp definputopts <* eof) "2015/1/1\n a 0\n"
-- Right (Right Journal (unknown) with 1 transactions, 1 accounts) -- Right (Right Journal (unknown) with 1 transactions, 1 accounts)
-- --
journalp :: MonadIO m => ErroringJournalParser m ParsedJournal journalp :: MonadIO m => InputOpts -> ErroringJournalParser m ParsedJournal
journalp = do journalp iopts = do
many addJournalItemP many $ addJournalItemP iopts
eof eof
get get
-- | A side-effecting parser; parses any kind of journal item -- | A side-effecting parser; parses any kind of journal item
-- and updates the parse state accordingly. -- and updates the parse state accordingly.
addJournalItemP :: MonadIO m => ErroringJournalParser m () addJournalItemP :: MonadIO m => InputOpts -> ErroringJournalParser m ()
addJournalItemP = addJournalItemP iopts =
-- all journal line types can be distinguished by the first -- all journal line types can be distinguished by the first
-- character, can use choice without backtracking -- character, can use choice without backtracking
choice [ choice [
directivep directivep iopts
, transactionp >>= modify' . addTransaction , transactionp >>= modify' . addTransaction
, transactionmodifierp >>= modify' . addTransactionModifier , transactionmodifierp >>= modify' . addTransactionModifier
, periodictransactionp >>= modify' . addPeriodicTransaction , periodictransactionp >>= modify' . addPeriodicTransaction
@ -258,11 +258,11 @@ addJournalItemP =
-- | Parse any journal directive and update the parse state accordingly. -- | Parse any journal directive and update the parse state accordingly.
-- Cf http://hledger.org/hledger.html#directives, -- Cf http://hledger.org/hledger.html#directives,
-- http://ledger-cli.org/3.0/doc/ledger3.html#Command-Directives -- http://ledger-cli.org/3.0/doc/ledger3.html#Command-Directives
directivep :: MonadIO m => ErroringJournalParser m () directivep :: MonadIO m => InputOpts -> ErroringJournalParser m ()
directivep = (do directivep iopts = (do
optional $ oneOf ['!','@'] optional $ oneOf ['!','@']
choice [ choice [
includedirectivep includedirectivep iopts
,aliasdirectivep ,aliasdirectivep
,endaliasesdirectivep ,endaliasesdirectivep
,accountdirectivep ,accountdirectivep
@ -296,10 +296,11 @@ directivep = (do
) <?> "directive" ) <?> "directive"
-- | Parse an include directive, and the file(s) it refers to, possibly recursively. -- | Parse an include directive, and the file(s) it refers to, possibly recursively.
-- Input options are required since they may affect parsing (of timeclock files, specifically).
-- include's argument is a file path or glob pattern (see findMatchedFiles for details), -- include's argument is a file path or glob pattern (see findMatchedFiles for details),
-- optionally with a file type prefix. Relative paths are relative to the current file. -- optionally with a file type prefix. Relative paths are relative to the current file.
includedirectivep :: MonadIO m => ErroringJournalParser m () includedirectivep :: MonadIO m => InputOpts -> ErroringJournalParser m ()
includedirectivep = do includedirectivep iopts = do
-- save the position at start of include directive, for error messages -- save the position at start of include directive, for error messages
eoff <- getOffset eoff <- getOffset
-- and the parent file's path, for error messages and debug output -- and the parent file's path, for error messages and debug output
@ -335,7 +336,7 @@ includedirectivep = do
Just fmt -> map ((show fmt++":")++) paths Just fmt -> map ((show fmt++":")++) paths
-- Parse each one, as if inlined here. -- Parse each one, as if inlined here.
forM_ prefixedpaths $ parseIncludedFile eoff forM_ prefixedpaths $ parseIncludedFile iopts eoff
where where
@ -432,8 +433,8 @@ includedirectivep = do
-- Parse the given included file (and any deeper includes, recursively) as if it was inlined in the current (parent) file. -- Parse the given included file (and any deeper includes, recursively) as if it was inlined in the current (parent) file.
-- The offset of the start of the include directive in the parent file is provided for error messages. -- The offset of the start of the include directive in the parent file is provided for error messages.
parseIncludedFile :: MonadIO m => Int -> PrefixedFilePath -> ErroringJournalParser m () parseIncludedFile :: MonadIO m => InputOpts -> Int -> PrefixedFilePath -> ErroringJournalParser m ()
parseIncludedFile eoff prefixedpath = do parseIncludedFile iopts1 eoff prefixedpath = do
let (_mprefix,filepath) = splitReaderPrefix prefixedpath let (_mprefix,filepath) = splitReaderPrefix prefixedpath
-- Throw an error if a cycle is detected -- Throw an error if a cycle is detected
@ -449,7 +450,7 @@ includedirectivep = do
-- Choose a reader based on the file path prefix or file extension, -- Choose a reader based on the file path prefix or file extension,
-- defaulting to JournalReader. Duplicating readJournal a bit here. -- defaulting to JournalReader. Duplicating readJournal a bit here.
let r = fromMaybe reader $ findReader Nothing (Just prefixedpath) let r = fromMaybe reader $ findReader Nothing (Just prefixedpath)
parser = rParser r parser = (rParser r) iopts1
dbg7IO "parseIncludedFile: trying reader" (rFormat r) dbg7IO "parseIncludedFile: trying reader" (rFormat r)
-- Parse the file (and its own includes, if any) to a Journal -- Parse the file (and its own includes, if any) to a Journal
@ -1220,8 +1221,8 @@ tests_JournalReader = testGroup "JournalReader" [
,testGroup "directivep" [ ,testGroup "directivep" [
testCase "supports !" $ do testCase "supports !" $ do
assertParseE directivep "!account a\n" assertParseE (directivep definputopts) "!account a\n"
assertParseE directivep "!D 1.0\n" assertParseE (directivep definputopts) "!D 1.0\n"
] ]
,testGroup "accountdirectivep" [ ,testGroup "accountdirectivep" [
@ -1255,8 +1256,8 @@ tests_JournalReader = testGroup "JournalReader" [
assertParse ignoredpricecommoditydirectivep "N $\n" assertParse ignoredpricecommoditydirectivep "N $\n"
,testGroup "includedirectivep" [ ,testGroup "includedirectivep" [
testCase "include" $ assertParseErrorE includedirectivep "include nosuchfile\n" "No files were matched by glob pattern: nosuchfile" testCase "include" $ assertParseErrorE (includedirectivep definputopts) "include nosuchfile\n" "No files were matched by glob pattern: nosuchfile"
,testCase "glob" $ assertParseErrorE includedirectivep "include nosuchfile*\n" "No files were matched by glob pattern: nosuchfile*" ,testCase "glob" $ assertParseErrorE (includedirectivep definputopts) "include nosuchfile*\n" "No files were matched by glob pattern: nosuchfile*"
] ]
,testCase "marketpricedirectivep" $ assertParseEq marketpricedirectivep ,testCase "marketpricedirectivep" $ assertParseEq marketpricedirectivep
@ -1283,12 +1284,12 @@ tests_JournalReader = testGroup "JournalReader" [
assertParse endtagdirectivep "end apply tag \n" assertParse endtagdirectivep "end apply tag \n"
,testGroup "journalp" [ ,testGroup "journalp" [
testCase "empty file" $ assertParseEqE journalp "" nulljournal testCase "empty file" $ assertParseEqE (journalp definputopts) "" nulljournal
] ]
-- these are defined here rather than in Common so they can use journalp -- these are defined here rather than in Common so they can use journalp
,testCase "parseAndFinaliseJournal" $ do ,testCase "parseAndFinaliseJournal" $ do
ej <- runExceptT $ parseAndFinaliseJournal journalp definputopts "" "2019-1-1\n" ej <- runExceptT $ parseAndFinaliseJournal (journalp definputopts) definputopts "" "2019-1-1\n"
let Right j = ej let Right j = ej
assertEqual "" [""] $ journalFilePaths j assertEqual "" [""] $ journalFilePaths j

View File

@ -164,7 +164,7 @@ reader = Reader
{rFormat = Timeclock {rFormat = Timeclock
,rExtensions = ["timeclock"] ,rExtensions = ["timeclock"]
,rReadFn = handleReadFnToTextReadFn parse ,rReadFn = handleReadFnToTextReadFn parse
,rParser = timeclockfilep definputopts ,rParser = timeclockfilep
} }
-- | Parse and post-process a "Journal" from timeclock.el's timeclock -- | Parse and post-process a "Journal" from timeclock.el's timeclock

View File

@ -74,7 +74,7 @@ reader = Reader
-- | Parse and post-process a "Journal" from the timedot format, or give an error. -- | Parse and post-process a "Journal" from the timedot format, or give an error.
parse :: InputOpts -> FilePath -> Text -> ExceptT String IO Journal parse :: InputOpts -> FilePath -> Text -> ExceptT String IO Journal
parse iopts fp t = initialiseAndParseJournal timedotp iopts fp t parse iopts fp t = initialiseAndParseJournal (timedotp iopts) iopts fp t
>>= liftEither . journalApplyAliases (aliasesFromOpts iopts) >>= liftEither . journalApplyAliases (aliasesFromOpts iopts)
>>= journalFinalise iopts fp t >>= journalFinalise iopts fp t
@ -106,8 +106,8 @@ Org headings before the first date line are ignored, regardless of content.
timedotfilep = timedotp -- XXX rename export above timedotfilep = timedotp -- XXX rename export above
timedotp :: JournalParser m ParsedJournal timedotp :: InputOpts -> JournalParser m ParsedJournal
timedotp = preamblep >> many dayp >> eof >> get timedotp _ = preamblep >> many dayp >> eof >> get
preamblep :: JournalParser m () preamblep :: JournalParser m ()
preamblep = do preamblep = do

View File

@ -0,0 +1,6 @@
# A file that requires --old-timeclock. (Update when needed.)
i 2000-01-01 09:00:00 a
o 2000-01-01 11:00:00
i 2000-01-01 10:00:00 a
o 2000-01-01 11:00:00

View File

@ -203,6 +203,19 @@ $ hledger -f timeclock:- print
>2 /Overlapping sessions with the same account name are not supported./ >2 /Overlapping sessions with the same account name are not supported./
>=!0 >=!0
# ** 15. --old-timeclock also affects included files.
<
include needs-old.timeclock
$ hledger -f- print --old-timeclock
2000-01-01 * 09:00-11:00
(a) 2.00h
2000-01-01 * 10:00-11:00
(a) 1.00h
>=
# ** OLD: # ** OLD:
## multi-day sessions get a new transaction for each day ## multi-day sessions get a new transaction for each day