{-# LANGUAGE OverloadedStrings #-} module Hledger.Data.JournalChecks.Uniqueleafnames ( journalCheckUniqueleafnames ) where import Data.Function (on) import Data.List (groupBy, sortBy) import Data.Text (Text) import qualified Data.Text as T import Text.Printf (printf) import Hledger.Data.AccountName (accountLeafName) import Hledger.Data.Errors (makePostingErrorExcerpt) import Hledger.Data.Journal (journalPostings, journalAccountNamesUsed) import Hledger.Data.Posting (isVirtual) import Hledger.Data.Types import Hledger.Utils (chomp, textChomp) -- | Check that all the journal's postings are to accounts with a unique leaf name. -- Otherwise, return an error message for the first offending posting. journalCheckUniqueleafnames :: Journal -> Either String () journalCheckUniqueleafnames j = do -- find all duplicate leafnames, and the full account names they appear in case finddupes $ journalLeafAndFullAccountNames j of [] -> Right () -- pick the first duplicated leafname and show the transactions of -- the first two postings using it, highlighting the second as the error. (leaf,fulls):_ -> case filter ((`elem` fulls).paccount) $ journalPostings j of ps@(p:p2:_) -> Left $ chomp $ printf ("%s:%d:\n%s\nChecking for unique account leaf names is enabled, and\n" ++"account leaf name %s is not unique.\n" ++"It appears in these account names, which are used in %d places:\n%s" ++"\nConsider changing these account names so their last parts are different." ) f l ex (show leaf) (length ps) accts where -- t = fromMaybe nulltransaction ptransaction -- XXX sloppy (_,_,_,ex1) = makePostingErrorExcerpt p (\_ _ _ -> Nothing) (f,l,_,ex2) = makePostingErrorExcerpt p2 finderrcols -- separate the two excerpts by a space-beginning line to help flycheck-hledger parse them ex = T.unlines [textChomp ex1, T.pack " ...", textChomp ex2] finderrcols p' _ _ = Just (col, Just col2) where a = paccount p' alen = T.length a llen = T.length $ accountLeafName a col = 5 + (if isVirtual p' then 1 else 0) + alen - llen col2 = col + llen - 1 accts = T.unlines fulls _ -> Right () -- shouldn't happen finddupes :: (Ord leaf, Eq full) => [(leaf, full)] -> [(leaf, [full])] finddupes leafandfullnames = zip dupLeafs dupAccountNames where dupLeafs = map (fst . head) d dupAccountNames = map (map snd) d d = dupes' leafandfullnames dupes' = filter ((> 1) . length) . groupBy ((==) `on` fst) . sortBy (compare `on` fst) journalLeafAndFullAccountNames :: Journal -> [(Text, AccountName)] journalLeafAndFullAccountNames = map leafAndAccountName . journalAccountNamesUsed where leafAndAccountName a = (accountLeafName a, a)