import math from typing import Sequence, Tuple def linear_scale(domain: Sequence[float], range: Sequence[float], value: float, clamp: bool) -> float: """Where value lands on the range, as a straight-line map from the domain. Outside the domain the line carries on, unless clamp stops it at the range's ends. """ d0, d1 = pair(domain, "domain") r0, r1 = pair(range, "range") t = normalise(d0, d1, value, "domain") if clamp: t = max(0.0, min(1.0, t)) return round6(interpolate(r0, r1, t)) # The helpers below are shared with invert_linear, which is the same line read # the other way; they are not part of the group's contract. def pair(values: Sequence[float], what: str) -> Tuple[float, float]: if len(values) != 2: raise ValueError(f"{what} must have exactly 2 values, [from, to]; got {len(values)}") return float(values[0]), float(values[1]) def normalise(start: float, end: float, value: float, what: str) -> float: # Equal ends would divide by zero, which Python raises as a bare # ZeroDivisionError that does not say which end is wrong. if start == end: raise ValueError(f"{what} has no width: both ends are {start}") return (value - start) / (end - start) def interpolate(start: float, end: float, t: float) -> float: return start + t * (end - start) def round6(x: float) -> float: """Half up to 6 decimal places, the same way in every language.""" return math.floor(x * 1e6 + 0.5) / 1e6