refactor: clarify that price amounts have only a single commodity
This commit is contained in:
parent
5a534f1c73
commit
64180b18ef
@ -72,6 +72,7 @@ module Hledger.Data.Amount (
|
|||||||
-- * MixedAmount
|
-- * MixedAmount
|
||||||
nullmixedamt,
|
nullmixedamt,
|
||||||
missingmixedamt,
|
missingmixedamt,
|
||||||
|
mixed,
|
||||||
amounts,
|
amounts,
|
||||||
normaliseMixedAmountPreservingFirstPrice,
|
normaliseMixedAmountPreservingFirstPrice,
|
||||||
normaliseMixedAmountPreservingPrices,
|
normaliseMixedAmountPreservingPrices,
|
||||||
@ -162,11 +163,11 @@ sumAmounts = normaliseMixedAmountPreservingPrices . Mixed
|
|||||||
|
|
||||||
-- | Set an amount's unit price.
|
-- | Set an amount's unit price.
|
||||||
at :: Amount -> Amount -> Amount
|
at :: Amount -> Amount -> Amount
|
||||||
amt `at` priceamt = amt{aprice=UnitPrice $ Mixed [priceamt]}
|
amt `at` priceamt = amt{aprice=UnitPrice priceamt}
|
||||||
|
|
||||||
-- | Set an amount's total price.
|
-- | Set an amount's total price.
|
||||||
(@@) :: Amount -> Amount -> Amount
|
(@@) :: Amount -> Amount -> Amount
|
||||||
amt @@ priceamt = amt{aprice=TotalPrice $ Mixed [priceamt]}
|
amt @@ priceamt = amt{aprice=TotalPrice priceamt}
|
||||||
|
|
||||||
tests_sumAmounts = [
|
tests_sumAmounts = [
|
||||||
"sumAmounts" ~: do
|
"sumAmounts" ~: do
|
||||||
@ -189,9 +190,8 @@ costOfAmount :: Amount -> Amount
|
|||||||
costOfAmount a@Amount{aquantity=q, aprice=price} =
|
costOfAmount a@Amount{aquantity=q, aprice=price} =
|
||||||
case price of
|
case price of
|
||||||
NoPrice -> a
|
NoPrice -> a
|
||||||
UnitPrice (Mixed [p@Amount{aquantity=pq}]) -> p{aquantity=pq * q}
|
UnitPrice p@Amount{aquantity=pq} -> p{aquantity=pq * q}
|
||||||
TotalPrice (Mixed [p@Amount{aquantity=pq}]) -> p{aquantity=pq * signum q}
|
TotalPrice p@Amount{aquantity=pq} -> p{aquantity=pq * signum q}
|
||||||
_ -> error' "costOfAmount: Malformed price encountered, programmer error"
|
|
||||||
|
|
||||||
-- | Divide an amount's quantity by a constant.
|
-- | Divide an amount's quantity by a constant.
|
||||||
divideAmount :: Amount -> Double -> Amount
|
divideAmount :: Amount -> Double -> Amount
|
||||||
@ -244,13 +244,13 @@ showAmountWithoutPriceOrCommodity a = showAmount a{acommodity="", aprice=NoPrice
|
|||||||
|
|
||||||
showPrice :: Price -> String
|
showPrice :: Price -> String
|
||||||
showPrice NoPrice = ""
|
showPrice NoPrice = ""
|
||||||
showPrice (UnitPrice pa) = " @ " ++ showMixedAmount pa
|
showPrice (UnitPrice pa) = " @ " ++ showAmount pa
|
||||||
showPrice (TotalPrice pa) = " @@ " ++ showMixedAmount pa
|
showPrice (TotalPrice pa) = " @@ " ++ showAmount pa
|
||||||
|
|
||||||
showPriceDebug :: Price -> String
|
showPriceDebug :: Price -> String
|
||||||
showPriceDebug NoPrice = ""
|
showPriceDebug NoPrice = ""
|
||||||
showPriceDebug (UnitPrice pa) = " @ " ++ showMixedAmountDebug pa
|
showPriceDebug (UnitPrice pa) = " @ " ++ showAmountDebug pa
|
||||||
showPriceDebug (TotalPrice pa) = " @@ " ++ showMixedAmountDebug pa
|
showPriceDebug (TotalPrice pa) = " @@ " ++ showAmountDebug pa
|
||||||
|
|
||||||
-- | Get the string representation of an amount, based on its commodity's
|
-- | Get the string representation of an amount, based on its commodity's
|
||||||
-- display settings. String representations equivalent to zero are
|
-- display settings. String representations equivalent to zero are
|
||||||
@ -343,6 +343,9 @@ missingamt = amount{acommodity="AUTO"}
|
|||||||
missingmixedamt :: MixedAmount
|
missingmixedamt :: MixedAmount
|
||||||
missingmixedamt = Mixed [missingamt]
|
missingmixedamt = Mixed [missingamt]
|
||||||
|
|
||||||
|
mixed :: Amount -> MixedAmount
|
||||||
|
mixed a = Mixed [a]
|
||||||
|
|
||||||
-- | Simplify a mixed amount's component amounts: we can combine amounts
|
-- | Simplify a mixed amount's component amounts: we can combine amounts
|
||||||
-- with the same commodity and unit price. Also remove any zero or missing
|
-- with the same commodity and unit price. Also remove any zero or missing
|
||||||
-- amounts and replace an empty amount list with a single zero amount.
|
-- amounts and replace an empty amount list with a single zero amount.
|
||||||
@ -510,9 +513,9 @@ tests_Hledger_Data_Amount = TestList $
|
|||||||
|
|
||||||
"costOfAmount" ~: do
|
"costOfAmount" ~: do
|
||||||
costOfAmount (eur 1) `is` eur 1
|
costOfAmount (eur 1) `is` eur 1
|
||||||
costOfAmount (eur 2){aprice=UnitPrice $ Mixed [usd 2]} `is` usd 4
|
costOfAmount (eur 2){aprice=UnitPrice $ usd 2} `is` usd 4
|
||||||
costOfAmount (eur 1){aprice=TotalPrice $ Mixed [usd 2]} `is` usd 2
|
costOfAmount (eur 1){aprice=TotalPrice $ usd 2} `is` usd 2
|
||||||
costOfAmount (eur (-1)){aprice=TotalPrice $ Mixed [usd 2]} `is` usd (-2)
|
costOfAmount (eur (-1)){aprice=TotalPrice $ usd 2} `is` usd (-2)
|
||||||
|
|
||||||
,"isZeroAmount" ~: do
|
,"isZeroAmount" ~: do
|
||||||
assertBool "" $ isZeroAmount $ amount
|
assertBool "" $ isZeroAmount $ amount
|
||||||
@ -521,7 +524,7 @@ tests_Hledger_Data_Amount = TestList $
|
|||||||
,"negating amounts" ~: do
|
,"negating amounts" ~: do
|
||||||
let a = usd 1
|
let a = usd 1
|
||||||
negate a `is` a{aquantity=(-1)}
|
negate a `is` a{aquantity=(-1)}
|
||||||
let b = (usd 1){aprice=UnitPrice $ Mixed [eur 2]}
|
let b = (usd 1){aprice=UnitPrice $ eur 2}
|
||||||
negate b `is` b{aquantity=(-1)}
|
negate b `is` b{aquantity=(-1)}
|
||||||
|
|
||||||
,"adding amounts without prices" ~: do
|
,"adding amounts without prices" ~: do
|
||||||
|
|||||||
@ -296,8 +296,8 @@ balanceTransaction styles t@Transaction{tpostings=ps}
|
|||||||
-- assign a balancing price. Use @@ for more exact output when possible.
|
-- assign a balancing price. Use @@ for more exact output when possible.
|
||||||
-- invariant: prices should always be positive. Enforced with "abs"
|
-- invariant: prices should always be positive. Enforced with "abs"
|
||||||
= if length ramountsinunpricedcommodity == 1
|
= if length ramountsinunpricedcommodity == 1
|
||||||
then TotalPrice $ Mixed [setAmountPrecision maxprecision $ abs $ targetcommodityamount]
|
then TotalPrice $ setAmountPrecision maxprecision $ abs $ targetcommodityamount
|
||||||
else UnitPrice $ Mixed [setAmountPrecision maxprecision $ abs $ targetcommodityamount `divideAmount` (aquantity unpricedamount)]
|
else UnitPrice $ setAmountPrecision maxprecision $ abs $ targetcommodityamount `divideAmount` (aquantity unpricedamount)
|
||||||
| otherwise = NoPrice
|
| otherwise = NoPrice
|
||||||
where
|
where
|
||||||
unpricedcommodity = head $ filter (`elem` (map acommodity rsumamounts)) rcommoditiesinorder
|
unpricedcommodity = head $ filter (`elem` (map acommodity rsumamounts)) rcommoditiesinorder
|
||||||
@ -320,8 +320,8 @@ balanceTransaction styles t@Transaction{tpostings=ps}
|
|||||||
where
|
where
|
||||||
conversionprice c | c == unpricedcommodity
|
conversionprice c | c == unpricedcommodity
|
||||||
= if length bvamountsinunpricedcommodity == 1
|
= if length bvamountsinunpricedcommodity == 1
|
||||||
then TotalPrice $ Mixed [setAmountPrecision maxprecision $ abs $ targetcommodityamount]
|
then TotalPrice $ setAmountPrecision maxprecision $ abs $ targetcommodityamount
|
||||||
else UnitPrice $ Mixed [setAmountPrecision maxprecision $ abs $ targetcommodityamount `divideAmount` (aquantity unpricedamount)]
|
else UnitPrice $ setAmountPrecision maxprecision $ abs $ targetcommodityamount `divideAmount` (aquantity unpricedamount)
|
||||||
| otherwise = NoPrice
|
| otherwise = NoPrice
|
||||||
where
|
where
|
||||||
unpricedcommodity = head $ filter (`elem` (map acommodity bvsumamounts)) bvcommoditiesinorder
|
unpricedcommodity = head $ filter (`elem` (map acommodity bvsumamounts)) bvcommoditiesinorder
|
||||||
|
|||||||
@ -48,11 +48,8 @@ type Commodity = String
|
|||||||
type Quantity = Double
|
type Quantity = Double
|
||||||
|
|
||||||
-- | An amount's price (none, per unit, or total) in another commodity.
|
-- | An amount's price (none, per unit, or total) in another commodity.
|
||||||
-- Note although a MixedAmount is used, it should be in a single
|
-- Note the price should be a positive number, although this is not enforced.
|
||||||
-- commodity, also the amount should be positive; these are not enforced
|
data Price = NoPrice | UnitPrice Amount | TotalPrice Amount deriving (Eq,Ord)
|
||||||
-- currently.
|
|
||||||
data Price = NoPrice | UnitPrice MixedAmount | TotalPrice MixedAmount
|
|
||||||
deriving (Eq,Ord)
|
|
||||||
|
|
||||||
-- | Display style for an amount.
|
-- | Display style for an amount.
|
||||||
data AmountStyle = AmountStyle {
|
data AmountStyle = AmountStyle {
|
||||||
@ -127,7 +124,7 @@ data TimeLogEntry = TimeLogEntry {
|
|||||||
data HistoricalPrice = HistoricalPrice {
|
data HistoricalPrice = HistoricalPrice {
|
||||||
hdate :: Day,
|
hdate :: Day,
|
||||||
hsymbol :: String,
|
hsymbol :: String,
|
||||||
hamount :: MixedAmount
|
hamount :: Amount
|
||||||
} deriving (Eq) -- & Show (in Amount.hs)
|
} deriving (Eq) -- & Show (in Amount.hs)
|
||||||
|
|
||||||
type Year = Integer
|
type Year = Integer
|
||||||
|
|||||||
@ -21,6 +21,7 @@ module Hledger.Read (
|
|||||||
accountname,
|
accountname,
|
||||||
amountp,
|
amountp,
|
||||||
amountp',
|
amountp',
|
||||||
|
mamountp',
|
||||||
-- * Tests
|
-- * Tests
|
||||||
samplejournal,
|
samplejournal,
|
||||||
tests_Hledger_Read,
|
tests_Hledger_Read,
|
||||||
|
|||||||
@ -427,7 +427,7 @@ transactionFromCsvRecord rules fields =
|
|||||||
currency = maybe (fromMaybe "" $ baseCurrency rules) (atDef "" fields) (currencyField rules)
|
currency = maybe (fromMaybe "" $ baseCurrency rules) (atDef "" fields) (currencyField rules)
|
||||||
amountstr'' = currency ++ amountstr'
|
amountstr'' = currency ++ amountstr'
|
||||||
amountparse = runParser amountp nullctx "" amountstr''
|
amountparse = runParser amountp nullctx "" amountstr''
|
||||||
a = either (const nullmixedamt) id amountparse
|
a = either (const nullmixedamt) mixed amountparse
|
||||||
-- Using costOfMixedAmount here to allow complex costs like "10 GBP @@ 15 USD".
|
-- 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"
|
-- Aim is to have "10 GBP @@ 15 USD" applied to account "acct", but have "-15USD" applied to "baseacct"
|
||||||
baseamount = costOfMixedAmount a
|
baseamount = costOfMixedAmount a
|
||||||
|
|||||||
@ -29,6 +29,7 @@ module Hledger.Read.JournalReader (
|
|||||||
accountname,
|
accountname,
|
||||||
amountp,
|
amountp,
|
||||||
amountp',
|
amountp',
|
||||||
|
mamountp',
|
||||||
emptyline,
|
emptyline,
|
||||||
-- * Tests
|
-- * Tests
|
||||||
tests_Hledger_Read_JournalReader
|
tests_Hledger_Read_JournalReader
|
||||||
@ -254,11 +255,8 @@ defaultcommoditydirective :: GenParser Char JournalContext JournalUpdate
|
|||||||
defaultcommoditydirective = do
|
defaultcommoditydirective = do
|
||||||
char 'D' <?> "default commodity"
|
char 'D' <?> "default commodity"
|
||||||
many1 spacenonewline
|
many1 spacenonewline
|
||||||
a <- amountp
|
Amount{..} <- amountp
|
||||||
-- amount always returns a MixedAmount containing one Amount, but let's be safe
|
setCommodityAndStyle (acommodity, astyle)
|
||||||
let as = amounts a
|
|
||||||
when (not $ null as) $
|
|
||||||
let Amount{..} = head as in setCommodityAndStyle (acommodity, astyle)
|
|
||||||
restofline
|
restofline
|
||||||
return $ return id
|
return $ return id
|
||||||
|
|
||||||
@ -559,7 +557,7 @@ spaceandamountormissing :: GenParser Char JournalContext MixedAmount
|
|||||||
spaceandamountormissing =
|
spaceandamountormissing =
|
||||||
try (do
|
try (do
|
||||||
many1 spacenonewline
|
many1 spacenonewline
|
||||||
amountp <|> return missingmixedamt
|
(Mixed . (:[])) `fmap` amountp <|> return missingmixedamt
|
||||||
) <|> return missingmixedamt
|
) <|> return missingmixedamt
|
||||||
|
|
||||||
tests_spaceandamountormissing = [
|
tests_spaceandamountormissing = [
|
||||||
@ -570,30 +568,35 @@ tests_spaceandamountormissing = [
|
|||||||
assertParseEqual (parseWithCtx nullctx spaceandamountormissing "") missingmixedamt
|
assertParseEqual (parseWithCtx nullctx spaceandamountormissing "") missingmixedamt
|
||||||
]
|
]
|
||||||
|
|
||||||
-- | Parse an amount, optionally with a left or right currency symbol,
|
-- | Parse a single-commodity amount, with optional symbol on the left or
|
||||||
-- price, and/or (ignored) ledger-style balance assertion.
|
-- right, optional unit or total price, and optional (ignored)
|
||||||
amountp :: GenParser Char JournalContext MixedAmount
|
-- ledger-style balance assertion or fixed lot price declaration.
|
||||||
|
amountp :: GenParser Char JournalContext Amount
|
||||||
amountp = try leftsymbolamount <|> try rightsymbolamount <|> nosymbolamount
|
amountp = try leftsymbolamount <|> try rightsymbolamount <|> nosymbolamount
|
||||||
|
|
||||||
tests_amountp = [
|
tests_amountp = [
|
||||||
"amountp" ~: do
|
"amountp" ~: do
|
||||||
assertParseEqual (parseWithCtx nullctx amountp "$47.18") (Mixed [usd 47.18])
|
assertParseEqual (parseWithCtx nullctx amountp "$47.18") (usd 47.18)
|
||||||
assertParseEqual (parseWithCtx nullctx amountp "$1.") (Mixed [setAmountPrecision 0 $ usd 1])
|
assertParseEqual (parseWithCtx nullctx amountp "$1.") (setAmountPrecision 0 $ usd 1)
|
||||||
,"amount with unit price" ~: do
|
,"amount with unit price" ~: do
|
||||||
assertParseEqual
|
assertParseEqual
|
||||||
(parseWithCtx nullctx amountp "$10 @ €0.5")
|
(parseWithCtx nullctx amountp "$10 @ €0.5")
|
||||||
(Mixed [usd 10 `withPrecision` 0 `at` (eur 0.5 `withPrecision` 1)])
|
(usd 10 `withPrecision` 0 `at` (eur 0.5 `withPrecision` 1))
|
||||||
,"amount with total price" ~: do
|
,"amount with total price" ~: do
|
||||||
assertParseEqual
|
assertParseEqual
|
||||||
(parseWithCtx nullctx amountp "$10 @@ €5")
|
(parseWithCtx nullctx amountp "$10 @@ €5")
|
||||||
(Mixed [usd 10 `withPrecision` 0 @@ (eur 5 `withPrecision` 0)])
|
(usd 10 `withPrecision` 0 @@ (eur 5 `withPrecision` 0))
|
||||||
]
|
]
|
||||||
|
|
||||||
-- | Run the amount parser on a string to get the result or an error.
|
-- | Parse an amount from a string, or get an error.
|
||||||
amountp' :: String -> MixedAmount
|
amountp' :: String -> Amount
|
||||||
amountp' s = either (error' . show) id $ parseWithCtx nullctx amountp s
|
amountp' s = either (error' . show) id $ parseWithCtx nullctx amountp s
|
||||||
|
|
||||||
leftsymbolamount :: GenParser Char JournalContext MixedAmount
|
-- | Parse a mixed amount from a string, or get an error.
|
||||||
|
mamountp' :: String -> MixedAmount
|
||||||
|
mamountp' = mixed . amountp'
|
||||||
|
|
||||||
|
leftsymbolamount :: GenParser Char JournalContext Amount
|
||||||
leftsymbolamount = do
|
leftsymbolamount = do
|
||||||
sign <- optionMaybe $ string "-"
|
sign <- optionMaybe $ string "-"
|
||||||
let applysign = if isJust sign then negate else id
|
let applysign = if isJust sign then negate else id
|
||||||
@ -602,20 +605,20 @@ leftsymbolamount = do
|
|||||||
(q,prec,dec,sep,seppos) <- number
|
(q,prec,dec,sep,seppos) <- number
|
||||||
let s = amountstyle{ascommodityside=L, ascommodityspaced=not $ null sp, asdecimalpoint=dec, asprecision=prec, asseparator=sep, asseparatorpositions=seppos}
|
let s = amountstyle{ascommodityside=L, ascommodityspaced=not $ null sp, asdecimalpoint=dec, asprecision=prec, asseparator=sep, asseparatorpositions=seppos}
|
||||||
p <- priceamount
|
p <- priceamount
|
||||||
return $ applysign $ Mixed [Amount c q p s]
|
return $ applysign $ Amount c q p s
|
||||||
<?> "left-symbol amount"
|
<?> "left-symbol amount"
|
||||||
|
|
||||||
rightsymbolamount :: GenParser Char JournalContext MixedAmount
|
rightsymbolamount :: GenParser Char JournalContext Amount
|
||||||
rightsymbolamount = do
|
rightsymbolamount = do
|
||||||
(q,prec,dec,sep,seppos) <- number
|
(q,prec,dec,sep,seppos) <- number
|
||||||
sp <- many spacenonewline
|
sp <- many spacenonewline
|
||||||
c <- commoditysymbol
|
c <- commoditysymbol
|
||||||
p <- priceamount
|
p <- priceamount
|
||||||
let s = amountstyle{ascommodityside=R, ascommodityspaced=not $ null sp, asdecimalpoint=dec, asprecision=prec, asseparator=sep, asseparatorpositions=seppos}
|
let s = amountstyle{ascommodityside=R, ascommodityspaced=not $ null sp, asdecimalpoint=dec, asprecision=prec, asseparator=sep, asseparatorpositions=seppos}
|
||||||
return $ Mixed [Amount c q p s]
|
return $ Amount c q p s
|
||||||
<?> "right-symbol amount"
|
<?> "right-symbol amount"
|
||||||
|
|
||||||
nosymbolamount :: GenParser Char JournalContext MixedAmount
|
nosymbolamount :: GenParser Char JournalContext Amount
|
||||||
nosymbolamount = do
|
nosymbolamount = do
|
||||||
(q,prec,dec,sep,seppos) <- number
|
(q,prec,dec,sep,seppos) <- number
|
||||||
p <- priceamount
|
p <- priceamount
|
||||||
@ -623,7 +626,7 @@ nosymbolamount = do
|
|||||||
let (c,s) = case defcs of
|
let (c,s) = case defcs of
|
||||||
Just (c',s') -> (c',s')
|
Just (c',s') -> (c',s')
|
||||||
Nothing -> ("", amountstyle{asdecimalpoint=dec, asprecision=prec, asseparator=sep, asseparatorpositions=seppos})
|
Nothing -> ("", amountstyle{asdecimalpoint=dec, asprecision=prec, asseparator=sep, asseparatorpositions=seppos})
|
||||||
return $ Mixed [Amount c q p s]
|
return $ Amount c q p s
|
||||||
<?> "no-symbol amount"
|
<?> "no-symbol amount"
|
||||||
|
|
||||||
commoditysymbol :: GenParser Char JournalContext String
|
commoditysymbol :: GenParser Char JournalContext String
|
||||||
@ -655,7 +658,7 @@ priceamount =
|
|||||||
return $ UnitPrice a))
|
return $ UnitPrice a))
|
||||||
<|> return NoPrice
|
<|> return NoPrice
|
||||||
|
|
||||||
balanceassertion :: GenParser Char JournalContext (Maybe MixedAmount)
|
balanceassertion :: GenParser Char JournalContext (Maybe Amount)
|
||||||
balanceassertion =
|
balanceassertion =
|
||||||
try (do
|
try (do
|
||||||
many spacenonewline
|
many spacenonewline
|
||||||
@ -666,7 +669,7 @@ balanceassertion =
|
|||||||
<|> return Nothing
|
<|> return Nothing
|
||||||
|
|
||||||
-- http://ledger-cli.org/3.0/doc/ledger3.html#Fixing-Lot-Prices
|
-- http://ledger-cli.org/3.0/doc/ledger3.html#Fixing-Lot-Prices
|
||||||
fixedlotprice :: GenParser Char JournalContext (Maybe MixedAmount)
|
fixedlotprice :: GenParser Char JournalContext (Maybe Amount)
|
||||||
fixedlotprice =
|
fixedlotprice =
|
||||||
try (do
|
try (do
|
||||||
many spacenonewline
|
many spacenonewline
|
||||||
@ -885,7 +888,7 @@ tests_Hledger_Read_JournalReader = TestList $ concat [
|
|||||||
assertParse (parseWithCtx nullctx defaultyeardirective "Y 10001\n")
|
assertParse (parseWithCtx nullctx defaultyeardirective "Y 10001\n")
|
||||||
|
|
||||||
,"historicalpricedirective" ~:
|
,"historicalpricedirective" ~:
|
||||||
assertParseEqual (parseWithCtx nullctx historicalpricedirective "P 2004/05/01 XYZ $55.00\n") (HistoricalPrice (parsedate "2004/05/01") "XYZ" $ Mixed [usd 55])
|
assertParseEqual (parseWithCtx nullctx historicalpricedirective "P 2004/05/01 XYZ $55.00\n") (HistoricalPrice (parsedate "2004/05/01") "XYZ" $ usd 55)
|
||||||
|
|
||||||
,"ignoredpricecommoditydirective" ~: do
|
,"ignoredpricecommoditydirective" ~: do
|
||||||
assertParse (parseWithCtx nullctx ignoredpricecommoditydirective "N $\n")
|
assertParse (parseWithCtx nullctx ignoredpricecommoditydirective "N $\n")
|
||||||
@ -910,16 +913,16 @@ tests_Hledger_Read_JournalReader = TestList $ concat [
|
|||||||
assertBool "accountname rejects an empty trailing component" (isLeft $ parsewith accountname "a:b:")
|
assertBool "accountname rejects an empty trailing component" (isLeft $ parsewith accountname "a:b:")
|
||||||
|
|
||||||
,"leftsymbolamount" ~: do
|
,"leftsymbolamount" ~: do
|
||||||
assertParseEqual (parseWithCtx nullctx leftsymbolamount "$1") (Mixed [usd 1 `withPrecision` 0])
|
assertParseEqual (parseWithCtx nullctx leftsymbolamount "$1") (usd 1 `withPrecision` 0)
|
||||||
assertParseEqual (parseWithCtx nullctx leftsymbolamount "$-1") (Mixed [usd (-1) `withPrecision` 0])
|
assertParseEqual (parseWithCtx nullctx leftsymbolamount "$-1") (usd (-1) `withPrecision` 0)
|
||||||
assertParseEqual (parseWithCtx nullctx leftsymbolamount "-$1") (Mixed [usd (-1) `withPrecision` 0])
|
assertParseEqual (parseWithCtx nullctx leftsymbolamount "-$1") (usd (-1) `withPrecision` 0)
|
||||||
|
|
||||||
,"amount" ~: do
|
,"amount" ~: do
|
||||||
let -- | compare a parse result with a MixedAmount, showing the debug representation for clarity
|
let -- | compare a parse result with an expected amount, showing the debug representation for clarity
|
||||||
assertMixedAmountParse parseresult mixedamount =
|
assertAmountParse parseresult amount =
|
||||||
(either (const "parse error") showMixedAmountDebug parseresult) ~?= (showMixedAmountDebug mixedamount)
|
(either (const "parse error") showAmountDebug parseresult) ~?= (showAmountDebug amount)
|
||||||
assertMixedAmountParse (parseWithCtx nullctx amountp "1 @ $2")
|
assertAmountParse (parseWithCtx nullctx amountp "1 @ $2")
|
||||||
(Mixed [amt 1 `withPrecision` 0 `at` (usd 2 `withPrecision` 0)])
|
(amt 1 `withPrecision` 0 `at` (usd 2 `withPrecision` 0))
|
||||||
|
|
||||||
]]
|
]]
|
||||||
|
|
||||||
|
|||||||
@ -61,7 +61,7 @@ import Text.ParserCombinators.Parsec
|
|||||||
import Text.Printf
|
import Text.Printf
|
||||||
|
|
||||||
import Hledger.Data
|
import Hledger.Data
|
||||||
import Hledger.Read (amountp')
|
import Hledger.Read (mamountp')
|
||||||
import Hledger.Query
|
import Hledger.Query
|
||||||
import Hledger.Utils
|
import Hledger.Utils
|
||||||
|
|
||||||
@ -759,36 +759,36 @@ tests_accountsReport =
|
|||||||
,"accountsReport with no args on sample journal" ~: do
|
,"accountsReport with no args on sample journal" ~: do
|
||||||
(defreportopts, samplejournal) `gives`
|
(defreportopts, samplejournal) `gives`
|
||||||
([
|
([
|
||||||
("assets","assets",0, amountp' "$-1.00")
|
("assets","assets",0, mamountp' "$-1.00")
|
||||||
,("assets:bank:saving","bank:saving",1, amountp' "$1.00")
|
,("assets:bank:saving","bank:saving",1, mamountp' "$1.00")
|
||||||
,("assets:cash","cash",1, amountp' "$-2.00")
|
,("assets:cash","cash",1, mamountp' "$-2.00")
|
||||||
,("expenses","expenses",0, amountp' "$2.00")
|
,("expenses","expenses",0, mamountp' "$2.00")
|
||||||
,("expenses:food","food",1, amountp' "$1.00")
|
,("expenses:food","food",1, mamountp' "$1.00")
|
||||||
,("expenses:supplies","supplies",1, amountp' "$1.00")
|
,("expenses:supplies","supplies",1, mamountp' "$1.00")
|
||||||
,("income","income",0, amountp' "$-2.00")
|
,("income","income",0, mamountp' "$-2.00")
|
||||||
,("income:gifts","gifts",1, amountp' "$-1.00")
|
,("income:gifts","gifts",1, mamountp' "$-1.00")
|
||||||
,("income:salary","salary",1, amountp' "$-1.00")
|
,("income:salary","salary",1, mamountp' "$-1.00")
|
||||||
,("liabilities:debts","liabilities:debts",0, amountp' "$1.00")
|
,("liabilities:debts","liabilities:debts",0, mamountp' "$1.00")
|
||||||
],
|
],
|
||||||
Mixed [nullamt])
|
Mixed [nullamt])
|
||||||
|
|
||||||
,"accountsReport with --depth=N" ~: do
|
,"accountsReport with --depth=N" ~: do
|
||||||
(defreportopts{depth_=Just 1}, samplejournal) `gives`
|
(defreportopts{depth_=Just 1}, samplejournal) `gives`
|
||||||
([
|
([
|
||||||
("assets", "assets", 0, amountp' "$-1.00")
|
("assets", "assets", 0, mamountp' "$-1.00")
|
||||||
,("expenses", "expenses", 0, amountp' "$2.00")
|
,("expenses", "expenses", 0, mamountp' "$2.00")
|
||||||
,("income", "income", 0, amountp' "$-2.00")
|
,("income", "income", 0, mamountp' "$-2.00")
|
||||||
,("liabilities", "liabilities", 0, amountp' "$1.00")
|
,("liabilities", "liabilities", 0, mamountp' "$1.00")
|
||||||
],
|
],
|
||||||
Mixed [nullamt])
|
Mixed [nullamt])
|
||||||
|
|
||||||
,"accountsReport with depth:N" ~: do
|
,"accountsReport with depth:N" ~: do
|
||||||
(defreportopts{query_="depth:1"}, samplejournal) `gives`
|
(defreportopts{query_="depth:1"}, samplejournal) `gives`
|
||||||
([
|
([
|
||||||
("assets", "assets", 0, amountp' "$-1.00")
|
("assets", "assets", 0, mamountp' "$-1.00")
|
||||||
,("expenses", "expenses", 0, amountp' "$2.00")
|
,("expenses", "expenses", 0, mamountp' "$2.00")
|
||||||
,("income", "income", 0, amountp' "$-2.00")
|
,("income", "income", 0, mamountp' "$-2.00")
|
||||||
,("liabilities", "liabilities", 0, amountp' "$1.00")
|
,("liabilities", "liabilities", 0, mamountp' "$1.00")
|
||||||
],
|
],
|
||||||
Mixed [nullamt])
|
Mixed [nullamt])
|
||||||
|
|
||||||
@ -798,32 +798,32 @@ tests_accountsReport =
|
|||||||
Mixed [nullamt])
|
Mixed [nullamt])
|
||||||
(defreportopts{query_="edate:'in 2009'"}, samplejournal2) `gives`
|
(defreportopts{query_="edate:'in 2009'"}, samplejournal2) `gives`
|
||||||
([
|
([
|
||||||
("assets:bank:checking","assets:bank:checking",0,amountp' "$1.00")
|
("assets:bank:checking","assets:bank:checking",0,mamountp' "$1.00")
|
||||||
,("income:salary","income:salary",0,amountp' "$-1.00")
|
,("income:salary","income:salary",0,mamountp' "$-1.00")
|
||||||
],
|
],
|
||||||
Mixed [nullamt])
|
Mixed [nullamt])
|
||||||
|
|
||||||
,"accountsReport with desc:" ~: do
|
,"accountsReport with desc:" ~: do
|
||||||
(defreportopts{query_="desc:income"}, samplejournal) `gives`
|
(defreportopts{query_="desc:income"}, samplejournal) `gives`
|
||||||
([
|
([
|
||||||
("assets:bank:checking","assets:bank:checking",0,amountp' "$1.00")
|
("assets:bank:checking","assets:bank:checking",0,mamountp' "$1.00")
|
||||||
,("income:salary","income:salary",0, amountp' "$-1.00")
|
,("income:salary","income:salary",0, mamountp' "$-1.00")
|
||||||
],
|
],
|
||||||
Mixed [nullamt])
|
Mixed [nullamt])
|
||||||
|
|
||||||
,"accountsReport with not:desc:" ~: do
|
,"accountsReport with not:desc:" ~: do
|
||||||
(defreportopts{query_="not:desc:income"}, samplejournal) `gives`
|
(defreportopts{query_="not:desc:income"}, samplejournal) `gives`
|
||||||
([
|
([
|
||||||
("assets","assets",0, amountp' "$-2.00")
|
("assets","assets",0, mamountp' "$-2.00")
|
||||||
,("assets:bank","bank",1, Mixed [nullamt])
|
,("assets:bank","bank",1, Mixed [nullamt])
|
||||||
,("assets:bank:checking","checking",2,amountp' "$-1.00")
|
,("assets:bank:checking","checking",2,mamountp' "$-1.00")
|
||||||
,("assets:bank:saving","saving",2, amountp' "$1.00")
|
,("assets:bank:saving","saving",2, mamountp' "$1.00")
|
||||||
,("assets:cash","cash",1, amountp' "$-2.00")
|
,("assets:cash","cash",1, mamountp' "$-2.00")
|
||||||
,("expenses","expenses",0, amountp' "$2.00")
|
,("expenses","expenses",0, mamountp' "$2.00")
|
||||||
,("expenses:food","food",1, amountp' "$1.00")
|
,("expenses:food","food",1, mamountp' "$1.00")
|
||||||
,("expenses:supplies","supplies",1, amountp' "$1.00")
|
,("expenses:supplies","supplies",1, mamountp' "$1.00")
|
||||||
,("income:gifts","income:gifts",0, amountp' "$-1.00")
|
,("income:gifts","income:gifts",0, mamountp' "$-1.00")
|
||||||
,("liabilities:debts","liabilities:debts",0, amountp' "$1.00")
|
,("liabilities:debts","liabilities:debts",0, mamountp' "$1.00")
|
||||||
],
|
],
|
||||||
Mixed [nullamt])
|
Mixed [nullamt])
|
||||||
|
|
||||||
|
|||||||
@ -527,7 +527,7 @@ handleAdd = do
|
|||||||
acct1E = maybe (Left "to account required") (Right . unpack) $ maybeNonNull acct1M
|
acct1E = maybe (Left "to account required") (Right . unpack) $ maybeNonNull acct1M
|
||||||
acct2E = maybe (Left "from account required") (Right . unpack) $ maybeNonNull acct2M
|
acct2E = maybe (Left "from account required") (Right . unpack) $ maybeNonNull acct2M
|
||||||
amt1E = maybe (Left "amount required") (either (const $ Left "could not parse amount") Right . parseWithCtx nullctx amountp . unpack) amt1M
|
amt1E = maybe (Left "amount required") (either (const $ Left "could not parse amount") Right . parseWithCtx nullctx amountp . unpack) amt1M
|
||||||
amt2E = maybe (Right missingmixedamt) (either (const $ Left "could not parse amount") Right . parseWithCtx nullctx amountp . unpack) amt2M
|
amt2E = maybe (Right missingamt) (either (const $ Left "could not parse amount") Right . parseWithCtx nullctx amountp . unpack) amt2M
|
||||||
journalE = maybe (Right $ journalFilePath j)
|
journalE = maybe (Right $ journalFilePath j)
|
||||||
(\f -> let f' = unpack f in
|
(\f -> let f' = unpack f in
|
||||||
if f' `elem` journalFilePaths j
|
if f' `elem` journalFilePaths j
|
||||||
@ -547,8 +547,8 @@ handleAdd = do
|
|||||||
tdate=parsedate date
|
tdate=parsedate date
|
||||||
,tdescription=desc
|
,tdescription=desc
|
||||||
,tpostings=[
|
,tpostings=[
|
||||||
Posting False acct1 amt1 "" RegularPosting [] Nothing
|
Posting False acct1 (mixed amt1) "" RegularPosting [] Nothing
|
||||||
,Posting False acct2 amt2 "" RegularPosting [] Nothing
|
,Posting False acct2 (mixed amt2) "" RegularPosting [] Nothing
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
-- display errors or add transaction
|
-- display errors or add transaction
|
||||||
|
|||||||
@ -17,7 +17,6 @@ import Data.Char (toUpper)
|
|||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
import Data.Time.Calendar
|
import Data.Time.Calendar
|
||||||
import Safe (headMay)
|
|
||||||
import System.Console.Haskeline (InputT, runInputT, defaultSettings, setComplete, getInputLine)
|
import System.Console.Haskeline (InputT, runInputT, defaultSettings, setComplete, getInputLine)
|
||||||
import System.Console.Haskeline.Completion
|
import System.Console.Haskeline.Completion
|
||||||
import System.IO ( stderr, hPutStrLn, hPutStr )
|
import System.IO ( stderr, hPutStrLn, hPutStr )
|
||||||
@ -148,22 +147,20 @@ getPostings st enteredps = do
|
|||||||
-- I think 1 or 4, whichever would show the most decimal places
|
-- I think 1 or 4, whichever would show the most decimal places
|
||||||
p = maxprecisionwithpoint
|
p = maxprecisionwithpoint
|
||||||
amountstr <- runInteractionDefault $ askFor (printf "amount %d" n) defaultamountstr validateamount
|
amountstr <- runInteractionDefault $ askFor (printf "amount %d" n) defaultamountstr validateamount
|
||||||
let a = fromparse $ runParser (amountp <|> return missingmixedamt) ctx "" amountstr
|
let a = fromparse $ runParser (amountp <|> return missingamt) ctx "" amountstr
|
||||||
a' = fromparse $ runParser (amountp <|> return missingmixedamt) nullctx "" amountstr
|
a' = fromparse $ runParser (amountp <|> return missingamt) nullctx "" amountstr
|
||||||
defaultamtused = Just (showMixedAmount a) == defaultamountstr
|
wasdefaultamtused = Just (showAmount a) == defaultamountstr
|
||||||
commodityadded | c == cwithnodef = Nothing
|
defaultcommodityadded | acommodity a == acommodity a' = Nothing
|
||||||
| otherwise = c
|
| otherwise = Just $ acommodity a
|
||||||
where c = maybemixedamountcommodity a
|
p = nullposting{paccount=stripbrackets account
|
||||||
cwithnodef = maybemixedamountcommodity a'
|
,pamount=mixed a
|
||||||
maybemixedamountcommodity = maybe Nothing (Just . acommodity) . headMay . amounts
|
,ptype=postingtype account
|
||||||
p = nullposting{paccount=stripbrackets account,
|
}
|
||||||
pamount=a,
|
st' = if wasdefaultamtused
|
||||||
ptype=postingtype account}
|
then st
|
||||||
st' = if defaultamtused then st
|
else st{psHistory=historicalps', psSuggestHistoricalAmount=False}
|
||||||
else st{psHistory = historicalps',
|
when (isJust defaultcommodityadded) $
|
||||||
psSuggestHistoricalAmount = False}
|
liftIO $ hPutStrLn stderr $ printf "using default commodity (%s)" (fromJust defaultcommodityadded)
|
||||||
when (isJust commodityadded) $
|
|
||||||
liftIO $ hPutStrLn stderr $ printf "using default commodity (%s)" (fromJust commodityadded)
|
|
||||||
getPostings st' (enteredps ++ [p])
|
getPostings st' (enteredps ++ [p])
|
||||||
where
|
where
|
||||||
j = psJournal st
|
j = psJournal st
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user