{-|
An 'Amount' is some quantity of money, shares, or anything else.
A simple amount is a 'Commodity', quantity pair:
@
$1
£-50
EUR 3.44
GOOG 500
1.5h
90apples
0
@
A 'MixedAmount' is zero or more simple amounts:
@
$50, EUR 3, AAPL 500
16h, $13.55, oranges 6
@
Not implemented:
Commodities may be convertible or not. A mixed amount containing only
convertible commodities can be converted to a simple amount. Arithmetic
examples:
@
$1 - $5 = $-4
$1 + EUR 0.76 = $2
EUR0.76 + $1 = EUR 1.52
EUR0.76 - $1 = 0
($5, 2h) + $1 = ($6, 2h)
($50, EUR 3, AAPL 500) + ($13.55, oranges 6) = $67.51, AAPL 500, oranges 6
($50, EUR 3) * $-1 = $-53.96
($50, AAPL 500) * $-1 = error
@
-}moduleLedger.AmountwhereimportqualifiedData.MapasMapimportLedger.UtilsimportLedger.TypesimportLedger.CommodityinstanceShowAmountwhereshow=showAmountinstanceShowMixedAmountwhereshow=showMixedAmountinstanceNumAmountwhereabs(Amountcqp)=Amountc(absq)psignum(Amountcqp)=Amountc(signumq)pfromIntegeri=Amount(comm"")(fromIntegeri)Nothing(+)=amountop(+)(-)=amountop(-)(*)=amountop(*)instanceOrdAmountwherecompare(Amountacaqap)(Amountbcbqbp)=compare(ac,aq,ap)(bc,bq,bp)instanceNumMixedAmountwherefromIntegeri=Mixed[Amount(comm"")(fromIntegeri)Nothing]negate(Mixedas)=Mixed$mapnegateAmountPreservingPriceas(+)(Mixedas)(Mixedbs)=normaliseMixedAmount$Mixed$as++bs(*)=error"programming error, mixed amounts do not support multiplication"abs=error"programming error, mixed amounts do not support abs"signum=error"programming error, mixed amounts do not support signum"instanceOrdMixedAmountwherecompare(Mixedas)(Mixedbs)=compareasbsnegateAmountPreservingPricea=(-a){price=pricea}-- | Apply a binary arithmetic operator to two amounts - converting to the-- second one's commodity, adopting the lowest precision, and discarding-- any price information. (Using the second commodity is best since sum-- and other folds start with a no-commodity amount.)amountop::(Double->Double->Double)->Amount->Amount->Amountamountopopa@(Amountacaqap)b@(Amountbcbqbp)=Amountbc((quantity$convertAmountTobca)`op`bq)Nothing-- | Convert an amount to the commodity of its saved price, if any.costOfAmount::Amount->AmountcostOfAmounta@(Amount__Nothing)=acostOfAmounta@(Amount_q(Justprice))|isZeroMixedAmountprice=nullamt|otherwise=Amountpc(pq*q)Nothingwhere(Amountpcpq_)=head$amountsprice-- | Convert an amount to the specified commodity using the appropriate-- exchange rate (which is currently always 1).convertAmountTo::Commodity->Amount->AmountconvertAmountToc2(Amountc1qp)=Amountc2(q*conversionRatec1c2)Nothing-- | Get the string representation of an amount, based on its commodity's-- display settings.showAmount::Amount->StringshowAmounta@(Amount(Commodity{symbol=sym,side=side,spaced=spaced})qpri)|sym=="AUTO"=""-- can display one of these in an error message|side==L=printf"%s%s%s%s"symspacequantityprice|side==R=printf"%s%s%s%s"quantityspacesympricewherespace=ifspacedthen" "else""quantity=showAmount'aprice=casepriof(Justpamt)->" @ "++showMixedAmountpamtNothing->""-- | Get the string representation (of the number part of) of an amountshowAmount'::Amount->StringshowAmount'(Amount(Commodity{comma=comma,precision=p})q_)=quantitywherequantity=commad$printf("%."++showp++"f")qcommad=ifcommathenpunctuatethousandselseid-- | Add thousands-separating commas to a decimal number stringpunctuatethousands::String->Stringpunctuatethousandss=sign++(addcommasint)++fracwhere(sign,num)=breakisDigits(int,frac)=break(=='.')numaddcommas=reverse.concat.intersperse",".triples.reversetriples[]=[]triplesl=[take3l]++(triples$drop3l)-- | Does this amount appear to be zero when displayed with its given precision ?isZeroAmount::Amount->BoolisZeroAmounta=nonzerodigits==""wherenonzerodigits=filter(`elem`"123456789")$showAmounta-- | Access a mixed amount's components.amounts::MixedAmount->[Amount]amounts(Mixedas)=as-- | Does this mixed amount appear to be zero - empty, or-- containing only simple amounts which appear to be zero ?isZeroMixedAmount::MixedAmount->BoolisZeroMixedAmount=allisZeroAmount.amounts.normaliseMixedAmount-- | MixedAmount derives Eq in Types.hs, but that doesn't know that we-- want $0 = EUR0 = 0. Yet we don't want to drag all this code in there.-- When zero equality is important, use this, for now; should be used-- everywhere.mixedAmountEquals::MixedAmount->MixedAmount->BoolmixedAmountEqualsab=amountsa'==amountsb'||(isZeroMixedAmounta'&&isZeroMixedAmountb')wherea'=normaliseMixedAmountab'=normaliseMixedAmountb-- | Get the string representation of a mixed amount, showing each of-- its component amounts. NB a mixed amount can have an empty amounts-- list in which case it shows as \"\".showMixedAmount::MixedAmount->StringshowMixedAmountm=concat$intersperse"\n"$mapshowfixedwidthaswhere(Mixedas)=normaliseMixedAmountmwidth=maximum$map(length.show)$asshowfixedwidth=printf(printf"%%%ds"width).show-- | Get the string representation of a mixed amount, and if it-- appears to be all zero just show a bare 0, ledger-style.showMixedAmountOrZero::MixedAmount->StringshowMixedAmountOrZeroa|isZeroMixedAmounta="0"|otherwise=showMixedAmounta-- | Simplify a mixed amount by combining any component amounts which have-- the same commodity and the same price. Also removes redundant zero amounts-- and adds a single zero amount if there are no amounts at all.normaliseMixedAmount::MixedAmount->MixedAmountnormaliseMixedAmount(Mixedas)=Mixedas''whereas''=mapsumAmountsPreservingPrice$group$sortas'sort=sortBycmpsymbolandpricecmpsymbolandpricea1a2=compare(syma1,pricea1)(syma2,pricea2)group=groupBysamesymbolandpricesamesymbolandpricea1a2=(syma1==syma2)&&(pricea1==pricea2)sym=symbol.commodityas'|nullnonzeros=[head$zeros++[nullamt]]|otherwise=nonzeros(zeros,nonzeros)=partitionisZeroAmountassumAmountsPreservingPrice[]=nullamtsumAmountsPreservingPriceas=(sumas){price=price$headas}-- | Convert a mixed amount's component amounts to the commodity of their-- saved price, if any.costOfMixedAmount::MixedAmount->MixedAmountcostOfMixedAmount(Mixedas)=Mixed$mapcostOfAmountas-- | The empty simple amount.nullamt::Amountnullamt=Amountunknown0Nothing-- | The empty mixed amount.nullmixedamt::MixedAmountnullmixedamt=Mixed[]-- | A temporary value for parsed transactions which had no amount specified.missingamt::MixedAmountmissingamt=Mixed[Amount(Commodity{symbol="AUTO",side=L,spaced=False,comma=False,precision=0})0Nothing]