mirror of
https://github.com/simonmichael/hledger.git
synced 2024-09-19 10:17:35 +03:00
fix: fix multi-file account display order; improve file read logging (#1909)
This commit is contained in:
parent
4be4525b90
commit
1f08a8a94e
@ -29,6 +29,7 @@ module Hledger.Data.Journal (
|
||||
journalAddPricesFromEquity,
|
||||
journalReverse,
|
||||
journalSetLastReadTime,
|
||||
journalRenumberAccountDeclarations,
|
||||
journalPivot,
|
||||
-- * Filtering
|
||||
filterJournalTransactions,
|
||||
@ -98,11 +99,13 @@ module Hledger.Data.Journal (
|
||||
-- * Misc
|
||||
canonicalStyleFrom,
|
||||
nulljournal,
|
||||
journalConcat,
|
||||
journalNumberTransactions,
|
||||
journalNumberAndTieTransactions,
|
||||
journalUntieTransactions,
|
||||
journalModifyTransactions,
|
||||
journalApplyAliases,
|
||||
dbgJournalAcctDeclOrder,
|
||||
-- * Tests
|
||||
samplejournal,
|
||||
samplejournalMaybeExplicit,
|
||||
@ -117,7 +120,7 @@ import Control.Monad.State.Strict (StateT)
|
||||
import Data.Char (toUpper, isDigit)
|
||||
import Data.Default (Default(..))
|
||||
import Data.Foldable (toList)
|
||||
import Data.List ((\\), find, foldl', sortBy, union)
|
||||
import Data.List ((\\), find, foldl', sortBy, union, intercalate)
|
||||
import Data.List.Extra (nubSort)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (catMaybes, fromMaybe, mapMaybe, maybeToList)
|
||||
@ -141,6 +144,7 @@ import Hledger.Data.Transaction
|
||||
import Hledger.Data.TransactionModifier
|
||||
import Hledger.Data.Valuation
|
||||
import Hledger.Query
|
||||
import System.FilePath (takeFileName)
|
||||
|
||||
|
||||
-- | A parser of text that runs in some monad, keeping a Journal as state.
|
||||
@ -188,9 +192,7 @@ instance Show Journal where
|
||||
-- The semigroup instance for Journal is useful for two situations.
|
||||
--
|
||||
-- 1. concatenating finalised journals, eg with multiple -f options:
|
||||
-- FIRST <> SECOND. The second's list fields are appended to the
|
||||
-- first's, map fields are combined, transaction counts are summed,
|
||||
-- the parse state of the second is kept.
|
||||
-- FIRST <> SECOND.
|
||||
--
|
||||
-- 2. merging a child parsed journal, eg with the include directive:
|
||||
-- CHILD <> PARENT. A parsed journal's data is in reverse order, so
|
||||
@ -198,9 +200,21 @@ instance Show Journal where
|
||||
--
|
||||
-- Note that (<>) is right-biased, so nulljournal is only a left identity.
|
||||
-- In particular, this prevents Journal from being a monoid.
|
||||
instance Semigroup Journal where
|
||||
j1 <> j2 =
|
||||
instance Semigroup Journal where j1 <> j2 = j1 `journalConcat` j2
|
||||
|
||||
-- | Merge two journals into one.
|
||||
-- Transaction counts are summed, map fields are combined,
|
||||
-- the second's list fields are appended to the first's,
|
||||
-- the second's parse state is kept.
|
||||
journalConcat :: Journal -> Journal -> Journal
|
||||
journalConcat j1 j2 =
|
||||
let
|
||||
f1 = takeFileName $ journalFilePath j1
|
||||
f2 = maybe "(unknown)" takeFileName $ headMay $ jincludefilestack j2 -- XXX more accurate than journalFilePath for some reason
|
||||
in
|
||||
dbgJournalAcctDeclOrder ("journalConcat: " <> f1 <> " <> " <> f2 <> ", acct decls renumbered: ") $
|
||||
journalRenumberAccountDeclarations $
|
||||
dbgJournalAcctDeclOrder ("journalConcat: " <> f1 <> " <> " <> f2 <> ", acct decls : ") $
|
||||
Journal {
|
||||
jparsedefaultyear = jparsedefaultyear j2
|
||||
,jparsedefaultcommodity = jparsedefaultcommodity j2
|
||||
@ -228,13 +242,33 @@ instance Semigroup Journal where
|
||||
,jlastreadtime = max (jlastreadtime j1) (jlastreadtime j2)
|
||||
}
|
||||
|
||||
-- | Renumber all the account declarations. Call this after combining two journals into one,
|
||||
-- so that account declarations have a total order again.
|
||||
-- | Renumber all the account declarations. This is useful to call when
|
||||
-- finalising or concatenating Journals, to give account declarations
|
||||
-- a total order across files.
|
||||
journalRenumberAccountDeclarations :: Journal -> Journal
|
||||
journalRenumberAccountDeclarations j = j{jdeclaredaccounts=jdas'}
|
||||
where
|
||||
jdas' = [(a, adi{adideclarationorder=n}) | (n, (a,adi)) <- zip [1..] $ jdeclaredaccounts j]
|
||||
-- XXX the per-file declaration order saved during parsing is discarded; it seems unneeded
|
||||
-- the per-file declaration order saved during parsing is discarded,
|
||||
-- it seems unneeded except perhaps for debugging
|
||||
|
||||
-- | Debug log the ordering of a journal's account declarations
|
||||
-- (at debug level 5+).
|
||||
dbgJournalAcctDeclOrder :: String -> Journal -> Journal
|
||||
dbgJournalAcctDeclOrder prefix
|
||||
| debugLevel >= 5 = traceWith ((prefix++) . showAcctDeclsSummary . jdeclaredaccounts)
|
||||
| otherwise = id
|
||||
where
|
||||
showAcctDeclsSummary :: [(AccountName,AccountDeclarationInfo)] -> String
|
||||
showAcctDeclsSummary adis
|
||||
| length adis < (2*num+2) = "[" <> showadis adis <> "]"
|
||||
| otherwise =
|
||||
"[" <> showadis (take num adis) <> " ... " <> showadis (takelast num adis) <> "]"
|
||||
where
|
||||
num = 3
|
||||
showadis = intercalate ", " . map showadi
|
||||
showadi (a,adi) = "("<>show (adideclarationorder adi)<>","<>T.unpack a<>")"
|
||||
takelast n = reverse . take n . reverse
|
||||
|
||||
instance Default Journal where
|
||||
def = nulljournal
|
||||
|
@ -69,7 +69,7 @@ import Safe (headDef)
|
||||
import System.Directory (doesFileExist, getHomeDirectory)
|
||||
import System.Environment (getEnv)
|
||||
import System.Exit (exitFailure)
|
||||
import System.FilePath ((<.>), (</>), splitDirectories, splitFileName)
|
||||
import System.FilePath ((<.>), (</>), splitDirectories, splitFileName, takeFileName)
|
||||
import System.Info (os)
|
||||
import System.IO (hPutStr, stderr)
|
||||
|
||||
@ -139,9 +139,8 @@ type PrefixedFilePath = FilePath
|
||||
-- since hledger 1.17, we prefer predictability.)
|
||||
readJournal :: InputOpts -> Maybe FilePath -> Text -> ExceptT String IO Journal
|
||||
readJournal iopts mpath txt = do
|
||||
let r :: Reader IO =
|
||||
fromMaybe JournalReader.reader $ findReader (mformat_ iopts) mpath
|
||||
dbg6IO "trying reader" (rFormat r)
|
||||
let r :: Reader IO = fromMaybe JournalReader.reader $ findReader (mformat_ iopts) mpath
|
||||
dbg6IO "readJournal: trying reader" (rFormat r)
|
||||
rReadFn r iopts (fromMaybe "(string)" mpath) txt
|
||||
|
||||
-- | Read a Journal from this file, or from stdin if the file path is -,
|
||||
@ -161,7 +160,9 @@ readJournalFile iopts prefixedfile = do
|
||||
(mfmt, f) = splitReaderPrefix prefixedfile
|
||||
iopts' = iopts{mformat_=asum [mfmt, mformat_ iopts]}
|
||||
liftIO $ requireJournalFileExists f
|
||||
t <- liftIO $ readFileOrStdinPortably f
|
||||
t <-
|
||||
traceAt 6 ("readJournalFile: "++takeFileName f) $
|
||||
liftIO $ readFileOrStdinPortably f
|
||||
-- <- T.readFile f -- or without line ending translation, for testing
|
||||
j <- readJournal iopts' (Just f) t
|
||||
if new_ iopts
|
||||
|
@ -148,6 +148,7 @@ import Hledger.Query (Query(..), filterQuery, parseQueryTerm, queryEndDate, quer
|
||||
import Hledger.Reports.ReportOptions (ReportOpts(..), queryFromFlags, rawOptsToReportOpts)
|
||||
import Hledger.Utils
|
||||
import Hledger.Read.InputOptions
|
||||
import System.FilePath (takeFileName)
|
||||
|
||||
--- ** doctest setup
|
||||
-- $setup
|
||||
@ -324,6 +325,10 @@ journalFinalise iopts@InputOpts{..} f txt pj = do
|
||||
>>= journalBalanceTransactions balancingopts_ -- Balance all transactions and maybe check balance assertions.
|
||||
<&> (if infer_equity_ then journalAddInferredEquityPostings else id) -- Add inferred equity postings, after balancing and generating auto postings
|
||||
<&> journalInferMarketPricesFromTransactions -- infer market prices from commodity-exchanging transactions
|
||||
<&> traceAt 6 ("journalFinalise: " <> takeFileName f) -- debug logging
|
||||
<&> dbgJournalAcctDeclOrder ("journalFinalise: " <> takeFileName f <> " acct decls : ")
|
||||
<&> journalRenumberAccountDeclarations
|
||||
<&> dbgJournalAcctDeclOrder ("journalFinalise: " <> takeFileName f <> " acct decls renumbered: ")
|
||||
when strict_ $ do
|
||||
journalCheckAccounts j -- If in strict mode, check all postings are to declared accounts
|
||||
journalCheckCommodities j -- and using declared commodities
|
||||
|
@ -187,9 +187,7 @@ reader = Reader
|
||||
parse :: InputOpts -> FilePath -> Text -> ExceptT String IO Journal
|
||||
parse iopts f = parseAndFinaliseJournal journalp' iopts f
|
||||
where
|
||||
journalp' =
|
||||
-- debug logging for account display order
|
||||
dbgJournalAcctDeclOrder (takeFileName f <> " acct decls: ") <$> do
|
||||
journalp' = do
|
||||
-- reverse parsed aliases to ensure that they are applied in order given on commandline
|
||||
mapM_ addAccountAlias (reverse $ aliasesFromOpts iopts)
|
||||
journalp
|
||||
@ -300,39 +298,35 @@ includedirectivep = do
|
||||
when (filepath `elem` parentfilestack) $
|
||||
Fail.fail ("Cyclic include: " ++ filepath)
|
||||
|
||||
childInput <- lift $ readFilePortably filepath
|
||||
`orRethrowIOError` (show parentpos ++ " reading " ++ filepath)
|
||||
childInput <-
|
||||
traceAt 6 ("parseChild: "++takeFileName filepath) $
|
||||
lift $ readFilePortably filepath
|
||||
`orRethrowIOError` (show parentpos ++ " reading " ++ filepath)
|
||||
let initChildj = newJournalWithParseStateFrom filepath parentj
|
||||
|
||||
-- Choose a reader/parser based on the file path prefix or file extension,
|
||||
-- defaulting to JournalReader. Duplicating readJournal a bit here.
|
||||
let r = fromMaybe reader $ findReader Nothing (Just prefixedpath)
|
||||
parser = rParser r
|
||||
dbg6IO "trying reader" (rFormat r)
|
||||
dbg6IO "parseChild: trying reader" (rFormat r)
|
||||
|
||||
-- Parse the file (of whichever format) to a Journal, with file path and source text attached.
|
||||
updatedChildj <- (journalAddFile (filepath, childInput)) <$>
|
||||
updatedChildj <- journalAddFile (filepath, childInput) <$>
|
||||
parseIncludeFile parser initChildj filepath childInput
|
||||
|
||||
-- Merge this child journal into the parent journal using Journal's Semigroup instance
|
||||
-- (with lots of debug logging for troubleshooting account display order).
|
||||
-- Merge this child journal into the parent journal
|
||||
-- (with debug logging for troubleshooting account display order).
|
||||
-- The parent journal is the second argument to journalConcat; this means
|
||||
-- its parse state is kept, and its lists are appended to child's (which
|
||||
-- ultimately produces the right list order, because parent's and child's
|
||||
-- lists are in reverse order at this stage. Cf #1909).
|
||||
let
|
||||
parentj' =
|
||||
dbgJournalAcctDeclOrder (" " <> parentfilename <> " acct decls now : ")
|
||||
$
|
||||
(
|
||||
-- The child journal has not yet been finalises and its lists are still reversed.
|
||||
-- To help calculate account declaration order across files (#1909),
|
||||
-- unreverse just the acct declarations without disturbing anything else.
|
||||
-- XXX still shows wrong order in some cases
|
||||
reverseAcctDecls $
|
||||
dbgJournalAcctDeclOrder (childfilename <> " include file acct decls: ") updatedChildj
|
||||
)
|
||||
<>
|
||||
dbgJournalAcctDeclOrder (" " <> parentfilename <> " acct decls were: ") parentj
|
||||
dbgJournalAcctDeclOrder ("parseChild: child " <> childfilename <> " acct decls: ") updatedChildj
|
||||
`journalConcat`
|
||||
dbgJournalAcctDeclOrder ("parseChild: parent " <> parentfilename <> " acct decls: ") parentj
|
||||
|
||||
where
|
||||
reverseAcctDecls j = j{jdeclaredaccounts = reverse $ jdeclaredaccounts j}
|
||||
childfilename = takeFileName filepath
|
||||
parentfilename = maybe "" takeFileName $ headMay $ jincludefilestack parentj -- more accurate than journalFilePath parentj somehow
|
||||
|
||||
@ -352,22 +346,6 @@ includedirectivep = do
|
||||
,jincludefilestack = filepath : jincludefilestack j
|
||||
}
|
||||
|
||||
dbgJournalAcctDeclOrder :: String -> Journal -> Journal
|
||||
dbgJournalAcctDeclOrder prefix
|
||||
| debugLevel >= 5 = traceWith ((prefix++) . showAcctDeclsSummary . jdeclaredaccounts)
|
||||
| otherwise = id
|
||||
where
|
||||
showAcctDeclsSummary :: [(AccountName,AccountDeclarationInfo)] -> String
|
||||
showAcctDeclsSummary adis
|
||||
| length adis < (2*num+2) = "[" <> showadis adis <> "]"
|
||||
| otherwise =
|
||||
"[" <> showadis (take num adis) <> " ... " <> showadis (takelast num adis) <> "]"
|
||||
where
|
||||
num = 3
|
||||
showadis = intercalate ", " . map showadi
|
||||
showadi (a,adi) = "("<>show (adideclarationorder adi)<>","<>T.unpack a<>")"
|
||||
takelast n = reverse . take n . reverse
|
||||
|
||||
-- | Lift an IO action into the exception monad, rethrowing any IO
|
||||
-- error with the given message prepended.
|
||||
orRethrowIOError :: MonadIO m => IO a -> String -> TextParser m a
|
||||
@ -443,10 +421,7 @@ addAccountDeclaration (a,cmt,tags,pos) = do
|
||||
d = (a, nullaccountdeclarationinfo{
|
||||
adicomment = cmt
|
||||
,aditags = tags
|
||||
-- this restarts from 1 in each file, which is not that useful
|
||||
-- when there are multiple files; so it gets renumbered
|
||||
-- automatically when combining Journals with <>
|
||||
,adideclarationorder = length decls + 1
|
||||
,adideclarationorder = length decls + 1 -- gets renumbered when Journals are finalised or merged
|
||||
,adisourcepos = pos
|
||||
})
|
||||
in
|
||||
|
Loading…
Reference in New Issue
Block a user