import re from typing import Optional from .fleet_emissions_data import GHG_FACTORS from .fleet_emissions_types import EmissionsUnit, FleetEmissions DECIMAL = re.compile(r"([0-9]+)(?:\.([0-9]+))?") MAX_SAFE = 9007199254740991 def _format_factor(factor: int) -> str: return "%d.%05d" % (factor // 100000, factor % 100000) def fleet_emissions(activity: str, quantity: str, unit: EmissionsUnit, factor_year: int) -> FleetEmissions: """Scope 1 emissions for fuel burned or distance driven, from the UK government's conversion factors for company reporting. The quantity is decimal text and the factor an integer in 1e-5 kg, so the product is exact and rounded once, to the gram. Multiplying floats instead drifts in the last place and can round a half-gram the wrong way. """ if unit not in ("litres", "km", "miles"): raise ValueError('unknown unit "%s": use litres, km or miles' % (unit,)) if isinstance(factor_year, bool) or not isinstance(factor_year, int): raise ValueError("factorYear must be a whole year, received %s" % (factor_year,)) match = DECIMAL.fullmatch(quantity) if isinstance(quantity, str) else None if match is None: raise ValueError('quantity must be plain decimal text, not negative, received "%s"' % (quantity,)) fraction = match.group(2) or "" digits = (match.group(1) + fraction).lstrip("0") if len(digits) > 15 or len(fraction) > 9: raise ValueError( 'quantity must have at most 15 significant digits and 9 decimal places, received "%s"' % (quantity,) ) found: Optional[int] = None year_known = False activity_known = False for row in GHG_FACTORS: if row.factor_year != factor_year: continue year_known = True if row.activity != activity: continue activity_known = True if row.unit == unit: found = row.factor if not year_known: raise ValueError("no conversion factors for %d: this package carries 2023 to 2026" % (factor_year,)) if not activity_known: raise ValueError('unknown activity "%s" in the %d factors' % (activity, factor_year)) if found is None: raise ValueError("no conversion factor for %s per %s in %d" % (activity, unit, factor_year)) # grams = quantity x factor x 1e-5 kg x 1000 g/kg = quantity x factor / 100 numerator = int(match.group(1) + fraction) * found denominator = 10 ** len(fraction) * 100 grams = (2 * numerator + denominator) // (2 * denominator) if grams > MAX_SAFE: raise ValueError("the result exceeds 2^53 - 1 grams") return FleetEmissions( co2e_grams=grams, factor=_format_factor(found), factor_year=factor_year, activity=activity, unit=unit )