print: --new shows only transactions added since last time

First cut, error messages could be refined etc.
This commit is contained in:
Simon Michael 2017-09-15 09:55:17 -07:00
parent e3c4a76119
commit 669fa706c0
4 changed files with 108 additions and 27 deletions

View File

@ -42,18 +42,20 @@ import qualified Control.Exception as C
import Control.Monad.Except import Control.Monad.Except
import Data.List import Data.List
import Data.Maybe import Data.Maybe
import Data.Ord
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
import Data.Time (Day)
import Safe import Safe
import System.Directory (doesFileExist, getHomeDirectory) import System.Directory (doesFileExist, getHomeDirectory)
import System.Environment (getEnv) import System.Environment (getEnv)
import System.Exit (exitFailure) import System.Exit (exitFailure)
import System.FilePath ((</>), takeExtension) import System.FilePath
import System.IO (stderr) import System.IO
import Test.HUnit import Test.HUnit
import Text.Printf import Text.Printf
import Hledger.Data.Dates (getCurrentDay) import Hledger.Data.Dates (getCurrentDay, parsedate, showDate)
import Hledger.Data.Types import Hledger.Data.Types
import Hledger.Read.Common import Hledger.Read.Common
import qualified Hledger.Read.JournalReader as JournalReader import qualified Hledger.Read.JournalReader as JournalReader
@ -259,7 +261,7 @@ tryReaders readers mrulesfile assrt path t = firstSuccessOrFirstError [] readers
path' = fromMaybe "(string)" path path' = fromMaybe "(string)" path
--- New versions of readJournal* with easier arguments --- New versions of readJournal* with easier arguments, and --new/last-seen handling.
readJournalFilesWithOpts :: InputOpts -> [FilePath] -> IO (Either String Journal) readJournalFilesWithOpts :: InputOpts -> [FilePath] -> IO (Either String Journal)
readJournalFilesWithOpts iopts = readJournalFilesWithOpts iopts =
@ -275,7 +277,67 @@ readJournalFileWithOpts iopts prefixedfile = do
(mfmt, f) = splitReaderPrefix prefixedfile (mfmt, f) = splitReaderPrefix prefixedfile
iopts' = iopts{mformat_=firstJust [mfmt, mformat_ iopts]} iopts' = iopts{mformat_=firstJust [mfmt, mformat_ iopts]}
requireJournalFileExists f requireJournalFileExists f
readFileOrStdinAnyLineEnding f >>= readJournalWithOpts iopts' (Just f) t <- readFileOrStdinAnyLineEnding f
ej <- readJournalWithOpts iopts' (Just f) t
case ej of
Left e -> return $ Left e
Right j | new_ iopts -> do
lastdates <- lastSeen f
let (newj, newlastdates) = journalFilterSinceLastDates lastdates j
when (not $ null newlastdates) $ saveLastSeen newlastdates f
return $ Right newj
Right j -> return $ Right j
-- | Given zero or more date values (all the same, representing the
-- latest previously seen transaction date, and how many transactions
-- were seen on that date), remove transactions with earlier dates
-- from the journal, and the same number of transactions on the
-- latest date, if any, leaving only transactions that we can assume
-- are newer. Also returns the new last dates of the new journal.
journalFilterSinceLastDates :: [Day] -> Journal -> (Journal, [Day])
journalFilterSinceLastDates [] j = (j, latestDates $ map tdate $ jtxns j)
journalFilterSinceLastDates ds@(d:_) j = (j', ds')
where
samedateorlaterts = filter ((>= d).tdate) $ jtxns j
(samedatets, laterts) = span ((== d).tdate) $ sortBy (comparing tdate) samedateorlaterts
newsamedatets = drop (length ds) samedatets
j' = j{jtxns=newsamedatets++laterts}
ds' = latestDates $ map tdate $ samedatets++laterts
-- | Get all instances of the latest date in an unsorted list of dates.
-- Ie, if the latest date appears once, return it in a one-element list,
-- if it appears three times (anywhere), return three of it.
latestDates :: [Day] -> [Day]
latestDates = headDef [] . take 1 . group . reverse . sort
-- | Where to save last-seen transactions info for the given file path
-- (.FILE.seen).
seenFileFor :: FilePath -> FilePath
seenFileFor f = dir </> fname' <.> "seen"
where
(dir, fname) = splitFileName f
fname' | "." `isPrefixOf` fname = fname
| otherwise = '.':fname
-- | What were the latest transaction dates seen the last time this
-- journal file was read ? If there were multiple transactions on the
-- latest date, that number of dates is returned, otherwise just one.
-- Or none if no transactions were seen.
lastSeen :: FilePath -> IO [Day]
lastSeen f = do
let seenfile = seenFileFor f
exists <- doesFileExist seenfile
if exists
then map (parsedate . strip) . lines . strip . T.unpack <$> readFileStrictly seenfile
else return []
readFileStrictly :: FilePath -> IO Text
readFileStrictly f = readFile' f >>= \t -> C.evaluate (T.length t) >> return t
-- | Remember that these transaction dates were the latest seen when
-- reading this journal file.
saveLastSeen :: [Day] -> FilePath -> IO ()
saveLastSeen dates f = writeFile (seenFileFor f) $ unlines $ map showDate dates
readJournalWithOpts :: InputOpts -> Maybe FilePath -> Text -> IO (Either String Journal) readJournalWithOpts :: InputOpts -> Maybe FilePath -> Text -> IO (Either String Journal)
readJournalWithOpts iopts mfile txt = readJournalWithOpts iopts mfile txt =

