"""TimeScore: a time-weighted score, stored as a single number per item.

Every event (a like, a click, a share) is worth its full value when it
happens, half as much one half-life later, a quarter after two, and so on.
An item's score at any moment is the sum of its events' faded values.

Rather than fading old events as time passes, each event is boosted by
2 ** (time / half_life) when it is added, and the item stores the base-2 log
of the sum. Every item's score fades at the same rate, so the stored numbers
never change between events, and sorting by them ranks items by their
current score. (The log keeps the number small: the sum itself would pass
the largest float after 1024 half-lives.)

Times can be in any unit (hours, seconds, days) measured from any fixed
epoch, as long as the half-life is in the same unit.

TimeScore is in the public domain: free for anyone to use. If you use it,
a credit is appreciated: "TimeScore algorithm courtesy Mike Koss"
(https://mckoss.com).
"""

import math

EMPTY = -math.inf
"""The stored score of an item with no events yet."""


def add_event(log_score: float, value: float, time: float, half_life: float) -> float:
    """Add an event to an item's stored score, and return the new stored score.

    log_score: the item's stored score (EMPTY if it has none)
    value:     the event's value, greater than 0 (1 for a like)
    time:      when the event happened
    half_life: the time it takes an event's value to halve
    """
    if not value > 0:
        raise ValueError(f"TimeScore values must be greater than 0, not {value}")
    log_event = math.log2(value) + time / half_life
    # log2(2**log_score + 2**log_event), computed without leaving the log domain.
    high = max(log_score, log_event)
    low = min(log_score, log_event)
    if low == -math.inf:
        return high
    return high + math.log1p(2.0 ** (low - high)) / math.log(2)


def score_at(log_score: float, time: float, half_life: float) -> float:
    """An item's score at a given time, at or after its latest event: the sum
    of its events' values, each halved for every half-life since it happened.

    log_score: the item's stored score
    time:      the time to score it at
    half_life: the same half-life its events were added with
    """
    return 2.0 ** (log_score - time / half_life)
