{-# 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)