max(0.0, 1.0 - ($age / $decayParam)), DecayFunction::Exponential => 0.5 ** ($age / $decayParam), }; } /** * Inverse of `value()`: roughly how many days until a current score * decays below `$threshold`, assuming no further reports. * * Exponential is exact (the sum scales by 0.5^(Δ/T) regardless of * report ages). Linear treats the current score as a lump at "now" * — an upper bound on time-to-threshold; the real curve drops faster * as individual reports age out. Used by the policy preview to give * operators an at-a-glance "when does this entry fall off?". * * Returns 0.0 if the score is already at or below the threshold, * `null` if the threshold is zero/negative (entry would never fall * off naturally). */ public static function daysUntilThreshold( DecayFunction $function, float $decayParam, float $currentScore, float $threshold, ): ?float { if ($decayParam <= 0.0) { return 0.0; } if ($threshold <= 0.0) { return null; } if ($currentScore <= $threshold) { return 0.0; } return match ($function) { DecayFunction::Linear => $decayParam * (1.0 - $threshold / $currentScore), DecayFunction::Exponential => $decayParam * (log($currentScore / $threshold) / log(2.0)), }; } }