penny-bin-0.12.0.0: penny-diff.hs
module Main where
import qualified Control.Exception as CEx
import qualified Control.Monad.Exception.Synchronous as Ex
import Data.Either (partitionEithers)
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 qualified Penny.Lincoln.Predicates as P
import qualified Penny.Copper as C
import qualified Penny.Copper.Render as CR
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_bin as PPB
groupingSpecs :: CR.GroupSpecs
groupingSpecs = CR.GroupSpecs CR.NoGrouping CR.NoGrouping
main :: IO ()
main = runPennyDiff groupingSpecs
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 (Either (IO ()) Args)]
allOpts = [ fmap Right optFile1
, fmap Right optFile2
, fmap Left (Ly.version PPB.version)
]
data File = File1 | File2
deriving (Eq, Show)
-- | All possible items, but excluding blank lines.
data NonBlankItem
= Transaction L.Transaction
| Price L.PricePoint
| Comment C.Comment
deriving (Eq, Show)
clonedNonBlankItem :: NonBlankItem -> NonBlankItem -> Bool
clonedNonBlankItem nb1 nb2 = case (nb1, nb2) of
(Transaction t1, Transaction t2) -> P.clonedTransactions t1 t2
(Price p1, Price p2) -> p1 == p2
(Comment c1, Comment c2) -> c1 == c2
_ -> False
toNonBlankItem :: C.Item -> Maybe NonBlankItem
toNonBlankItem i = case i of
C.Transaction t -> Just (Transaction t)
C.PricePoint p -> Just (Price p)
C.IComment c -> Just (Comment c)
_ -> 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
:: CR.GroupSpecs
-> File
-> L.Transaction
-> Maybe X.Text
renderTransaction gs f t = fmap addHeader $ CR.transaction gs t
where
(L.Family tl _ _ _) = L.unTransaction t
lin = case L.tMemo tl of
Nothing -> L.unTopLineLine . fromJust
. L.tTopLineLine $ tl
Just _ -> L.unTopMemoLine . fromJust
. L.tTopMemoLine $ tl
addHeader x = (showLineNum f lin) `X.append` x
renderPrice :: CR.GroupSpecs -> File -> L.PricePoint -> Maybe X.Text
renderPrice gs f p = fmap addHeader $ CR.price gs p
where
lin = L.unPriceLine . fromJust . L.priceLine $ p
addHeader x = (showLineNum f lin) `X.append` x
renderNonBlankItem :: CR.GroupSpecs -> File -> NonBlankItem -> Maybe X.Text
renderNonBlankItem gs f n = case n of
Transaction t -> renderTransaction gs f t
Price p -> renderPrice gs f p
Comment c -> CR.comment c
runPennyDiff :: CR.GroupSpecs -> IO ()
runPennyDiff co = do
(f1, f2, dts) <- parseCommandLine
l1 <- parseFile f1
l2 <- parseFile f2
let (r1, r2) = doDiffs l1 l2
showDiffs co dts (r1, r2)
case (r1, r2) of
([], []) -> E.exitSuccess
_ -> E.exitWith (E.ExitFailure 1)
showDiffs
:: CR.GroupSpecs
-> DiffsToShow
-> ([NonBlankItem], [NonBlankItem])
-> IO ()
showDiffs co dts (l1, l2) =
case dts of
File1Only -> showFile1
File2Only -> showFile2
BothFiles -> showFile1 >> showFile2
where
showFile1 = showNonBlankItems co File1 l1
showFile2 = showNonBlankItems co File2 l2
failure :: String -> IO a
failure s = IO.hPutStrLn IO.stderr s
>> E.exitWith (E.ExitFailure 2)
showNonBlankItems
:: CR.GroupSpecs
-> File
-> [NonBlankItem]
-> IO ()
showNonBlankItems o f ls =
mapM_ (showNonBlankItem o f) ls
showNonBlankItem :: CR.GroupSpecs -> File -> NonBlankItem -> IO ()
showNonBlankItem co f nbi = maybe e TIO.putStr
(renderNonBlankItem co 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.Ledger
-> C.Ledger
-> ([NonBlankItem], [NonBlankItem])
doDiffs l1 l2 = (r1, r2)
where
mkNbList = mapMaybe toNonBlankItem . C.unLedger
(nb1, nb2) = (mkNbList l1, mkNbList l2)
df = deleteFirstsBy clonedNonBlankItem
(r1, r2) = (nb1 `df` nb2, nb2 `df` nb1)
parseFile :: String -> IO C.Ledger
parseFile s = do
eiTxt <- CEx.try $ TIO.readFile s
txt <- case eiTxt of
Left e -> failure (show (e :: IOError))
Right g -> return g
let fn = L.Filename . X.pack $ s
c = C.FileContents txt
parsed = C.parse [(fn, c)]
case parsed of
Ex.Exception e -> failure (show e)
Ex.Success g -> return g
-- | 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.simpleWithHelp help M.Intersperse allOpts
(Right . Filename)
let (doVer, args) = partitionEithers as
toFilename a = case a of
Filename s -> Just s
_ -> Nothing
case doVer of
[] -> return ()
x:_ -> x
(fn1, fn2) <- case mapMaybe toFilename args of
x:y:[] -> return (x, y)
_ -> failure "penny-diff: error: you must supply two filenames."
let getDiffs
| ((ArgFile File1) `elem` args)
&& ((ArgFile File2) `elem` args) = BothFiles
| ((ArgFile File1) `elem` args) = File1Only
| ((ArgFile File2) `elem` args) = File2Only
| otherwise = BothFiles
return (fn1, fn2, getDiffs)