Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import re
from typing import Tuple
from .fleet_fuel_cost_types import DistanceUnit, EconomyUnit, FuelCost
from .math_round_div import RoundingMode ← from math.round-div ^1.0.0 · built alongside by fune
from .math_round_div_big import round_div_big ← from math.round-div-big ^1.0.0 · built alongside by fune
from .money_amount import money ← from money.amount ^1.0.0 · built alongside by fune
from .units_convert import convert_units ← from units.convert ^1.0.0 · built alongside by fune
DECIMAL = re.compile(r"^[0-9]+(\.[0-9]+)?$")
MAX_SAFE = 9007199254740991
def _decimal(name: str, text: str) -> Tuple[int, int]:
"""A plain decimal as an exact fraction: (digits without the point, power of ten)."""
if not isinstance(text, str) or not DECIMAL.fullmatch(text):
raise ValueError('%s must be a plain decimal of 0 or more, received "%s"' % (name, text))
point = text.find(".")
places = 0 if point < 0 else len(text) - point - 1
return int(text.replace(".", "")), 10 ** places
def fuel_cost(
distance: str,
distance_unit: DistanceUnit,
economy: str,
economy_unit: EconomyUnit,
price_per_litre_tenths: int,
currency: str,
mode: RoundingMode,
) -> FuelCost:
"""The fuel a trip uses and what it costs.
Everything stays an exact fraction until the one rounding step: litres come
from the distance and economy, and the cost from those exact litres.
Rounding the litres first, or using 4.546 for the imperial gallon, is a
penny out often enough to break a reconciliation. The gallon and the mile
come from units.convert, which defines them exactly.
"""
if distance_unit not in ("mi", "km"):
raise ValueError('unknown distance unit "%s"' % (distance_unit,))
if economy_unit not in ("mpg-uk", "mpg-us", "l-per-100km"):
raise ValueError('unknown economy unit "%s"' % (economy_unit,))
d_num, d_den = _decimal("distance", distance)
e_num, e_den = _decimal("economy", economy)
if e_num == 0:
raise ValueError("economy must be greater than zero")
if isinstance(price_per_litre_tenths, bool) or not isinstance(price_per_litre_tenths, int):
raise TypeError("pricePerLitreTenths must be an integer, received %r" % (price_per_litre_tenths,))
if price_per_litre_tenths < 0:
raise ValueError("pricePerLitreTenths must not be negative, received %d" % (price_per_litre_tenths,))
money(0, currency)
km_num, km_den = _decimal("mile", convert_units("1", "mi", "km", 12))
if economy_unit == "l-per-100km":
# litres = km x (L/100km) / 100
l_num = d_num * e_num * (km_num if distance_unit == "mi" else 1)
l_den = d_den * e_den * 100 * (km_den if distance_unit == "mi" else 1)
else:
# litres = miles / mpg x litres per gallon
g_num, g_den = _decimal("gallon", convert_units("1", "gal_imp" if economy_unit == "mpg-uk" else "gal_us", "L", 12))
l_num = d_num * e_den * g_num * (km_den if distance_unit == "km" else 1)
l_den = d_den * e_num * g_den * (km_num if distance_unit == "km" else 1)
cost = int(round_div_big(str(l_num * price_per_litre_tenths), str(l_den * 10), mode))
if cost > MAX_SAFE:
raise ValueError("the fuel cost exceeds 2^53 - 1 minor units")
thousandths = int(round_div_big(str(l_num * 1000), str(l_den), "half-up"))
litres = "%d.%03d" % (thousandths // 1000, thousandths % 1000)
return FuelCost(litres=litres, cost=money(cost, currency))