From 4567e91409fdeddb42273f558e3f90f13e74cb52 Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Mon, 19 Nov 2012 21:20:10 +0000 Subject: [PATCH] refactor: move amount display settings out of commodity, simplify amount construction --- hledger-lib/Hledger/Data/Amount.hs | 309 +++++++++++++--------- hledger-lib/Hledger/Data/Commodity.hs | 64 ++--- hledger-lib/Hledger/Data/Journal.hs | 108 ++++---- hledger-lib/Hledger/Data/Ledger.hs | 6 +- hledger-lib/Hledger/Data/TimeLog.hs | 6 +- hledger-lib/Hledger/Data/Transaction.hs | 130 +++++---- hledger-lib/Hledger/Data/Types.hs | 40 +-- hledger-lib/Hledger/Read.hs | 4 +- hledger-lib/Hledger/Read/CsvReader.hs | 4 +- hledger-lib/Hledger/Read/JournalReader.hs | 127 +++++---- hledger-lib/Hledger/Reports.hs | 105 ++++---- hledger/Hledger/Cli.hs | 73 ++--- hledger/Hledger/Cli/Add.hs | 10 +- hledger/Hledger/Cli/Stats.hs | 2 +- 14 files changed, 502 insertions(+), 486 deletions(-) diff --git a/hledger-lib/Hledger/Data/Amount.hs b/hledger-lib/Hledger/Data/Amount.hs index 7a7482944..2f4a0d5b6 100644 --- a/hledger-lib/Hledger/Data/Amount.hs +++ b/hledger-lib/Hledger/Data/Amount.hs @@ -1,4 +1,4 @@ -{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE StandaloneDeriving, RecordWildCards #-} {-| A simple 'Amount' is some quantity of money, shares, or anything else. It has a (possibly null) 'Commodity' and a numeric quantity: @@ -43,30 +43,38 @@ exchange rates. module Hledger.Data.Amount ( -- * Amount + amount, nullamt, missingamt, + amt, + usd, + eur, + gbp, + hrs, + at, + (@@), amountWithCommodity, - canonicaliseAmountCommodity, - setAmountPrecision, -- ** arithmetic costOfAmount, divideAmount, sumAmounts, -- ** rendering + amountstyle, showAmount, showAmountDebug, showAmountWithoutPrice, maxprecision, maxprecisionwithpoint, + setAmountPrecision, + withPrecision, + canonicaliseAmount, + canonicalStyles, -- * MixedAmount nullmixedamt, missingmixedamt, amounts, normaliseMixedAmountPreservingFirstPrice, normaliseMixedAmountPreservingPrices, - canonicaliseMixedAmountCommodity, - mixedAmountWithCommodity, - setMixedAmountPrecision, -- ** arithmetic costOfMixedAmount, divideMixedAmount, @@ -78,6 +86,8 @@ module Hledger.Data.Amount ( showMixedAmountDebug, showMixedAmountWithoutPrice, showMixedAmountWithPrecision, + setMixedAmountPrecision, + canonicaliseMixedAmount, -- * misc. ltraceamount, tests_Hledger_Data_Amount @@ -86,9 +96,10 @@ module Hledger.Data.Amount ( import Data.Char (isDigit) import Data.List import Data.Map (findWithDefault) +import Data.Ord (comparing) import Test.HUnit import Text.Printf -import qualified Data.Map as Map +import qualified Data.Map as M import Hledger.Data.Types import Hledger.Data.Commodity @@ -97,52 +108,75 @@ import Hledger.Utils deriving instance Show HistoricalPrice +amountstyle = AmountStyle L False 0 '.' ',' [] + ------------------------------------------------------------------------------- -- Amount instance Show Amount where show = showAmountDebug instance Num Amount where - abs (Amount c q p) = Amount c (abs q) p - signum (Amount c q p) = Amount c (signum q) p - fromInteger i = Amount (comm "") (fromInteger i) Nothing - negate a@Amount{quantity=q} = a{quantity=(-q)} - (+) = similarAmountsOp (+) - (-) = similarAmountsOp (-) - (*) = similarAmountsOp (*) + abs a@Amount{aquantity=q} = a{aquantity=abs q} + signum a@Amount{aquantity=q} = a{aquantity=signum q} + fromInteger i = nullamt{aquantity=fromInteger i} + negate a@Amount{aquantity=q} = a{aquantity=(-q)} + (+) = similarAmountsOp (+) + (-) = similarAmountsOp (-) + (*) = similarAmountsOp (*) -- | The empty simple amount. -nullamt :: Amount -nullamt = Amount unknown 0 Nothing +amount :: Amount +amount = Amount{acommodity="", aquantity=0, aprice=Nothing, astyle=amountstyle} +nullamt = amount --- | Apply a binary arithmetic operator to two amounts, ignoring and --- discarding any assigned prices, and converting the first to the --- commodity of the second in a simplistic way (1-1 exchange rate). --- The highest precision of either amount is preserved in the result. +-- handy amount constructors for tests +amt n = amount{acommodity="", aquantity=n} +usd n = amount{acommodity="$", aquantity=n, astyle=amountstyle{asprecision=2}} +eur n = amount{acommodity="€", aquantity=n, astyle=amountstyle{asprecision=2}} +gbp n = amount{acommodity="£", aquantity=n, astyle=amountstyle{asprecision=2}} +hrs n = amount{acommodity="h", aquantity=n, astyle=amountstyle{asprecision=1, ascommodityside=R}} + +-- | Apply a binary arithmetic operator to two amounts in the same +-- commodity. Warning, as a kludge to support folds (eg sum) we assign +-- the second's commodity to the first so the same commodity requirement +-- is not checked. The highest precision of either amount is preserved in +-- the result. Any prices are currently ignored and discarded. The display +-- style is that of the first amount, with precision set to the highest of +-- either amount. similarAmountsOp :: (Double -> Double -> Double) -> Amount -> Amount -> Amount -similarAmountsOp op a@(Amount Commodity{precision=ap} _ _) (Amount bc@Commodity{precision=bp} bq _) = - Amount bc{precision=max ap bp} (quantity (amountWithCommodity bc a) `op` bq) Nothing +similarAmountsOp op Amount{acommodity=_, aquantity=aq, astyle=AmountStyle{asprecision=ap}} + Amount{acommodity=bc, aquantity=bq, astyle=bs@AmountStyle{asprecision=bp}} = + -- trace ("a:"++showAmount a) $ trace ("b:"++showAmount b++"\n") $ tracewith (("=:"++).showAmount) + amount{acommodity=bc, aquantity=aq `op` bq, astyle=bs{asprecision=max ap bp}} + -- | ac==bc = amount{acommodity=ac, aquantity=aq `op` bq, astyle=as{asprecision=max ap bp}} + -- | otherwise = error "tried to do simple arithmetic with amounts in different commodities" -- | Convert an amount to the specified commodity, ignoring and discarding -- any assigned prices and assuming an exchange rate of 1. amountWithCommodity :: Commodity -> Amount -> Amount -amountWithCommodity c (Amount _ q _) = Amount c q Nothing +amountWithCommodity c a = a{acommodity=c, aprice=Nothing} -- | A more complete amount adding operation. sumAmounts :: [Amount] -> MixedAmount sumAmounts = normaliseMixedAmountPreservingPrices . Mixed +-- | Set an amount's unit price. +at :: Amount -> Amount -> Amount +amt `at` priceamt = amt{aprice=Just $ UnitPrice $ Mixed [priceamt]} + +-- | Set an amount's total price. +(@@) :: Amount -> Amount -> Amount +amt @@ priceamt = amt{aprice=Just $ TotalPrice $ Mixed [priceamt]} + tests_sumAmounts = [ "sumAmounts" ~: do -- when adding, we don't convert to the price commodity - just -- combine what amounts we can. -- amounts with same unit price - (sumAmounts [(Amount dollar 1 (Just $ UnitPrice $ Mixed [euros 1])), (Amount dollar 1 (Just $ UnitPrice $ Mixed [euros 1]))]) - `is` (Mixed [Amount dollar 2 (Just $ UnitPrice $ Mixed [euros 1])]) + sumAmounts [usd 1 `at` eur 1, usd 1 `at` eur 1] `is` Mixed [usd 2 `at` eur 1] -- amounts with different unit prices -- amounts with total prices - (sumAmounts [(Amount dollar 1 (Just $ TotalPrice $ Mixed [euros 1])), (Amount dollar 1 (Just $ TotalPrice $ Mixed [euros 1]))]) - `is` (Mixed [(Amount dollar 1 (Just $ TotalPrice $ Mixed [euros 1])), (Amount dollar 1 (Just $ TotalPrice $ Mixed [euros 1]))]) + sumAmounts [usd 1 @@ eur 1, usd 1 @@ eur 1] `is` Mixed [usd 1 @@ eur 1, usd 1 @@ eur 1] -- amounts with no, unit, and/or total prices ] @@ -152,20 +186,20 @@ tests_sumAmounts = [ -- -- - price amounts should be positive, though this is not currently enforced costOfAmount :: Amount -> Amount -costOfAmount a@(Amount _ q price) = +costOfAmount a@Amount{aquantity=q, aprice=price} = case price of Nothing -> a - Just (UnitPrice (Mixed [Amount pc pq Nothing])) -> Amount pc (pq*q) Nothing - Just (TotalPrice (Mixed [Amount pc pq Nothing])) -> Amount pc (pq*signum q) Nothing + Just (UnitPrice (Mixed [p@Amount{aquantity=pq}])) -> p{aquantity=pq * q} + Just (TotalPrice (Mixed [p@Amount{aquantity=pq}])) -> p{aquantity=pq * signum q} _ -> error' "costOfAmount: Malformed price encountered, programmer error" -- | Divide an amount's quantity by a constant. divideAmount :: Amount -> Double -> Amount -divideAmount a@Amount{quantity=q} d = a{quantity=q/d} +divideAmount a@Amount{aquantity=q} d = a{aquantity=q/d} -- | Is this amount negative ? The price is ignored. isNegativeAmount :: Amount -> Bool -isNegativeAmount Amount{quantity=q} = q < 0 +isNegativeAmount Amount{aquantity=q} = q < 0 digits = "123456789" :: String @@ -178,7 +212,7 @@ isZeroAmount a -- a==missingamt = False -- Since we are using floating point, for now just test to some high precision. isReallyZeroAmount :: Amount -> Bool isReallyZeroAmount a -- a==missingamt = False - | otherwise = (null . filter (`elem` digits) . printf ("%."++show zeroprecision++"f") . quantity) a + | otherwise = (null . filter (`elem` digits) . printf ("%."++show zeroprecision++"f") . aquantity) a where zeroprecision = 8 -- | Get the string representation of an amount, based on its commodity's @@ -186,23 +220,27 @@ isReallyZeroAmount a -- a==missingamt = False showAmountWithPrecision :: Int -> Amount -> String showAmountWithPrecision p = showAmount . setAmountPrecision p --- | Set the display precision in the amount's commodity. +-- | Set an amount's display precision. setAmountPrecision :: Int -> Amount -> Amount -setAmountPrecision p a@Amount{commodity=c} = a{commodity=c{precision=p}} +setAmountPrecision p a@Amount{astyle=s} = a{astyle=s{asprecision=p}} + +-- | Set an amount's display precision, flipped. +withPrecision :: Amount -> Int -> Amount +withPrecision = flip setAmountPrecision -- | Get the unambiguous string representation of an amount, for debugging. showAmountDebug :: Amount -> String -showAmountDebug (Amount (Commodity {symbol="AUTO"}) _ _) = "(missing)" -showAmountDebug (Amount c q pri) = printf "Amount {commodity = %s, quantity = %s, price = %s}" - (show c) (show q) (maybe "Nothing" showPriceDebug pri) +showAmountDebug Amount{acommodity="AUTO"} = "(missing)" +showAmountDebug Amount{..} = printf "Amount {acommodity=%s, aquantity=%s, aprice=%s, astyle=%s}" + (show acommodity) (show aquantity) (maybe "Nothing" showPriceDebug aprice) (show astyle) -- | Get the string representation of an amount, without any \@ price. showAmountWithoutPrice :: Amount -> String -showAmountWithoutPrice a = showAmount a{price=Nothing} +showAmountWithoutPrice a = showAmount a{aprice=Nothing} -- | Get the string representation of an amount, without any price or commodity symbol. showAmountWithoutPriceOrCommodity :: Amount -> String -showAmountWithoutPriceOrCommodity a@Amount{commodity=c} = showAmount a{commodity=c{symbol=""}, price=Nothing} +showAmountWithoutPriceOrCommodity a = showAmount a{acommodity="", aprice=Nothing} showPrice :: Price -> String showPrice (UnitPrice pa) = " @ " ++ showMixedAmount pa @@ -216,23 +254,23 @@ showPriceDebug (TotalPrice pa) = " @@ " ++ showMixedAmountDebug pa -- display settings. String representations equivalent to zero are -- converted to just \"0\". showAmount :: Amount -> String -showAmount (Amount (Commodity {symbol="AUTO"}) _ _) = "" -showAmount a@(Amount (Commodity {symbol=sym,side=side,spaced=spaced}) _ pri) = - case side of - L -> printf "%s%s%s%s" sym' space quantity' price - R -> printf "%s%s%s%s" quantity' space sym' price +showAmount Amount{acommodity="AUTO"} = "" +showAmount a@(Amount{acommodity=c, aprice=p, astyle=AmountStyle{..}}) = + case ascommodityside of + L -> printf "%s%s%s%s" c' space quantity' price + R -> printf "%s%s%s%s" quantity' space c' price where quantity = showamountquantity a displayingzero = null $ filter (`elem` digits) $ quantity - (quantity',sym') | displayingzero = ("0","") - | otherwise = (quantity,quoteCommoditySymbolIfNeeded sym) - space = if (not (null sym') && spaced) then " " else "" :: String - price = maybe "" showPrice pri + (quantity',c') | displayingzero = ("0","") + | otherwise = (quantity, quoteCommoditySymbolIfNeeded c) + space = if (not (null c') && ascommodityspaced) then " " else "" :: String + price = maybe "" showPrice p -- | Get the string representation of the number part of of an amount, -- using the display settings from its commodity. showamountquantity :: Amount -> String -showamountquantity (Amount (Commodity {decimalpoint=d,precision=p,separator=s,separatorpositions=spos}) q _) = +showamountquantity Amount{aquantity=q, astyle=AmountStyle{asprecision=p, asdecimalpoint=d, asseparator=s, asseparatorpositions=spos}} = punctuatenumber d s spos $ qstr where -- isint n = fromIntegral (round n) == n @@ -242,7 +280,8 @@ showamountquantity (Amount (Commodity {decimalpoint=d,precision=p,separator=s,se | otherwise = printf ("%."++show p++"f") q -- | Replace a number string's decimal point with the specified character, --- and add the specified digit group separators. +-- and add the specified digit group separators. The last digit group will +-- be repeated as needed. punctuatenumber :: Char -> Char -> [Int] -> String -> String punctuatenumber dec sep grps str = sign ++ reverse (addseps sep (extend grps) (reverse int)) ++ frac'' where @@ -271,15 +310,12 @@ maxprecision = 999998 maxprecisionwithpoint :: Int maxprecisionwithpoint = 999999 --- | Replace an amount's commodity with the canonicalised version from --- the provided commodity map. -canonicaliseAmountCommodity :: Maybe (Map.Map String Commodity) -> Amount -> Amount -canonicaliseAmountCommodity Nothing = id -canonicaliseAmountCommodity (Just canonicalcommoditymap) = fixamount +-- like journalCanonicaliseAmounts +-- | Canonicalise an amount's display style using the provided commodity style map. +canonicaliseAmount :: M.Map Commodity AmountStyle -> Amount -> Amount +canonicaliseAmount styles a@Amount{acommodity=c, astyle=s} = a{astyle=s'} where - -- like journalCanonicaliseAmounts - fixamount a@Amount{commodity=c} = a{commodity=fixcommodity c} - fixcommodity c@Commodity{symbol=s} = findWithDefault c s canonicalcommoditymap + s' = findWithDefault s c styles ------------------------------------------------------------------------------- -- MixedAmount @@ -287,7 +323,7 @@ canonicaliseAmountCommodity (Just canonicalcommoditymap) = fixamount instance Show MixedAmount where show = showMixedAmountDebug instance Num MixedAmount where - fromInteger i = Mixed [Amount (comm "") (fromInteger i) Nothing] + fromInteger i = Mixed [fromInteger i] negate (Mixed as) = Mixed $ map negate as (+) (Mixed as) (Mixed bs) = normaliseMixedAmountPreservingPrices $ Mixed $ as ++ bs (*) = error' "programming error, mixed amounts do not support multiplication" @@ -300,7 +336,7 @@ nullmixedamt = Mixed [] -- | A temporary value for parsed transactions which had no amount specified. missingamt :: Amount -missingamt = Amount unknown{symbol="AUTO"} 0 Nothing +missingamt = amount{acommodity="AUTO"} missingmixedamt :: MixedAmount missingmixedamt = Mixed [missingamt] @@ -312,30 +348,29 @@ normaliseMixedAmountPreservingPrices :: MixedAmount -> MixedAmount normaliseMixedAmountPreservingPrices (Mixed as) = Mixed as'' where as'' = if null nonzeros then [nullamt] else nonzeros - (_,nonzeros) = partition isReallyZeroAmount $ filter (/= missingamt) as' - as' = map sumAmountsUsingFirstPrice $ group $ sort as - sort = sortBy (\a1 a2 -> compare (sym a1,price a1) (sym a2,price a2)) - sym = symbol . commodity - group = groupBy (\a1 a2 -> sym a1 == sym a2 && sameunitprice a1 a2) + (_,nonzeros) = partition isReallyZeroAmount as' + as' = map sumAmountsUsingFirstPrice $ group $ sort $ filter (/= missingamt) as + sort = sortBy (\a1 a2 -> compare (acommodity a1, aprice a1) (acommodity a2, aprice a2)) + group = groupBy (\a1 a2 -> acommodity a1 == acommodity a2 && sameunitprice a1 a2) where sameunitprice a1 a2 = - case (price a1, price a2) of + case (aprice a1, aprice a2) of (Nothing, Nothing) -> True (Just (UnitPrice p1), Just (UnitPrice p2)) -> p1 == p2 _ -> False tests_normaliseMixedAmountPreservingPrices = [ "normaliseMixedAmountPreservingPrices" ~: do - assertEqual "discard missing amount" (Mixed [nullamt]) (normaliseMixedAmountPreservingPrices $ Mixed [dollars 0, missingamt]) - assertEqual "combine unpriced same-commodity amounts" (Mixed [dollars 2]) (normaliseMixedAmountPreservingPrices $ Mixed [dollars 0, dollars 2]) + assertEqual "discard missing amount" (Mixed [nullamt]) (normaliseMixedAmountPreservingPrices $ Mixed [usd 0, missingamt]) + assertEqual "combine unpriced same-commodity amounts" (Mixed [usd 2]) (normaliseMixedAmountPreservingPrices $ Mixed [usd 0, usd 2]) assertEqual "don't combine total-priced amounts" (Mixed - [Amount dollar 1 (Just $ TotalPrice $ Mixed [euros 1]) - ,Amount dollar (-2) (Just $ TotalPrice $ Mixed [euros 1]) + [usd 1 @@ eur 1 + ,usd (-2) @@ eur 1 ]) (normaliseMixedAmountPreservingPrices $ Mixed - [Amount dollar 1 (Just $ TotalPrice $ Mixed [euros 1]) - ,Amount dollar (-2) (Just $ TotalPrice $ Mixed [euros 1]) + [usd 1 @@ eur 1 + ,usd (-2) @@ eur 1 ]) ] @@ -351,9 +386,8 @@ normaliseMixedAmountPreservingFirstPrice (Mixed as) = Mixed as'' as'' = if null nonzeros then [nullamt] else nonzeros (_,nonzeros) = partition (\a -> isReallyZeroAmount a && a /= missingamt) as' as' = map sumAmountsUsingFirstPrice $ group $ sort as - sort = sortBy (\a1 a2 -> compare (sym a1) (sym a2)) - group = groupBy (\a1 a2 -> sym a1 == sym a2) - sym = symbol . commodity + sort = sortBy (\a1 a2 -> compare (acommodity a1) (acommodity a2)) + group = groupBy (\a1 a2 -> acommodity a1 == acommodity a2) -- discardPrice :: Amount -> Amount -- discardPrice a = a{price=Nothing} @@ -362,7 +396,7 @@ normaliseMixedAmountPreservingFirstPrice (Mixed as) = Mixed as'' -- discardPrices (Mixed as) = Mixed $ map discardPrice as sumAmountsUsingFirstPrice [] = nullamt -sumAmountsUsingFirstPrice as = (sum as){price=price $ head as} +sumAmountsUsingFirstPrice as = (sum as){aprice=aprice $ head as} -- | Get a mixed amount's component amounts. amounts :: MixedAmount -> [Amount] @@ -396,12 +430,6 @@ isReallyZeroMixedAmount = all isReallyZeroAmount . amounts . normaliseMixedAmoun isReallyZeroMixedAmountCost :: MixedAmount -> Bool isReallyZeroMixedAmountCost = isReallyZeroMixedAmount . costOfMixedAmount --- -- | Convert a mixed amount to the specified commodity, assuming an exchange rate of 1. -mixedAmountWithCommodity :: Commodity -> MixedAmount -> Amount -mixedAmountWithCommodity c (Mixed as) = Amount c total Nothing - where - total = sum $ map (quantity . amountWithCommodity c) as - -- -- | MixedAmount derived Eq instance in Types.hs doesn't know that we -- -- want $0 = EUR0 = 0. Yet we don't want to drag all this code over there. -- -- For now, use this when cross-commodity zero equality is important. @@ -414,7 +442,7 @@ mixedAmountWithCommodity c (Mixed as) = Amount c total Nothing -- its component amounts. NB a mixed amount can have an empty amounts -- list in which case it shows as \"\". showMixedAmount :: MixedAmount -> String -showMixedAmount m = vConcatRightAligned $ map showAmount $ amounts $ normaliseMixedAmountPreservingFirstPrice m +showMixedAmount m = vConcatRightAligned $ map showAmount $ amounts $ normaliseMixedAmountPreservingFirstPrice m -- | Compact labelled trace of a mixed amount. ltraceamount :: String -> MixedAmount -> MixedAmount @@ -443,14 +471,30 @@ showMixedAmountWithoutPrice :: MixedAmount -> String showMixedAmountWithoutPrice m = concat $ intersperse "\n" $ map showfixedwidth as where (Mixed as) = normaliseMixedAmountPreservingFirstPrice $ stripPrices m - stripPrices (Mixed as) = Mixed $ map stripprice as where stripprice a = a{price=Nothing} + stripPrices (Mixed as) = Mixed $ map stripprice as where stripprice a = a{aprice=Nothing} width = maximum $ map (length . showAmount) as showfixedwidth = printf (printf "%%%ds" width) . showAmountWithoutPrice --- | Replace a mixed amount's commodity with the canonicalised version from --- the provided commodity map. -canonicaliseMixedAmountCommodity :: Maybe (Map.Map String Commodity) -> MixedAmount -> MixedAmount -canonicaliseMixedAmountCommodity canonicalcommoditymap (Mixed as) = Mixed $ map (canonicaliseAmountCommodity canonicalcommoditymap) as +-- | Canonicalise a mixed amount's display styles using the provided commodity style map. +canonicaliseMixedAmount :: M.Map Commodity AmountStyle -> MixedAmount -> MixedAmount +canonicaliseMixedAmount styles (Mixed as) = Mixed $ map (canonicaliseAmount styles) as + +-- | Given a list of amounts in parse order, build a map from commodities +-- to canonical display styles for amounts in that commodity. +canonicalStyles :: [Amount] -> M.Map Commodity AmountStyle +canonicalStyles amts = M.fromList commstyles + where + samecomm = \a1 a2 -> acommodity a1 == acommodity a2 + commamts = [(acommodity $ head as, as) | as <- groupBy samecomm $ sortBy (comparing acommodity) amts] + commstyles = [(c, s) + | (c,as) <- commamts + , let styles = map astyle as + , let maxprec = maximum $ map asprecision styles + , let s = (head styles){asprecision=maxprec} + ] + +-- lookupStyle :: M.Map Commodity AmountStyle -> Commodity -> AmountStyle +-- lookupStyle ------------------------------------------------------------------------------- -- misc @@ -463,77 +507,78 @@ tests_Hledger_Data_Amount = TestList $ -- Amount "costOfAmount" ~: do - costOfAmount (euros 1) `is` euros 1 - costOfAmount (euros 2){price=Just $ UnitPrice $ Mixed [dollars 2]} `is` dollars 4 - costOfAmount (euros 1){price=Just $ TotalPrice $ Mixed [dollars 2]} `is` dollars 2 - costOfAmount (euros (-1)){price=Just $ TotalPrice $ Mixed [dollars 2]} `is` dollars (-2) + costOfAmount (eur 1) `is` eur 1 + costOfAmount (eur 2){aprice=Just $ UnitPrice $ Mixed [usd 2]} `is` usd 4 + costOfAmount (eur 1){aprice=Just $ TotalPrice $ Mixed [usd 2]} `is` usd 2 + costOfAmount (eur (-1)){aprice=Just $ TotalPrice $ Mixed [usd 2]} `is` usd (-2) ,"isZeroAmount" ~: do - assertBool "" $ isZeroAmount $ Amount unknown 0 Nothing - assertBool "" $ isZeroAmount $ dollars 0 + assertBool "" $ isZeroAmount $ amount + assertBool "" $ isZeroAmount $ usd 0 ,"negating amounts" ~: do - let a = dollars 1 - negate a `is` a{quantity=(-1)} - let b = (dollars 1){price=Just $ UnitPrice $ Mixed [euros 2]} - negate b `is` b{quantity=(-1)} + let a = usd 1 + negate a `is` a{aquantity=(-1)} + let b = (usd 1){aprice=Just $ UnitPrice $ Mixed [eur 2]} + negate b `is` b{aquantity=(-1)} ,"adding amounts without prices" ~: do - let a1 = dollars 1.23 - let a2 = dollars (-1.23) - let a3 = dollars (-1.23) - (a1 + a2) `is` Amount (comm "$") 0 Nothing - (a1 + a3) `is` Amount (comm "$") 0 Nothing - (a2 + a3) `is` Amount (comm "$") (-2.46) Nothing - (a3 + a3) `is` Amount (comm "$") (-2.46) Nothing - sum [a1,a2,a3,-a3] `is` Amount (comm "$") 0 Nothing + let a1 = usd 1.23 + let a2 = usd (-1.23) + let a3 = usd (-1.23) + (a1 + a2) `is` usd 0 + (a1 + a3) `is` usd 0 + (a2 + a3) `is` usd (-2.46) + (a3 + a3) `is` usd (-2.46) + sum [a1,a2,a3,-a3] `is` usd 0 -- highest precision is preserved - let ap1 = (dollars 1){commodity=dollar{precision=1}} - ap3 = (dollars 1){commodity=dollar{precision=3}} - (sum [ap1,ap3]) `is` ap3{quantity=2} - (sum [ap3,ap1]) `is` ap3{quantity=2} + let ap1 = setAmountPrecision 1 $ usd 1 + ap3 = setAmountPrecision 3 $ usd 1 + (asprecision $ astyle $ sum [ap1,ap3]) `is` 3 + (asprecision $ astyle $ sum [ap3,ap1]) `is` 3 -- adding different commodities assumes conversion rate 1 - assertBool "" $ isZeroAmount (a1 - euros 1.23) + assertBool "" $ isZeroAmount (a1 - eur 1.23) ,"showAmount" ~: do - showAmount (dollars 0 + pounds 0) `is` "0" + showAmount (usd 0 + gbp 0) `is` "0" -- MixedAmount ,"normaliseMixedAmountPreservingFirstPrice" ~: do normaliseMixedAmountPreservingFirstPrice (Mixed []) `is` Mixed [nullamt] - assertBool "" $ isZeroMixedAmount $ normaliseMixedAmountPreservingFirstPrice (Mixed [Amount {commodity=dollar, quantity=10, price=Nothing} - ,Amount {commodity=dollar, quantity=10, price=Just (TotalPrice (Mixed [Amount {commodity=euro, quantity=7, price=Nothing}]))} - ,Amount {commodity=dollar, quantity=(-10), price=Nothing} - ,Amount {commodity=dollar, quantity=(-10), price=Just (TotalPrice (Mixed [Amount {commodity=euro, quantity=7, price=Nothing}]))} - ]) + assertBool "" $ isZeroMixedAmount $ normaliseMixedAmountPreservingFirstPrice + (Mixed [usd 10 + ,usd 10 @@ eur 7 + ,usd (-10) + ,usd (-10) @@ eur 7 + ]) ,"adding mixed amounts" ~: do - let dollar0 = dollar{precision=0} (sum $ map (Mixed . (\a -> [a])) - [Amount dollar 1.25 Nothing, - Amount dollar0 (-1) Nothing, - Amount dollar (-0.25) Nothing]) - `is` Mixed [Amount unknown 0 Nothing] - + [usd 1.25 + ,setAmountPrecision 0 $ usd (-1) + ,usd (-0.25) + ]) + `is` Mixed [amount{aquantity=0}] + ,"adding mixed amounts with total prices" ~: do (sum $ map (Mixed . (\a -> [a])) - [Amount dollar 1 (Just $ TotalPrice $ Mixed [euros 1]) - ,Amount dollar (-2) (Just $ TotalPrice $ Mixed [euros 1]) + [usd 1 @@ eur 1 + ,usd (-2) @@ eur 1 ]) - `is` (Mixed [Amount dollar 1 (Just $ TotalPrice $ Mixed [euros 1]) - ,Amount dollar (-2) (Just $ TotalPrice $ Mixed [euros 1]) + `is` (Mixed [usd 1 @@ eur 1 + ,usd (-2) @@ eur 1 ]) ,"showMixedAmount" ~: do - showMixedAmount (Mixed [dollars 1]) `is` "$1.00" - showMixedAmount (Mixed [(dollars 1){price=Just $ UnitPrice $ Mixed [euros 2]}]) `is` "$1.00 @ €2.00" - showMixedAmount (Mixed [dollars 0]) `is` "0" + showMixedAmount (Mixed [usd 1]) `is` "$1.00" + showMixedAmount (Mixed [usd 1 `at` eur 2]) `is` "$1.00 @ €2.00" + showMixedAmount (Mixed [usd 0]) `is` "0" showMixedAmount (Mixed []) `is` "0" showMixedAmount missingmixedamt `is` "" ,"showMixedAmountWithoutPrice" ~: do - let a = (dollars 1){price=Just $ UnitPrice $ Mixed [euros 2]} + let a = usd 1 `at` eur 2 showMixedAmountWithoutPrice (Mixed [a]) `is` "$1.00" showMixedAmountWithoutPrice (Mixed [a, (-a)]) `is` "0" diff --git a/hledger-lib/Hledger/Data/Commodity.hs b/hledger-lib/Hledger/Data/Commodity.hs index 3f807557e..557dda4e6 100644 --- a/hledger-lib/Hledger/Data/Commodity.hs +++ b/hledger-lib/Hledger/Data/Commodity.hs @@ -9,10 +9,9 @@ are thousands separated by comma, significant decimal places and so on. module Hledger.Data.Commodity where import Data.List -import Data.Map ((!)) -import Data.Maybe +import Data.Maybe (fromMaybe) import Test.HUnit -import qualified Data.Map as Map +-- import qualified Data.Map as M import Hledger.Data.Types import Hledger.Utils @@ -24,44 +23,47 @@ nonsimplecommoditychars = "0123456789-.@;\n \"{}" :: String quoteCommoditySymbolIfNeeded s | any (`elem` nonsimplecommoditychars) s = "\"" ++ s ++ "\"" | otherwise = s --- convenient amount and commodity constructors, for tests etc. +commodity = "" -unknown = Commodity {symbol="", side=L,spaced=False,decimalpoint='.',precision=0,separator=',',separatorpositions=[]} -dollar = Commodity {symbol="$",side=L,spaced=False,decimalpoint='.',precision=2,separator=',',separatorpositions=[]} -euro = Commodity {symbol="€",side=L,spaced=False,decimalpoint='.',precision=2,separator=',',separatorpositions=[]} -pound = Commodity {symbol="£",side=L,spaced=False,decimalpoint='.',precision=2,separator=',',separatorpositions=[]} -hour = Commodity {symbol="h",side=R,spaced=False,decimalpoint='.',precision=1,separator=',',separatorpositions=[]} +-- handy constructors for tests +-- unknown = commodity +-- usd = "$" +-- eur = "€" +-- gbp = "£" +-- hour = "h" -dollars n = Amount dollar n Nothing -euros n = Amount euro n Nothing -pounds n = Amount pound n Nothing -hours n = Amount hour n Nothing +-- Some sample commodity' names and symbols, for use in tests.. +commoditysymbols = + [("unknown","") + ,("usd","$") + ,("eur","€") + ,("gbp","£") + ,("hour","h") + ] -defaultcommodities = [dollar, euro, pound, hour, unknown] - --- | Look up one of the hard-coded default commodities. For use in tests. +-- | Look up one of the sample commodities' symbol by name. comm :: String -> Commodity -comm sym = fromMaybe +comm name = snd $ fromMaybe (error' "commodity lookup failed") - $ find (\(Commodity{symbol=s}) -> s==sym) defaultcommodities + (find (\n -> fst n == name) commoditysymbols) -- | Find the conversion rate between two commodities. Currently returns 1. conversionRate :: Commodity -> Commodity -> Double conversionRate _ _ = 1 --- | Convert a list of commodities to a map from commodity symbols to --- unique, display-preference-canonicalised commodities. -canonicaliseCommodities :: [Commodity] -> Map.Map String Commodity -canonicaliseCommodities cs = - Map.fromList [(s,firstc{precision=maxp}) | s <- symbols, - let cs = commoditymap ! s, - let firstc = head cs, - let maxp = maximum $ map precision cs - ] - where - commoditymap = Map.fromList [(s, commoditieswithsymbol s) | s <- symbols] - commoditieswithsymbol s = filter ((s==) . symbol) cs - symbols = nub $ map symbol cs +-- -- | Convert a list of commodities to a map from commodity symbols to +-- -- unique, display-preference-canonicalised commodities. +-- canonicaliseCommodities :: [Commodity] -> Map.Map String Commodity +-- canonicaliseCommodities cs = +-- Map.fromList [(s,firstc{precision=maxp}) | s <- symbols, +-- let cs = commoditymap ! s, +-- let firstc = head cs, +-- let maxp = maximum $ map precision cs +-- ] +-- where +-- commoditymap = Map.fromList [(s, commoditieswithsymbol s) | s <- symbols] +-- commoditieswithsymbol s = filter ((s==) . symbol) cs +-- symbols = nub $ map symbol cs tests_Hledger_Data_Commodity = TestList [ ] diff --git a/hledger-lib/Hledger/Data/Journal.hs b/hledger-lib/Hledger/Data/Journal.hs index 98b1d5551..dd3d51e04 100644 --- a/hledger-lib/Hledger/Data/Journal.hs +++ b/hledger-lib/Hledger/Data/Journal.hs @@ -25,9 +25,9 @@ module Hledger.Data.Journal ( -- * Querying journalAccountNames, journalAccountNamesUsed, - journalAmountAndPriceCommodities, + -- journalAmountAndPriceCommodities, journalAmounts, - journalCanonicalCommodities, + -- journalCanonicalCommodities, journalDateSpan, journalFilePath, journalFilePaths, @@ -51,7 +51,7 @@ module Hledger.Data.Journal ( ) where import Data.List -import Data.Map (findWithDefault) +-- import Data.Map (findWithDefault) import Data.Ord import Data.Time.Calendar import Data.Time.LocalTime @@ -60,13 +60,13 @@ import Safe (headDef) import System.Time (ClockTime(TOD)) import Test.HUnit import Text.Printf -import qualified Data.Map as Map +import qualified Data.Map as M import Hledger.Utils import Hledger.Data.Types import Hledger.Data.AccountName import Hledger.Data.Amount -import Hledger.Data.Commodity +-- import Hledger.Data.Commodity import Hledger.Data.Dates import Hledger.Data.Transaction import Hledger.Data.Posting @@ -75,13 +75,14 @@ import Hledger.Query instance Show Journal where - show j = printf "Journal %s with %d transactions, %d accounts: %s" + show j = printf "Journal %s with %d transactions, %d accounts: %s, commodity styles: %s" (journalFilePath j) (length (jtxns j) + length (jmodifiertxns j) + length (jperiodictxns j)) (length accounts) (show accounts) + (show $ jcommoditystyles j) -- ++ (show $ journalTransactions l) where accounts = flatten $ journalAccountNameTree j @@ -107,10 +108,11 @@ nulljournal = Journal { jmodifiertxns = [] , jContext = nullctx , files = [] , filereadtime = TOD 0 0 + , jcommoditystyles = M.fromList [] } nullctx :: JournalContext -nullctx = Ctx { ctxYear = Nothing, ctxCommodity = Nothing, ctxAccount = [], ctxAliases = [] } +nullctx = Ctx { ctxYear = Nothing, ctxCommodityAndStyle = Nothing, ctxAccount = [], ctxAliases = [] } journalFilePath :: Journal -> FilePath journalFilePath = fst . mainfile @@ -369,24 +371,28 @@ journalFinalise tclock tlocal path txt ctx j@Journal{files=fs} = -- amounts and working out the canonical commodities, since balancing -- depends on display precision. Reports only the first error encountered. journalBalanceTransactions :: Journal -> Either String Journal -journalBalanceTransactions j@Journal{jtxns=ts} = +journalBalanceTransactions j@Journal{jtxns=ts, jcommoditystyles=ss} = case sequence $ map balance ts of Right ts' -> Right j{jtxns=ts'} Left e -> Left e - where balance = balanceTransaction (Just $ journalCanonicalCommodities j) + where balance = balanceTransaction (Just ss) -- | Convert all the journal's posting amounts (not price amounts) to -- their canonical display settings. Ie, all amounts in a given -- commodity will use (a) the display settings of the first, and (b) -- the greatest precision, of the posting amounts in that commodity. journalCanonicaliseAmounts :: Journal -> Journal -journalCanonicaliseAmounts j@Journal{jtxns=ts} = j{jtxns=map fixtransaction ts} +journalCanonicaliseAmounts j@Journal{jtxns=ts} = j'' where + j'' = j'{jtxns=map fixtransaction ts} + j' = j{jcommoditystyles = canonicalStyles $ journalAmounts j} fixtransaction t@Transaction{tpostings=ps} = t{tpostings=map fixposting ps} fixposting p@Posting{pamount=a} = p{pamount=fixmixedamount a} fixmixedamount (Mixed as) = Mixed $ map fixamount as - fixamount a@Amount{commodity=c} = a{commodity=fixcommodity c} - fixcommodity c@Commodity{symbol=s} = findWithDefault c s canonicalcommoditymap - canonicalcommoditymap = journalCanonicalCommodities j + fixamount a@Amount{acommodity=c} = a{astyle=journalCommodityStyle j' c} + +-- | Get this journal's canonical amount style for the given commodity, or the null style. +journalCommodityStyle :: Journal -> Commodity -> AmountStyle +journalCommodityStyle j c = M.findWithDefault amountstyle c $ jcommoditystyles j -- -- | Apply this journal's historical price records to unpriced amounts where possible. -- journalApplyHistoricalPrices :: Journal -> Journal @@ -421,30 +427,34 @@ journalConvertAmountsToCost j@Journal{jtxns=ts} = j{jtxns=map fixtransaction ts} fixtransaction t@Transaction{tpostings=ps} = t{tpostings=map fixposting ps} fixposting p@Posting{pamount=a} = p{pamount=fixmixedamount a} fixmixedamount (Mixed as) = Mixed $ map fixamount as - fixamount = canonicaliseAmountCommodity (Just $ journalCanonicalCommodities j) . costOfAmount + fixamount = canonicaliseAmount (jcommoditystyles j) . costOfAmount --- | Get this journal's unique, display-preference-canonicalised commodities, by symbol. -journalCanonicalCommodities :: Journal -> Map.Map String Commodity -journalCanonicalCommodities j = canonicaliseCommodities $ journalAmountCommodities j +-- -- | Get this journal's unique, display-preference-canonicalised commodities, by symbol. +-- journalCanonicalCommodities :: Journal -> M.Map String Commodity +-- journalCanonicalCommodities j = canonicaliseCommodities $ journalAmountCommodities j --- | Get all this journal's amounts' commodities, in the order parsed. -journalAmountCommodities :: Journal -> [Commodity] -journalAmountCommodities = map commodity . concatMap amounts . journalAmounts +-- -- | Get all this journal's amounts' commodities, in the order parsed. +-- journalAmountCommodities :: Journal -> [Commodity] +-- journalAmountCommodities = map acommodity . concatMap amounts . journalAmounts --- | Get all this journal's amount and price commodities, in the order parsed. -journalAmountAndPriceCommodities :: Journal -> [Commodity] -journalAmountAndPriceCommodities = concatMap amountCommodities . concatMap amounts . journalAmounts +-- -- | Get all this journal's amount and price commodities, in the order parsed. +-- journalAmountAndPriceCommodities :: Journal -> [Commodity] +-- journalAmountAndPriceCommodities = concatMap amountCommodities . concatMap amounts . journalAmounts --- | Get this amount's commodity and any commodities referenced in its price. -amountCommodities :: Amount -> [Commodity] -amountCommodities Amount{commodity=c,price=p} = - case p of Nothing -> [c] - Just (UnitPrice ma) -> c:(concatMap amountCommodities $ amounts ma) - Just (TotalPrice ma) -> c:(concatMap amountCommodities $ amounts ma) +-- -- | Get this amount's commodity and any commodities referenced in its price. +-- amountCommodities :: Amount -> [Commodity] +-- amountCommodities Amount{acommodity=c,aprice=p} = +-- case p of Nothing -> [c] +-- Just (UnitPrice ma) -> c:(concatMap amountCommodities $ amounts ma) +-- Just (TotalPrice ma) -> c:(concatMap amountCommodities $ amounts ma) --- | Get all this journal's amounts, in the order parsed. -journalAmounts :: Journal -> [MixedAmount] -journalAmounts = map pamount . journalPostings +-- | Get all this journal's (mixed) amounts, in the order parsed. +journalMixedAmounts :: Journal -> [MixedAmount] +journalMixedAmounts = map pamount . journalPostings + +-- | Get all this journal's component amounts, roughly in the order parsed. +journalAmounts :: Journal -> [Amount] +journalAmounts = concatMap flatten . journalMixedAmounts where flatten (Mixed as) = as -- | The (fully specified) date span containing this journal's transactions, -- or DateSpan Nothing Nothing if there are none. @@ -475,8 +485,8 @@ isnegativepat = (negateprefix `isPrefixOf`) abspat pat = if isnegativepat pat then drop (length negateprefix) pat else pat -- debug helpers --- traceAmountPrecision a = trace (show $ map (precision . commodity) $ amounts a) a --- tracePostingsCommodities ps = trace (show $ map ((map (precision . commodity) . amounts) . pamount) ps) ps +-- traceAmountPrecision a = trace (show $ map (precision . acommodity) $ amounts a) a +-- tracePostingsCommodities ps = trace (show $ map ((map (precision . acommodity) . amounts) . pamount) ps) ps -- tests @@ -503,10 +513,9 @@ abspat pat = if isnegativepat pat then drop (length negateprefix) pat else pat -- liabilities:debts $1 -- assets:bank:checking -- -Right samplejournal = journalBalanceTransactions $ Journal - [] - [] - [ +Right samplejournal = journalBalanceTransactions $ + nulljournal + {jtxns = [ txnTieKnot $ Transaction { tdate=parsedate "2008/01/01", teffectivedate=Nothing, @@ -519,7 +528,7 @@ Right samplejournal = journalBalanceTransactions $ Journal Posting { pstatus=False, paccount="assets:bank:checking", - pamount=(Mixed [dollars 1]), + pamount=(Mixed [usd 1]), pcomment="", ptype=RegularPosting, ptags=[], @@ -550,7 +559,7 @@ Right samplejournal = journalBalanceTransactions $ Journal Posting { pstatus=False, paccount="assets:bank:checking", - pamount=(Mixed [dollars 1]), + pamount=(Mixed [usd 1]), pcomment="", ptype=RegularPosting, ptags=[], @@ -581,7 +590,7 @@ Right samplejournal = journalBalanceTransactions $ Journal Posting { pstatus=False, paccount="assets:bank:saving", - pamount=(Mixed [dollars 1]), + pamount=(Mixed [usd 1]), pcomment="", ptype=RegularPosting, ptags=[], @@ -590,7 +599,7 @@ Right samplejournal = journalBalanceTransactions $ Journal Posting { pstatus=False, paccount="assets:bank:checking", - pamount=(Mixed [dollars (-1)]), + pamount=(Mixed [usd (-1)]), pcomment="", ptype=RegularPosting, ptags=[], @@ -612,7 +621,7 @@ Right samplejournal = journalBalanceTransactions $ Journal Posting { pstatus=False, paccount="expenses:food", - pamount=(Mixed [dollars 1]), + pamount=(Mixed [usd 1]), pcomment="", ptype=RegularPosting, ptags=[], @@ -621,7 +630,7 @@ Right samplejournal = journalBalanceTransactions $ Journal Posting { pstatus=False, paccount="expenses:supplies", - pamount=(Mixed [dollars 1]), + pamount=(Mixed [usd 1]), pcomment="", ptype=RegularPosting, ptags=[], @@ -652,7 +661,7 @@ Right samplejournal = journalBalanceTransactions $ Journal Posting { pstatus=False, paccount="liabilities:debts", - pamount=(Mixed [dollars 1]), + pamount=(Mixed [usd 1]), pcomment="", ptype=RegularPosting, ptags=[], @@ -661,7 +670,7 @@ Right samplejournal = journalBalanceTransactions $ Journal Posting { pstatus=False, paccount="assets:bank:checking", - pamount=(Mixed [dollars (-1)]), + pamount=(Mixed [usd (-1)]), pcomment="", ptype=RegularPosting, ptags=[], @@ -671,12 +680,7 @@ Right samplejournal = journalBalanceTransactions $ Journal tpreceding_comment_lines="" } ] - [] - [] - "" - nullctx - [] - (TOD 0 0) + } tests_Hledger_Data_Journal = TestList $ [ diff --git a/hledger-lib/Hledger/Data/Ledger.hs b/hledger-lib/Hledger/Data/Ledger.hs index 069c570da..24bf175ba 100644 --- a/hledger-lib/Hledger/Data/Ledger.hs +++ b/hledger-lib/Hledger/Data/Ledger.hs @@ -82,9 +82,9 @@ ledgerPostings = journalPostings . ljournal ledgerDateSpan :: Ledger -> DateSpan ledgerDateSpan = postingsDateSpan . ledgerPostings --- | All commodities used in this ledger, as a map keyed by symbol. -ledgerCommodities :: Ledger -> M.Map String Commodity -ledgerCommodities = journalCanonicalCommodities . ljournal +-- | All commodities used in this ledger. +ledgerCommodities :: Ledger -> [Commodity] +ledgerCommodities = M.keys . jcommoditystyles . ljournal tests_ledgerFromJournal = [ diff --git a/hledger-lib/Hledger/Data/TimeLog.hs b/hledger-lib/Hledger/Data/TimeLog.hs index cc2dd8121..c06e8fdb8 100644 --- a/hledger-lib/Hledger/Data/TimeLog.hs +++ b/hledger-lib/Hledger/Data/TimeLog.hs @@ -20,7 +20,7 @@ import Text.Printf import Hledger.Utils import Hledger.Data.Types import Hledger.Data.Dates -import Hledger.Data.Commodity +import Hledger.Data.Amount import Hledger.Data.Transaction instance Show TimeLogEntry where @@ -92,8 +92,8 @@ entryFromTimeLogInOut i o itod = localTimeOfDay itime otod = localTimeOfDay otime idate = localDay itime - hrs = elapsedSeconds (toutc otime) (toutc itime) / 3600 where toutc = localTimeToUTC utc - amount = Mixed [hours hrs] + hours = elapsedSeconds (toutc otime) (toutc itime) / 3600 where toutc = localTimeToUTC utc + amount = Mixed [hrs hours] ps = [Posting{pstatus=False,paccount=acctname,pamount=amount, pcomment="",ptype=VirtualPosting,ptags=[],ptransaction=Just t}] diff --git a/hledger-lib/Hledger/Data/Transaction.hs b/hledger-lib/Hledger/Data/Transaction.hs index eba5f8033..f6914d3a5 100644 --- a/hledger-lib/Hledger/Data/Transaction.hs +++ b/hledger-lib/Hledger/Data/Transaction.hs @@ -47,7 +47,6 @@ import Hledger.Data.Types import Hledger.Data.Dates import Hledger.Data.Posting import Hledger.Data.Amount -import Hledger.Data.Commodity instance Show Transaction where show = showTransactionUnelided @@ -108,7 +107,7 @@ tests_showTransactionUnelided = [ nullposting{ pstatus=True, paccount="a", - pamount=Mixed [dollars 1, hours 2], + pamount=Mixed [usd 1, hrs 2], pcomment="pcomment1\npcomment2\n", ptype=RegularPosting, ptags=[("ptag1","val1"),("ptag2","val2")] @@ -183,7 +182,7 @@ tests_postingAsLines = [ nullposting{ pstatus=True, paccount="a", - pamount=Mixed [dollars 1, hours 2], + pamount=Mixed [usd 1, hrs 2], pcomment="pcomment1\npcomment2\n", ptype=RegularPosting, ptags=[("ptag1","val1"),("ptag2","val2")] @@ -236,14 +235,15 @@ transactionPostingBalances t = (sumPostings $ realPostings t -- | Is this transaction balanced ? A balanced transaction's real -- (non-virtual) postings sum to 0, and any balanced virtual postings -- also sum to 0. -isTransactionBalanced :: Maybe (Map.Map String Commodity) -> Transaction -> Bool -isTransactionBalanced canonicalcommoditymap t = +isTransactionBalanced :: Maybe (Map.Map Commodity AmountStyle) -> Transaction -> Bool +isTransactionBalanced styles t = -- isReallyZeroMixedAmountCost rsum && isReallyZeroMixedAmountCost bvsum isZeroMixedAmount rsum' && isZeroMixedAmount bvsum' where (rsum, _, bvsum) = transactionPostingBalances t - rsum' = canonicaliseMixedAmountCommodity canonicalcommoditymap $ costOfMixedAmount rsum - bvsum' = canonicaliseMixedAmountCommodity canonicalcommoditymap $ costOfMixedAmount bvsum + rsum' = canonicalise $ costOfMixedAmount rsum + bvsum' = canonicalise $ costOfMixedAmount bvsum + canonicalise = maybe id canonicaliseMixedAmount styles -- | Ensure this transaction is balanced, possibly inferring a missing -- amount or conversion price, or return an error message. @@ -260,11 +260,11 @@ isTransactionBalanced canonicalcommoditymap t = -- and the sum of real postings' amounts is exactly two -- non-explicitly-priced amounts in different commodities (likewise -- for balanced virtual postings). -balanceTransaction :: Maybe (Map.Map String Commodity) -> Transaction -> Either String Transaction -balanceTransaction canonicalcommoditymap t@Transaction{tpostings=ps} +balanceTransaction :: Maybe (Map.Map Commodity AmountStyle) -> Transaction -> Either String Transaction +balanceTransaction styles t@Transaction{tpostings=ps} | length rwithoutamounts > 1 || length bvwithoutamounts > 1 = Left $ printerr "could not balance this transaction (too many missing amounts)" - | not $ isTransactionBalanced canonicalcommoditymap t''' = Left $ printerr $ nonzerobalanceerror t''' + | not $ isTransactionBalanced styles t''' = Left $ printerr $ nonzerobalanceerror t''' | otherwise = Right t''' where -- maybe infer missing amounts @@ -281,53 +281,53 @@ balanceTransaction canonicalcommoditymap t@Transaction{tpostings=ps} -- maybe infer conversion prices, for real postings rmixedamountsinorder = map pamount $ realPostings t' ramountsinorder = concatMap amounts rmixedamountsinorder - rcommoditiesinorder = map commodity ramountsinorder + rcommoditiesinorder = map acommodity ramountsinorder rsumamounts = amounts $ sum rmixedamountsinorder -- assumption: the sum of mixed amounts is normalised (one simple amount per commodity) - t'' = if length rsumamounts == 2 && all (isNothing.price) rsumamounts && t'==t + t'' = if length rsumamounts == 2 && all (isNothing.aprice) rsumamounts && t'==t then t'{tpostings=map inferprice ps} else t' where -- assumption: a posting's mixed amount contains one simple amount - inferprice p@Posting{pamount=Mixed [a@Amount{commodity=c,price=Nothing}], ptype=RegularPosting} - = p{pamount=Mixed [a{price=conversionprice c}]} + inferprice p@Posting{pamount=Mixed [a@Amount{acommodity=c,aprice=Nothing}], ptype=RegularPosting} + = p{pamount=Mixed [a{aprice=conversionprice c}]} where conversionprice c | c == unpricedcommodity -- assign a balancing price. Use @@ for more exact output when possible. -- invariant: prices should always be positive. Enforced with "abs" = if length ramountsinunpricedcommodity == 1 then Just $ TotalPrice $ Mixed [setAmountPrecision maxprecision $ abs $ targetcommodityamount] - else Just $ UnitPrice $ Mixed [setAmountPrecision maxprecision $ abs $ targetcommodityamount `divideAmount` (quantity unpricedamount)] + else Just $ UnitPrice $ Mixed [setAmountPrecision maxprecision $ abs $ targetcommodityamount `divideAmount` (aquantity unpricedamount)] | otherwise = Nothing where - unpricedcommodity = head $ filter (`elem` (map commodity rsumamounts)) rcommoditiesinorder - unpricedamount = head $ filter ((==unpricedcommodity).commodity) rsumamounts - targetcommodityamount = head $ filter ((/=unpricedcommodity).commodity) rsumamounts - ramountsinunpricedcommodity = filter ((==unpricedcommodity).commodity) ramountsinorder + unpricedcommodity = head $ filter (`elem` (map acommodity rsumamounts)) rcommoditiesinorder + unpricedamount = head $ filter ((==unpricedcommodity).acommodity) rsumamounts + targetcommodityamount = head $ filter ((/=unpricedcommodity).acommodity) rsumamounts + ramountsinunpricedcommodity = filter ((==unpricedcommodity).acommodity) ramountsinorder inferprice p = p -- maybe infer prices for balanced virtual postings. Just duplicates the above for now. bvmixedamountsinorder = map pamount $ balancedVirtualPostings t'' bvamountsinorder = concatMap amounts bvmixedamountsinorder - bvcommoditiesinorder = map commodity bvamountsinorder + bvcommoditiesinorder = map acommodity bvamountsinorder bvsumamounts = amounts $ sum bvmixedamountsinorder - t''' = if length bvsumamounts == 2 && all (isNothing.price) bvsumamounts && t'==t -- XXX could check specifically for bv amount inferring + t''' = if length bvsumamounts == 2 && all (isNothing.aprice) bvsumamounts && t'==t -- XXX could check specifically for bv amount inferring then t''{tpostings=map inferprice ps} else t'' where - inferprice p@Posting{pamount=Mixed [a@Amount{commodity=c,price=Nothing}], ptype=BalancedVirtualPosting} - = p{pamount=Mixed [a{price=conversionprice c}]} + inferprice p@Posting{pamount=Mixed [a@Amount{acommodity=c,aprice=Nothing}], ptype=BalancedVirtualPosting} + = p{pamount=Mixed [a{aprice=conversionprice c}]} where conversionprice c | c == unpricedcommodity = if length bvamountsinunpricedcommodity == 1 then Just $ TotalPrice $ Mixed [setAmountPrecision maxprecision $ abs $ targetcommodityamount] - else Just $ UnitPrice $ Mixed [setAmountPrecision maxprecision $ abs $ targetcommodityamount `divideAmount` (quantity unpricedamount)] + else Just $ UnitPrice $ Mixed [setAmountPrecision maxprecision $ abs $ targetcommodityamount `divideAmount` (aquantity unpricedamount)] | otherwise = Nothing where - unpricedcommodity = head $ filter (`elem` (map commodity bvsumamounts)) bvcommoditiesinorder - unpricedamount = head $ filter ((==unpricedcommodity).commodity) bvsumamounts - targetcommodityamount = head $ filter ((/=unpricedcommodity).commodity) bvsumamounts - bvamountsinunpricedcommodity = filter ((==unpricedcommodity).commodity) bvamountsinorder + unpricedcommodity = head $ filter (`elem` (map acommodity bvsumamounts)) bvcommoditiesinorder + unpricedamount = head $ filter ((==unpricedcommodity).acommodity) bvsumamounts + targetcommodityamount = head $ filter ((/=unpricedcommodity).acommodity) bvsumamounts + bvamountsinunpricedcommodity = filter ((==unpricedcommodity).acommodity) bvamountsinorder inferprice p = p printerr s = intercalate "\n" [s, showTransactionUnelided t] @@ -376,8 +376,8 @@ tests_Hledger_Data_Transaction = TestList $ concat [ ,"" ]) (let t = Transaction (parsedate "2007/01/28") Nothing False "" "coopportunity" "" [] - [Posting False "expenses:food:groceries" (Mixed [dollars 47.18]) "" RegularPosting [] (Just t) - ,Posting False "assets:checking" (Mixed [dollars (-47.18)]) "" RegularPosting [] (Just t) + [Posting False "expenses:food:groceries" (Mixed [usd 47.18]) "" RegularPosting [] (Just t) + ,Posting False "assets:checking" (Mixed [usd (-47.18)]) "" RegularPosting [] (Just t) ] "" in showTransaction t) @@ -390,8 +390,8 @@ tests_Hledger_Data_Transaction = TestList $ concat [ ,"" ]) (let t = Transaction (parsedate "2007/01/28") Nothing False "" "coopportunity" "" [] - [Posting False "expenses:food:groceries" (Mixed [dollars 47.18]) "" RegularPosting [] (Just t) - ,Posting False "assets:checking" (Mixed [dollars (-47.18)]) "" RegularPosting [] (Just t) + [Posting False "expenses:food:groceries" (Mixed [usd 47.18]) "" RegularPosting [] (Just t) + ,Posting False "assets:checking" (Mixed [usd (-47.18)]) "" RegularPosting [] (Just t) ] "" in showTransactionUnelided t) @@ -406,8 +406,8 @@ tests_Hledger_Data_Transaction = TestList $ concat [ ]) (showTransaction (txnTieKnot $ Transaction (parsedate "2007/01/28") Nothing False "" "coopportunity" "" [] - [Posting False "expenses:food:groceries" (Mixed [dollars 47.18]) "" RegularPosting [] Nothing - ,Posting False "assets:checking" (Mixed [dollars (-47.19)]) "" RegularPosting [] Nothing + [Posting False "expenses:food:groceries" (Mixed [usd 47.18]) "" RegularPosting [] Nothing + ,Posting False "assets:checking" (Mixed [usd (-47.19)]) "" RegularPosting [] Nothing ] "")) ,"showTransaction" ~: do @@ -419,7 +419,7 @@ tests_Hledger_Data_Transaction = TestList $ concat [ ]) (showTransaction (txnTieKnot $ Transaction (parsedate "2007/01/28") Nothing False "" "coopportunity" "" [] - [Posting False "expenses:food:groceries" (Mixed [dollars 47.18]) "" RegularPosting [] Nothing + [Posting False "expenses:food:groceries" (Mixed [usd 47.18]) "" RegularPosting [] Nothing ] "")) ,"showTransaction" ~: do @@ -444,7 +444,7 @@ tests_Hledger_Data_Transaction = TestList $ concat [ ]) (showTransaction (txnTieKnot $ Transaction (parsedate "2010/01/01") Nothing False "" "x" "" [] - [Posting False "a" (Mixed [Amount unknown 1 (Just $ UnitPrice $ Mixed [Amount dollar{precision=0} 2 Nothing])]) "" RegularPosting [] Nothing + [Posting False "a" (Mixed [amt 1 `at` (setAmountPrecision 0 $ usd 2)]) "" RegularPosting [] Nothing ,Posting False "b" missingmixedamt "" RegularPosting [] Nothing ] "")) @@ -452,8 +452,8 @@ tests_Hledger_Data_Transaction = TestList $ concat [ assertBool "detect unbalanced entry, sign error" (isLeft $ balanceTransaction Nothing (Transaction (parsedate "2007/01/28") Nothing False "" "test" "" [] - [Posting False "a" (Mixed [dollars 1]) "" RegularPosting [] Nothing, - Posting False "b" (Mixed [dollars 1]) "" RegularPosting [] Nothing + [Posting False "a" (Mixed [usd 1]) "" RegularPosting [] Nothing, + Posting False "b" (Mixed [usd 1]) "" RegularPosting [] Nothing ] "")) assertBool "detect unbalanced entry, multiple missing amounts" @@ -464,79 +464,75 @@ tests_Hledger_Data_Transaction = TestList $ concat [ ] "")) let e = balanceTransaction Nothing (Transaction (parsedate "2007/01/28") Nothing False "" "" "" [] - [Posting False "a" (Mixed [dollars 1]) "" RegularPosting [] Nothing, + [Posting False "a" (Mixed [usd 1]) "" RegularPosting [] Nothing, Posting False "b" missingmixedamt "" RegularPosting [] Nothing ] "") assertBool "balanceTransaction allows one missing amount" (isRight e) assertEqual "balancing amount is inferred" - (Mixed [dollars (-1)]) + (Mixed [usd (-1)]) (case e of Right e' -> (pamount $ last $ tpostings e') Left _ -> error' "should not happen") let e = balanceTransaction Nothing (Transaction (parsedate "2011/01/01") Nothing False "" "" "" [] - [Posting False "a" (Mixed [dollars 1.35]) "" RegularPosting [] Nothing, - Posting False "b" (Mixed [euros (-1)]) "" RegularPosting [] Nothing + [Posting False "a" (Mixed [usd 1.35]) "" RegularPosting [] Nothing, + Posting False "b" (Mixed [eur (-1)]) "" RegularPosting [] Nothing ] "") assertBool "balanceTransaction can infer conversion price" (isRight e) assertEqual "balancing conversion price is inferred" - (Mixed [Amount{commodity=dollar{precision=2}, - quantity=1.35, - price=(Just $ TotalPrice $ Mixed [Amount{commodity=euro{precision=maxprecision}, - quantity=1, - price=Nothing}])}]) + (Mixed [usd 1.35 @@ (setAmountPrecision maxprecision $ eur 1)]) (case e of Right e' -> (pamount $ head $ tpostings e') Left _ -> error' "should not happen") assertBool "balanceTransaction balances based on cost if there are unit prices" (isRight $ balanceTransaction Nothing (Transaction (parsedate "2011/01/01") Nothing False "" "" "" [] - [Posting False "a" (Mixed [Amount dollar 1 (Just $ UnitPrice $ Mixed [euros 2])]) "" RegularPosting [] Nothing - ,Posting False "a" (Mixed [Amount dollar (-2) (Just $ UnitPrice $ Mixed [euros 1])]) "" RegularPosting [] Nothing + [Posting False "a" (Mixed [usd 1 `at` eur 2]) "" RegularPosting [] Nothing + ,Posting False "a" (Mixed [usd (-2) `at` eur 1]) "" RegularPosting [] Nothing ] "")) assertBool "balanceTransaction balances based on cost if there are total prices" (isRight $ balanceTransaction Nothing (Transaction (parsedate "2011/01/01") Nothing False "" "" "" [] - [Posting False "a" (Mixed [Amount dollar 1 (Just $ TotalPrice $ Mixed [euros 1])]) "" RegularPosting [] Nothing - ,Posting False "a" (Mixed [Amount dollar (-2) (Just $ TotalPrice $ Mixed [euros 1])]) "" RegularPosting [] Nothing + [Posting False "a" (Mixed [usd 1 @@ eur 1]) "" RegularPosting [] Nothing + ,Posting False "a" (Mixed [usd (-2) @@ eur 1]) "" RegularPosting [] Nothing ] "")) ,"isTransactionBalanced" ~: do let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" "" [] - [Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting [] (Just t) - ,Posting False "c" (Mixed [dollars (-1.00)]) "" RegularPosting [] (Just t) + [Posting False "b" (Mixed [usd 1.00]) "" RegularPosting [] (Just t) + ,Posting False "c" (Mixed [usd (-1.00)]) "" RegularPosting [] (Just t) ] "" assertBool "detect balanced" (isTransactionBalanced Nothing t) let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" "" [] - [Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting [] (Just t) - ,Posting False "c" (Mixed [dollars (-1.01)]) "" RegularPosting [] (Just t) + [Posting False "b" (Mixed [usd 1.00]) "" RegularPosting [] (Just t) + ,Posting False "c" (Mixed [usd (-1.01)]) "" RegularPosting [] (Just t) ] "" assertBool "detect unbalanced" (not $ isTransactionBalanced Nothing t) let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" "" [] - [Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting [] (Just t) + [Posting False "b" (Mixed [usd 1.00]) "" RegularPosting [] (Just t) ] "" assertBool "detect unbalanced, one posting" (not $ isTransactionBalanced Nothing t) let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" "" [] - [Posting False "b" (Mixed [dollars 0]) "" RegularPosting [] (Just t) + [Posting False "b" (Mixed [usd 0]) "" RegularPosting [] (Just t) ] "" assertBool "one zero posting is considered balanced for now" (isTransactionBalanced Nothing t) let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" "" [] - [Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting [] (Just t) - ,Posting False "c" (Mixed [dollars (-1.00)]) "" RegularPosting [] (Just t) - ,Posting False "d" (Mixed [dollars 100]) "" VirtualPosting [] (Just t) + [Posting False "b" (Mixed [usd 1.00]) "" RegularPosting [] (Just t) + ,Posting False "c" (Mixed [usd (-1.00)]) "" RegularPosting [] (Just t) + ,Posting False "d" (Mixed [usd 100]) "" VirtualPosting [] (Just t) ] "" assertBool "virtual postings don't need to balance" (isTransactionBalanced Nothing t) let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" "" [] - [Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting [] (Just t) - ,Posting False "c" (Mixed [dollars (-1.00)]) "" RegularPosting [] (Just t) - ,Posting False "d" (Mixed [dollars 100]) "" BalancedVirtualPosting [] (Just t) + [Posting False "b" (Mixed [usd 1.00]) "" RegularPosting [] (Just t) + ,Posting False "c" (Mixed [usd (-1.00)]) "" RegularPosting [] (Just t) + ,Posting False "d" (Mixed [usd 100]) "" BalancedVirtualPosting [] (Just t) ] "" assertBool "balanced virtual postings need to balance among themselves" (not $ isTransactionBalanced Nothing t) let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" "" [] - [Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting [] (Just t) - ,Posting False "c" (Mixed [dollars (-1.00)]) "" RegularPosting [] (Just t) - ,Posting False "d" (Mixed [dollars 100]) "" BalancedVirtualPosting [] (Just t) - ,Posting False "e" (Mixed [dollars (-100)]) "" BalancedVirtualPosting [] (Just t) + [Posting False "b" (Mixed [usd 1.00]) "" RegularPosting [] (Just t) + ,Posting False "c" (Mixed [usd (-1.00)]) "" RegularPosting [] (Just t) + ,Posting False "d" (Mixed [usd 100]) "" BalancedVirtualPosting [] (Just t) + ,Posting False "e" (Mixed [usd (-100)]) "" BalancedVirtualPosting [] (Just t) ] "" assertBool "balanced virtual postings need to balance among themselves (2)" (isTransactionBalanced Nothing t) diff --git a/hledger-lib/Hledger/Data/Types.hs b/hledger-lib/Hledger/Data/Types.hs index 0716fbc31..b7cc0a4e5 100644 --- a/hledger-lib/Hledger/Data/Types.hs +++ b/hledger-lib/Hledger/Data/Types.hs @@ -20,6 +20,7 @@ For more detailed documentation on each type, see the corresponding modules. module Hledger.Data.Types where import Control.Monad.Error (ErrorT) +import qualified Data.Map as M import Data.Time.Calendar import Data.Time.LocalTime import Data.Typeable @@ -42,31 +43,32 @@ type AccountName = String data Side = L | R deriving (Eq,Show,Read,Ord) -data Commodity = Commodity { - symbol :: String, -- ^ the commodity's symbol - -- display preferences for amounts of this commodity - side :: Side, -- ^ should the symbol appear on the left or the right - spaced :: Bool, -- ^ should there be a space between symbol and quantity - precision :: Int, -- ^ number of decimal places to display - -- XXX these three might be better belonging to Journal - decimalpoint :: Char, -- ^ character to use as decimal point - separator :: Char, -- ^ character to use for separating digit groups (eg thousands) - separatorpositions :: [Int] -- ^ positions of separators, counting leftward from decimal point - } deriving (Eq,Ord,Show,Read) - +type Commodity = String + type Quantity = Double -- | An amount's price in another commodity may be written as \@ unit -- price or \@\@ total price. Note although a MixedAmount is used, it -- should be in a single commodity, also the amount should be positive; -- these are not enforced currently. -data Price = UnitPrice MixedAmount | TotalPrice MixedAmount +data Price = {- NoPrice | -} UnitPrice MixedAmount | TotalPrice MixedAmount deriving (Eq,Ord) +-- | Display style for an amount. +data AmountStyle = AmountStyle { + ascommodityside :: Side, -- ^ does the symbol appear on the left or the right ? + ascommodityspaced :: Bool, -- ^ space between symbol and quantity ? + asprecision :: Int, -- ^ number of digits displayed after the decimal point + asdecimalpoint :: Char, -- ^ character used as decimal point + asseparator :: Char, -- ^ character used for separating digit groups (eg thousands) + asseparatorpositions :: [Int] -- ^ positions of digit group separators, counting leftward from decimal point +} deriving (Eq,Ord,Show,Read) + data Amount = Amount { - commodity :: Commodity, - quantity :: Quantity, - price :: Maybe Price -- ^ the price for this amount at posting time + acommodity :: Commodity, + aquantity :: Quantity, + aprice :: Maybe Price, -- ^ the price for this amount, fixed at posting time + astyle :: AmountStyle } deriving (Eq,Ord) newtype MixedAmount = Mixed [Amount] deriving (Eq,Ord) @@ -136,7 +138,7 @@ type Year = Integer -- is saved for later use by eg the add command. data JournalContext = Ctx { ctxYear :: !(Maybe Year) -- ^ the default year most recently specified with Y - , ctxCommodity :: !(Maybe Commodity) -- ^ the default commodity most recently specified with D + , ctxCommodityAndStyle :: !(Maybe (Commodity,AmountStyle)) -- ^ the default commodity and amount style most recently specified with D , ctxAccount :: ![AccountName] -- ^ the current stack of parent accounts/account name components -- specified with "account" directive(s). Concatenated, these -- are the account prefix prepended to parsed account names. @@ -155,7 +157,8 @@ data Journal = Journal { -- any included journal files. The main file is -- first followed by any included files in the -- order encountered (XXX reversed, cf journalAddFile). - filereadtime :: ClockTime -- ^ when this journal was last read from its file(s) + filereadtime :: ClockTime, -- ^ when this journal was last read from its file(s) + jcommoditystyles :: M.Map Commodity AmountStyle -- ^ how to display amounts in each commodity } deriving (Eq, Typeable) -- | A JournalUpdate is some transformation of a Journal. It can do I/O or @@ -239,6 +242,7 @@ data Account = Account { aname :: AccountName, -- ^ this account's full name aebalance :: MixedAmount, -- ^ this account's balance, excluding subaccounts asubs :: [Account], -- ^ sub-accounts + -- anumpostings :: Int -- ^ number of postings to this account -- derived from the above: aibalance :: MixedAmount, -- ^ this account's balance, including subaccounts aparent :: Maybe Account, -- ^ parent account diff --git a/hledger-lib/Hledger/Read.hs b/hledger-lib/Hledger/Read.hs index be030c37e..a2c95e86f 100644 --- a/hledger-lib/Hledger/Read.hs +++ b/hledger-lib/Hledger/Read.hs @@ -19,8 +19,8 @@ module Hledger.Read ( ensureJournalFileExists, -- * Parsers used elsewhere accountname, - amount, - amount', + amountp, + amountp', -- * Tests samplejournal, tests_Hledger_Read, diff --git a/hledger-lib/Hledger/Read/CsvReader.hs b/hledger-lib/Hledger/Read/CsvReader.hs index a1721975b..1d6068a5c 100644 --- a/hledger-lib/Hledger/Read/CsvReader.hs +++ b/hledger-lib/Hledger/Read/CsvReader.hs @@ -61,7 +61,7 @@ import Prelude hiding (getContents) import Hledger.Utils.UTF8IOCompat (getContents) import Hledger.Utils import Hledger.Data.FormatStrings as FormatStrings -import Hledger.Read.JournalReader (accountname, amount) +import Hledger.Read.JournalReader (accountname, amountp) reader :: Reader @@ -426,7 +426,7 @@ transactionFromCsvRecord rules fields = strnegate s = '-':s currency = maybe (fromMaybe "" $ baseCurrency rules) (atDef "" fields) (currencyField rules) amountstr'' = currency ++ amountstr' - amountparse = runParser amount nullctx "" amountstr'' + amountparse = runParser amountp nullctx "" amountstr'' a = either (const nullmixedamt) id amountparse -- Using costOfMixedAmount here to allow complex costs like "10 GBP @@ 15 USD". -- Aim is to have "10 GBP @@ 15 USD" applied to account "acct", but have "-15USD" applied to "baseacct" diff --git a/hledger-lib/Hledger/Read/JournalReader.hs b/hledger-lib/Hledger/Read/JournalReader.hs index ab582ed21..d76944c27 100644 --- a/hledger-lib/Hledger/Read/JournalReader.hs +++ b/hledger-lib/Hledger/Read/JournalReader.hs @@ -27,8 +27,8 @@ module Hledger.Read.JournalReader ( historicalpricedirective, datetime, accountname, - amount, - amount', + amountp, + amountp', emptyline, -- * Tests tests_Hledger_Read_JournalReader @@ -102,11 +102,11 @@ setYear y = updateState (\ctx -> ctx{ctxYear=Just y}) getYear :: GenParser tok JournalContext (Maybe Integer) getYear = liftM ctxYear getState -setCommodity :: Commodity -> GenParser tok JournalContext () -setCommodity c = updateState (\ctx -> ctx{ctxCommodity=Just c}) +setCommodityAndStyle :: (Commodity,AmountStyle) -> GenParser tok JournalContext () +setCommodityAndStyle cs = updateState (\ctx -> ctx{ctxCommodityAndStyle=Just cs}) -getCommodity :: GenParser tok JournalContext (Maybe Commodity) -getCommodity = liftM ctxCommodity getState +getCommodityAndStyle :: GenParser tok JournalContext (Maybe (Commodity,AmountStyle)) +getCommodityAndStyle = ctxCommodityAndStyle `fmap` getState pushParentAccount :: String -> GenParser tok JournalContext () pushParentAccount parent = updateState addParentAccount @@ -254,10 +254,11 @@ defaultcommoditydirective :: GenParser Char JournalContext JournalUpdate defaultcommoditydirective = do char 'D' "default commodity" many1 spacenonewline - a <- amount + a <- amountp -- amount always returns a MixedAmount containing one Amount, but let's be safe - let as = amounts a - when (not $ null as) $ setCommodity $ commodity $ head as + let as = amounts a + when (not $ null as) $ + let Amount{..} = head as in setCommodityAndStyle (acommodity, astyle) restofline return $ return id @@ -269,7 +270,7 @@ historicalpricedirective = do many1 spacenonewline symbol <- commoditysymbol many spacenonewline - price <- amount + price <- amountp restofline return $ HistoricalPrice date symbol price @@ -285,11 +286,11 @@ commodityconversiondirective :: GenParser Char JournalContext JournalUpdate commodityconversiondirective = do char 'C' "commodity conversion" many1 spacenonewline - amount + amountp many spacenonewline char '=' many spacenonewline - amount + amountp restofline return $ return id @@ -370,7 +371,7 @@ tests_transaction = [ nullposting{ pstatus=True, paccount="a", - pamount=Mixed [dollars 1], + pamount=Mixed [usd 1], pcomment="pcomment1\npcomment2\n", ptype=RegularPosting, ptags=[("ptag1","val1"),("ptag2","val2")], @@ -514,7 +515,7 @@ tests_posting = [ same ptransaction " expenses:food:dining $10.00 ; a: a a \n ; b: b b \n" `gives` - (Posting False "expenses:food:dining" (Mixed [dollars 10]) "" RegularPosting [("a","a a"), ("b","b b")] Nothing) + (Posting False "expenses:food:dining" (Mixed [usd 10]) "" RegularPosting [("a","a a"), ("b","b b")] Nothing) assertBool "posting parses a quoted commodity with numbers" (isRight $ parseWithCtx nullctx posting " a 1 \"DE123\"\n") @@ -558,12 +559,12 @@ spaceandamountormissing :: GenParser Char JournalContext MixedAmount spaceandamountormissing = try (do many1 spacenonewline - amount <|> return missingmixedamt + amountp <|> return missingmixedamt ) <|> return missingmixedamt tests_spaceandamountormissing = [ "spaceandamountormissing" ~: do - assertParseEqual (parseWithCtx nullctx spaceandamountormissing " $47.18") (Mixed [dollars 47.18]) + assertParseEqual (parseWithCtx nullctx spaceandamountormissing " $47.18") (Mixed [usd 47.18]) assertParseEqual (parseWithCtx nullctx spaceandamountormissing "$47.18") missingmixedamt assertParseEqual (parseWithCtx nullctx spaceandamountormissing " ") missingmixedamt assertParseEqual (parseWithCtx nullctx spaceandamountormissing "") missingmixedamt @@ -571,65 +572,58 @@ tests_spaceandamountormissing = [ -- | Parse an amount, optionally with a left or right currency symbol, -- price, and/or (ignored) ledger-style balance assertion. -amount :: GenParser Char JournalContext MixedAmount -amount = try leftsymbolamount <|> try rightsymbolamount <|> nosymbolamount +amountp :: GenParser Char JournalContext MixedAmount +amountp = try leftsymbolamount <|> try rightsymbolamount <|> nosymbolamount -tests_amount = [ - "amount" ~: do - assertParseEqual (parseWithCtx nullctx amount "$47.18") (Mixed [dollars 47.18]) - assertParseEqual (parseWithCtx nullctx amount "$1.") - (Mixed [Amount Commodity {symbol="$",side=L,spaced=False,decimalpoint='.',precision=0,separator=',',separatorpositions=[]} 1 Nothing]) +tests_amountp = [ + "amountp" ~: do + assertParseEqual (parseWithCtx nullctx amountp "$47.18") (Mixed [usd 47.18]) + assertParseEqual (parseWithCtx nullctx amountp "$1.") (Mixed [setAmountPrecision 0 $ usd 1]) ,"amount with unit price" ~: do assertParseEqual - (parseWithCtx nullctx amount "$10 @ €0.5") - (Mixed [Amount{commodity=dollar{precision=0}, - quantity=10, - price=(Just $ UnitPrice $ Mixed [Amount{commodity=euro{precision=1}, - quantity=0.5, - price=Nothing}])}]) + (parseWithCtx nullctx amountp "$10 @ €0.5") + (Mixed [usd 10 `withPrecision` 0 `at` (eur 0.5 `withPrecision` 1)]) ,"amount with total price" ~: do assertParseEqual - (parseWithCtx nullctx amount "$10 @@ €5") - (Mixed [Amount{commodity=dollar{precision=0}, - quantity=10, - price=(Just $ TotalPrice $ Mixed [Amount{commodity=euro{precision=0}, - quantity=5, - price=Nothing}])}]) + (parseWithCtx nullctx amountp "$10 @@ €5") + (Mixed [usd 10 `withPrecision` 0 @@ (eur 5 `withPrecision` 0)]) ] -- | Run the amount parser on a string to get the result or an error. -amount' :: String -> MixedAmount -amount' s = either (error' . show) id $ parseWithCtx nullctx amount s +amountp' :: String -> MixedAmount +amountp' s = either (error' . show) id $ parseWithCtx nullctx amountp s leftsymbolamount :: GenParser Char JournalContext MixedAmount leftsymbolamount = do sign <- optionMaybe $ string "-" let applysign = if isJust sign then negate else id - sym <- commoditysymbol + c <- commoditysymbol sp <- many spacenonewline - (q,p,d,s,spos) <- number - pri <- priceamount - let c = Commodity {symbol=sym,side=L,spaced=not $ null sp,decimalpoint=d,precision=p,separator=s,separatorpositions=spos} - return $ applysign $ Mixed [Amount c q pri] + (q,prec,dec,sep,seppos) <- number + let s = amountstyle{ascommodityside=L, ascommodityspaced=not $ null sp, asdecimalpoint=dec, asprecision=prec, asseparator=sep, asseparatorpositions=seppos} + p <- priceamount + return $ applysign $ Mixed [Amount c q p s] "left-symbol amount" rightsymbolamount :: GenParser Char JournalContext MixedAmount rightsymbolamount = do - (q,p,d,s,spos) <- number + (q,prec,dec,sep,seppos) <- number sp <- many spacenonewline - sym <- commoditysymbol - pri <- priceamount - let c = Commodity {symbol=sym,side=R,spaced=not $ null sp,decimalpoint=d,precision=p,separator=s,separatorpositions=spos} - return $ Mixed [Amount c q pri] + c <- commoditysymbol + p <- priceamount + let s = amountstyle{ascommodityside=R, ascommodityspaced=not $ null sp, asdecimalpoint=dec, asprecision=prec, asseparator=sep, asseparatorpositions=seppos} + return $ Mixed [Amount c q p s] "right-symbol amount" nosymbolamount :: GenParser Char JournalContext MixedAmount nosymbolamount = do - (q,p,d,s,spos) <- number - pri <- priceamount - defc <- getCommodity - let c = fromMaybe Commodity{symbol="",side=L,spaced=False,decimalpoint=d,precision=p,separator=s,separatorpositions=spos} defc - return $ Mixed [Amount c q pri] + (q,prec,dec,sep,seppos) <- number + p <- priceamount + defcs <- getCommodityAndStyle + let (c,s) = case defcs of + Just (c',s') -> (c',s') + Nothing -> ("", amountstyle{asdecimalpoint=dec, asprecision=prec, asseparator=sep, asseparatorpositions=seppos}) + return $ Mixed [Amount c q p s] "no-symbol amount" commoditysymbol :: GenParser Char JournalContext String @@ -653,11 +647,11 @@ priceamount = try (do char '@' many spacenonewline - a <- amount -- XXX can parse more prices ad infinitum, shouldn't + a <- amountp -- XXX can parse more prices ad infinitum, shouldn't return $ Just $ TotalPrice a) <|> (do many spacenonewline - a <- amount -- XXX can parse more prices ad infinitum, shouldn't + a <- amountp -- XXX can parse more prices ad infinitum, shouldn't return $ Just $ UnitPrice a)) <|> return Nothing @@ -667,7 +661,7 @@ balanceassertion = many spacenonewline char '=' many spacenonewline - a <- amount -- XXX should restrict to a simple amount + a <- amountp -- XXX should restrict to a simple amount return $ Just a) <|> return Nothing @@ -680,7 +674,7 @@ fixedlotprice = many spacenonewline char '=' many spacenonewline - a <- amount -- XXX should restrict to a simple amount + a <- amountp -- XXX should restrict to a simple amount many spacenonewline char '}' return $ Just a) @@ -841,7 +835,7 @@ tests_tagcomment = [ tests_Hledger_Read_JournalReader = TestList $ concat [ tests_number, - tests_amount, + tests_amountp, tests_spaceandamountormissing, tests_tagcomment, tests_inlinecomment, @@ -891,7 +885,7 @@ tests_Hledger_Read_JournalReader = TestList $ concat [ assertParse (parseWithCtx nullctx defaultyeardirective "Y 10001\n") ,"historicalpricedirective" ~: - assertParseEqual (parseWithCtx nullctx historicalpricedirective "P 2004/05/01 XYZ $55.00\n") (HistoricalPrice (parsedate "2004/05/01") "XYZ" $ Mixed [dollars 55]) + assertParseEqual (parseWithCtx nullctx historicalpricedirective "P 2004/05/01 XYZ $55.00\n") (HistoricalPrice (parsedate "2004/05/01") "XYZ" $ Mixed [usd 55]) ,"ignoredpricecommoditydirective" ~: do assertParse (parseWithCtx nullctx ignoredpricecommoditydirective "N $\n") @@ -916,19 +910,16 @@ tests_Hledger_Read_JournalReader = TestList $ concat [ assertBool "accountname rejects an empty trailing component" (isLeft $ parsewith accountname "a:b:") ,"leftsymbolamount" ~: do - assertParseEqual (parseWithCtx nullctx leftsymbolamount "$1") - (Mixed [Amount Commodity {symbol="$",side=L,spaced=False,decimalpoint='.',precision=0,separator=',',separatorpositions=[]} 1 Nothing]) - assertParseEqual (parseWithCtx nullctx leftsymbolamount "$-1") - (Mixed [Amount Commodity {symbol="$",side=L,spaced=False,decimalpoint='.',precision=0,separator=',',separatorpositions=[]} (-1) Nothing]) - assertParseEqual (parseWithCtx nullctx leftsymbolamount "-$1") - (Mixed [Amount Commodity {symbol="$",side=L,spaced=False,decimalpoint='.',precision=0,separator=',',separatorpositions=[]} (-1) Nothing]) + assertParseEqual (parseWithCtx nullctx leftsymbolamount "$1") (Mixed [usd 1 `withPrecision` 0]) + assertParseEqual (parseWithCtx nullctx leftsymbolamount "$-1") (Mixed [usd (-1) `withPrecision` 0]) + assertParseEqual (parseWithCtx nullctx leftsymbolamount "-$1") (Mixed [usd (-1) `withPrecision` 0]) ,"amount" ~: do let -- | compare a parse result with a MixedAmount, showing the debug representation for clarity assertMixedAmountParse parseresult mixedamount = (either (const "parse error") showMixedAmountDebug parseresult) ~?= (showMixedAmountDebug mixedamount) - assertMixedAmountParse (parseWithCtx nullctx amount "1 @ $2") - (Mixed [Amount unknown 1 (Just $ UnitPrice $ Mixed [Amount dollar{precision=0} 2 Nothing])]) + assertMixedAmountParse (parseWithCtx nullctx amountp "1 @ $2") + (Mixed [amt 1 `withPrecision` 0 `at` (usd 2 `withPrecision` 0)]) ]] @@ -941,6 +932,6 @@ entry1_str = unlines entry1 = txnTieKnot $ Transaction (parsedate "2007/01/28") Nothing False "" "coopportunity" "" [] - [Posting False "expenses:food:groceries" (Mixed [dollars 47.18]) "" RegularPosting [] Nothing, - Posting False "assets:checking" (Mixed [dollars (-47.18)]) "" RegularPosting [] Nothing] "" + [Posting False "expenses:food:groceries" (Mixed [usd 47.18]) "" RegularPosting [] Nothing, + Posting False "assets:checking" (Mixed [usd (-47.18)]) "" RegularPosting [] Nothing] "" diff --git a/hledger-lib/Hledger/Reports.hs b/hledger-lib/Hledger/Reports.hs index a92db02be..fb5e62ff3 100644 --- a/hledger-lib/Hledger/Reports.hs +++ b/hledger-lib/Hledger/Reports.hs @@ -56,13 +56,12 @@ import Data.Time.Calendar -- import Data.Tree import Safe (headMay, lastMay) import System.Console.CmdArgs -- for defaults support -import System.Time (ClockTime(TOD)) import Test.HUnit import Text.ParserCombinators.Parsec import Text.Printf import Hledger.Data -import Hledger.Read (amount') +import Hledger.Read (amountp') import Hledger.Query import Hledger.Utils @@ -425,7 +424,7 @@ type TransactionsReportItem = (Transaction -- the corresponding transaction triDate (t,_,_,_,_,_) = tdate t triBalance (_,_,_,_,_,Mixed a) = case a of [] -> "0" - (Amount{quantity=q}):_ -> show q + (Amount{aquantity=q}):_ -> show q -- | Select transactions from the whole journal for a transactions report, -- with no \"current\" account. The end result is similar to @@ -760,36 +759,36 @@ tests_accountsReport = ,"accountsReport with no args on sample journal" ~: do (defreportopts, samplejournal) `gives` ([ - ("assets","assets",0, amount' "$-1.00") - ,("assets:bank:saving","bank:saving",1, amount' "$1.00") - ,("assets:cash","cash",1, amount' "$-2.00") - ,("expenses","expenses",0, amount' "$2.00") - ,("expenses:food","food",1, amount' "$1.00") - ,("expenses:supplies","supplies",1, amount' "$1.00") - ,("income","income",0, amount' "$-2.00") - ,("income:gifts","gifts",1, amount' "$-1.00") - ,("income:salary","salary",1, amount' "$-1.00") - ,("liabilities:debts","liabilities:debts",0, amount' "$1.00") + ("assets","assets",0, amountp' "$-1.00") + ,("assets:bank:saving","bank:saving",1, amountp' "$1.00") + ,("assets:cash","cash",1, amountp' "$-2.00") + ,("expenses","expenses",0, amountp' "$2.00") + ,("expenses:food","food",1, amountp' "$1.00") + ,("expenses:supplies","supplies",1, amountp' "$1.00") + ,("income","income",0, amountp' "$-2.00") + ,("income:gifts","gifts",1, amountp' "$-1.00") + ,("income:salary","salary",1, amountp' "$-1.00") + ,("liabilities:debts","liabilities:debts",0, amountp' "$1.00") ], Mixed [nullamt]) ,"accountsReport with --depth=N" ~: do (defreportopts{depth_=Just 1}, samplejournal) `gives` ([ - ("assets", "assets", 0, amount' "$-1.00") - ,("expenses", "expenses", 0, amount' "$2.00") - ,("income", "income", 0, amount' "$-2.00") - ,("liabilities", "liabilities", 0, amount' "$1.00") + ("assets", "assets", 0, amountp' "$-1.00") + ,("expenses", "expenses", 0, amountp' "$2.00") + ,("income", "income", 0, amountp' "$-2.00") + ,("liabilities", "liabilities", 0, amountp' "$1.00") ], Mixed [nullamt]) ,"accountsReport with depth:N" ~: do (defreportopts{query_="depth:1"}, samplejournal) `gives` ([ - ("assets", "assets", 0, amount' "$-1.00") - ,("expenses", "expenses", 0, amount' "$2.00") - ,("income", "income", 0, amount' "$-2.00") - ,("liabilities", "liabilities", 0, amount' "$1.00") + ("assets", "assets", 0, amountp' "$-1.00") + ,("expenses", "expenses", 0, amountp' "$2.00") + ,("income", "income", 0, amountp' "$-2.00") + ,("liabilities", "liabilities", 0, amountp' "$1.00") ], Mixed [nullamt]) @@ -799,32 +798,32 @@ tests_accountsReport = Mixed [nullamt]) (defreportopts{query_="edate:'in 2009'"}, samplejournal2) `gives` ([ - ("assets:bank:checking","assets:bank:checking",0,amount' "$1.00") - ,("income:salary","income:salary",0,amount' "$-1.00") + ("assets:bank:checking","assets:bank:checking",0,amountp' "$1.00") + ,("income:salary","income:salary",0,amountp' "$-1.00") ], Mixed [nullamt]) ,"accountsReport with desc:" ~: do (defreportopts{query_="desc:income"}, samplejournal) `gives` ([ - ("assets:bank:checking","assets:bank:checking",0,amount' "$1.00") - ,("income:salary","income:salary",0, amount' "$-1.00") + ("assets:bank:checking","assets:bank:checking",0,amountp' "$1.00") + ,("income:salary","income:salary",0, amountp' "$-1.00") ], Mixed [nullamt]) ,"accountsReport with not:desc:" ~: do (defreportopts{query_="not:desc:income"}, samplejournal) `gives` ([ - ("assets","assets",0, amount' "$-2.00") + ("assets","assets",0, amountp' "$-2.00") ,("assets:bank","bank",1, Mixed [nullamt]) - ,("assets:bank:checking","checking",2,amount' "$-1.00") - ,("assets:bank:saving","saving",2, amount' "$1.00") - ,("assets:cash","cash",1, amount' "$-2.00") - ,("expenses","expenses",0, amount' "$2.00") - ,("expenses:food","food",1, amount' "$1.00") - ,("expenses:supplies","supplies",1, amount' "$1.00") - ,("income:gifts","income:gifts",0, amount' "$-1.00") - ,("liabilities:debts","liabilities:debts",0, amount' "$1.00") + ,("assets:bank:checking","checking",2,amountp' "$-1.00") + ,("assets:bank:saving","saving",2, amountp' "$1.00") + ,("assets:cash","cash",1, amountp' "$-2.00") + ,("expenses","expenses",0, amountp' "$2.00") + ,("expenses:food","food",1, amountp' "$1.00") + ,("expenses:supplies","supplies",1, amountp' "$1.00") + ,("income:gifts","income:gifts",0, amountp' "$-1.00") + ,("liabilities:debts","liabilities:debts",0, amountp' "$1.00") ], Mixed [nullamt]) @@ -945,10 +944,9 @@ tests_accountsReport = -} ] -Right samplejournal2 = journalBalanceTransactions $ Journal - [] - [] - [ +Right samplejournal2 = journalBalanceTransactions $ + nulljournal + {jtxns = [ txnTieKnot $ Transaction { tdate=parsedate "2008/01/01", teffectivedate=Just $ parsedate "2009/01/01", @@ -961,7 +959,7 @@ Right samplejournal2 = journalBalanceTransactions $ Journal Posting { pstatus=False, paccount="assets:bank:checking", - pamount=(Mixed [dollars 1]), + pamount=(Mixed [usd 1]), pcomment="", ptype=RegularPosting, ptags=[], @@ -980,13 +978,8 @@ Right samplejournal2 = journalBalanceTransactions $ Journal tpreceding_comment_lines="" } ] - [] - [] - "" - nullctx - [] - (TOD 0 0) - + } + -- tests_isInterestingIndented = [ -- "isInterestingIndented" ~: do -- let (opts, journal, acctname) `gives` r = isInterestingIndented opts l acctname `is` r @@ -1010,10 +1003,10 @@ tests_Hledger_Reports = TestList $ -- (summarisePostingsInDateSpan (mkdatespan b e) depth showempty ps `is`) -- let ps = -- [ - -- nullposting{lpdescription="desc",lpaccount="expenses:food:groceries",lpamount=Mixed [dollars 1]} - -- ,nullposting{lpdescription="desc",lpaccount="expenses:food:dining", lpamount=Mixed [dollars 2]} - -- ,nullposting{lpdescription="desc",lpaccount="expenses:food", lpamount=Mixed [dollars 4]} - -- ,nullposting{lpdescription="desc",lpaccount="expenses:food:dining", lpamount=Mixed [dollars 8]} + -- nullposting{lpdescription="desc",lpaccount="expenses:food:groceries",lpamount=Mixed [usd 1]} + -- ,nullposting{lpdescription="desc",lpaccount="expenses:food:dining", lpamount=Mixed [usd 2]} + -- ,nullposting{lpdescription="desc",lpaccount="expenses:food", lpamount=Mixed [usd 4]} + -- ,nullposting{lpdescription="desc",lpaccount="expenses:food:dining", lpamount=Mixed [usd 8]} -- ] -- ("2008/01/01","2009/01/01",0,9999,False,[]) `gives` -- [] @@ -1023,21 +1016,21 @@ tests_Hledger_Reports = TestList $ -- ] -- ("2008/01/01","2009/01/01",0,9999,False,ts) `gives` -- [ - -- nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food", lpamount=Mixed [dollars 4]} - -- ,nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food:dining", lpamount=Mixed [dollars 10]} - -- ,nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food:groceries",lpamount=Mixed [dollars 1]} + -- nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food", lpamount=Mixed [usd 4]} + -- ,nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food:dining", lpamount=Mixed [usd 10]} + -- ,nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food:groceries",lpamount=Mixed [usd 1]} -- ] -- ("2008/01/01","2009/01/01",0,2,False,ts) `gives` -- [ - -- nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food",lpamount=Mixed [dollars 15]} + -- nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses:food",lpamount=Mixed [usd 15]} -- ] -- ("2008/01/01","2009/01/01",0,1,False,ts) `gives` -- [ - -- nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses",lpamount=Mixed [dollars 15]} + -- nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="expenses",lpamount=Mixed [usd 15]} -- ] -- ("2008/01/01","2009/01/01",0,0,False,ts) `gives` -- [ - -- nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="",lpamount=Mixed [dollars 15]} + -- nullposting{lpdate=parsedate "2008/01/01",lpdescription="- 2008/12/31",lpaccount="",lpamount=Mixed [usd 15]} -- ] ] diff --git a/hledger/Hledger/Cli.hs b/hledger/Hledger/Cli.hs index 9b65f3adf..f1a25d2b7 100644 --- a/hledger/Hledger/Cli.hs +++ b/hledger/Hledger/Cli.hs @@ -23,9 +23,7 @@ module Hledger.Cli ( tests_Hledger_Cli ) where -import qualified Data.Map as Map import Data.Time.Calendar -import System.Time (ClockTime(TOD)) import Test.HUnit import Hledger @@ -110,12 +108,9 @@ tests_Hledger_Cli = TestList "expenses","expenses:food","expenses:food:dining","expenses:phone","expenses:vacation", "liabilities","liabilities:credit cards","liabilities:credit cards:discover"] - ,"journalCanonicaliseAmounts" ~: - "use the greatest precision" ~: - (map precision $ journalAmountAndPriceCommodities $ journalCanonicaliseAmounts $ journalWithAmounts ["1","2.00"]) `is` [2,2] - - ,"commodities" ~: - Map.elems (ledgerCommodities ledger7) `is` [Commodity {symbol="$", side=L, spaced=False, decimalpoint='.', precision=2, separator=',', separatorpositions=[]}] + -- ,"journalCanonicaliseAmounts" ~: + -- "use the greatest precision" ~: + -- (map asprecision $ journalAmountAndPriceCommodities $ journalCanonicaliseAmounts $ journalWithAmounts ["1","2.00"]) `is` [2,2] -- don't know what this should do -- ,"elideAccountName" ~: do @@ -129,9 +124,9 @@ tests_Hledger_Cli = TestList tdate (head $ jtxns j) `is` fromGregorian 2009 1 1 return () - ,"show dollars" ~: showAmount (dollars 1) ~?= "$1.00" + ,"show dollars" ~: showAmount (usd 1) ~?= "$1.00" - ,"show hours" ~: showAmount (hours 1) ~?= "1.0h" + ,"show hours" ~: showAmount (hrs 1) ~?= "1.0h" ] @@ -337,9 +332,7 @@ defaultyear_journal_str = unlines -- ,"" -- ] -journal7 = Journal - [] - [] +journal7 = nulljournal {jtxns = [ txnTieKnot $ Transaction { tdate=parsedate "2007/01/01", @@ -353,7 +346,7 @@ journal7 = Journal Posting { pstatus=False, paccount="assets:cash", - pamount=(Mixed [dollars 4.82]), + pamount=(Mixed [usd 4.82]), pcomment="", ptype=RegularPosting, ptags=[], @@ -362,7 +355,7 @@ journal7 = Journal Posting { pstatus=False, paccount="equity:opening balances", - pamount=(Mixed [dollars (-4.82)]), + pamount=(Mixed [usd (-4.82)]), pcomment="", ptype=RegularPosting, ptags=[], @@ -384,7 +377,7 @@ journal7 = Journal Posting { pstatus=False, paccount="expenses:vacation", - pamount=(Mixed [dollars 179.92]), + pamount=(Mixed [usd 179.92]), pcomment="", ptype=RegularPosting, ptags=[], @@ -393,7 +386,7 @@ journal7 = Journal Posting { pstatus=False, paccount="assets:checking", - pamount=(Mixed [dollars (-179.92)]), + pamount=(Mixed [usd (-179.92)]), pcomment="", ptype=RegularPosting, ptags=[], @@ -415,7 +408,7 @@ journal7 = Journal Posting { pstatus=False, paccount="assets:saving", - pamount=(Mixed [dollars 200]), + pamount=(Mixed [usd 200]), pcomment="", ptype=RegularPosting, ptags=[], @@ -424,7 +417,7 @@ journal7 = Journal Posting { pstatus=False, paccount="assets:checking", - pamount=(Mixed [dollars (-200)]), + pamount=(Mixed [usd (-200)]), pcomment="", ptype=RegularPosting, ptags=[], @@ -446,7 +439,7 @@ journal7 = Journal Posting { pstatus=False, paccount="expenses:food:dining", - pamount=(Mixed [dollars 4.82]), + pamount=(Mixed [usd 4.82]), pcomment="", ptype=RegularPosting, ptags=[], @@ -455,7 +448,7 @@ journal7 = Journal Posting { pstatus=False, paccount="assets:cash", - pamount=(Mixed [dollars (-4.82)]), + pamount=(Mixed [usd (-4.82)]), pcomment="", ptype=RegularPosting, ptags=[], @@ -477,7 +470,7 @@ journal7 = Journal Posting { pstatus=False, paccount="expenses:phone", - pamount=(Mixed [dollars 95.11]), + pamount=(Mixed [usd 95.11]), pcomment="", ptype=RegularPosting, ptags=[], @@ -486,7 +479,7 @@ journal7 = Journal Posting { pstatus=False, paccount="assets:checking", - pamount=(Mixed [dollars (-95.11)]), + pamount=(Mixed [usd (-95.11)]), pcomment="", ptype=RegularPosting, ptags=[], @@ -508,7 +501,7 @@ journal7 = Journal Posting { pstatus=False, paccount="liabilities:credit cards:discover", - pamount=(Mixed [dollars 80]), + pamount=(Mixed [usd 80]), pcomment="", ptype=RegularPosting, ptags=[], @@ -517,7 +510,7 @@ journal7 = Journal Posting { pstatus=False, paccount="assets:checking", - pamount=(Mixed [dollars (-80)]), + pamount=(Mixed [usd (-80)]), pcomment="", ptype=RegularPosting, ptags=[], @@ -527,12 +520,7 @@ journal7 = Journal tpreceding_comment_lines="" } ] - [] - [] - "" - nullctx - [] - (TOD 0 0) + } ledger7 = ledgerFromJournal Any journal7 @@ -549,20 +537,13 @@ ledger7 = ledgerFromJournal Any journal7 -- timelogentry2_str = "o 2007/03/11 16:30:00\n" -- timelogentry2 = TimeLogEntry Out (parsedatetime "2007/03/11 16:30:00") "" --- a1 = Mixed [(hours 1){price=Just $ Mixed [Amount (comm "$") 10 Nothing]}] --- a2 = Mixed [(hours 2){price=Just $ Mixed [Amount (comm "EUR") 10 Nothing]}] +-- a1 = Mixed [(hrs 1){aprice=Just $ Mixed [Amount (comm "$") 10 Nothing]}] +-- a2 = Mixed [(hrs 2){aprice=Just $ Mixed [Amount (comm "EUR") 10 Nothing]}] -- a3 = Mixed $ amounts a1 ++ amounts a2 -journalWithAmounts :: [String] -> Journal -journalWithAmounts as = - Journal - [] - [] - [t | a <- as, let t = nulltransaction{tdescription=a,tpostings=[nullposting{pamount=parse a,ptransaction=Just t}]}] - [] - [] - "" - nullctx - [] - (TOD 0 0) - where parse = fromparse . parseWithCtx nullctx amount +-- journalWithAmounts :: [String] -> Journal +-- journalWithAmounts as = +-- nulljournal{jtxns= +-- [t | a <- as, let t = nulltransaction{tdescription=a,tpostings=[nullposting{pamount=parse a,ptransaction=Just t}]}] +-- } +-- where parse = fromparse . parseWithCtx nullctx amountp diff --git a/hledger/Hledger/Cli/Add.hs b/hledger/Hledger/Cli/Add.hs index 998265b9a..98cebbd76 100644 --- a/hledger/Hledger/Cli/Add.hs +++ b/hledger/Hledger/Cli/Add.hs @@ -148,14 +148,14 @@ getPostings st enteredps = do -- I think 1 or 4, whichever would show the most decimal places p = maxprecisionwithpoint amountstr <- runInteractionDefault $ askFor (printf "amount %d" n) defaultamountstr validateamount - let a = fromparse $ runParser (amount <|> return missingmixedamt) ctx "" amountstr - a' = fromparse $ runParser (amount <|> return missingmixedamt) nullctx "" amountstr + let a = fromparse $ runParser (amountp <|> return missingmixedamt) ctx "" amountstr + a' = fromparse $ runParser (amountp <|> return missingmixedamt) nullctx "" amountstr defaultamtused = Just (showMixedAmount a) == defaultamountstr commodityadded | c == cwithnodef = Nothing | otherwise = c where c = maybemixedamountcommodity a cwithnodef = maybemixedamountcommodity a' - maybemixedamountcommodity = maybe Nothing (Just . commodity) . headMay . amounts + maybemixedamountcommodity = maybe Nothing (Just . acommodity) . headMay . amounts p = nullposting{paccount=stripbrackets account, pamount=a, ptype=postingtype account} @@ -163,7 +163,7 @@ getPostings st enteredps = do else st{psHistory = historicalps', psSuggestHistoricalAmount = False} when (isJust commodityadded) $ - liftIO $ hPutStrLn stderr $ printf "using default commodity (%s)" (symbol $ fromJust commodityadded) + liftIO $ hPutStrLn stderr $ printf "using default commodity (%s)" (fromJust commodityadded) getPostings st' (enteredps ++ [p]) where j = psJournal st @@ -179,7 +179,7 @@ getPostings st enteredps = do postingtype _ = RegularPosting stripbrackets = dropWhile (`elem` "([") . reverse . dropWhile (`elem` "])") . reverse validateamount = Just $ \s -> (null s && not (null enteredrealps)) - || isRight (runParser (amount>>many spacenonewline>>eof) ctx "" s) + || isRight (runParser (amountp >> many spacenonewline >> eof) ctx "" s) -- | Prompt for and read a string value, optionally with a default value -- and a validator. A validator causes the prompt to repeat until the diff --git a/hledger/Hledger/Cli/Stats.hs b/hledger/Hledger/Cli/Stats.hs index 7142a519f..89d1510d5 100644 --- a/hledger/Hledger/Cli/Stats.hs +++ b/hledger/Hledger/Cli/Stats.hs @@ -62,7 +62,7 @@ showLedgerStats l today span = path = journalFilePath j ts = sortBy (comparing tdate) $ filter (spanContainsDate span . tdate) $ jtxns j as = nub $ map paccount $ concatMap tpostings ts - cs = Map.keys $ canonicaliseCommodities $ nub $ map commodity $ concatMap amounts $ map pamount $ concatMap tpostings ts + cs = Map.keys $ canonicalStyles $ concatMap amounts $ map pamount $ concatMap tpostings ts lastdate | null ts = Nothing | otherwise = Just $ tdate $ last ts lastelapsed = maybe Nothing (Just . diffDays today) lastdate