price precisions don't influence display precision; balancing is based on display precision (#23)

Like ledger, price amounts are now "unobserved", ie their precision does
not affect the canonical display precisions used when displaying amounts,
and transaction balancing is done based on display precision, ie amounts
are considered to balance if their sum appears to be zero when using the
canonical display precision.
This commit is contained in:
Simon Michael 2010-11-14 22:44:37 +00:00
parent 1551a6914b
commit 811e71aba7
10 changed files with 103 additions and 32 deletions

View File

@ -110,7 +110,7 @@ showAmount a@(Amount (Commodity {symbol=sym,side=side,spaced=spaced}) _ pri) =
R -> printf "%s%s%s%s" quantity space sym' price R -> printf "%s%s%s%s" quantity space sym' price
where where
sym' = quoteCommoditySymbolIfNeeded sym sym' = quoteCommoditySymbolIfNeeded sym
space = if spaced then " " else "" space = if (spaced && not (null sym')) then " " else ""
quantity = showAmount' a quantity = showAmount' a
price = case pri of (Just pamt) -> " @ " ++ showMixedAmount pamt price = case pri of (Just pamt) -> " @ " ++ showMixedAmount pamt
Nothing -> "" Nothing -> ""
@ -132,6 +132,10 @@ showAmountDebug (Amount c q pri) = printf "Amount {commodity = %s, quantity = %s
showAmountWithoutPrice :: Amount -> String showAmountWithoutPrice :: Amount -> String
showAmountWithoutPrice a = showAmount a{price=Nothing} showAmountWithoutPrice a = showAmount a{price=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}
-- | Get the string representation of the number part of of an amount, -- | Get the string representation of the number part of of an amount,
-- using the display precision from its commodity. -- using the display precision from its commodity.
showAmount' :: Amount -> String showAmount' :: Amount -> String
@ -158,7 +162,7 @@ punctuatethousands s =
-- | Does this amount appear to be zero when displayed with its given precision ? -- | Does this amount appear to be zero when displayed with its given precision ?
isZeroAmount :: Amount -> Bool isZeroAmount :: Amount -> Bool
isZeroAmount = null . filter (`elem` "123456789") . showAmountWithoutPrice isZeroAmount = null . filter (`elem` "123456789") . showAmountWithoutPriceOrCommodity
-- | Is this amount "really" zero, regardless of the display precision ? -- | Is this amount "really" zero, regardless of the display precision ?
-- Since we are using floating point, for now just test to some high precision. -- Since we are using floating point, for now just test to some high precision.

View File

@ -18,7 +18,7 @@ import Hledger.Data.AccountName
import Hledger.Data.Amount import Hledger.Data.Amount
import Hledger.Data.Commodity (canonicaliseCommodities) import Hledger.Data.Commodity (canonicaliseCommodities)
import Hledger.Data.Dates (nulldatespan) import Hledger.Data.Dates (nulldatespan)
import Hledger.Data.Transaction (journalTransactionWithDate) import Hledger.Data.Transaction (journalTransactionWithDate,balanceTransaction)
import Hledger.Data.Posting import Hledger.Data.Posting
import Hledger.Data.TimeLog import Hledger.Data.TimeLog
@ -225,13 +225,24 @@ journalSelectingDate EffectiveDate j =
j{jtxns=map (journalTransactionWithDate EffectiveDate) $ jtxns j} j{jtxns=map (journalTransactionWithDate EffectiveDate) $ jtxns j}
-- | Do post-parse processing on a journal, to make it ready for use. -- | Do post-parse processing on a journal, to make it ready for use.
journalFinalise :: ClockTime -> LocalTime -> FilePath -> String -> JournalContext -> Journal -> Journal journalFinalise :: ClockTime -> LocalTime -> FilePath -> String -> JournalContext -> Journal -> Either String Journal
journalFinalise tclock tlocal path txt ctx j@Journal{files=fs} = journalFinalise tclock tlocal path txt ctx j@Journal{files=fs} =
journalBalanceTransactions $
journalCanonicaliseAmounts $ journalCanonicaliseAmounts $
journalApplyHistoricalPrices $ journalApplyHistoricalPrices $
journalCloseTimeLogEntries tlocal journalCloseTimeLogEntries tlocal
j{files=(path,txt):fs, filereadtime=tclock, jContext=ctx} j{files=(path,txt):fs, filereadtime=tclock, jContext=ctx}
-- | Fill in any missing amounts and check that all journal transactions
-- balance, or return an error message. This is done after parsing all
-- 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} =
case sequence $ map balance ts of Right ts' -> Right j{jtxns=ts'}
Left e -> Left e
where balance = balanceTransaction (Just $ journalCanonicalCommodities j)
-- | Convert all the journal's amounts to their canonical display -- | Convert all the journal's amounts to their canonical display
-- settings. Ie, all amounts in a given commodity will use (a) the -- settings. Ie, all amounts in a given commodity will use (a) the
-- display settings of the first, and (b) the greatest precision, of the -- display settings of the first, and (b) the greatest precision, of the
@ -283,7 +294,7 @@ journalConvertAmountsToCost j@Journal{jtxns=ts} = j{jtxns=map fixtransaction ts}
-- | Get this journal's unique, display-preference-canonicalised commodities, by symbol. -- | Get this journal's unique, display-preference-canonicalised commodities, by symbol.
journalCanonicalCommodities :: Journal -> Map.Map String Commodity journalCanonicalCommodities :: Journal -> Map.Map String Commodity
journalCanonicalCommodities j = canonicaliseCommodities $ journalAmountAndPriceCommodities j journalCanonicalCommodities j = canonicaliseCommodities $ journalAmountCommodities j
-- | Get all this journal's amounts' commodities, in the order parsed. -- | Get all this journal's amounts' commodities, in the order parsed.
journalAmountCommodities :: Journal -> [Commodity] journalAmountCommodities :: Journal -> [Commodity]

