mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 20:27:15 +01:00
earned_time 0.13.0: sick day = 20:00 + gaming pay of what was done (was max(20:00, earned))
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2ec1c6e9b1
commit
f1378e97f1
@ -45,12 +45,13 @@ its earner pays in gaming (workout 120, LeetCode 60, reading 60, tutor 1/unit
|
||||
to 60), so the floor drops 18:50 -> 18:00 under the same 23:00 ceiling:
|
||||
nothing 3 h / 18:00, workout 5 h / 20:00, +LeetCode 21:00, +reading 22:00.
|
||||
|
||||
**Sick days (0.11.0, `_sick.py`).** `resolve(..., sick=True)` floors the
|
||||
*final* shutdown at 20:00 (`SICK_SHUTDOWN_FLOOR_MINUTES`), capped at the
|
||||
ceiling: 18:50 becomes 20:00, 21:30 stays 21:30 (nothing stacks on the floor).
|
||||
**Sick days (0.13.0, `_sick.py`).** `resolve(..., sick=True)` shuts down at
|
||||
20:00 (`SICK_SHUTDOWN_FLOOR_MINUTES`) plus every term's *gaming* pay, capped
|
||||
at the ceiling: LeetCode + reading is 22:00 (0.11.0's non-stacking max gave
|
||||
20:10). Never below 0.11.0's `max(20:00, earned)`, so it landed same-day.
|
||||
Base, terms, gaming, waivers and penalties are unchanged; earners still earn.
|
||||
`sick` is the consumer's answer (`sickdays.is_sick_day`, never imported here).
|
||||
Live credits are paid out of `Resolution.sick_lift_minutes` first (a grace lift).
|
||||
A live consumer raises the config to the sick resolution after each credit.
|
||||
|
||||
**The tutor cutover (`TUTOR_FROM`, one constant in `_ladder.py`).** The
|
||||
registry is per day: `earners_for(day)` is `EARNERS` before it and
|
||||
@ -156,7 +157,7 @@ How far to trust a gate is computed from its ledger, never listed by name:
|
||||
## API
|
||||
|
||||
- `resolve(answers, day=None, earners=None, *, first_credits=None, sick=False)`
|
||||
`-> Resolution` — the one sum. `sick_shutdown_minutes(minutes, day) -> int`.
|
||||
`-> Resolution` — the one sum. `sick_shutdown_from(paid_minutes, day) -> int`.
|
||||
- `base_for(day=None, earners=EARNERS) -> Base` — the day's floors.
|
||||
- `done_today(earner, ledger, key_file, *, now=None) -> bool | None`
|
||||
- `first_credit_at(earner, ledger, key_file, day) -> float | None` — unix time
|
||||
@ -240,7 +241,7 @@ on the day itself is logged and does not count.
|
||||
## Install
|
||||
|
||||
```console
|
||||
pip install --user --break-system-packages --no-deps "earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.12.1#subdirectory=earned_time"
|
||||
pip install --user --break-system-packages --no-deps "earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.13.0#subdirectory=earned_time"
|
||||
```
|
||||
|
||||
The root `steam-backlog-enforcer.service` sets `HOME=/home/kuhy`, so it imports
|
||||
|
||||
@ -84,7 +84,7 @@ from earned_time._policy import (
|
||||
from earned_time._quota import TUTOR_QUOTA_FROM, TUTOR_QUOTA_MINUTES, tutor_quota_for
|
||||
from earned_time._registry import all_earners, earners_for, registries
|
||||
from earned_time._resolve import Base, Resolution, Term, base_for, resolve
|
||||
from earned_time._sick import SICK_SHUTDOWN_FLOOR_MINUTES, sick_shutdown_minutes
|
||||
from earned_time._sick import SICK_SHUTDOWN_FLOOR_MINUTES, sick_shutdown_from
|
||||
|
||||
__all__ = [
|
||||
"ANKI",
|
||||
@ -139,7 +139,7 @@ __all__ = [
|
||||
"resolve",
|
||||
"shutdown_ceiling_for",
|
||||
"shutdown_minutes_for",
|
||||
"sick_shutdown_minutes",
|
||||
"sick_shutdown_from",
|
||||
"today_window",
|
||||
"tutor_quota_for",
|
||||
"verified",
|
||||
|
||||
@ -35,7 +35,7 @@ from earned_time._policy import (
|
||||
)
|
||||
from earned_time._registry import all_earners, earners_for
|
||||
from earned_time._resolution import Base, Resolution, Term
|
||||
from earned_time._sick import sick_shutdown_minutes
|
||||
from earned_time._sick import sick_shutdown_from
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Mapping
|
||||
@ -151,7 +151,8 @@ def resolve(
|
||||
for never, or could not check), e.g. ``Maturity.first_credit``.
|
||||
Only ever delays a penalty; omitted, 0.5.0's ``penalty_from``.
|
||||
sick: The day is a sick day (the consumer asks ``sickdays``): shutdown
|
||||
is at least 20:00 (:func:`~earned_time._sick.sick_shutdown_minutes`).
|
||||
is 20:00 plus the terms' gaming pay
|
||||
(:func:`~earned_time._sick.sick_shutdown_from`, 0.13.0).
|
||||
Gaming and every term are unchanged.
|
||||
|
||||
Returns:
|
||||
@ -186,7 +187,8 @@ def resolve(
|
||||
shutdown_ceiling_for(target),
|
||||
base.shutdown_minutes + sum(t.shutdown_minutes for t in terms),
|
||||
)
|
||||
shutdown = sick_shutdown_minutes(earned, target) if sick else earned
|
||||
paid = sum(t.gaming_minutes for t in terms)
|
||||
shutdown = max(earned, sick_shutdown_from(paid, target)) if sick else earned
|
||||
return Resolution(
|
||||
day=target,
|
||||
base=base,
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
# Copyright (c) 2026 Krzysztof Rudnicki
|
||||
"""The sick-day shutdown floor (0.11.0).
|
||||
"""The sick-day shutdown: 20:00 plus what the day earned, at gaming pay.
|
||||
|
||||
On a sick day shutdown is never earlier than 20:00: the resolved shutdown is
|
||||
``max(SICK_SHUTDOWN_FLOOR_MINUTES, the normal ladder result)``, still capped
|
||||
at the day's ceiling. It is applied to the *final* sum, never to the base --
|
||||
raising the base would stack every earner's minutes on top of 20:00.
|
||||
0.11.0 floored the final shutdown at 20:00 without stacking, so a sick day
|
||||
that did LeetCode and reading still shut down at 20:10 (kuhy, 2026-10-17:
|
||||
"+1 h from book and +1 h from LeetCode" was expected). Since 0.13.0 a sick
|
||||
day shuts down at ``SICK_SHUTDOWN_FLOOR_MINUTES`` plus every term's *gaming*
|
||||
minutes (shutdown follows the gaming bonus), capped at the day's ceiling.
|
||||
Never below 0.11.0's ``max(20:00, earned)``: every earner's gaming pay is at
|
||||
least its rung and the ladder floor is below 20:00, so it may land same-day.
|
||||
|
||||
Only shutdown moves. Gaming time, the registry, waivers and penalties are
|
||||
untouched, and every earner still earns on a sick day.
|
||||
@ -26,14 +29,14 @@ if TYPE_CHECKING:
|
||||
SICK_SHUTDOWN_FLOOR_MINUTES: Final = 20 * 60
|
||||
|
||||
|
||||
def sick_shutdown_minutes(minutes: int, day: date) -> int:
|
||||
"""``minutes`` (a resolved shutdown) with the sick-day floor applied.
|
||||
def sick_shutdown_from(paid_minutes: int, day: date) -> int:
|
||||
"""A sick day's shutdown: 20:00 plus ``paid_minutes``, capped.
|
||||
|
||||
Args:
|
||||
minutes: The normal resolved shutdown, minutes after midnight.
|
||||
paid_minutes: The day's terms at gaming pay (``sum(t.gaming_minutes)``).
|
||||
day: The day, which picks the ceiling.
|
||||
|
||||
Returns:
|
||||
``min(ceiling, max(SICK_SHUTDOWN_FLOOR_MINUTES, minutes))``.
|
||||
``min(ceiling, SICK_SHUTDOWN_FLOOR_MINUTES + paid_minutes)``.
|
||||
"""
|
||||
return min(shutdown_ceiling_for(day), max(SICK_SHUTDOWN_FLOOR_MINUTES, minutes))
|
||||
return min(shutdown_ceiling_for(day), SICK_SHUTDOWN_FLOOR_MINUTES + paid_minutes)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# Copyright (c) 2026 Krzysztof Rudnicki
|
||||
"""The sick-day shutdown floor (0.11.0): 20:00 at least, gaming untouched."""
|
||||
"""The sick-day shutdown (0.13.0): 20:00 plus gaming pay, gaming untouched."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@ -11,7 +11,7 @@ from earned_time import (
|
||||
SICK_SHUTDOWN_FLOOR_MINUTES,
|
||||
resolve,
|
||||
shutdown_ceiling_for,
|
||||
sick_shutdown_minutes,
|
||||
sick_shutdown_from,
|
||||
)
|
||||
|
||||
LADDER_DAY = date(2026, 10, 16)
|
||||
@ -47,13 +47,27 @@ def test_a_sick_day_that_earned_nothing_shuts_down_at_eight() -> None:
|
||||
assert normal.sick_lift_minutes == 0
|
||||
|
||||
|
||||
def test_earning_past_the_floor_is_not_stacked_on_it() -> None:
|
||||
"""max(20:00, earned) -- never 20:00 plus what was earned."""
|
||||
normal = resolve(SOME, LADDER_DAY)
|
||||
sick = resolve(SOME, LADDER_DAY, sick=True)
|
||||
assert normal.shutdown_minutes > 20 * 60
|
||||
assert sick.shutdown_minutes == normal.shutdown_minutes
|
||||
assert sick.sick_lift_minutes == 0
|
||||
def test_earning_stacks_on_the_floor_at_gaming_pay() -> None:
|
||||
"""0.13.0: 20:00 plus every term's gaming pay (LeetCode + reading = +2 h)."""
|
||||
answers = {**NOTHING, "leetcode": True, "reading": True}
|
||||
sick = resolve(answers, LADDER_DAY, sick=True)
|
||||
assert sick.shutdown_minutes == 22 * 60
|
||||
assert sick.shutdown_minutes == sick_shutdown_from(120, LADDER_DAY)
|
||||
some = resolve(SOME, LADDER_DAY, sick=True) # workout 120 + LeetCode 60
|
||||
assert some.shutdown_minutes == 23 * 60
|
||||
|
||||
|
||||
@pytest.mark.parametrize("answers", [NOTHING, SOME, EVERYTHING])
|
||||
@pytest.mark.parametrize("day", [LADDER_DAY, PRE_LADDER_DAY])
|
||||
def test_never_below_the_old_max_rule(
|
||||
answers: dict[str, int | bool | None], day: date
|
||||
) -> None:
|
||||
"""Raise-only against 0.11.0's max(20:00, earned), so it may land same-day."""
|
||||
if day == PRE_LADDER_DAY:
|
||||
answers = {k: v for k, v in answers.items() if k != "tutor"}
|
||||
normal = resolve(answers, day).shutdown_minutes
|
||||
old = min(shutdown_ceiling_for(day), max(20 * 60, normal))
|
||||
assert resolve(answers, day, sick=True).shutdown_minutes >= old
|
||||
|
||||
|
||||
def test_a_fully_done_sick_day_still_stops_at_the_ceiling() -> None:
|
||||
@ -81,9 +95,10 @@ def test_gaming_and_every_term_are_unchanged(
|
||||
|
||||
|
||||
def test_the_helper_is_capped_at_the_ceiling(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
assert sick_shutdown_minutes(18 * 60, LADDER_DAY) == 20 * 60
|
||||
assert sick_shutdown_minutes(21 * 60, LADDER_DAY) == 21 * 60
|
||||
assert sick_shutdown_from(0, LADDER_DAY) == 20 * 60
|
||||
assert sick_shutdown_from(60, LADDER_DAY) == 21 * 60
|
||||
assert sick_shutdown_from(600, LADDER_DAY) == 23 * 60
|
||||
monkeypatch.setattr(
|
||||
"earned_time._sick.shutdown_ceiling_for", lambda _day: 19 * 60 + 30
|
||||
)
|
||||
assert sick_shutdown_minutes(18 * 60, LADDER_DAY) == 19 * 60 + 30
|
||||
assert sick_shutdown_from(0, LADDER_DAY) == 19 * 60 + 30
|
||||
|
||||
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "earned-time"
|
||||
version = "0.12.1"
|
||||
version = "0.13.0"
|
||||
description = "Penalty-then-reward registry: what every gate app earns in gaming time and shutdown time"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = []
|
||||
|
||||
Loading…
Reference in New Issue
Block a user