View File

@ -55,13 +55,14 @@ data InputOpts = InputOpts {
,aliases_ :: [String] -- ^ account name aliases to apply ,aliases_ :: [String] -- ^ account name aliases to apply
,anon_ :: Bool -- ^ do light anonymisation/obfuscation of the data ,anon_ :: Bool -- ^ do light anonymisation/obfuscation of the data
,ignore_assertions_ :: Bool -- ^ don't check balance assertions ,ignore_assertions_ :: Bool -- ^ don't check balance assertions
,new_ :: Bool -- ^ read only new transactions since this file was last read
,pivot_ :: String -- ^ use the given field's value as the account name ,pivot_ :: String -- ^ use the given field's value as the account name
} deriving (Show, Data) --, Typeable) } deriving (Show, Data) --, Typeable)
instance Default InputOpts where def = definputopts instance Default InputOpts where def = definputopts
definputopts :: InputOpts definputopts :: InputOpts
definputopts = InputOpts def def def def def def definputopts = InputOpts def def def def def def def
rawOptsToInputOpts :: RawOpts -> InputOpts rawOptsToInputOpts :: RawOpts -> InputOpts
rawOptsToInputOpts rawopts = InputOpts{ rawOptsToInputOpts rawopts = InputOpts{
@ -71,6 +72,7 @@ rawOptsToInputOpts rawopts = InputOpts{
,aliases_ = map (T.unpack . stripquotes . T.pack) $ listofstringopt "alias" rawopts ,aliases_ = map (T.unpack . stripquotes . T.pack) $ listofstringopt "alias" rawopts
,anon_ = boolopt "anon" rawopts ,anon_ = boolopt "anon" rawopts
,ignore_assertions_ = boolopt "ignore-assertions" rawopts ,ignore_assertions_ = boolopt "ignore-assertions" rawopts
,new_ = boolopt "new" rawopts
,pivot_ = stringopt "pivot" rawopts ,pivot_ = stringopt "pivot" rawopts
} }

View File

@ -31,13 +31,13 @@ printmode = (defCommandMode $ ["print"] ++ aliases) {
modeHelp = "show transaction journal entries, sorted by date. With --date2, sort by secondary date instead." `withAliases` aliases modeHelp = "show transaction journal entries, sorted by date. With --date2, sort by secondary date instead." `withAliases` aliases
,modeGroupFlags = Group { ,modeGroupFlags = Group {
groupUnnamed = [ groupUnnamed = [
let matcharg = "STR" let arg = "STR" in
in flagReq ["match","m"] (\s opts -> Right $ setopt "match" s opts) arg
flagReq ["match","m"] (\s opts -> Right $ setopt "match" s opts) matcharg ("show the transaction whose description is most similar to "++arg++", and is most recent")
("show the transaction whose description is most similar to "++matcharg ,flagNone ["explicit","x"] (setboolopt "explicit")
++ ", and is most recent"), "show all amounts explicitly"
flagNone ["explicit","x"] (setboolopt "explicit") ,flagNone ["new"] (setboolopt "new")
"show all amounts explicitly" "show only more recent transactions added to each file since last run"
] ]
++ outputflags ++ outputflags
,groupHidden = [] ,groupHidden = []

View File

@ -464,12 +464,15 @@ Print all market prices from the journal.
## print ## print
Show transactions from the journal. Aliases: p, txns. Show transactions from the journal. Aliases: p, txns.
`-x --explicit`
: show all amounts explicitly
`-m STR --match=STR ` `-m STR --match=STR `
: show the transaction whose description is most similar to STR, and is most recent : show the transaction whose description is most similar to STR, and is most recent
` --new`
: show only more recent transactions added to each file since last run
`-x --explicit`
: show all amounts explicitly
`-O FMT --output-format=FMT ` `-O FMT --output-format=FMT `
: select the output format. Supported formats: : select the output format. Supported formats:
txt, csv. txt, csv.
@ -501,22 +504,36 @@ $ hledger print
assets:bank:checking $-1 assets:bank:checking $-1
``` ```
The print command displays full journal entries (transactions) from the journal file, tidily formatted. The print command displays full journal entries (transactions) from the journal file in date order, tidily formatted.
print's output is always a valid [hledger journal](/journal.html).
It preserves all transaction information, but it does not preserve directives or inter-transaction comments
As of hledger 1.2, print's output is always a valid [hledger journal](/journal.html). Normally, the journal entry's explicit or implicit amount style is preserved.
However it may not preserve all original content, eg it does not print directives or inter-transaction comments. Ie when an amount is omitted in the journal, it will be omitted in the output.
You can use the `-x`/`--explicit` flag to make all amounts explicit, which can be
Normally, transactions' implicit/explicit amount style is preserved:
when an amount is omitted in the journal, it will be omitted in the output.
You can use the `-x/--explicit` flag to make all amounts explicit, which can be
useful for troubleshooting or for making your journal more readable and useful for troubleshooting or for making your journal more readable and
robust against data entry errors. robust against data entry errors.
Note, in this mode postings with a multi-commodity amount Note, `-x` will cause postings with a multi-commodity amount
(possible with an implicit amount in a multi-commodity transaction) (these can arise when a multi-commodity transaction has an implicit amount)
will be split into multiple single-commodity postings, for valid journal output. will be split into multiple single-commodity postings, for valid journal output.
With -B/--cost, amounts with [transaction prices](/journal.html#transaction-prices) With `-B`/`--cost`, amounts with [transaction prices](/journal.html#transaction-prices)
are converted to cost (using the transaction price). are converted to cost using that price.
With `-m`/`--match` and a STR argument, print will show at most one transaction: the one
one whose description is most similar to STR, and is most recent. STR should contain at
least two characters. If there is no similar-enough match, no transaction will be shown.
With `--new`, for each FILE being read, hledger reads (and writes) a special .FILE.seen file in the same directory,
containing the latest transaction date(s) that were seen last time FILE was read.
When this file is found, only transactions with newer dates (and new transactions on the latest date) are printed.
This is useful for ignoring already-seen entries in import data, such as downloaded CSV files.
Eg:
```console
$ hledger -f bank1.csv print --new
# shows transactions added since last print --new on this file
```
It assumes that only same-or-newer-dated transactions are added to FILE, and that the order of same-date transactions remains stable.
The print command also supports The print command also supports
[output destination](#output-destination) [output destination](#output-destination)