module Penny.Denver.Diff (main) where import Control.Arrow (first, second) import Data.Maybe (fromJust) import Data.List (deleteFirstsBy) import qualified System.Console.MultiArg as M import qualified Penny.Liberty as Ly import qualified Penny.Lincoln as L import Penny.Lincoln ((==~)) import qualified Penny.Copper as C import qualified Penny.Copper.Render as CR import qualified Data.Sums as S import Data.Maybe (mapMaybe) import qualified Data.Text as X import qualified Data.Text.IO as TIO import qualified System.Exit as E import qualified System.IO as IO import qualified Paths_penny as PPB main :: IO () main = runPennyDiff help :: String -> String help pn = unlines [ "usage: " ++ pn ++ " [-12] FILE1 FILE2" , "Shows items that exist in FILE1 but not in FILE2," , "as well as items that exist in FILE2 but not in FILE1." , "Options:" , "-1 Show only items that exist in FILE1 but not in FILE2" , "-2 Show only items that exist in FILE2 but not in FILE1" , "" , "--help, -h - show this help and exit" , "--version Show version and exit" ] data Args = ArgFile File | Filename String deriving (Eq, Show) data DiffsToShow = File1Only | File2Only | BothFiles optFile1 :: M.OptSpec Args optFile1 = M.OptSpec [] "1" (M.NoArg (ArgFile File1)) optFile2 :: M.OptSpec Args optFile2 = M.OptSpec [] "2" (M.NoArg (ArgFile File2)) allOpts :: [M.OptSpec Args] allOpts = [ optFile1 , optFile2 ] data File = File1 | File2 deriving (Eq, Show) -- | All possible items, but excluding blank lines. type NonBlankItem = S.S3 L.Transaction L.PricePoint C.Comment removeMeta :: L.Transaction -> (L.TopLineCore, L.Ents L.PostingCore) removeMeta = first L.tlCore . second (fmap L.pdCore) . L.unTransaction clonedNonBlankItem :: NonBlankItem -> NonBlankItem -> Bool clonedNonBlankItem nb1 nb2 = case (nb1, nb2) of (S.S3a t1, S.S3a t2) -> removeMeta t1 ==~ removeMeta t2 (S.S3b p1, S.S3b p2) -> p1 ==~ p2 (S.S3c c1, S.S3c c2) -> c1 == c2 _ -> False toNonBlankItem :: C.LedgerItem -> Maybe NonBlankItem toNonBlankItem = S.caseS4 (Just . S.S3a) (Just . S.S3b) (Just . S.S3c) (const Nothing) showLineNum :: File -> Int -> X.Text showLineNum f i = X.pack ("\n" ++ arrow ++ " " ++ show i ++ "\n") where arrow = case f of File1 -> "<==" File2 -> "==>" -- | Renders a transaction, along with a line showing what file it -- came from and its line number. If there is a TransactionMemo, shows -- the line number for the top line for that; otherwise, shows the -- line number for the TopLine. renderTransaction :: File -> L.Transaction -> Maybe X.Text renderTransaction f t = fmap addHeader $ CR.transaction Nothing (noMeta t) where lin = case L.tMemo . L.tlCore . fst . L.unTransaction $ t of Nothing -> L.unTopLineLine . L.tTopLineLine . fromJust . L.tlFileMeta . fst . L.unTransaction $ t Just _ -> L.unTopMemoLine . fromJust . L.tTopMemoLine . fromJust . L.tlFileMeta . fst . L.unTransaction $ t addHeader x = (showLineNum f lin) `X.append` x noMeta txn = let (tl, es) = L.unTransaction txn in (L.tlCore tl, fmap L.pdCore es) renderPrice :: File -> L.PricePoint -> Maybe X.Text renderPrice f p = fmap addHeader $ CR.price p where lin = L.unPriceLine . fromJust . L.priceLine $ p addHeader x = (showLineNum f lin) `X.append` x renderNonBlankItem :: File -> NonBlankItem -> Maybe X.Text renderNonBlankItem f = S.caseS3 (renderTransaction f) (renderPrice f) CR.comment runPennyDiff :: IO () runPennyDiff = do (f1, f2, dts) <- parseCommandLine l1 <- C.open [f1] l2 <- C.open [f2] let (r1, r2) = doDiffs l1 l2 showDiffs dts (r1, r2) case (r1, r2) of ([], []) -> E.exitSuccess _ -> E.exitWith (E.ExitFailure 1) showDiffs :: DiffsToShow -> ([NonBlankItem], [NonBlankItem]) -> IO () showDiffs dts (l1, l2) = case dts of File1Only -> showFile1 File2Only -> showFile2 BothFiles -> showFile1 >> showFile2 where showFile1 = showNonBlankItems File1 l1 showFile2 = showNonBlankItems File2 l2 failure :: String -> IO a failure s = IO.hPutStrLn IO.stderr s >> E.exitWith (E.ExitFailure 2) showNonBlankItems :: File -> [NonBlankItem] -> IO () showNonBlankItems f ls = mapM_ (showNonBlankItem f) ls showNonBlankItem :: File -> NonBlankItem -> IO () showNonBlankItem f nbi = maybe e TIO.putStr (renderNonBlankItem f nbi) where e = failure $ "could not render item: " ++ show nbi -- | Returns a pair p, where fst p is the items that appear in file1 -- but not in file2, and snd p is items that appear in file2 but not -- in file1. doDiffs :: [C.LedgerItem] -> [C.LedgerItem] -> ([NonBlankItem], [NonBlankItem]) doDiffs l1 l2 = (r1, r2) where mkNbList = mapMaybe toNonBlankItem (nb1, nb2) = (mkNbList l1, mkNbList l2) df = deleteFirstsBy clonedNonBlankItem (r1, r2) = (nb1 `df` nb2, nb2 `df` nb1) -- | Returns a tuple with the first filename, the second filename, and -- an indication of which differences to show. parseCommandLine :: IO (String, String, DiffsToShow) parseCommandLine = do as <- M.simpleHelpVersion help (Ly.version PPB.version) allOpts M.Intersperse (return . Filename) let toFilename a = case a of Filename s -> Just s _ -> Nothing (fn1, fn2) <- case mapMaybe toFilename as of x:y:[] -> return (x, y) _ -> failure "penny-diff: error: you must supply two filenames." let getDiffs | ((ArgFile File1) `elem` as) && ((ArgFile File2) `elem` as) = BothFiles | ((ArgFile File1) `elem` as) = File1Only | ((ArgFile File2) `elem` as) = File2Only | otherwise = BothFiles return (fn1, fn2, getDiffs)