    {-|

    Convert amounts to some related value in various ways. This involves
    looking up historical market prices (exchange rates) between commodities.

    -}

    {-# LANGUAGE NamedFieldPuns #-}
    {-# LANGUAGE OverloadedStrings #-}
    {-# LANGUAGE RecordWildCards #-}
    {-# LANGUAGE ScopedTypeVariables #-}
    {-# LANGUAGE DeriveGeneric #-}

    module Hledger.Data.Valuation (
       ValuationType(..)
      ,PriceOracle
      ,journalPriceOracle
      ,unsupportedValueThenError
      -- ,amountApplyValuation
      -- ,amountValueAtDate
      ,mixedAmountApplyValuation
      ,mixedAmountValueAtDate
      ,marketPriceReverse
      ,priceDirectiveToMarketPrice
      -- ,priceLookup
      ,tests_Valuation
    )
    where

    import Control.Applicative ((<|>))
    import Data.Decimal (roundTo)
    import Data.Function ((&), on)
    import Data.Graph.Inductive  (Gr, Node, NodeMap, mkMapGraph, mkNode, lab, out, sp)
    import Data.List
    import Data.List.Extra (nubSortBy)
    import qualified Data.Map as M
    import Data.Maybe
    import qualified Data.Text as T
    import Data.Time.Calendar (Day, fromGregorian)
    import Data.MemoUgly (memo)
    import GHC.Generics (Generic)
    import Safe (headMay)

    import Hledger.Utils
    import Hledger.Data.Types
    import Hledger.Data.Amount

    ------------------------------------------------------------------------------
    -- Types

    -- | What kind of value conversion should be done on amounts ?
    -- CLI: --value=cost|then|end|now|DATE[,COMM]
    data ValuationType =
        AtCost     (Maybe CommoditySymbol)  -- ^ convert to cost commodity using transaction prices, then optionally to given commodity using market prices at posting date
      | AtThen     (Maybe CommoditySymbol)  -- ^ convert to default or given valuation commodity, using market prices at each posting's date
      | AtEnd      (Maybe CommoditySymbol)  -- ^ convert to default or given valuation commodity, using market prices at period end(s)
      | AtNow      (Maybe CommoditySymbol)  -- ^ convert to default or given valuation commodity, using current market prices
      | AtDate Day (Maybe CommoditySymbol)  -- ^ convert to default or given valuation commodity, using market prices on some date
      | AtDefault  (Maybe CommoditySymbol)  -- ^ works like AtNow in single period reports, like AtEnd in multiperiod reports
      deriving (Int -> ValuationType -> ShowS
    [ValuationType] -> ShowS
    ValuationType -> String
    (Int -> ValuationType -> ShowS)
    -> (ValuationType -> String)
    -> ([ValuationType] -> ShowS)
    -> Show ValuationType
    forall a.
    (Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
    showList :: [ValuationType] -> ShowS
    $cshowList :: [ValuationType] -> ShowS
    show :: ValuationType -> String
    $cshow :: ValuationType -> String
    showsPrec :: Int -> ValuationType -> ShowS
    $cshowsPrec :: Int -> ValuationType -> ShowS
    Show,ValuationType -> ValuationType -> Bool
    (ValuationType -> ValuationType -> Bool)
    -> (ValuationType -> ValuationType -> Bool) -> Eq ValuationType
    forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
    /= :: ValuationType -> ValuationType -> Bool
    $c/= :: ValuationType -> ValuationType -> Bool
    == :: ValuationType -> ValuationType -> Bool
    $c== :: ValuationType -> ValuationType -> Bool
    Eq)

    -- | A snapshot of the known exchange rates between commodity pairs at a given date,
    -- as a graph allowing fast lookup and path finding, along with some helper data.
    data PriceGraph = PriceGraph {
       PriceGraph -> Gr CommoditySymbol Quantity
    prGraph   :: Gr CommoditySymbol Quantity
        -- ^ A directed graph of exchange rates between commodity pairs.
        -- Node labels are commodities and edge labels are exchange rates,
        -- which were either:
        -- declared by P directives,
        -- inferred from transaction prices,
        -- inferred by reversing a declared rate,
        -- or inferred by reversing a transaction-inferred rate.
        -- There will be at most one edge between each directed pair of commodities,
        -- eg there can be one USD->EUR and one EUR->USD.
      ,PriceGraph -> NodeMap CommoditySymbol
    prNodemap :: NodeMap CommoditySymbol
        -- ^ Mapping of graph node ids to commodity symbols.
      ,PriceGraph -> Map CommoditySymbol CommoditySymbol
    prDefaultValuationCommodities :: M.Map CommoditySymbol CommoditySymbol
        -- ^ The default valuation commodity for each source commodity.
        --   These are used when a valuation commodity is not specified
        --   (-V). They are the destination commodity of the latest
        --   (declared or inferred, but not reverse) each
        --   source commodity's latest market price (on the date of this
        --   graph).
      }
      deriving (Int -> PriceGraph -> ShowS
    [PriceGraph] -> ShowS
    PriceGraph -> String
    (Int -> PriceGraph -> ShowS)
    -> (PriceGraph -> String)
    -> ([PriceGraph] -> ShowS)
    -> Show PriceGraph
    forall a.
    (Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
    showList :: [PriceGraph] -> ShowS
    $cshowList :: [PriceGraph] -> ShowS
    show :: PriceGraph -> String
    $cshow :: PriceGraph -> String
    showsPrec :: Int -> PriceGraph -> ShowS
    $cshowsPrec :: Int -> PriceGraph -> ShowS
    Show,(forall x. PriceGraph -> Rep PriceGraph x)
    -> (forall x. Rep PriceGraph x -> PriceGraph) -> Generic PriceGraph
    forall x. Rep PriceGraph x -> PriceGraph
    forall x. PriceGraph -> Rep PriceGraph x
    forall a.
    (forall x. a -> Rep a x) -> (forall x. Rep a x -> a) -> Generic a
    $cto :: forall x. Rep PriceGraph x -> PriceGraph
    $cfrom :: forall x. PriceGraph -> Rep PriceGraph x
    Generic)

    -- | A price oracle is a magic memoising function that efficiently
    -- looks up market prices (exchange rates) from one commodity to
    -- another (or if unspecified, to a default valuation commodity) on a
    -- given date.
    type PriceOracle = (Day, CommoditySymbol, Maybe CommoditySymbol) -> Maybe (CommoditySymbol, Quantity)

    -- | Generate a price oracle (memoising price lookup function) from a
    -- journal's directive-declared and transaction-inferred market
    -- prices. For best performance, generate this only once per journal,
    -- reusing it across reports if there are more than one, as
    -- compoundBalanceCommand does.
    -- The boolean argument is whether to infer market prices from
    -- transactions or not.
    journalPriceOracle :: Bool -> Journal -> PriceOracle
    journalPriceOracle :: Bool -> Journal -> PriceOracle
    journalPriceOracle infer :: Bool
    infer Journal{[PriceDirective]
    jpricedirectives :: Journal -> [PriceDirective]
    jpricedirectives :: [PriceDirective]
    jpricedirectives, [MarketPrice]
    jinferredmarketprices :: Journal -> [MarketPrice]
    jinferredmarketprices :: [MarketPrice]
    jinferredmarketprices} =
      let
        declaredprices :: [MarketPrice]
    declaredprices = (PriceDirective -> MarketPrice)
    -> [PriceDirective] -> [MarketPrice]
    forall a b. (a -> b) -> [a] -> [b]
    map PriceDirective -> MarketPrice
    priceDirectiveToMarketPrice [PriceDirective]
    jpricedirectives
        inferredprices :: [MarketPrice]
    inferredprices = if Bool
    infer then [MarketPrice]
    jinferredmarketprices else []
        makepricegraph :: Day -> PriceGraph
    makepricegraph = (Day -> PriceGraph) -> Day -> PriceGraph
    forall a b. Ord a => (a -> b) -> a -> b
    memo ((Day -> PriceGraph) -> Day -> PriceGraph)
    -> (Day -> PriceGraph) -> Day -> PriceGraph
    forall a b. (a -> b) -> a -> b
    $ [MarketPrice] -> [MarketPrice] -> Day -> PriceGraph
    makePriceGraph [MarketPrice]
    declaredprices [MarketPrice]
    inferredprices
      in
        PriceOracle -> PriceOracle
    forall a b. Ord a => (a -> b) -> a -> b
    memo (PriceOracle -> PriceOracle) -> PriceOracle -> PriceOracle
    forall a b. (a -> b) -> a -> b
    $ (Day
     -> CommoditySymbol
     -> Maybe CommoditySymbol
     -> Maybe (CommoditySymbol, Quantity))
    -> PriceOracle
    forall a b c d. (a -> b -> c -> d) -> (a, b, c) -> d
    uncurry3 ((Day
      -> CommoditySymbol
      -> Maybe CommoditySymbol
      -> Maybe (CommoditySymbol, Quantity))
     -> PriceOracle)
    -> (Day
        -> CommoditySymbol
        -> Maybe CommoditySymbol
        -> Maybe (CommoditySymbol, Quantity))
    -> PriceOracle
    forall a b. (a -> b) -> a -> b
    $ (Day -> PriceGraph)
    -> Day
    -> CommoditySymbol
    -> Maybe CommoditySymbol
    -> Maybe (CommoditySymbol, Quantity)
    priceLookup Day -> PriceGraph
    makepricegraph

    priceDirectiveToMarketPrice :: PriceDirective -> MarketPrice
    priceDirectiveToMarketPrice :: PriceDirective -> MarketPrice
    priceDirectiveToMarketPrice PriceDirective{..} =
      MarketPrice :: Day
    -> CommoditySymbol -> CommoditySymbol -> Quantity -> MarketPrice
    MarketPrice{ mpdate :: Day
    mpdate = Day
    pddate
                 , mpfrom :: CommoditySymbol
    mpfrom = CommoditySymbol
    pdcommodity
                 , mpto :: CommoditySymbol
    mpto   = Amount -> CommoditySymbol
    acommodity Amount
    pdamount
                 , mprate :: Quantity
    mprate = Amount -> Quantity
    aquantity Amount
    pdamount
                 }

    ------------------------------------------------------------------------------
    -- Converting things to value

    -- | Apply a specified valuation to this mixed amount, using the
    -- provided price oracle, commodity styles, reference dates, and
    -- whether this is for a multiperiod report or not.
    -- See amountApplyValuation.
    mixedAmountApplyValuation :: PriceOracle -> M.Map CommoditySymbol AmountStyle -> Day -> Maybe Day -> Day -> Bool -> ValuationType -> MixedAmount -> MixedAmount
    mixedAmountApplyValuation :: PriceOracle
    -> Map CommoditySymbol AmountStyle
    -> Day
    -> Maybe Day
    -> Day
    -> Bool
    -> ValuationType
    -> MixedAmount
    -> MixedAmount
    mixedAmountApplyValuation priceoracle :: PriceOracle
    priceoracle styles :: Map CommoditySymbol AmountStyle
    styles periodlast :: Day
    periodlast mreportlast :: Maybe Day
    mreportlast today :: Day
    today ismultiperiod :: Bool
    ismultiperiod v :: ValuationType
    v (Mixed as :: [Amount]
    as) =
      [Amount] -> MixedAmount
    Mixed ([Amount] -> MixedAmount) -> [Amount] -> MixedAmount
    forall a b. (a -> b) -> a -> b
    $ (Amount -> Amount) -> [Amount] -> [Amount]
    forall a b. (a -> b) -> [a] -> [b]
    map (PriceOracle
    -> Map CommoditySymbol AmountStyle
    -> Day
    -> Maybe Day
    -> Day
    -> Bool
    -> ValuationType
    -> Amount
    -> Amount
    amountApplyValuation PriceOracle
    priceoracle Map CommoditySymbol AmountStyle
    styles Day
    periodlast Maybe Day
    mreportlast Day
    today Bool
    ismultiperiod ValuationType
    v) [Amount]
    as

    -- | Apply a specified valuation to this amount, using the provided
    -- price oracle, reference dates, and whether this is for a
    -- multiperiod report or not. Also fix up its display style using the
    -- provided commodity styles.
    --
    -- When the valuation requires converting to another commodity, a
    -- valuation (conversion) date is chosen based on the valuation type,
    -- the provided reference dates, and whether this is for a
    -- single-period or multi-period report. It will be one of:
    --
    -- - a fixed date specified by the ValuationType itself
    --   (--value=DATE).
    --
    -- - the provided "period end" date - this is typically the last day
    --   of a subperiod (--value=end with a multi-period report), or of
    --   the specified report period or the journal (--value=end with a
    --   single-period report).
    --
    -- - the provided "report end" date - the last day of the specified
    --   report period, if any (-V/-X with a report end date).
    --
    -- - the provided "today" date - (--value=now, or -V/X with no report
    --   end date).
    --
    -- Note --value=then is not supported by this function, and will cause an error;
    -- use postingApplyValuation for that.
    --
    -- This is all a bit complicated. See the reference doc at
    -- https://hledger.org/hledger.html#effect-of-valuation-on-reports
    -- (hledger_options.m4.md "Effect of valuation on reports"), and #1083.
    --
    amountApplyValuation :: PriceOracle -> M.Map CommoditySymbol AmountStyle -> Day -> Maybe Day -> Day -> Bool -> ValuationType -> Amount -> Amount
    amountApplyValuation :: PriceOracle
    -> Map CommoditySymbol AmountStyle
    -> Day
    -> Maybe Day
    -> Day
    -> Bool
    -> ValuationType
    -> Amount
    -> Amount
    amountApplyValuation priceoracle :: PriceOracle
    priceoracle styles :: Map CommoditySymbol AmountStyle
    styles periodlast :: Day
    periodlast mreportlast :: Maybe Day
    mreportlast today :: Day
    today ismultiperiod :: Bool
    ismultiperiod v :: ValuationType
    v a :: Amount
    a =
      case ValuationType
    v of
        AtCost    Nothing            -> Map CommoditySymbol AmountStyle -> Amount -> Amount
    styleAmount Map CommoditySymbol AmountStyle
    styles (Amount -> Amount) -> Amount -> Amount
    forall a b. (a -> b) -> a -> b
    $ Amount -> Amount
    amountCost Amount
    a
        AtCost    mc :: Maybe CommoditySymbol
    mc                 -> PriceOracle
    -> Map CommoditySymbol AmountStyle
    -> Maybe CommoditySymbol
    -> Day
    -> Amount
    -> Amount
    amountValueAtDate PriceOracle
    priceoracle Map CommoditySymbol AmountStyle
    styles Maybe CommoditySymbol
    mc Day
    periodlast (Amount -> Amount) -> Amount -> Amount
    forall a b. (a -> b) -> a -> b
    $ Map CommoditySymbol AmountStyle -> Amount -> Amount
    styleAmount Map CommoditySymbol AmountStyle
    styles (Amount -> Amount) -> Amount -> Amount
    forall a b. (a -> b) -> a -> b
    $ Amount -> Amount
    amountCost Amount
    a
        AtThen    _mc :: Maybe CommoditySymbol
    _mc                -> String -> Amount
    forall a. String -> a
    error' String
    unsupportedValueThenError  -- PARTIAL:
                                     -- amountValueAtDate priceoracle styles mc periodlast a  -- posting date unknown, handle like AtEnd
        AtEnd     mc :: Maybe CommoditySymbol
    mc                 -> PriceOracle
    -> Map CommoditySymbol AmountStyle
    -> Maybe CommoditySymbol
    -> Day
    -> Amount
    -> Amount
    amountValueAtDate PriceOracle
    priceoracle Map CommoditySymbol AmountStyle
    styles Maybe CommoditySymbol
    mc Day
    periodlast Amount
    a
        AtNow     mc :: Maybe CommoditySymbol
    mc                 -> PriceOracle
    -> Map CommoditySymbol AmountStyle
    -> Maybe CommoditySymbol
    -> Day
    -> Amount
    -> Amount
    amountValueAtDate PriceOracle
    priceoracle Map CommoditySymbol AmountStyle
    styles Maybe CommoditySymbol
    mc Day
    today Amount
    a
        AtDefault mc :: Maybe CommoditySymbol
    mc | Bool
    ismultiperiod -> PriceOracle
    -> Map CommoditySymbol AmountStyle
    -> Maybe CommoditySymbol
    -> Day
    -> Amount
    -> Amount
    amountValueAtDate PriceOracle
    priceoracle Map CommoditySymbol AmountStyle
    styles Maybe CommoditySymbol
    mc Day
    periodlast Amount
    a
        AtDefault mc :: Maybe CommoditySymbol
    mc                 -> PriceOracle
    -> Map CommoditySymbol AmountStyle
    -> Maybe CommoditySymbol
    -> Day
    -> Amount
    -> Amount
    amountValueAtDate PriceOracle
    priceoracle Map CommoditySymbol AmountStyle
    styles Maybe CommoditySymbol
    mc (Day -> Maybe Day -> Day
    forall a. a -> Maybe a -> a
    fromMaybe Day
    today Maybe Day
    mreportlast) Amount
    a
        AtDate d :: Day
    d  mc :: Maybe CommoditySymbol
    mc                 -> PriceOracle
    -> Map CommoditySymbol AmountStyle
    -> Maybe CommoditySymbol
    -> Day
    -> Amount
    -> Amount
    amountValueAtDate PriceOracle
    priceoracle Map CommoditySymbol AmountStyle
    styles Maybe CommoditySymbol
    mc Day
    d Amount
    a

    -- | Standard error message for a report not supporting --value=then.
    unsupportedValueThenError :: String
    unsupportedValueThenError :: String
    unsupportedValueThenError = "Sorry, --value=then is not yet implemented for this kind of report."

    -- | Find the market value of each component amount in the given
    -- commodity, or its default valuation commodity, at the given
    -- valuation date, using the given market price oracle.
    -- When market prices available on that date are not sufficient to
    -- calculate the value, amounts are left unchanged.
    mixedAmountValueAtDate :: PriceOracle -> M.Map CommoditySymbol AmountStyle -> Maybe CommoditySymbol -> Day -> MixedAmount -> MixedAmount
    mixedAmountValueAtDate :: PriceOracle
    -> Map CommoditySymbol AmountStyle
    -> Maybe CommoditySymbol
    -> Day
    -> MixedAmount
    -> MixedAmount
    mixedAmountValueAtDate priceoracle :: PriceOracle
    priceoracle styles :: Map CommoditySymbol AmountStyle
    styles mc :: Maybe CommoditySymbol
    mc d :: Day
    d (Mixed as :: [Amount]
    as) = [Amount] -> MixedAmount
    Mixed ([Amount] -> MixedAmount) -> [Amount] -> MixedAmount
    forall a b. (a -> b) -> a -> b
    $ (Amount -> Amount) -> [Amount] -> [Amount]
    forall a b. (a -> b) -> [a] -> [b]
    map (PriceOracle
    -> Map CommoditySymbol AmountStyle
    -> Maybe CommoditySymbol
    -> Day
    -> Amount
    -> Amount
    amountValueAtDate PriceOracle
    priceoracle Map CommoditySymbol AmountStyle
    styles Maybe CommoditySymbol
    mc Day
    d) [Amount]
    as

    -- | Find the market value of this amount in the given valuation
    -- commodity if any, otherwise the default valuation commodity, at the
    -- given valuation date. (The default valuation commodity is the
    -- commodity of the latest applicable market price before the
    -- valuation date.)
    --
    -- The returned amount will have its commodity's canonical style applied,
    -- but with the precision adjusted to show all significant decimal digits
    -- up to a maximum of 8. (experimental)
    --
    -- If the market prices available on that date are not sufficient to
    -- calculate this value, the amount is left unchanged.
    amountValueAtDate :: PriceOracle -> M.Map CommoditySymbol AmountStyle -> Maybe CommoditySymbol -> Day -> Amount -> Amount
    amountValueAtDate :: PriceOracle
    -> Map CommoditySymbol AmountStyle
    -> Maybe CommoditySymbol
    -> Day
    -> Amount
    -> Amount
    amountValueAtDate priceoracle :: PriceOracle
    priceoracle styles :: Map CommoditySymbol AmountStyle
    styles mto :: Maybe CommoditySymbol
    mto d :: Day
    d a :: Amount
    a =
      case PriceOracle
    priceoracle (Day
    d, Amount -> CommoditySymbol
    acommodity Amount
    a, Maybe CommoditySymbol
    mto) of
        Nothing           -> Amount
    a
        Just (comm :: CommoditySymbol
    comm, rate :: Quantity
    rate) ->
          -- setNaturalPrecisionUpTo 8 $  -- XXX force higher precision in case amount appears to be zero ?
                                          -- Make default display style use precision 2 instead of 0 ?
                                          -- Leave as is for now; mentioned in manual.
          Map CommoditySymbol AmountStyle -> Amount -> Amount
    styleAmount Map CommoditySymbol AmountStyle
    styles
          Amount
    amount{acommodity :: CommoditySymbol
    acommodity=CommoditySymbol
    comm, aquantity :: Quantity
    aquantity=Quantity
    rate Quantity -> Quantity -> Quantity
    forall a. Num a => a -> a -> a
    * Amount -> Quantity
    aquantity Amount
    a}

    ------------------------------------------------------------------------------
    -- Market price lookup

    -- | Given a memoising price graph generator, a valuation date, a
    -- source commodity and an optional valuation commodity, find the
    -- value on that date of one unit of the source commodity in the
    -- valuation commodity, or in a default valuation commodity. Returns
    -- the valuation commodity that was specified or chosen, and the
    -- quantity of it that one unit of the source commodity is worth. Or
    -- if no applicable market price can be found or calculated, or if the
    -- source commodity and the valuation commodity are the same, returns
    -- Nothing.
    --
    -- See makePriceGraph for how prices are determined.
    -- Note that both market prices and default valuation commodities can
    -- vary with valuation date, since that determines which market prices
    -- are visible.
    --
    priceLookup :: (Day -> PriceGraph) -> Day -> CommoditySymbol -> Maybe CommoditySymbol -> Maybe (CommoditySymbol, Quantity)
    priceLookup :: (Day -> PriceGraph)
    -> Day
    -> CommoditySymbol
    -> Maybe CommoditySymbol
    -> Maybe (CommoditySymbol, Quantity)
    priceLookup makepricegraph :: Day -> PriceGraph
    makepricegraph d :: Day
    d from :: CommoditySymbol
    from mto :: Maybe CommoditySymbol
    mto =
      -- trace ("priceLookup ("++show d++", "++show from++", "++show mto++")") $
      let
        -- build a graph of the commodity exchange rates in effect on this day
        -- XXX should hide these fgl details better
        PriceGraph{prGraph :: PriceGraph -> Gr CommoditySymbol Quantity
    prGraph=Gr CommoditySymbol Quantity
    g, prNodemap :: PriceGraph -> NodeMap CommoditySymbol
    prNodemap=NodeMap CommoditySymbol
    m, prDefaultValuationCommodities :: PriceGraph -> Map CommoditySymbol CommoditySymbol
    prDefaultValuationCommodities=Map CommoditySymbol CommoditySymbol
    defaultdests} =
          Int -> String -> PriceGraph -> PriceGraph
    forall a. Int -> String -> a -> a
    traceAt 1 ("valuation date: "String -> ShowS
    forall a. [a] -> [a] -> [a]
    ++Day -> String
    forall a. Show a => a -> String
    show Day
    d) (PriceGraph -> PriceGraph) -> PriceGraph -> PriceGraph
    forall a b. (a -> b) -> a -> b
    $ Day -> PriceGraph
    makepricegraph Day
    d
        fromnode :: Int
    fromnode = NodeMap CommoditySymbol -> CommoditySymbol -> Int
    forall a. Ord a => NodeMap a -> a -> Int
    node NodeMap CommoditySymbol
    m CommoditySymbol
    from
        mto' :: Maybe CommoditySymbol
    mto' = Maybe CommoditySymbol
    mto Maybe CommoditySymbol
    -> Maybe CommoditySymbol -> Maybe CommoditySymbol
    forall (f :: * -> *) a. Alternative f => f a -> f a -> f a
    <|> Maybe CommoditySymbol
    mdefaultto
          where
            mdefaultto :: Maybe CommoditySymbol
    mdefaultto = String -> Maybe CommoditySymbol -> Maybe CommoditySymbol
    forall a. Show a => String -> a -> a
    dbg1 ("default valuation commodity for "String -> ShowS
    forall a. [a] -> [a] -> [a]
    ++CommoditySymbol -> String
    T.unpack CommoditySymbol
    from) (Maybe CommoditySymbol -> Maybe CommoditySymbol)
    -> Maybe CommoditySymbol -> Maybe CommoditySymbol
    forall a b. (a -> b) -> a -> b
    $
                         CommoditySymbol
    -> Map CommoditySymbol CommoditySymbol -> Maybe CommoditySymbol
    forall k a. Ord k => k -> Map k a -> Maybe a
    M.lookup CommoditySymbol
    from Map CommoditySymbol CommoditySymbol
    defaultdests
      in
        case Maybe CommoditySymbol
    mto' of
          Nothing            -> Maybe (CommoditySymbol, Quantity)
    forall a. Maybe a
    Nothing
          Just to :: CommoditySymbol
    to | CommoditySymbol
    toCommoditySymbol -> CommoditySymbol -> Bool
    forall a. Eq a => a -> a -> Bool
    ==CommoditySymbol
    from -> Maybe (CommoditySymbol, Quantity)
    forall a. Maybe a
    Nothing
          Just to :: CommoditySymbol
    to            ->
            -- We have a commodity to convert to. Find the most direct price available.
            case Maybe Quantity
    mindirectprice of
              Nothing -> Maybe (CommoditySymbol, Quantity)
    forall a. Maybe a
    Nothing
              Just q :: Quantity
    q  -> (CommoditySymbol, Quantity) -> Maybe (CommoditySymbol, Quantity)
    forall a. a -> Maybe a
    Just (CommoditySymbol
    to, Quantity
    q)
            where
              tonode :: Int
    tonode = NodeMap CommoditySymbol -> CommoditySymbol -> Int
    forall a. Ord a => NodeMap a -> a -> Int
    node NodeMap CommoditySymbol
    m CommoditySymbol
    to
              Maybe Quantity
    mindirectprice :: Maybe Quantity =
                -- Find the shortest path, if any, between from and to.
                case Int -> Int -> Gr CommoditySymbol Quantity -> Maybe Path
    forall (gr :: * -> * -> *) b a.
    (Graph gr, Real b) =>
    Int -> Int -> gr a b -> Maybe Path
    sp Int
    fromnode Int
    tonode Gr CommoditySymbol Quantity
    g :: Maybe [Node] of
                  Nothing    -> Maybe Quantity
    forall a. Maybe a
    Nothing
                  Just nodes :: Path
    nodes ->
                    String -> Maybe Quantity -> Maybe Quantity
    forall i.
    (Integral i, Show i) =>
    String -> Maybe (DecimalRaw i) -> Maybe (DecimalRaw i)
    dbg ("market price for "String -> ShowS
    forall a. [a] -> [a] -> [a]
    ++String -> [String] -> String
    forall a. [a] -> [[a]] -> [a]
    intercalate " -> " ((CommoditySymbol -> String) -> [CommoditySymbol] -> [String]
    forall a b. (a -> b) -> [a] -> [b]
    map CommoditySymbol -> String
    T.unpack [CommoditySymbol]
    comms)) (Maybe Quantity -> Maybe Quantity)
    -> Maybe Quantity -> Maybe Quantity
    forall a b. (a -> b) -> a -> b
    $
                      -- TODO: it would be nice to include price date as part of the label
                      -- in PriceGraph, so we could show the dates of market prices here
                    Quantity -> Maybe Quantity
    forall a. a -> Maybe a
    Just (Quantity -> Maybe Quantity) -> Quantity -> Maybe Quantity
    forall a b. (a -> b) -> a -> b
    $ [Quantity] -> Quantity
    forall (t :: * -> *) a. (Foldable t, Num a) => t a -> a
    product ([Quantity] -> Quantity) -> [Quantity] -> Quantity
    forall a b. (a -> b) -> a -> b
    $ Gr CommoditySymbol Quantity -> Path -> [Quantity]
    forall b a. (Show b, Ord b) => Gr a b -> Path -> [b]
    pathEdgeLabels Gr CommoditySymbol Quantity
    g Path
    nodes  -- convert to a single exchange rate
                    where comms :: [CommoditySymbol]
    comms = [Maybe CommoditySymbol] -> [CommoditySymbol]
    forall a. [Maybe a] -> [a]
    catMaybes ([Maybe CommoditySymbol] -> [CommoditySymbol])
    -> [Maybe CommoditySymbol] -> [CommoditySymbol]
    forall a b. (a -> b) -> a -> b
    $ (Int -> Maybe CommoditySymbol) -> Path -> [Maybe CommoditySymbol]
    forall a b. (a -> b) -> [a] -> [b]
    map (Gr CommoditySymbol Quantity -> Int -> Maybe CommoditySymbol
    forall (gr :: * -> * -> *) a b.
    Graph gr =>
    gr a b -> Int -> Maybe a
    lab Gr CommoditySymbol Quantity
    g) Path
    nodes

              -- log a message and a Maybe Quantity, hiding Just/Nothing and limiting decimal places
              dbg :: String -> Maybe (DecimalRaw i) -> Maybe (DecimalRaw i)
    dbg msg :: String
    msg = (Maybe (DecimalRaw i) -> String)
    -> Maybe (DecimalRaw i) -> Maybe (DecimalRaw i)
    forall a. Show a => (a -> String) -> a -> a
    dbg1With (((String
    msgString -> ShowS
    forall a. [a] -> [a] -> [a]
    ++": ")String -> ShowS
    forall a. [a] -> [a] -> [a]
    ++) ShowS
    -> (Maybe (DecimalRaw i) -> String)
    -> Maybe (DecimalRaw i)
    -> String
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . String
    -> (DecimalRaw i -> String) -> Maybe (DecimalRaw i) -> String
    forall b a. b -> (a -> b) -> Maybe a -> b
    maybe "" (DecimalRaw i -> String
    forall a. Show a => a -> String
    show (DecimalRaw i -> String)
    -> (DecimalRaw i -> DecimalRaw i) -> DecimalRaw i -> String
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Word8 -> DecimalRaw i -> DecimalRaw i
    forall i. Integral i => Word8 -> DecimalRaw i -> DecimalRaw i
    roundTo 8))

    tests_priceLookup :: TestTree
    tests_priceLookup =
      let
        p :: Integer
    -> Int
    -> Int
    -> CommoditySymbol
    -> Quantity
    -> CommoditySymbol
    -> MarketPrice
    p y :: Integer
    y m :: Int
    m d :: Int
    d from :: CommoditySymbol
    from q :: Quantity
    q to :: CommoditySymbol
    to = MarketPrice :: Day
    -> CommoditySymbol -> CommoditySymbol -> Quantity -> MarketPrice
    MarketPrice{mpdate :: Day
    mpdate=Integer -> Int -> Int -> Day
    fromGregorian Integer
    y Int
    m Int
    d, mpfrom :: CommoditySymbol
    mpfrom=CommoditySymbol
    from, mpto :: CommoditySymbol
    mpto=CommoditySymbol
    to, mprate :: Quantity
    mprate=Quantity
    q}
        ps1 :: [MarketPrice]
    ps1 = [
           Integer
    -> Int
    -> Int
    -> CommoditySymbol
    -> Quantity
    -> CommoditySymbol
    -> MarketPrice
    p 2000 01 01 "A" 10 "B"
          ,Integer
    -> Int
    -> Int
    -> CommoditySymbol
    -> Quantity
    -> CommoditySymbol
    -> MarketPrice
    p 2000 01 01 "B" 10 "C"
          ,Integer
    -> Int
    -> Int
    -> CommoditySymbol
    -> Quantity
    -> CommoditySymbol
    -> MarketPrice
    p 2000 01 01 "C" 10 "D"
          ,Integer
    -> Int
    -> Int
    -> CommoditySymbol
    -> Quantity
    -> CommoditySymbol
    -> MarketPrice
    p 2000 01 01 "E"  2 "D"
          ,Integer
    -> Int
    -> Int
    -> CommoditySymbol
    -> Quantity
    -> CommoditySymbol
    -> MarketPrice
    p 2001 01 01 "A" 11 "B"
          ]
        makepricegraph :: Day -> PriceGraph
    makepricegraph = [MarketPrice] -> [MarketPrice] -> Day -> PriceGraph
    makePriceGraph [MarketPrice]
    ps1 []
      in String -> Assertion -> TestTree
    test "priceLookup" (Assertion -> TestTree) -> Assertion -> TestTree
    forall a b. (a -> b) -> a -> b
    $ do
        (Day -> PriceGraph)
    -> Day
    -> CommoditySymbol
    -> Maybe CommoditySymbol
    -> Maybe (CommoditySymbol, Quantity)
    priceLookup Day -> PriceGraph
    makepricegraph (Integer -> Int -> Int -> Day
    fromGregorian 1999 01 01) "A" Maybe CommoditySymbol
    forall a. Maybe a
    Nothing    Maybe (CommoditySymbol, Quantity)
    -> Maybe (CommoditySymbol, Quantity) -> Assertion
    forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
    @?= Maybe (CommoditySymbol, Quantity)
    forall a. Maybe a
    Nothing
        (Day -> PriceGraph)
    -> Day
    -> CommoditySymbol
    -> Maybe CommoditySymbol
    -> Maybe (CommoditySymbol, Quantity)
    priceLookup Day -> PriceGraph
    makepricegraph (Integer -> Int -> Int -> Day
    fromGregorian 2000 01 01) "A" Maybe CommoditySymbol
    forall a. Maybe a
    Nothing    Maybe (CommoditySymbol, Quantity)
    -> Maybe (CommoditySymbol, Quantity) -> Assertion
    forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
    @?= (CommoditySymbol, Quantity) -> Maybe (CommoditySymbol, Quantity)
    forall a. a -> Maybe a
    Just ("B",10)
        (Day -> PriceGraph)
    -> Day
    -> CommoditySymbol
    -> Maybe CommoditySymbol
    -> Maybe (CommoditySymbol, Quantity)
    priceLookup Day -> PriceGraph
    makepricegraph (Integer -> Int -> Int -> Day
    fromGregorian 2000 01 01) "B" (CommoditySymbol -> Maybe CommoditySymbol
    forall a. a -> Maybe a
    Just "A") Maybe (CommoditySymbol, Quantity)
    -> Maybe (CommoditySymbol, Quantity) -> Assertion
    forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
    @?= (CommoditySymbol, Quantity) -> Maybe (CommoditySymbol, Quantity)
    forall a. a -> Maybe a
    Just ("A",0.1)
        (Day -> PriceGraph)
    -> Day
    -> CommoditySymbol
    -> Maybe CommoditySymbol
    -> Maybe (CommoditySymbol, Quantity)
    priceLookup Day -> PriceGraph
    makepricegraph (Integer -> Int -> Int -> Day
    fromGregorian 2000 01 01) "A" (CommoditySymbol -> Maybe CommoditySymbol
    forall a. a -> Maybe a
    Just "E") Maybe (CommoditySymbol, Quantity)
    -> Maybe (CommoditySymbol, Quantity) -> Assertion
    forall a. (Eq a, Show a, HasCallStack) => a -> a -> Assertion
    @?= (CommoditySymbol, Quantity) -> Maybe (CommoditySymbol, Quantity)
    forall a. a -> Maybe a
    Just ("E",500)

    -- | Build the graph of commodity conversion prices for a given day.
    -- Converts a list of declared market prices in parse order, and a
    -- list of transaction-inferred market prices in parse order, to a
    -- graph of all known exchange rates between commodity pairs in effect
    -- on that day. Cf hledger.m4.md -> Valuation:
    --
    -- hledger looks for a market price (exchange rate) from commodity A
    -- to commodity B in one or more of these ways, in this order of
    -- preference:
    --
    -- 1. A *declared market price* or *inferred market price*:
    --    A's latest market price in B on or before the valuation date
    --    as declared by a P directive, or (with the `--infer-value` flag)
    --    inferred from transaction prices.
    --
    -- 2. A *reverse market price*:
    --    the inverse of a declared or inferred market price from B to A.
    --
    -- 3. A *chained market price*:
    --    a synthetic price formed by combining the shortest chain of market
    --    prices (any of the above types) leading from A to B.
    --
    -- 1 and 2 form the edges of the price graph, and we can query it for
    -- 3 (which is the reason we use a graph).
    --
    -- We also identify each commodity's default valuation commodity, if
    -- any. For each commodity A, hledger picks a default valuation
    -- commodity as follows, in this order of preference:
    --
    -- 1. The price commodity from the latest declared market price for A
    --    on or before valuation date.
    --
    -- 2. The price commodity from the latest declared market price for A
    --    on any date. (Allows conversion to proceed if there are inferred
    --    prices before the valuation date.)
    --
    -- 3. If there are no P directives at all (any commodity or date), and
    --    the `--infer-value` flag is used, then the price commodity from
    --    the latest transaction price for A on or before valuation date.
    --
    makePriceGraph :: [MarketPrice] -> [MarketPrice] -> Day -> PriceGraph
    makePriceGraph :: [MarketPrice] -> [MarketPrice] -> Day -> PriceGraph
    makePriceGraph alldeclaredprices :: [MarketPrice]
    alldeclaredprices allinferredprices :: [MarketPrice]
    allinferredprices d :: Day
    d =
      String -> PriceGraph -> PriceGraph
    forall a. Show a => String -> a -> a
    dbg9 ("makePriceGraph "String -> ShowS
    forall a. [a] -> [a] -> [a]
    ++Day -> String
    forall a. Show a => a -> String
    show Day
    d) (PriceGraph -> PriceGraph) -> PriceGraph -> PriceGraph
    forall a b. (a -> b) -> a -> b
    $
      PriceGraph :: Gr CommoditySymbol Quantity
    -> NodeMap CommoditySymbol
    -> Map CommoditySymbol CommoditySymbol
    -> PriceGraph
    PriceGraph{prGraph :: Gr CommoditySymbol Quantity
    prGraph=Gr CommoditySymbol Quantity
    g, prNodemap :: NodeMap CommoditySymbol
    prNodemap=NodeMap CommoditySymbol
    m, prDefaultValuationCommodities :: Map CommoditySymbol CommoditySymbol
    prDefaultValuationCommodities=Map CommoditySymbol CommoditySymbol
    defaultdests}
      where
        -- prices in effect on date d, either declared or inferred
        visibledeclaredprices :: [MarketPrice]
    visibledeclaredprices = (MarketPrice -> Bool) -> [MarketPrice] -> [MarketPrice]
    forall a. (a -> Bool) -> [a] -> [a]
    filter ((Day -> Day -> Bool
    forall a. Ord a => a -> a -> Bool
    <=Day
    d)(Day -> Bool) -> (MarketPrice -> Day) -> MarketPrice -> Bool
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    .MarketPrice -> Day
    mpdate) [MarketPrice]
    alldeclaredprices
        visibleinferredprices :: [MarketPrice]
    visibleinferredprices = (MarketPrice -> Bool) -> [MarketPrice] -> [MarketPrice]
    forall a. (a -> Bool) -> [a] -> [a]
    filter ((Day -> Day -> Bool
    forall a. Ord a => a -> a -> Bool
    <=Day
    d)(Day -> Bool) -> (MarketPrice -> Day) -> MarketPrice -> Bool
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    .MarketPrice -> Day
    mpdate) [MarketPrice]
    allinferredprices
        declaredandinferredprices :: [MarketPrice]
    declaredandinferredprices = String -> [MarketPrice] -> [MarketPrice]
    forall a. Show a => String -> a -> a
    dbg2 "declaredandinferredprices" ([MarketPrice] -> [MarketPrice]) -> [MarketPrice] -> [MarketPrice]
    forall a b. (a -> b) -> a -> b
    $
          [MarketPrice] -> [MarketPrice] -> [MarketPrice]
    effectiveMarketPrices [MarketPrice]
    visibledeclaredprices [MarketPrice]
    visibleinferredprices

        -- infer any additional reverse prices not already declared or inferred
        reverseprices :: [MarketPrice]
    reverseprices = String -> [MarketPrice] -> [MarketPrice]
    forall a. Show a => String -> a -> a
    dbg2 "reverseprices" ([MarketPrice] -> [MarketPrice]) -> [MarketPrice] -> [MarketPrice]
    forall a b. (a -> b) -> a -> b
    $
          (MarketPrice -> MarketPrice) -> [MarketPrice] -> [MarketPrice]
    forall a b. (a -> b) -> [a] -> [b]
    map MarketPrice -> MarketPrice
    marketPriceReverse [MarketPrice]
    declaredandinferredprices [MarketPrice] -> [MarketPrice] -> [MarketPrice]
    forall a. Eq a => [a] -> [a] -> [a]
    \\ [MarketPrice]
    declaredandinferredprices

        -- build the graph and associated node map
        (g :: Gr CommoditySymbol Quantity
    g, m :: NodeMap CommoditySymbol
    m) =
          [CommoditySymbol]
    -> [(CommoditySymbol, CommoditySymbol, Quantity)]
    -> (Gr CommoditySymbol Quantity, NodeMap CommoditySymbol)
    forall a (g :: * -> * -> *) b.
    (Ord a, DynGraph g) =>
    [a] -> [(a, a, b)] -> (g a b, NodeMap a)
    mkMapGraph
          (String -> [CommoditySymbol] -> [CommoditySymbol]
    forall a. Show a => String -> a -> a
    dbg9 "price graph labels" ([CommoditySymbol] -> [CommoditySymbol])
    -> [CommoditySymbol] -> [CommoditySymbol]
    forall a b. (a -> b) -> a -> b
    $ [CommoditySymbol] -> [CommoditySymbol]
    forall a. Ord a => [a] -> [a]
    sort [CommoditySymbol]
    allcomms) -- this must include all nodes mentioned in edges
          (String
    -> [(CommoditySymbol, CommoditySymbol, Quantity)]
    -> [(CommoditySymbol, CommoditySymbol, Quantity)]
    forall a. Show a => String -> a -> a
    dbg9 "price graph edges" ([(CommoditySymbol, CommoditySymbol, Quantity)]
     -> [(CommoditySymbol, CommoditySymbol, Quantity)])
    -> [(CommoditySymbol, CommoditySymbol, Quantity)]
    -> [(CommoditySymbol, CommoditySymbol, Quantity)]
    forall a b. (a -> b) -> a -> b
    $ [(CommoditySymbol
    mpfrom, CommoditySymbol
    mpto, Quantity
    mprate) | MarketPrice{..} <- [MarketPrice]
    prices])
          :: (Gr CommoditySymbol Quantity, NodeMap CommoditySymbol)
          where
            prices :: [MarketPrice]
    prices   = [MarketPrice]
    declaredandinferredprices [MarketPrice] -> [MarketPrice] -> [MarketPrice]
    forall a. [a] -> [a] -> [a]
    ++ [MarketPrice]
    reverseprices
            allcomms :: [CommoditySymbol]
    allcomms = (MarketPrice -> CommoditySymbol)
    -> [MarketPrice] -> [CommoditySymbol]
    forall a b. (a -> b) -> [a] -> [b]
    map MarketPrice -> CommoditySymbol
    mpfrom [MarketPrice]
    prices

        -- determine a default valuation commodity for each source commodity
        -- somewhat but not quite like effectiveMarketPrices
        defaultdests :: Map CommoditySymbol CommoditySymbol
    defaultdests = [(CommoditySymbol, CommoditySymbol)]
    -> Map CommoditySymbol CommoditySymbol
    forall k a. Ord k => [(k, a)] -> Map k a
    M.fromList [(CommoditySymbol
    mpfrom,CommoditySymbol
    mpto) | MarketPrice{..} <- [MarketPrice]
    pricesfordefaultcomms]
          where
            pricesfordefaultcomms :: [MarketPrice]
    pricesfordefaultcomms = String -> [MarketPrice] -> [MarketPrice]
    forall a. Show a => String -> a -> a
    dbg2 "prices for choosing default valuation commodities, by date then parse order" ([MarketPrice] -> [MarketPrice]) -> [MarketPrice] -> [MarketPrice]
    forall a b. (a -> b) -> a -> b
    $
              [MarketPrice]
    ps
              [MarketPrice]
    -> ([MarketPrice] -> [(Integer, MarketPrice)])
    -> [(Integer, MarketPrice)]
    forall a b. a -> (a -> b) -> b
    & [Integer] -> [MarketPrice] -> [(Integer, MarketPrice)]
    forall a b. [a] -> [b] -> [(a, b)]
    zip [1..]  -- label items with their parse order
              [(Integer, MarketPrice)]
    -> ([(Integer, MarketPrice)] -> [(Integer, MarketPrice)])
    -> [(Integer, MarketPrice)]
    forall a b. a -> (a -> b) -> b
    & ((Integer, MarketPrice) -> (Integer, MarketPrice) -> Ordering)
    -> [(Integer, MarketPrice)] -> [(Integer, MarketPrice)]
    forall a. (a -> a -> Ordering) -> [a] -> [a]
    sortBy ((Day, Integer) -> (Day, Integer) -> Ordering
    forall a. Ord a => a -> a -> Ordering
    compare ((Day, Integer) -> (Day, Integer) -> Ordering)
    -> ((Integer, MarketPrice) -> (Day, Integer))
    -> (Integer, MarketPrice)
    -> (Integer, MarketPrice)
    -> Ordering
    forall b c a. (b -> b -> c) -> (a -> b) -> a -> a -> c
    `on` (\(parseorder :: Integer
    parseorder,MarketPrice{..})->(Day
    mpdate,Integer
    parseorder)))  -- sort by increasing date then increasing parse order
              [(Integer, MarketPrice)]
    -> ([(Integer, MarketPrice)] -> [MarketPrice]) -> [MarketPrice]
    forall a b. a -> (a -> b) -> b
    & ((Integer, MarketPrice) -> MarketPrice)
    -> [(Integer, MarketPrice)] -> [MarketPrice]
    forall a b. (a -> b) -> [a] -> [b]
    map (Integer, MarketPrice) -> MarketPrice
    forall a b. (a, b) -> b
    snd    -- discard labels
              where
                ps :: [MarketPrice]
    ps | Bool -> Bool
    not (Bool -> Bool) -> Bool -> Bool
    forall a b. (a -> b) -> a -> b
    $ [MarketPrice] -> Bool
    forall (t :: * -> *) a. Foldable t => t a -> Bool
    null [MarketPrice]
    visibledeclaredprices = [MarketPrice]
    visibledeclaredprices
                   | Bool -> Bool
    not (Bool -> Bool) -> Bool -> Bool
    forall a b. (a -> b) -> a -> b
    $ [MarketPrice] -> Bool
    forall (t :: * -> *) a. Foldable t => t a -> Bool
    null [MarketPrice]
    alldeclaredprices     = [MarketPrice]
    alldeclaredprices
                   | Bool
    otherwise                        = [MarketPrice]
    visibleinferredprices  -- will be null without --infer-value

    -- | Given a list of P-declared market prices in parse order and a
    -- list of transaction-inferred market prices in parse order, select
    -- just the latest prices that are in effect for each commodity pair.
    -- That is, for each commodity pair, the latest price by date then
    -- parse order, with declared prices having precedence over inferred
    -- prices on the same day.
    effectiveMarketPrices :: [MarketPrice] -> [MarketPrice] -> [MarketPrice]
    effectiveMarketPrices :: [MarketPrice] -> [MarketPrice] -> [MarketPrice]
    effectiveMarketPrices declaredprices :: [MarketPrice]
    declaredprices inferredprices :: [MarketPrice]
    inferredprices =
      let
        -- label each item with its same-day precedence, then parse order
        declaredprices' :: [(Integer, Integer, MarketPrice)]
    declaredprices' = [(1, Integer
    i, MarketPrice
    p) | (i :: Integer
    i,p :: MarketPrice
    p) <- [Integer] -> [MarketPrice] -> [(Integer, MarketPrice)]
    forall a b. [a] -> [b] -> [(a, b)]
    zip [1..] [MarketPrice]
    declaredprices]
        inferredprices' :: [(Integer, Integer, MarketPrice)]
    inferredprices' = [(0, Integer
    i, MarketPrice
    p) | (i :: Integer
    i,p :: MarketPrice
    p) <- [Integer] -> [MarketPrice] -> [(Integer, MarketPrice)]
    forall a b. [a] -> [b] -> [(a, b)]
    zip [1..] [MarketPrice]
    inferredprices]
      in
        -- combine
        [(Integer, Integer, MarketPrice)]
    declaredprices' [(Integer, Integer, MarketPrice)]
    -> [(Integer, Integer, MarketPrice)]
    -> [(Integer, Integer, MarketPrice)]
    forall a. [a] -> [a] -> [a]
    ++ [(Integer, Integer, MarketPrice)]
    inferredprices'
        -- sort by decreasing date then decreasing precedence then decreasing parse order
        [(Integer, Integer, MarketPrice)]
    -> ([(Integer, Integer, MarketPrice)]
        -> [(Integer, Integer, MarketPrice)])
    -> [(Integer, Integer, MarketPrice)]
    forall a b. a -> (a -> b) -> b
    & ((Integer, Integer, MarketPrice)
     -> (Integer, Integer, MarketPrice) -> Ordering)
    -> [(Integer, Integer, MarketPrice)]
    -> [(Integer, Integer, MarketPrice)]
    forall a. (a -> a -> Ordering) -> [a] -> [a]
    sortBy (((Day, Integer, Integer) -> (Day, Integer, Integer) -> Ordering)
    -> (Day, Integer, Integer) -> (Day, Integer, Integer) -> Ordering
    forall a b c. (a -> b -> c) -> b -> a -> c
    flip (Day, Integer, Integer) -> (Day, Integer, Integer) -> Ordering
    forall a. Ord a => a -> a -> Ordering
    compare ((Day, Integer, Integer) -> (Day, Integer, Integer) -> Ordering)
    -> ((Integer, Integer, MarketPrice) -> (Day, Integer, Integer))
    -> (Integer, Integer, MarketPrice)
    -> (Integer, Integer, MarketPrice)
    -> Ordering
    forall b c a. (b -> b -> c) -> (a -> b) -> a -> a -> c
    `on` (\(precedence :: Integer
    precedence,parseorder :: Integer
    parseorder,mp :: MarketPrice
    mp)->(MarketPrice -> Day
    mpdate MarketPrice
    mp,Integer
    precedence,Integer
    parseorder)))
        -- discard the sorting labels
        [(Integer, Integer, MarketPrice)]
    -> ([(Integer, Integer, MarketPrice)] -> [MarketPrice])
    -> [MarketPrice]
    forall a b. a -> (a -> b) -> b
    & ((Integer, Integer, MarketPrice) -> MarketPrice)
    -> [(Integer, Integer, MarketPrice)] -> [MarketPrice]
    forall a b. (a -> b) -> [a] -> [b]
    map (Integer, Integer, MarketPrice) -> MarketPrice
    forall a b c. (a, b, c) -> c
    third3
        -- keep only the first (ie the newest, highest precedence, latest parsed) price for each pair
        [MarketPrice] -> ([MarketPrice] -> [MarketPrice]) -> [MarketPrice]
    forall a b. a -> (a -> b) -> b
    & (MarketPrice -> MarketPrice -> Ordering)
    -> [MarketPrice] -> [MarketPrice]
    forall a. (a -> a -> Ordering) -> [a] -> [a]
    nubSortBy ((CommoditySymbol, CommoditySymbol)
    -> (CommoditySymbol, CommoditySymbol) -> Ordering
    forall a. Ord a => a -> a -> Ordering
    compare ((CommoditySymbol, CommoditySymbol)
     -> (CommoditySymbol, CommoditySymbol) -> Ordering)
    -> (MarketPrice -> (CommoditySymbol, CommoditySymbol))
    -> MarketPrice
    -> MarketPrice
    -> Ordering
    forall b c a. (b -> b -> c) -> (a -> b) -> a -> a -> c
    `on` (\(MarketPrice{..})->(CommoditySymbol
    mpfrom,CommoditySymbol
    mpto)))

    marketPriceReverse :: MarketPrice -> MarketPrice
    marketPriceReverse :: MarketPrice -> MarketPrice
    marketPriceReverse mp :: MarketPrice
    mp@MarketPrice{..} = MarketPrice
    mp{mpfrom :: CommoditySymbol
    mpfrom=CommoditySymbol
    mpto, mpto :: CommoditySymbol
    mpto=CommoditySymbol
    mpfrom, mprate :: Quantity
    mprate=1Quantity -> Quantity -> Quantity
    forall a. Fractional a => a -> a -> a
    /Quantity
    mprate}

    ------------------------------------------------------------------------------
    -- fgl helpers

    -- | Look up an existing graph node by its label.
    -- (If the node does not exist, a new one will be generated, but not
    -- persisted in the nodemap.)
    node :: Ord a => NodeMap a -> a -> Node
    node :: NodeMap a -> a -> Int
    node m :: NodeMap a
    m = (Int, a) -> Int
    forall a b. (a, b) -> a
    fst ((Int, a) -> Int) -> (a -> (Int, a)) -> a -> Int
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . ((Int, a), NodeMap a) -> (Int, a)
    forall a b. (a, b) -> a
    fst (((Int, a), NodeMap a) -> (Int, a))
    -> (a -> ((Int, a), NodeMap a)) -> a -> (Int, a)
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . NodeMap a -> a -> ((Int, a), NodeMap a)
    forall a. Ord a => NodeMap a -> a -> (LNode a, NodeMap a)
    mkNode NodeMap a
    m

    -- | Convert a valid path within the given graph to the corresponding
    -- edge labels. When there are multiple edges between two nodes, the
    -- lowest-sorting label is used.
    pathEdgeLabels :: (Show b, Ord b) => Gr a b -> [Node] -> [b]
    pathEdgeLabels :: Gr a b -> Path -> [b]
    pathEdgeLabels g :: Gr a b
    g = (Maybe b -> b) -> [Maybe b] -> [b]
    forall a b. (a -> b) -> [a] -> [b]
    map Maybe b -> b
    forall a. Maybe a -> a
    frommaybe ([Maybe b] -> [b]) -> (Path -> [Maybe b]) -> Path -> [b]
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . ((Int, Int) -> Maybe b) -> [(Int, Int)] -> [Maybe b]
    forall a b. (a -> b) -> [a] -> [b]
    map (Gr a b -> (Int, Int) -> Maybe b
    forall b a. Ord b => Gr a b -> (Int, Int) -> Maybe b
    nodesEdgeLabel Gr a b
    g) ([(Int, Int)] -> [Maybe b])
    -> (Path -> [(Int, Int)]) -> Path -> [Maybe b]
    forall b c a. (b -> c) -> (a -> b) -> a -> c
    . Path -> [(Int, Int)]
    pathEdges
      where frommaybe :: Maybe a -> a
    frommaybe = a -> Maybe a -> a
    forall a. a -> Maybe a -> a
    fromMaybe (String -> a
    forall a. String -> a
    error' "pathEdgeLabels: expected no Nothings here")  -- PARTIAL:

    -- | Convert a path to node pairs representing the path's edges.
    pathEdges :: [Node] -> [(Node,Node)]
    pathEdges :: Path -> [(Int, Int)]
    pathEdges p :: Path
    p = [(Int
    f,Int
    t) | f :: Int
    f:t :: Int
    t:_ <- Path -> [Path]
    forall a. [a] -> [[a]]
    tails Path
    p]

    -- | Get the label of a graph edge from one node to another.
    -- When there are multiple such edges, the lowest-sorting label is used.
    nodesEdgeLabel :: Ord b => Gr a b -> (Node, Node) -> Maybe b
    nodesEdgeLabel :: Gr a b -> (Int, Int) -> Maybe b
    nodesEdgeLabel g :: Gr a b
    g (from :: Int
    from,to :: Int
    to) = [b] -> Maybe b
    forall a. [a] -> Maybe a
    headMay ([b] -> Maybe b) -> [b] -> Maybe b
    forall a b. (a -> b) -> a -> b
    $ [b] -> [b]
    forall a. Ord a => [a] -> [a]
    sort [b
    l | (_,t :: Int
    t,l :: b
    l) <- Gr a b -> Int -> [(Int, Int, b)]
    forall (gr :: * -> * -> *) a b.
    Graph gr =>
    gr a b -> Int -> [LEdge b]
    out Gr a b
    g Int
    from, Int
    tInt -> Int -> Bool
    forall a. Eq a => a -> a -> Bool
    ==Int
    to]

    ------------------------------------------------------------------------------

    tests_Valuation :: TestTree
    tests_Valuation = String -> [TestTree] -> TestTree
    tests "Valuation" [
       TestTree
    tests_priceLookup
      ]