mirror of
https://github.com/simonmichael/hledger.git
synced 2024-11-10 05:39:31 +03:00
lib: valuation: don't hang when finding prices (fixes #1439)
Searching for prices during valuation no longer now properly excludes price loops, avoiding near infinite looping with certain configurations of market prices. Also we now always use a direct price when available, rather than searching unnecessarily. Price searching progress info, useful for troubleshooting, is now displayed with --debug=2. There could still be some corner cases we don't handle correctly. We now give up with an error message if the searched price chains get too long (> 1000). More importantly, we should also give up if the search iterates too many times, but this is not done yet.
This commit is contained in:
parent
c96734474c
commit
73678393b1
@ -25,6 +25,8 @@ import qualified Data.Text as T
|
|||||||
import Hledger.Data.Types
|
import Hledger.Data.Types
|
||||||
import Hledger.Utils
|
import Hledger.Utils
|
||||||
|
|
||||||
|
-- Show space-containing commodity symbols quoted, as they are in a journal.
|
||||||
|
showCommoditySymbol = quoteIfNeeded
|
||||||
|
|
||||||
-- characters that may not be used in a non-quoted commodity symbol
|
-- characters that may not be used in a non-quoted commodity symbol
|
||||||
isNonsimpleCommodityChar :: Char -> Bool
|
isNonsimpleCommodityChar :: Char -> Bool
|
||||||
|
@ -30,7 +30,7 @@ where
|
|||||||
import Control.Applicative ((<|>))
|
import Control.Applicative ((<|>))
|
||||||
import Data.Foldable (asum)
|
import Data.Foldable (asum)
|
||||||
import Data.Function ((&), on)
|
import Data.Function ((&), on)
|
||||||
import Data.List ( (\\), sortBy )
|
import Data.List (intercalate, (\\), sortBy )
|
||||||
import Data.List.Extra (nubSortBy)
|
import Data.List.Extra (nubSortBy)
|
||||||
import qualified Data.Map as M
|
import qualified Data.Map as M
|
||||||
import qualified Data.Set as S
|
import qualified Data.Set as S
|
||||||
@ -44,6 +44,7 @@ import Hledger.Utils
|
|||||||
import Hledger.Data.Types
|
import Hledger.Data.Types
|
||||||
import Hledger.Data.Amount
|
import Hledger.Data.Amount
|
||||||
import Hledger.Data.Dates (nulldate)
|
import Hledger.Data.Dates (nulldate)
|
||||||
|
import Hledger.Data.Commodity (showCommoditySymbol)
|
||||||
|
|
||||||
|
|
||||||
------------------------------------------------------------------------------
|
------------------------------------------------------------------------------
|
||||||
@ -215,9 +216,13 @@ priceLookup makepricegraph d from mto =
|
|||||||
Just to ->
|
Just to ->
|
||||||
-- We have a commodity to convert to. Find the most direct price available,
|
-- We have a commodity to convert to. Find the most direct price available,
|
||||||
-- according to the rules described in makePriceGraph.
|
-- according to the rules described in makePriceGraph.
|
||||||
case
|
let msg = "seeking " ++ pshowedge' "" from to ++ " price"
|
||||||
pricesShortestPath forwardprices from to <|>
|
in case
|
||||||
pricesShortestPath allprices from to
|
(traceAt 2 (msg++" using forward prices") $
|
||||||
|
pricesShortestPath forwardprices from to)
|
||||||
|
<|>
|
||||||
|
(traceAt 2 (msg++" using forward and reverse prices") $
|
||||||
|
pricesShortestPath allprices from to)
|
||||||
of
|
of
|
||||||
Nothing -> Nothing
|
Nothing -> Nothing
|
||||||
Just [] -> Nothing
|
Just [] -> Nothing
|
||||||
@ -275,11 +280,18 @@ data PriceGraph = PriceGraph {
|
|||||||
-- USD->EUR price and one EUR->USD price.
|
-- USD->EUR price and one EUR->USD price.
|
||||||
pricesShortestPath :: [Edge] -> CommoditySymbol -> CommoditySymbol -> Maybe Path
|
pricesShortestPath :: [Edge] -> CommoditySymbol -> CommoditySymbol -> Maybe Path
|
||||||
pricesShortestPath edges start end =
|
pricesShortestPath edges start end =
|
||||||
dbg1 ("shortest price path for "++T.unpack start++" -> "++T.unpack end) $
|
-- dbg0With ((("shortest "++pshowedge' "" start end++" price path: ")++) . pshow . fmap (pshowpath "")) $
|
||||||
asum $ map (findPath end edgesremaining) initialpaths
|
dbg2 ("shortest "++pshowedge' "" start end++" price path") $
|
||||||
|
case quicksolution of
|
||||||
|
(path:_) -> Just path
|
||||||
|
[] -> asum $ map (findPath end edgesremaining) initialpaths
|
||||||
where
|
where
|
||||||
initialpaths = dbg9 "initial price paths" $ [[p] | p <- edges, mpfrom p == start]
|
initialpaths =
|
||||||
edgesremaining = dbg9 "initial edges remaining" $ edges \\ concat initialpaths
|
dbg2With (prefix "initial paths" . intercalate ", " . map (pshowpath "")) $
|
||||||
|
[[p] | p <- dbgedges "known prices" edges, mpfrom p == start]
|
||||||
|
quicksolution = [path | path@(MarketPrice{..}:_) <- initialpaths, mpfrom==start && mpto==end]
|
||||||
|
edgesremaining = dbgedges "initial prices remaining" $
|
||||||
|
[e | e <- edges, mpto e /= start] \\ concat initialpaths
|
||||||
|
|
||||||
-- Helper: breadth-first search for a continuation of the given path
|
-- Helper: breadth-first search for a continuation of the given path
|
||||||
-- using zero or more of the given edges, to the specified end commodity.
|
-- using zero or more of the given edges, to the specified end commodity.
|
||||||
@ -289,18 +301,40 @@ findPath end _ path | mpathend == Just end = Just path -- path is complete
|
|||||||
where
|
where
|
||||||
mpathend = mpto <$> lastMay path
|
mpathend = mpto <$> lastMay path
|
||||||
findPath _ [] _ = Nothing -- no more edges are available
|
findPath _ [] _ = Nothing -- no more edges are available
|
||||||
|
-- Guard against infinite loops as in #1439:
|
||||||
|
-- give up if path grows to an unlikely length.
|
||||||
|
-- XXX we need to limit the number of findPath iterations also.
|
||||||
|
findPath _ _ path | length path >= maxlength = error' err
|
||||||
|
where
|
||||||
|
maxlength = 1000
|
||||||
|
err = intercalate "\n" [
|
||||||
|
"giving up after searching price chains up to "++show maxlength++" long;"
|
||||||
|
,"please report this as a bug."
|
||||||
|
]
|
||||||
findPath end edgesremaining path = -- try continuing with all the remaining edges
|
findPath end edgesremaining path = -- try continuing with all the remaining edges
|
||||||
asum [
|
asum [
|
||||||
findPath end edgesremaining' path'
|
findPath end edgesremaining' path'
|
||||||
| e <- nextedges
|
| e <- nextedges
|
||||||
, let path' = path++[e]
|
, not $ mpto e `elem` map mpto path -- avoid loops
|
||||||
, let edgesremaining' = filter (/=e) edgesremaining
|
, let path' = dbgpath "findPath trying" $ path++[e]
|
||||||
|
, let edgesremaining' = filter (/= e) edgesremaining
|
||||||
]
|
]
|
||||||
where
|
where
|
||||||
nextedges = [ e | e <- edgesremaining, Just (mpfrom e) == mpathend ]
|
nextedges =
|
||||||
|
[ e | e <- edgesremaining, Just (mpfrom e) == mpathend ]
|
||||||
where
|
where
|
||||||
mpathend = mpto <$> lastMay path
|
mpathend = mpto <$> lastMay path
|
||||||
|
|
||||||
|
dbgpath label = dbg2With (pshowpath label)
|
||||||
|
dbgedges label = dbg2With (pshowedges label)
|
||||||
|
_dbgedge label = dbg2With (pshowedge label)
|
||||||
|
|
||||||
|
pshowpath label = prefix label . unwords . map (pshowedge "")
|
||||||
|
pshowedges label = prefix label . intercalate ", " . map (pshowedge "")
|
||||||
|
pshowedge label MarketPrice{..} = pshowedge' label mpfrom mpto
|
||||||
|
pshowedge' label from to = prefix label $ showCommoditySymbol (T.unpack from) ++ ">" ++ showCommoditySymbol (T.unpack to)
|
||||||
|
prefix l = if null l then (""++) else ((l++": ")++)
|
||||||
|
|
||||||
-- | A snapshot of the known exchange rates between commodity pairs at a given date.
|
-- | A snapshot of the known exchange rates between commodity pairs at a given date.
|
||||||
-- This is a home-made version, more tailored to our needs.
|
-- This is a home-made version, more tailored to our needs.
|
||||||
-- | Build the graph of commodity conversion prices for a given day.
|
-- | Build the graph of commodity conversion prices for a given day.
|
||||||
@ -363,12 +397,12 @@ makePriceGraph alldeclaredprices allinferredprices d =
|
|||||||
}
|
}
|
||||||
where
|
where
|
||||||
-- prices in effect on date d, either declared or inferred
|
-- prices in effect on date d, either declared or inferred
|
||||||
visibledeclaredprices = dbg2 "visibledeclaredprices" $ filter ((<=d).mpdate) alldeclaredprices
|
visibledeclaredprices = dbg9 "visibledeclaredprices" $ filter ((<=d).mpdate) alldeclaredprices
|
||||||
visibleinferredprices = dbg2 "visibleinferredprices" $ filter ((<=d).mpdate) allinferredprices
|
visibleinferredprices = dbg9 "visibleinferredprices" $ filter ((<=d).mpdate) allinferredprices
|
||||||
forwardprices = effectiveMarketPrices visibledeclaredprices visibleinferredprices
|
forwardprices = effectiveMarketPrices visibledeclaredprices visibleinferredprices
|
||||||
|
|
||||||
-- infer any additional reverse prices not already declared or inferred
|
-- infer any additional reverse prices not already declared or inferred
|
||||||
reverseprices = dbg2 "additional reverse prices" $
|
reverseprices = dbg9 "additional reverse prices" $
|
||||||
[p | p@MarketPrice{..} <- map marketPriceReverse forwardprices
|
[p | p@MarketPrice{..} <- map marketPriceReverse forwardprices
|
||||||
, not $ (mpfrom,mpto) `S.member` forwardpairs
|
, not $ (mpfrom,mpto) `S.member` forwardpairs
|
||||||
]
|
]
|
||||||
@ -380,7 +414,7 @@ makePriceGraph alldeclaredprices allinferredprices d =
|
|||||||
-- somewhat but not quite like effectiveMarketPrices
|
-- somewhat but not quite like effectiveMarketPrices
|
||||||
defaultdests = M.fromList [(mpfrom,mpto) | MarketPrice{..} <- pricesfordefaultcomms]
|
defaultdests = M.fromList [(mpfrom,mpto) | MarketPrice{..} <- pricesfordefaultcomms]
|
||||||
where
|
where
|
||||||
pricesfordefaultcomms = dbg2 "prices for choosing default valuation commodities, by date then parse order" $
|
pricesfordefaultcomms = dbg9 "prices for choosing default valuation commodities, by date then parse order" $
|
||||||
ps
|
ps
|
||||||
& zip [1..] -- label items with their parse order
|
& zip [1..] -- label items with their parse order
|
||||||
& sortBy (compare `on` (\(parseorder,MarketPrice{..})->(mpdate,parseorder))) -- sort by increasing date then increasing parse order
|
& sortBy (compare `on` (\(parseorder,MarketPrice{..})->(mpdate,parseorder))) -- sort by increasing date then increasing parse order
|
||||||
@ -403,7 +437,7 @@ effectiveMarketPrices declaredprices inferredprices =
|
|||||||
declaredprices' = [(1, i, p) | (i,p) <- zip [1..] declaredprices]
|
declaredprices' = [(1, i, p) | (i,p) <- zip [1..] declaredprices]
|
||||||
inferredprices' = [(0, i, p) | (i,p) <- zip [1..] inferredprices]
|
inferredprices' = [(0, i, p) | (i,p) <- zip [1..] inferredprices]
|
||||||
in
|
in
|
||||||
dbg2 "effective forward prices" $
|
dbg9 "effective forward prices" $
|
||||||
-- combine
|
-- combine
|
||||||
declaredprices' ++ inferredprices'
|
declaredprices' ++ inferredprices'
|
||||||
-- sort by decreasing date then decreasing precedence then decreasing parse order
|
-- sort by decreasing date then decreasing precedence then decreasing parse order
|
||||||
|
Loading…
Reference in New Issue
Block a user