{-# LANGUAGE OverloadedStrings #-}
module Inflation (nominalToRealInflationAdjuster) where
import Text.Pandoc
import Text.Printf (printf, PrintfArg)
import Data.List (intercalate, unfoldr)
-- Experimental module for implementing automatic inflation adjustment of nominal date-stamped dollar amounts to provide real prices; this is particularly critical in any economics or technology discussion where a nominal price from 1950 is 11x a 2019 real price!
{- Examples:
Markdown~>HTML:
'[$50.50]($1970)'
~>
'$50.501970$343.83'
Testbed:
$ echo '[$50.50]($1970)' | pandoc -w native
[Para [Link ("",[],[]) [Str "$50.50"] ("$1970","")]]
> nominalToRealInflationAdjuster $ Link ("",[],[]) [Str "$50.50"] ("$1970","")
Span ("",["inflationAdjusted"],[("originalYear","1970"),("originalAmount","50.50"),("currentYear","2019"),("currentAmount","343.83")]) [Str "$50.50",Subscript [Str "1970"],Superscript [Str "$343.83"]]
$ echo 'Span ("",["inflationAdjusted"],[("originalYear","1970"),("originalAmount","50.50"),("currentYear","2019"),("currentAmount","343.83")]) [Str "$50.50",Subscript [Str "1970"],Superscript [Str "$343.83"]]' | pandoc -f native -w html
$50.501970$343.83
-}
minPercentage :: Float
minPercentage = 1 + 0.15
currentYear :: Int
currentYear = 2019
nominalToRealInflationAdjuster :: Inline -> Inline
nominalToRealInflationAdjuster (Link _ text (target, _))
| head target == '$' = if (adjustedDollar / oldDollar) < minPercentage
then Str ("$"++oldDollarString) -- if the adjustment is <15%, don't bother, it's not misleading enough yet to need adjusting
else Span ("",
["inflationAdjusted"],
-- provide all 4 variables as metadata the tags for CSS or JS processing
[("originalYear",oldYear),("originalAmount",oldDollarString),
("currentYear",show currentYear),("currentAmount",adjustedDollarString)])
-- [Str ("$" ++ oldDollarString), Subscript [Str oldYear, Superscript [Str ("$"++adjustedDollarString)]]]
[Str ("$"++oldDollarString), Math InlineMath ("_{"++oldYear++"}^{\\$"++adjustedDollarString++"}")]
where oldYear = tail target -- '$1970' ~> '1970'
oldDollarString = filter (/= '$') $ inlinesToString text -- '$50.50' ~> '50.50'
oldDollar = read (filter (/=',') oldDollarString) :: Float
-- control potentially spurious precision:
-- round to 2 digits when converting to String if a decimal was present and the inflation factor is <10x, otherwise, round to whole numbers.
-- So, '$1.05' becomes '$20.55', but '$1' becomes '$20' instead of '$20.2359002', and '$0.05' can still become '$0.97'
precision = if ('.' `elem` oldDollarString) && ((adjustedDollar < 10*oldDollar) || (adjustedDollar < 1)) then "2" else "0"
adjustedDollar = dollarAdjust oldDollar oldYear
adjustedDollarString = formatDecimal adjustedDollar precision
nominalToRealInflationAdjuster x = x
inlinesToString :: [Inline] -> String
inlinesToString = concatMap go
where go x = case x of
Str s -> s
Code _ s -> s
_ -> " "
-- dollarAdjust "5.50" "1950" ~> "59.84"
dollarAdjust :: Float -> String -> Float
dollarAdjust amount year = let oldYear = read year :: Int in inflationAdjustUS amount oldYear currentYear
-- http://www.usinflationcalculator.com/inflation/consumer-price-index-and-annual-percent-changes-from-1913-to-2008/
-- 0th: 1913 ... 104th: 2017; repeat last inflation rate indefinitely to project forward for 2018+
inflationRatesUS :: [Float]
inflationRatesUS = [0.0,1.0,2.0,12.6,18.1,20.4,14.5,2.6,-10.8,-2.3,2.4,0.0,3.5,-1.1,-2.3,-1.2,0.6,-6.4,-9.3,-10.3,0.8,1.5,3.0,1.4,2.9,-2.8,0.0,0.7,9.9,9.0,3.0,2.3,2.2,18.1,8.8,3.0,-2.1,5.9,6.0,0.8,0.7,-0.7,0.4,3.0,2.9,1.8,1.7,1.4,0.7,1.3,1.6,1.0,1.9,3.5,3.0,4.7,6.2,5.6,3.3,3.4,8.7,12.3,6.9,4.9,6.7,9.0,13.3,12.5,8.9,3.8,3.8,3.9,3.8,1.1,4.4,4.4,4.6,6.1,3.1,2.9,2.7,2.7,2.5,3.3,1.7,1.6,2.7,3.4,1.6,2.4,1.9,3.3,3.4,2.5,4.1,0.1,2.7,1.5,3.0,1.7,1.5,0.8,0.7,2.1,2.1] ++ repeat 2.1
-- inflationAdjustUS 1 1950 2019 ~> 10.88084
-- inflationAdjustUS 5.50 1950 2019 ~> 59.84462
inflationAdjustUS :: Float -> Int -> Int -> Float
inflationAdjustUS d yOld yCurrent = if yOld>=1913 && yCurrent>=1913 then d * totalFactor else d
where slice from to xs = take (to - from + 1) (drop from xs)
percents = slice (yOld-1913) (yCurrent-1913) inflationRatesUS
rates = map (\r -> 1 + (r/100)) percents
totalFactor = product rates
-- prettyprint decimals with commas for generating larger amounts like "$50,000"
-- https://stackoverflow.com/a/4408556
formatDecimal :: (Ord a, Fractional a, Text.Printf.PrintfArg a) => a -> String -> String
formatDecimal d prec
| d < 0.0 = "-" ++ formatPositiveDecimal (-d)
| otherwise = formatPositiveDecimal d
where formatPositiveDecimal = uncurry (++) . mapFst addCommas . span (/= '.') . printf ("%0."++ prec ++ "f")
addCommas = reverse . intercalate "," . unfoldr splitIntoBlocksOfThree . reverse
splitIntoBlocksOfThree l = case splitAt 3 l of ([], _) -> Nothing; p-> Just p
-- https://hackage.haskell.org/package/fgl-5.7.0.1/docs/src/Data.Graph.Inductive.Query.Monad.html#mapFst
mapFst :: (a -> b) -> (a, c) -> (b, c)
mapFst f (x,y) = (f x,y)