cleatsquad/php-state-decay

Temporal exponential half-life decay for counters and scores, in dependency-free PHP

Maintainers

Package info

github.com/CleatSquad/php-state-decay

Homepage

Issues

pkg:composer/cleatsquad/php-state-decay

Transparency log

Statistics

Installs: 11

Dependents: 0

Suggesters: 0

Stars: 0

v1.0.0 2026-08-14 13:52 UTC

This package is auto-updated.

Last update: 2026-08-15 08:00:10 UTC


README

License: MIT PHP Version

Exponential half-life decay for counters and scores, in dependency-free PHP.

Evidence gathered a year ago should not weigh as much as evidence gathered yesterday. This library computes that discount: after one half-life a value is worth half of what it was, after two it is worth a quarter, and so on.

Installation

composer require cleatsquad/php-state-decay

Requires PHP 8.2 or later. No runtime dependencies.

Usage

Decay a score

use CleatSquad\StateDecay\DecayConfig;
use CleatSquad\StateDecay\HalfLifeDecay;

$decay = new HalfLifeDecay(new DecayConfig(halfLifeDays: 30));

$decay->applyToValue(100.0, 30 * 86400); // 50.0
$decay->applyToValue(100.0, 60 * 86400); // 25.0

Decay integer counters

$counters = ['successes' => 40, 'failures' => 10];

$decay->applyToCounters($counters, 30 * 86400);
// ['successes' => 20, 'failures' => 5]

Counters are rounded, not floored. A pass that runs every hour would otherwise floor barely-aged evidence down to nothing, one unit at a time.

Read the multiplier directly

$decay->factor(0);             // 1.0
$decay->factor(30 * 86400);    // 0.5
$decay->factor(-3600);         // 1.0 — clock skew never resurrects state

Short half-lives

Days are the usual unit, but anything shorter is expressible:

DecayConfig::fromSeconds(1800);  // 30 minutes
DecayConfig::fromDays(0.25);     // 6 hours
DecayConfig::never();            // decay disabled; factor() stays 1.0

Design notes

Elapsed time is the caller's business. Nothing here reads a clock. You pass the number of seconds that went by, which keeps the library free of any time dependency and makes every test deterministic. If you use a PSR-20 clock, take the difference yourself and hand it over.

Time never runs backwards. A negative or zero elapsed time yields a factor of 1.0. Clock skew between machines cannot make aged state young again.

Counters cannot be owed. A negative count is treated as 0 rather than propagated.

Invalid configuration fails loudly. A negative, NAN or INF half-life throws InvalidArgumentException instead of silently poisoning every later computation.

When to use it

Good fits: recency ranking, reputation and trust scores, abuse and rate heuristics, health scores, recommendation weights — anywhere accumulated evidence should fade rather than be dropped by a cutoff.

Poor fits: you need a hard expiry (use a TTL), or a fixed-size sliding window (count within the window instead).

Testing

composer install
composer test      # PHPUnit
composer analyse   # PHPStan, max level

License

MIT. See LICENSE.