View File

@ -8,6 +8,9 @@ plus a date and optional metadata like description and cleared status.
module Hledger.Data.Transaction module Hledger.Data.Transaction
where where
import qualified Data.Map as Map
import Data.Map (findWithDefault)
import Hledger.Data.Utils import Hledger.Data.Utils
import Hledger.Data.Types import Hledger.Data.Types
import Hledger.Data.Dates import Hledger.Data.Dates
@ -75,7 +78,7 @@ showTransaction' elide effective t =
showdate = printf "%-10s" . showDate showdate = printf "%-10s" . showDate
showedate = printf "=%s" . showdate showedate = printf "=%s" . showdate
showpostings ps showpostings ps
| elide && length ps > 1 && isTransactionBalanced t | elide && length ps > 1 && isTransactionBalanced Nothing t -- imprecise balanced check
= map showposting (init ps) ++ [showpostingnoamt (last ps)] = map showposting (init ps) ++ [showpostingnoamt (last ps)]
| otherwise = map showposting ps | otherwise = map showposting ps
where where
@ -122,20 +125,34 @@ transactionPostingBalances t = (sumPostings $ realPostings t
-- | Is this transaction balanced ? A balanced transaction's real -- | Is this transaction balanced ? A balanced transaction's real
-- (non-virtual) postings sum to 0, and any balanced virtual postings -- (non-virtual) postings sum to 0, and any balanced virtual postings
-- also sum to 0. -- also sum to 0.
isTransactionBalanced :: Transaction -> Bool isTransactionBalanced :: Maybe (Map.Map String Commodity) -> Transaction -> Bool
isTransactionBalanced t = isReallyZeroMixedAmountCost rsum && isReallyZeroMixedAmountCost bvsum isTransactionBalanced canonicalcommoditymap t =
where (rsum, _, bvsum) = transactionPostingBalances t -- isReallyZeroMixedAmountCost rsum && isReallyZeroMixedAmountCost bvsum
isZeroMixedAmount rsum' && isZeroMixedAmount bvsum'
where
(rsum, _, bvsum) = transactionPostingBalances t
rsum' = canonicaliseMixedAmount canonicalcommoditymap $ costOfMixedAmount rsum
bvsum' = canonicaliseMixedAmount canonicalcommoditymap $ costOfMixedAmount bvsum
canonicaliseMixedAmount :: Maybe (Map.Map String Commodity) -> MixedAmount -> MixedAmount
canonicaliseMixedAmount Nothing = id
canonicaliseMixedAmount (Just canonicalcommoditymap) = fixmixedamount
where
-- like journalCanonicaliseAmounts
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
-- | Ensure that this entry is balanced, possibly auto-filling a missing -- | Ensure that this entry is balanced, possibly auto-filling a missing
-- amount first. We can auto-fill if there is just one non-virtual -- amount first. We can auto-fill if there is just one non-virtual
-- transaction without an amount. The auto-filled balance will be -- transaction without an amount. The auto-filled balance will be
-- converted to cost basis if possible. If the entry can not be balanced, -- converted to cost basis if possible. If the entry can not be balanced,
-- return an error message instead. -- return an error message instead.
balanceTransaction :: Transaction -> Either String Transaction balanceTransaction :: Maybe (Map.Map String Commodity) -> Transaction -> Either String Transaction
balanceTransaction t@Transaction{tpostings=ps} balanceTransaction canonicalcommoditymap t@Transaction{tpostings=ps}
| length rwithoutamounts > 1 || length bvwithoutamounts > 1 | length rwithoutamounts > 1 || length bvwithoutamounts > 1
= Left $ printerr "could not balance this transaction (too many missing amounts)" = Left $ printerr "could not balance this transaction (too many missing amounts)"
| not $ isTransactionBalanced t' = Left $ printerr $ nonzerobalanceerror t' | not $ isTransactionBalanced canonicalcommoditymap t' = Left $ printerr $ nonzerobalanceerror t'
| otherwise = Right t' | otherwise = Right t'
where where
rps = filter isReal ps rps = filter isReal ps
@ -145,9 +162,9 @@ balanceTransaction t@Transaction{tpostings=ps}
t' = t{tpostings=map balance ps} t' = t{tpostings=map balance ps}
where where
balance p | not (hasAmount p) && isReal p balance p | not (hasAmount p) && isReal p
= p{pamount = costOfMixedAmount (-(sum $ map pamount rwithamounts))} = p{pamount = (-(sum $ map pamount rwithamounts))}
| not (hasAmount p) && isBalancedVirtual p | not (hasAmount p) && isBalancedVirtual p
= p{pamount = costOfMixedAmount (-(sum $ map pamount bvwithamounts))} = p{pamount = (-(sum $ map pamount bvwithamounts))}
| otherwise = p | otherwise = p
printerr s = intercalate "\n" [s, showTransactionUnelided t] printerr s = intercalate "\n" [s, showTransactionUnelided t]

View File

@ -332,9 +332,11 @@ ledgerTransaction = do
md <- try ledgermetadata <|> return [] md <- try ledgermetadata <|> return []
postings <- ledgerpostings postings <- ledgerpostings
let t = txnTieKnot $ Transaction date edate status code description comment md postings "" let t = txnTieKnot $ Transaction date edate status code description comment md postings ""
case balanceTransaction t of -- case balanceTransaction Nothing t of
Right t' -> return t' -- Right t' -> return t'
Left err -> fail err -- Left err -> fail err
-- check it later, after we have worked out commodity display precisions
return t
ledgerdate :: GenParser Char JournalContext Day ledgerdate :: GenParser Char JournalContext Day
ledgerdate = do ledgerdate = do

View File

@ -26,8 +26,12 @@ parseJournalWith p f s = do
tc <- liftIO getClockTime tc <- liftIO getClockTime
tl <- liftIO getCurrentLocalTime tl <- liftIO getCurrentLocalTime
case runParser p nullctx f s of case runParser p nullctx f s of
Right (updates,ctx) -> liftM (journalFinalise tc tl f s ctx) $ updates `ap` return nulljournal Right (updates,ctx) -> do
Left err -> throwError $ show err j <- updates `ap` return nulljournal
case journalFinalise tc tl f s ctx j of
Right j' -> return j'
Left estr -> throwError estr
Left e -> throwError $ show e
setYear :: Integer -> GenParser tok JournalContext () setYear :: Integer -> GenParser tok JournalContext ()
setYear y = updateState (\ctx -> ctx{ctxYear=Just y}) setYear y = updateState (\ctx -> ctx{ctxYear=Just y})

View File

@ -881,7 +881,7 @@ postAddForm = do
-- if no errors so far, generate a transaction and balance it or get the error. -- if no errors so far, generate a transaction and balance it or get the error.
tE | not $ null errs = Left errs tE | not $ null errs = Left errs
| otherwise = either (\e -> Left ["unbalanced postings: " ++ (head $ lines e)]) Right | otherwise = either (\e -> Left ["unbalanced postings: " ++ (head $ lines e)]) Right
(balanceTransaction $ nulltransaction { (balanceTransaction Nothing $ nulltransaction { -- imprecise balancing
tdate=parsedate date tdate=parsedate date
,tdescription=desc ,tdescription=desc
,tpostings=[ ,tpostings=[

View File

@ -87,7 +87,7 @@ getTransaction j opts args defaultDate = do
retry msg = do retry msg = do
liftIO $ hPutStrLn stderr $ "\n" ++ msg ++ "please re-enter." liftIO $ hPutStrLn stderr $ "\n" ++ msg ++ "please re-enter."
getpostingsandvalidate getpostingsandvalidate
either retry (return . flip (,) date) $ balanceTransaction t either retry (return . flip (,) date) $ balanceTransaction Nothing t -- imprecise balancing
unless (null historymatches) unless (null historymatches)
(liftIO $ do (liftIO $ do
hPutStrLn stderr "Similar transactions found, using the first for defaults:\n" hPutStrLn stderr "Similar transactions found, using the first for defaults:\n"

View File

@ -271,18 +271,18 @@ tests = TestList [
,"balanceTransaction" ~: do ,"balanceTransaction" ~: do
assertBool "detect unbalanced entry, sign error" assertBool "detect unbalanced entry, sign error"
(isLeft $ balanceTransaction (isLeft $ balanceTransaction Nothing
(Transaction (parsedate "2007/01/28") Nothing False "" "test" "" [] (Transaction (parsedate "2007/01/28") Nothing False "" "test" "" []
[Posting False "a" (Mixed [dollars 1]) "" RegularPosting [] Nothing, [Posting False "a" (Mixed [dollars 1]) "" RegularPosting [] Nothing,
Posting False "b" (Mixed [dollars 1]) "" RegularPosting [] Nothing Posting False "b" (Mixed [dollars 1]) "" RegularPosting [] Nothing
] "")) ] ""))
assertBool "detect unbalanced entry, multiple missing amounts" assertBool "detect unbalanced entry, multiple missing amounts"
(isLeft $ balanceTransaction (isLeft $ balanceTransaction Nothing
(Transaction (parsedate "2007/01/28") Nothing False "" "test" "" [] (Transaction (parsedate "2007/01/28") Nothing False "" "test" "" []
[Posting False "a" missingamt "" RegularPosting [] Nothing, [Posting False "a" missingamt "" RegularPosting [] Nothing,
Posting False "b" missingamt "" RegularPosting [] Nothing Posting False "b" missingamt "" RegularPosting [] Nothing
] "")) ] ""))
let e = balanceTransaction (Transaction (parsedate "2007/01/28") Nothing False "" "test" "" [] let e = balanceTransaction Nothing (Transaction (parsedate "2007/01/28") Nothing False "" "test" "" []
[Posting False "a" (Mixed [dollars 1]) "" RegularPosting [] Nothing, [Posting False "a" (Mixed [dollars 1]) "" RegularPosting [] Nothing,
Posting False "b" missingamt "" RegularPosting [] Nothing Posting False "b" missingamt "" RegularPosting [] Nothing
] "") ] "")
@ -343,39 +343,39 @@ tests = TestList [
[Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting [] (Just t) [Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting [] (Just t)
,Posting False "c" (Mixed [dollars (-1.00)]) "" RegularPosting [] (Just t) ,Posting False "c" (Mixed [dollars (-1.00)]) "" RegularPosting [] (Just t)
] "" ] ""
assertBool "detect balanced" (isTransactionBalanced t) assertBool "detect balanced" (isTransactionBalanced Nothing t)
let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" "" [] let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" "" []
[Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting [] (Just t) [Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting [] (Just t)
,Posting False "c" (Mixed [dollars (-1.01)]) "" RegularPosting [] (Just t) ,Posting False "c" (Mixed [dollars (-1.01)]) "" RegularPosting [] (Just t)
] "" ] ""
assertBool "detect unbalanced" (not $ isTransactionBalanced t) assertBool "detect unbalanced" (not $ isTransactionBalanced Nothing t)
let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" "" [] let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" "" []
[Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting [] (Just t) [Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting [] (Just t)
] "" ] ""
assertBool "detect unbalanced, one posting" (not $ isTransactionBalanced t) assertBool "detect unbalanced, one posting" (not $ isTransactionBalanced Nothing t)
let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" "" [] let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" "" []
[Posting False "b" (Mixed [dollars 0]) "" RegularPosting [] (Just t) [Posting False "b" (Mixed [dollars 0]) "" RegularPosting [] (Just t)
] "" ] ""
assertBool "one zero posting is considered balanced for now" (isTransactionBalanced t) assertBool "one zero posting is considered balanced for now" (isTransactionBalanced Nothing t)
let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" "" [] let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" "" []
[Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting [] (Just t) [Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting [] (Just t)
,Posting False "c" (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 "d" (Mixed [dollars 100]) "" VirtualPosting [] (Just t)
] "" ] ""
assertBool "virtual postings don't need to balance" (isTransactionBalanced t) assertBool "virtual postings don't need to balance" (isTransactionBalanced Nothing t)
let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" "" [] let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" "" []
[Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting [] (Just t) [Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting [] (Just t)
,Posting False "c" (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 "d" (Mixed [dollars 100]) "" BalancedVirtualPosting [] (Just t)
] "" ] ""
assertBool "balanced virtual postings need to balance among themselves" (not $ isTransactionBalanced t) assertBool "balanced virtual postings need to balance among themselves" (not $ isTransactionBalanced Nothing t)
let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" "" [] let t = Transaction (parsedate "2009/01/01") Nothing False "" "a" "" []
[Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting [] (Just t) [Posting False "b" (Mixed [dollars 1.00]) "" RegularPosting [] (Just t)
,Posting False "c" (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 "d" (Mixed [dollars 100]) "" BalancedVirtualPosting [] (Just t)
,Posting False "e" (Mixed [dollars (-100)]) "" BalancedVirtualPosting [] (Just t) ,Posting False "e" (Mixed [dollars (-100)]) "" BalancedVirtualPosting [] (Just t)
] "" ] ""
assertBool "balanced virtual postings need to balance among themselves (2)" (isTransactionBalanced t) assertBool "balanced virtual postings need to balance among themselves (2)" (isTransactionBalanced Nothing t)
,"isSubAccountNameOf" ~: do ,"isSubAccountNameOf" ~: do
"assets" `isSubAccountNameOf` "assets" `is` False "assets" `isSubAccountNameOf` "assets" `is` False

View File

@ -8,5 +8,5 @@ bin/hledger -f - print
>>> >>>
2010/01/01 x 2010/01/01 x
a 1 @ $2 a 1 @ $2
b $-2 b -1 @ $2

33
tests/precision.test Normal file
View File

@ -0,0 +1,33 @@
# http://code.google.com/p/hledger/issues/detail?id=23
#
# with explicit price:
# prices' commodities are unobserved, so $'s display precision here should be 2 not 4
bin/hledger -f - print --cost
<<<
2010/1/1
a $0.00
a 1C @ $1.0049
a
>>>
2010/01/01
a 0
a $1.00
a $-1.00
>>>2
# with $'s display precision at 3 or more, this txn should not balance
bin/hledger -f - balance --no-total --cost --empty
<<<
2010/1/1
a 1C @ $1.0049
a $-1.000
>>>2 /off by \$0.005/
>>>= 1
# with $'s display precision at 2 or less, this txn should balance
bin/hledger -f - balance --no-total --cost --empty
<<<
2010/1/1
a 1C @ $1.0049
a $-1.00
>>>
$0.00 a