2017-01-08 18:19:25 +03:00
|
|
|
#!/usr/bin/env stack
|
|
|
|
{- stack runghc --verbosity info
|
|
|
|
--package hledger-lib
|
|
|
|
--package hledger
|
2017-01-23 17:17:17 +03:00
|
|
|
--package here
|
2017-01-08 18:19:25 +03:00
|
|
|
--package text
|
|
|
|
-}
|
|
|
|
|
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
2017-01-23 17:17:17 +03:00
|
|
|
{-# LANGUAGE QuasiQuotes #-}
|
2017-01-08 18:19:25 +03:00
|
|
|
|
|
|
|
import Data.Char (toUpper)
|
|
|
|
import Data.List
|
2017-01-23 17:17:17 +03:00
|
|
|
import Data.String.Here
|
2017-01-08 18:19:25 +03:00
|
|
|
import qualified Data.Text as T
|
|
|
|
import System.Console.CmdArgs
|
|
|
|
import System.Console.CmdArgs.Explicit
|
|
|
|
|
|
|
|
import Hledger
|
|
|
|
import Hledger.Cli.CliOptions
|
|
|
|
import Hledger.Cli ( withJournalDo, postingsReportAsText )
|
|
|
|
|
2017-01-23 17:17:17 +03:00
|
|
|
------------------------------------------------------------------------------
|
2017-01-24 19:59:22 +03:00
|
|
|
cmdmode = (defAddonCommandMode "register-match") {
|
|
|
|
modeHelp = [here|
|
2017-01-23 17:17:17 +03:00
|
|
|
A helper for ledger-autosync. This prints 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.
|
2017-01-24 19:59:22 +03:00
|
|
|
|]
|
|
|
|
,modeHelpSuffix=lines [here|
|
|
|
|
|]
|
|
|
|
}
|
2017-01-23 17:17:17 +03:00
|
|
|
------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
main = do
|
2017-01-24 19:59:22 +03:00
|
|
|
opts <- getHledgerCliOpts cmdmode
|
2017-01-23 17:17:17 +03:00
|
|
|
withJournalDo opts match
|
2017-01-08 18:19:25 +03:00
|
|
|
|
|
|
|
match :: CliOpts -> Journal -> IO ()
|
|
|
|
match opts@CliOpts{rawopts_=rawopts,reportopts_=ropts} j = do
|
|
|
|
let args' = listofstringopt "args" rawopts
|
|
|
|
case args' of
|
|
|
|
[desc] -> do
|
|
|
|
d <- getCurrentDay
|
|
|
|
let q = queryFromOptsOnly d ropts
|
|
|
|
(_,pris) = postingsReport ropts q j
|
|
|
|
ps = [p | (_,_,_,p,_) <- pris]
|
|
|
|
case similarPosting ps desc of
|
|
|
|
Nothing -> putStrLn "no matches found."
|
|
|
|
Just p -> putStr $ postingsReportAsText opts ("",[pri])
|
|
|
|
where pri = (Just (postingDate p)
|
|
|
|
,Nothing
|
|
|
|
,Just $ T.unpack (maybe "" tdescription $ ptransaction p)
|
|
|
|
,p
|
|
|
|
,0)
|
|
|
|
_ -> 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 :: [Char] -> [Char] -> 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 _ = []
|
|
|
|
|