imp: cli: merge register-match with register command

register-match is now the --match mode of the register command.
It was used by ledger-autosync at one point, hopefully the new flag
works similarly.
This commit is contained in:
Simon Michael 2023-01-25 09:53:39 -10:00
parent 1899b43073
commit fbbae55101
14 changed files with 86 additions and 121 deletions

View File

@ -445,7 +445,6 @@ src/hledger/
Print.md
README.md
Register.md
Registermatch.md
Rewrite.md
Roi.md
Stats.md

View File

@ -62,7 +62,6 @@ Syntax: https://www.pandoc.org/MANUAL.html#tables -> pipe_tables
| [prices](https://github.com/simonmichael/hledger/issues?q=is:open+label:prices) | [bugs](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+BUG%22+label:prices) ([first](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+BUG%22+label:%22good+first+issue%22+label:prices)/[easy](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+BUG%22+-label:%22good+first+issue%22+label:easy?+label:prices)/[neither](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+BUG%22+-label:%22good+first+issue%22+-label:easy?+label:prices)) | [wishes](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+WISH%22+label:prices) | [PRs](https://github.com/simonmichael/hledger/issues?q=is:open+is:pr+label:prices) | [other](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+-label:%22A+BUG%22+-label:%22A+WISH%22+label:prices)
| [print](https://github.com/simonmichael/hledger/issues?q=is:open+label:print) | [bugs](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+BUG%22+label:print) ([first](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+BUG%22+label:%22good+first+issue%22+label:print)/[easy](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+BUG%22+-label:%22good+first+issue%22+label:easy?+label:print)/[neither](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+BUG%22+-label:%22good+first+issue%22+-label:easy?+label:print)) | [wishes](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+WISH%22+label:print) | [PRs](https://github.com/simonmichael/hledger/issues?q=is:open+is:pr+label:print) | [other](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+-label:%22A+BUG%22+-label:%22A+WISH%22+label:print)
| [register](https://github.com/simonmichael/hledger/issues?q=is:open+label:register) | [bugs](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+BUG%22+label:register) ([first](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+BUG%22+label:%22good+first+issue%22+label:register)/[easy](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+BUG%22+-label:%22good+first+issue%22+label:easy?+label:register)/[neither](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+BUG%22+-label:%22good+first+issue%22+-label:easy?+label:register)) | [wishes](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+WISH%22+label:register) | [PRs](https://github.com/simonmichael/hledger/issues?q=is:open+is:pr+label:register) | [other](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+-label:%22A+BUG%22+-label:%22A+WISH%22+label:register)
| [registermatch](https://github.com/simonmichael/hledger/issues?q=is:open+label:registermatch) | [bugs](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+BUG%22+label:registermatch) ([first](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+BUG%22+label:%22good+first+issue%22+label:registermatch)/[easy](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+BUG%22+-label:%22good+first+issue%22+label:easy?+label:registermatch)/[neither](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+BUG%22+-label:%22good+first+issue%22+-label:easy?+label:registermatch)) | [wishes](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+WISH%22+label:registermatch) | [PRs](https://github.com/simonmichael/hledger/issues?q=is:open+is:pr+label:registermatch) | [other](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+-label:%22A+BUG%22+-label:%22A+WISH%22+label:registermatch)
| [rewrite](https://github.com/simonmichael/hledger/issues?q=is:open+label:rewrite) | [bugs](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+BUG%22+label:rewrite) ([first](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+BUG%22+label:%22good+first+issue%22+label:rewrite)/[easy](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+BUG%22+-label:%22good+first+issue%22+label:easy?+label:rewrite)/[neither](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+BUG%22+-label:%22good+first+issue%22+-label:easy?+label:rewrite)) | [wishes](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+WISH%22+label:rewrite) | [PRs](https://github.com/simonmichael/hledger/issues?q=is:open+is:pr+label:rewrite) | [other](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+-label:%22A+BUG%22+-label:%22A+WISH%22+label:rewrite)
| [roi](https://github.com/simonmichael/hledger/issues?q=is:open+label:roi) | [bugs](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+BUG%22+label:roi) ([first](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+BUG%22+label:%22good+first+issue%22+label:roi)/[easy](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+BUG%22+-label:%22good+first+issue%22+label:easy?+label:roi)/[neither](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+BUG%22+-label:%22good+first+issue%22+-label:easy?+label:roi)) | [wishes](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+WISH%22+label:roi) | [PRs](https://github.com/simonmichael/hledger/issues?q=is:open+is:pr+label:roi) | [other](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+-label:%22A+BUG%22+-label:%22A+WISH%22+label:roi)
| [stats](https://github.com/simonmichael/hledger/issues?q=is:open+label:stats) | [bugs](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+BUG%22+label:stats) ([first](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+BUG%22+label:%22good+first+issue%22+label:stats)/[easy](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+BUG%22+-label:%22good+first+issue%22+label:easy?+label:stats)/[neither](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+BUG%22+-label:%22good+first+issue%22+-label:easy?+label:stats)) | [wishes](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+label:%22A+WISH%22+label:stats) | [PRs](https://github.com/simonmichael/hledger/issues?q=is:open+is:pr+label:stats) | [other](https://github.com/simonmichael/hledger/issues?q=is:open+is:issue+-label:%22A+BUG%22+-label:%22A+WISH%22+label:stats)

View File

@ -37,7 +37,6 @@ module Hledger.Cli.Commands (
,module Hledger.Cli.Commands.Prices
,module Hledger.Cli.Commands.Print
,module Hledger.Cli.Commands.Register
,module Hledger.Cli.Commands.Registermatch
,module Hledger.Cli.Commands.Rewrite
,module Hledger.Cli.Commands.Stats
,module Hledger.Cli.Commands.Tags
@ -79,7 +78,6 @@ import Hledger.Cli.Commands.Payees
import Hledger.Cli.Commands.Prices
import Hledger.Cli.Commands.Print
import Hledger.Cli.Commands.Register
import Hledger.Cli.Commands.Registermatch
import Hledger.Cli.Commands.Rewrite
import Hledger.Cli.Commands.Roi
import Hledger.Cli.Commands.Stats
@ -114,7 +112,6 @@ builtinCommands = [
,(payeesmode , payees)
,(pricesmode , prices)
,(printmode , print')
,(registermatchmode , registermatch)
,(registermode , register)
,(rewritemode , rewrite)
,(roimode , roi)

View File

@ -33,7 +33,7 @@ printmode = hledgerCommandMode
$(embedFileRelative "Hledger/Cli/Commands/Print.txt")
([let arg = "DESC" in
flagReq ["match","m"] (\s opts -> Right $ setopt "match" s opts) arg
("fuzzy search for the transaction with description closest to "++arg++", and also most recent")
("fuzzy search for one recent transaction with description closest to "++arg)
,flagNone ["explicit","x"] (setboolopt "explicit")
"show all amounts explicitly"
,flagNone ["show-costs"] (setboolopt "show-costs")
@ -60,6 +60,8 @@ print' opts j = do
case maybestringopt "match" $ rawopts_ opts of
Nothing -> printEntries opts j'
Just desc ->
-- match mode, prints one recent transaction most similar to given description
-- XXX should match similarly to register --match
case journalSimilarTransaction opts j' (dbg1 "finding best match for description" $ T.pack desc) of
Just t -> printEntries opts j'{jtxns=[t]}
Nothing -> putStrLn "no matches found." >> exitFailure

View File

@ -76,8 +76,8 @@ keeping the output parseable.
With `-B`/`--cost`, amounts with [costs](https://hledger.org/hledger.html#costs)
are converted to cost using that price. This can be used for troubleshooting.
With `-m DESC`/`--match=DESC`, print does a fuzzy search for the one transaction
whose description is most similar to DESC, also preferring recent tranactions.
With `-m DESC`/`--match=DESC`, print does a fuzzy search for one recent transaction
whose description is most similar to DESC.
DESC should contain at least two characters.
If there is no similar-enough match,
no transaction will be shown and the program exit code will be non-zero.

View File

@ -22,6 +22,7 @@ import Data.Default (def)
import Data.Maybe (fromMaybe, isJust)
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.IO as TL
import qualified Data.Text.Lazy.Builder as TB
import System.Console.CmdArgs.Explicit (flagNone, flagReq)
@ -30,6 +31,10 @@ import Hledger.Read.CsvReader (CSV, CsvRecord, printCSV)
import Hledger.Cli.CliOptions
import Hledger.Cli.Utils
import Text.Tabular.AsciiWide hiding (render)
import Data.List (sortBy)
import Data.Char (toUpper)
import Data.List.Extra (intersect)
import System.Exit (exitFailure)
registermode = hledgerCommandMode
$(embedFileRelative "Hledger/Cli/Commands/Register.txt")
@ -39,6 +44,9 @@ registermode = hledgerCommandMode
"show historical running total/balance (includes postings before report start date)\n "
,flagNone ["average","A"] (setboolopt "average")
"show running average of posting amounts instead of total (implies --empty)"
,let arg = "DESC" in
flagReq ["match","m"] (\s opts -> Right $ setopt "match" s opts) arg
("fuzzy search for one recent posting with description closest to "++arg)
,flagNone ["related","r"] (setboolopt "related") "show postings' siblings instead"
,flagNone ["invert"] (setboolopt "invert") "display all amounts with reversed sign"
,flagReq ["width","w"] (\s opts -> Right $ setopt "width" s opts) "N"
@ -60,14 +68,28 @@ registermode = hledgerCommandMode
-- | Print a (posting) register report.
register :: CliOpts -> Journal -> IO ()
register opts@CliOpts{reportspec_=rspec} j =
writeOutputLazyText opts . render $ postingsReport rspec j
register opts@CliOpts{rawopts_=rawopts, reportspec_=rspec} j
-- match mode, print one recent posting most similar to given description, if any
-- XXX should match similarly to print --match
| Just desc <- maybestringopt "match" rawopts = do
let ps = [p | (_,_,_,p,_) <- rpt]
case similarPosting ps desc of
Nothing -> putStrLn "no matches found." >> exitFailure
Just p -> TL.putStr $ postingsReportAsText opts [pri]
where pri = (Just (postingDate p)
,Nothing
,tdescription <$> ptransaction p
,p
,nullmixedamt)
-- normal register report, list postings
| otherwise = writeOutputLazyText opts $ render rpt
where
fmt = outputFormatFromOpts opts
rpt = postingsReport rspec j
render | fmt=="txt" = postingsReportAsText opts
| fmt=="csv" = printCSV . postingsReportAsCsv
| fmt=="json" = toJsonText
| otherwise = error' $ unsupportedOutputFormatError fmt -- PARTIAL:
where fmt = outputFormatFromOpts opts
postingsReportAsCsv :: PostingsReport -> CSV
postingsReportAsCsv is =
@ -179,6 +201,55 @@ postingsReportItemAsText opts preferredamtwidth preferredbalwidth ((mdate, mperi
VirtualPosting -> (wrap "(" ")", acctwidth-2)
_ -> (id,acctwidth)
-- for register --match:
-- Identify the closest recent match for this description in the given date-sorted postings.
similarPosting :: [Posting] -> String -> Maybe Posting
similarPosting ps desc =
let matches =
sortBy compareRelevanceAndRecency
$ filter ((> threshold).fst)
[(maybe 0 (\t -> compareDescriptions desc (T.unpack $ tdescription t)) (ptransaction p), p) | p <- ps]
where
compareRelevanceAndRecency (n1,p1) (n2,p2) = compare (n2,postingDate p2) (n1,postingDate p1)
threshold = 0
in case matches of [] -> Nothing
m:_ -> Just $ snd m
-- -- Identify the closest recent match for this description in past transactions.
-- similarTransaction :: Journal -> Query -> String -> Maybe Transaction
-- similarTransaction j q desc =
-- case historymatches = transactionsSimilarTo j q desc of
-- ((,t):_) = Just t
-- [] = Nothing
compareDescriptions :: String -> String -> Double
compareDescriptions s t = compareStrings s' t'
where s' = simplify s
t' = simplify t
simplify = filter (not . (`elem` ("0123456789"::String)))
-- | Return a similarity measure, from 0 to 1, for two strings.
-- This is Simon White's letter pairs algorithm from
-- http://www.catalysoft.com/articles/StrikeAMatch.html
-- with a modification for short strings.
compareStrings :: String -> String -> Double
compareStrings "" "" = 1
compareStrings [_] "" = 0
compareStrings "" [_] = 0
compareStrings [a] [b] = if toUpper a == toUpper b then 1 else 0
compareStrings s1 s2 = 2.0 * fromIntegral i / fromIntegral u
where
i = length $ intersect pairs1 pairs2
u = length pairs1 + length pairs2
pairs1 = wordLetterPairs $ uppercase s1
pairs2 = wordLetterPairs $ uppercase s2
wordLetterPairs = concatMap letterPairs . words
letterPairs (a:b:rest) = [a,b] : letterPairs (b:rest)
letterPairs _ = []
-- tests
tests_Register = testGroup "Register" [

View File

@ -23,7 +23,7 @@ $ hledger register checking
2008/12/31 pay off assets:bank:checking $-1 0
```
With --date2, it shows and sorts by secondary date instead.
With `--date2`, it shows and sorts by secondary date instead.
For performance reasons, column widths are chosen based on the first 1000 lines;
this means unusually wide values in later lines can cause visual discontinuities
@ -104,6 +104,12 @@ will be adjusted outward if necessary to contain a whole number of
intervals. This ensures that the first and last intervals are full
length and comparable to the others in the report.
With `-m DESC`/`--match=DESC`, register does a fuzzy search for one recent posting
whose description is most similar to DESC.
DESC should contain at least two characters.
If there is no similar-enough match,
no posting will be shown and the program exit code will be non-zero.
### Custom register output
register uses the full terminal width by default, except on windows.

View File

@ -1,86 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
module Hledger.Cli.Commands.Registermatch (
registermatchmode
,registermatch
)
where
import Data.Char (toUpper)
import Data.List
import qualified Data.Text as T
import qualified Data.Text.Lazy.IO as TL
import Hledger
import Hledger.Cli.CliOptions
import Hledger.Cli.Commands.Register
registermatchmode = hledgerCommandMode
$(embedFileRelative "Hledger/Cli/Commands/Registermatch.txt")
[]
[generalflagsgroup1]
hiddenflags
([], Just $ argsFlag "DESC")
registermatch :: CliOpts -> Journal -> IO ()
registermatch opts@CliOpts{rawopts_=rawopts,reportspec_=rspec} j =
case listofstringopt "args" rawopts of
[desc] -> do
let ps = [p | (_,_,_,p,_) <- postingsReport rspec j]
case similarPosting ps desc of
Nothing -> putStrLn "no matches found."
Just p -> TL.putStr $ postingsReportAsText opts [pri]
where pri = (Just (postingDate p)
,Nothing
,tdescription <$> ptransaction p
,p
,nullmixedamt)
_ -> putStrLn "please provide one description argument."
-- Identify the closest recent match for this description in the given date-sorted postings.
similarPosting :: [Posting] -> String -> Maybe Posting
similarPosting ps desc =
let matches =
sortBy compareRelevanceAndRecency
$ filter ((> threshold).fst)
[(maybe 0 (\t -> compareDescriptions desc (T.unpack $ tdescription t)) (ptransaction p), p) | p <- ps]
where
compareRelevanceAndRecency (n1,p1) (n2,p2) = compare (n2,postingDate p2) (n1,postingDate p1)
threshold = 0
in case matches of [] -> Nothing
m:_ -> Just $ snd m
-- -- Identify the closest recent match for this description in past transactions.
-- similarTransaction :: Journal -> Query -> String -> Maybe Transaction
-- similarTransaction j q desc =
-- case historymatches = transactionsSimilarTo j q desc of
-- ((,t):_) = Just t
-- [] = Nothing
compareDescriptions :: String -> String -> Double
compareDescriptions s t = compareStrings s' t'
where s' = simplify s
t' = simplify t
simplify = filter (not . (`elem` ("0123456789"::String)))
-- | Return a similarity measure, from 0 to 1, for two strings.
-- This is Simon White's letter pairs algorithm from
-- http://www.catalysoft.com/articles/StrikeAMatch.html
-- with a modification for short strings.
compareStrings :: String -> String -> Double
compareStrings "" "" = 1
compareStrings [_] "" = 0
compareStrings "" [_] = 0
compareStrings [a] [b] = if toUpper a == toUpper b then 1 else 0
compareStrings s1 s2 = 2.0 * fromIntegral i / fromIntegral u
where
i = length $ intersect pairs1 pairs2
u = length pairs1 + length pairs2
pairs1 = wordLetterPairs $ uppercase s1
pairs2 = wordLetterPairs $ uppercase s2
wordLetterPairs = concatMap letterPairs . words
letterPairs (a:b:rest) = [a,b] : letterPairs (b:rest)
letterPairs _ = []

View File

@ -1,8 +0,0 @@
register-match\
Print the one posting whose transaction description is closest to DESC,
in the style of the register command.
If there are multiple equally good matches, it shows the most recent.
Query options (options, not arguments) can be used to restrict the search space.
Helps ledger-autosync detect already-seen transactions when importing.
_FLAGS

View File

@ -1,8 +0,0 @@
register-match
Print the one posting whose transaction description is closest to DESC,
in the style of the register command. If there are multiple equally good
matches, it shows the most recent. Query options (options, not
arguments) can be used to restrict the search space. Helps
ledger-autosync detect already-seen transactions when importing.
_FLAGS

View File

@ -26,7 +26,6 @@ _command_({{$1 payees}} ,{{Payees}})
_command_({{$1 prices}} ,{{Prices}})
_command_({{$1 print}} ,{{Print}})
_command_({{$1 register}} ,{{Register}})
_command_({{$1 register-match}} ,{{Registermatch}})
_command_({{$1 rewrite}} ,{{Rewrite}})
_command_({{$1 roi}} ,{{Roi}})
_command_({{$1 stats}} ,{{Stats}})

View File

@ -69,7 +69,6 @@ extra-source-files:
Hledger/Cli/Commands/Prices.txt
Hledger/Cli/Commands/Print.txt
Hledger/Cli/Commands/Register.txt
Hledger/Cli/Commands/Registermatch.txt
Hledger/Cli/Commands/Rewrite.txt
Hledger/Cli/Commands/Roi.txt
Hledger/Cli/Commands/Stats.txt
@ -123,7 +122,6 @@ library
Hledger.Cli.Commands.Prices
Hledger.Cli.Commands.Print
Hledger.Cli.Commands.Register
Hledger.Cli.Commands.Registermatch
Hledger.Cli.Commands.Rewrite
Hledger.Cli.Commands.Roi
Hledger.Cli.Commands.Stats

View File

@ -539,7 +539,6 @@ Here are those commands and the formats currently supported:
| notes | | | | | |
| payees | | | | | |
| prices | | | | | |
| register-match | | | | | |
| rewrite | | | | | |
| roi | | | | | |
| stats | | | | | |
@ -5745,7 +5744,6 @@ These data entry commands are the only ones which can modify your journal file.
- [prices](#prices) - show market price records
- **[print](#print)** - show transactions (journal entries)
- **[register](#register) (reg)** - show postings in one or more accounts & running total
- [register-match](#register-match) - show a recent posting that best matches a description
- [stats](#stats) - show journal statistics
- [tags](#tags) - show tag names
- [test](#test) - run self tests

View File

@ -64,7 +64,6 @@ extra-source-files:
- Hledger/Cli/Commands/Prices.txt
- Hledger/Cli/Commands/Print.txt
- Hledger/Cli/Commands/Register.txt
- Hledger/Cli/Commands/Registermatch.txt
- Hledger/Cli/Commands/Rewrite.txt
- Hledger/Cli/Commands/Roi.txt
- Hledger/Cli/Commands/Stats.txt
@ -171,7 +170,6 @@ library:
- Hledger.Cli.Commands.Prices
- Hledger.Cli.Commands.Print
- Hledger.Cli.Commands.Register
- Hledger.Cli.Commands.Registermatch
- Hledger.Cli.Commands.Rewrite
- Hledger.Cli.Commands.Roi
- Hledger.Cli.Commands.Stats