← run

py-04-lru-ttl-cache

0.000
0/1 tests· data-structures
Challenge · difficulty 4/5
# LRU Cache with TTL

Implement a file **`solution.py`** containing a class:

```python
class LRUCache:
    def __init__(self, capacity: int, ttl: float, now=time.monotonic):
        ...

    def get(self, key):
        ...

    def put(self, key, value):
        ...

    def __len__(self) -> int:
        ...
```

A bounded cache that evicts by **least-recently-used** order and also **expires**
entries by age.

Behavior:

- `capacity` is the maximum number of live entries. `ttl` is the maximum age, in
  seconds, an entry may live before it is considered expired.
- `get(key)` returns the stored value, or `None` if the key is absent or expired.
  A successful `get` counts as a **use** (it refreshes recency, but **not** the
  entry's age / expiry time).
- `put(key, value)` inserts or updates an entry, marking it most-recently-used and
  **resetting its age** (its timestamp becomes "now"). If inserting a new key
  would exceed `capacity`, evict the least-recently-used live entry first.
- An **expired** entry must never be returned by `get` and must not count toward
  `__len__`. Expired entries may be purged lazily.
- `__len__` returns the number of **live (non-expired)** entries.

## Injectable clock

The constructor takes a `now` callable (defaulting to `time.monotonic`) that
returns the current time as a float. **All time decisions must use `now()`**, not
a hardcoded clock. This lets tests pass a controllable clock for deterministic
behavior. An entry inserted at time `t0` is expired once `now() - t0 >= ttl`
(i.e. an age strictly less than `ttl` is still live).

Example:

```python
clock = [0.0]
cache = LRUCache(capacity=2, ttl=10.0, now=lambda: clock[0])
cache.put("a", 1)
clock[0] = 5.0
assert cache.get("a") == 1     # age 5 < ttl 10 -> live
clock[0] = 10.0
assert cache.get("a") is None  # age 10 >= ttl 10 -> expired
```
tests/test_lru_ttl_cache.py
from solution import LRUCache


def make(capacity=2, ttl=10.0):
    clock = [0.0]
    cache = LRUCache(capacity=capacity, ttl=ttl, now=lambda: clock[0])
    return cache, clock


def test_basic_get_put():
    cache, _ = make()
    cache.put("a", 1)
    cache.put("b", 2)
    assert cache.get("a") == 1
    assert cache.get("b") == 2
    assert cache.get("missing") is None


def test_lru_eviction_on_capacity():
    cache, _ = make(capacity=2)
    cache.put("a", 1)
    cache.put("b", 2)
    cache.put("c", 3)  # exceeds capacity -> evict LRU ("a")
    assert cache.get("a") is None
    assert cache.get("b") == 2
    assert cache.get("c") == 3
    assert len(cache) == 2


def test_get_refreshes_recency():
    cache, _ = make(capacity=2)
    cache.put("a", 1)
    cache.put("b", 2)
    assert cache.get("a") == 1   # "a" now most-recently-used
    cache.put("c", 3)            # should evict "b", not "a"
    assert cache.get("a") == 1
    assert cache.get("b") is None
    assert cache.get("c") == 3


def test_ttl_expiry_boundary():
    cache, clock = make(ttl=10.0)
    cache.put("a", 1)
    clock[0] = 9.999
    assert cache.get("a") == 1   # age < ttl -> live
    clock[0] = 10.0
    assert cache.get("a") is None  # age == ttl -> expired


def test_get_does_not_reset_age():
    cache, clock = make(ttl=10.0)
    cache.put("a", 1)
    clock[0] = 5.0
    assert cache.get("a") == 1   # use does not refresh age
    clock[0] = 10.0
    assert cache.get("a") is None  # still expires at original t0 + ttl


def test_put_resets_age():
    cache, clock = make(ttl=10.0)
    cache.put("a", 1)
    clock[0] = 8.0
    cache.put("a", 99)           # re-put resets age and value
    clock[0] = 15.0              # 7s after re-put -> still live
    assert cache.get("a") == 99


def test_len_excludes_expired():
    cache, clock = make(capacity=5, ttl=10.0)
    cache.put("a", 1)
    cache.put("b", 2)
    assert len(cache) == 2
    clock[0] = 11.0
    assert len(cache) == 0


def test_expired_entry_does_not_count_against_capacity():
    cache, clock = make(capacity=2, ttl=10.0)
    cache.put("a", 1)
    cache.put("b", 2)
    clock[0] = 11.0              # both expired
    cache.put("c", 3)
    cache.put("d", 4)           # should fit; expired ones purged
    assert cache.get("c") == 3
    assert cache.get("d") == 4
    assert cache.get("a") is None
    assert len(cache) == 2
System prompt
You are an expert programmer. Solve the task exactly as specified. Output your solution as fenced code blocks using the required file name(s) and the exact function/type signatures requested. Prefer correctness; do not include prose outside code unless asked.

Errors (stderr)

no code extracted from response
Reasoning (chain-of-thought)
The user wants an implementation of an LRU Cache with TTL (Time To Live) in Python.

Key requirements:
1. Class `LRUCache` with `__init__`, `get`, `put`, `__len__`.
2. `capacity`: max number of live entries.
3. `ttl`: max age in seconds.
4. `now`: callable for current time (default `time.monotonic`).
5. `get(key)`: returns value or `None` if absent/expired. Updates recency (MRU), does NOT reset age.
6. `put(key, value)`: inserts/updates. Resets age (timestamp = now). Updates recency. Evicts LRU if capacity exceeded.
7. `__len__()`: count of live (non-expired) entries.
8. Expired entries: never returned, don't count in `__len__`, can be purged lazily.
9. Expiry condition: `now() - t0 >= ttl`.

Data structures needed:
- To track LRU order: A doubly linked list or an `OrderedDict`. `OrderedDict` is convenient in Python as it maintains insertion order (which we can manipulate to represent usage order).
- To store values and timestamps: A dictionary mapping `key` -> `(value, timestamp)`.

Let's refine the approach using `OrderedDict`.
`OrderedDict` keys will be the cache keys.
The values in `OrderedDict` will be `(value, timestamp)`.
- When `put` is called:
  - If key exists: update value and timestamp, move to end (MRU).
  - If key doesn't exist:
    - Check if capacity is full. If so, pop from beginning (LRU).
    - Insert at end.
- When `get` is called:
  - Check if key exists.
  - If exists, check expiry.
    - If expired: remove from dict, return `None`.
    - If live: move to end (MRU), return value.
  - If not exists: return `None`.
- When `__len__` is called:
  - Need to count live entries.
  - Since lazy purging is allowed, we might have expired entries sitting in the dict.
  - We could iterate and count, or clean up expired entries during `get`/`put` and maybe `__len__`.
  - Iterating `__len__` is O(N). If N is large, this is slow. However, for a standard interview-style problem, O(N) for `__len__` is often acceptable unless specified otherwise. But we can do better or just rely on lazy cleanup.
  - Actually, `__len__` usually implies O(1). But with TTL, maintaining an exact count of live entries without scanning or a separate structure (like a min-heap for expiry times) is tricky.
  - Let's look at the requirements again: "Expired entries may be purged lazily."
  - If we purge lazily on access (`get`/`put`), `__len__` might return a count including expired entries if they haven't been accessed yet.
  - However, usually `__len__` should be accurate.
  - Let's consider the constraints. If strict O(1) `__len__` is required, we need to track count. But if an entry expires, we need to decrement count. We can't know when it expires without checking time.
  - Maybe `__len__` is allowed to be O(N) or we clean up expired entries during `__len__`?
  - Or, we can just return `len(self._cache)` assuming we clean up aggressively?
  - Let's re-read: "Expired entries may be purged lazily." This suggests we don't need a background thread or strict O(1) expiry handling.
  - If `__len__` returns the number of *live* entries, and we have expired entries in the map, we must filter them.
  - Let's implement `__len__` by iterating and cleaning up expired entries. This ensures correctness.

Wait, `OrderedDict` move_to_end is O(1).
`popitem(last=False)` is O(1).
`__contains__` is O(1).

Algorithm details:
- `self._cache`: `OrderedDict` mapping `key` -> `(value, timestamp)`.
- `self._capacity`: int.
- `self._ttl`: float.
- `self._now`: callable.

`put(key, value)`:
1. `current_time = self._now()`
2. If `key` in `self._cache`:
   - Update entry: `self._cache[key] = (value, current_time)`
   - Move to end: `self._cache.move_to_end(key)`
   - Return.
3. If `len(self._cache) >= self._capacity`:
   - We need to evict.
   - The LRU item is at the front (`popitem(last=False)`).
   - However, the LRU item might be expired. If it's expired, we should remove it anyway.
   - Actually, if we just pop the front, we remove the LRU. If it was expired, good. If it was live, we evicted a live entry.
   - Is it possible that the LRU entry is expired but there are other expired entries? Yes.
   - Does it matter? The requirement says "evict the least-recently-used live entry first".
   - Wait, "evict the least-recently-used **live** entry first".
   - This implies if the LRU entry is expired, it's not a "live" entry. Should we evict it?
   - Expired entries don't count toward capacity?
   - "capacity is the maximum number of live entries."
   - So if we have expired entries taking up space in the dict, they shouldn't count towards capacity.
   - This complicates things. If `len(self._cache)` includes expired entries, checking `len >= capacity` is wrong.
   - We need to know the number of *live* entries.
   - Or, we can clean up expired entries before checking capacity.
   - Let's refine:
     - Before inserting a new key, ensure we have space for a *live* entry.
     - While `count_live_entries >= capacity`:
       - Remove LRU entry (front of OrderedDict).
       - Decrement count if it was live.
     - But counting live entries is O(N).
     - Maybe we can maintain a count of live entries?
     - When an entry expires, we decrement count. But we only detect expiry on access.
     - If we rely on lazy purging, we might have "ghost" entries.
     - Let's look at the wording: "capacity is the maximum number of live entries."
     - "If inserting a new key would exceed capacity, evict the least-recently-used live entry first."
     - This strongly suggests that expired entries do not consume capacity.
     - So, if I have capacity 2, and I have 2 live entries, and 1 expired entry. Total items in dict = 3.
     - If I put a new key, I must evict an LRU live entry. The expired entry should probably be cleaned up too, but strictly speaking, it doesn't block capacity.
     - However, keeping expired entries forever is bad.
     - Let's assume a simpler interpretation often used in these problems: The cache size limit applies to the internal storage, or we just evict LRU regardless of expiry status, but expired ones are effectively gone.
     - But the text says "maximum nu
…[53988 chars elided]…
 
            So `_enforce_capacity` logic:
            1. If `len(self.cache) <= self.capacity`: return.
            2. Scan to count live and collect expired keys.
            3. Remove expired keys.
            4. While `live_count > self.capacity`:
               - Pop LRU (which is now the first item, guaranteed live since expired removed).
               - `live_count -= 1`.
            
            Wait, step 4: "Pop LRU (which is now the first item, guaranteed live)".
            Is it guaranteed live?
            We removed all expired keys in step 3.
            So yes, remaining keys are live.
            And `OrderedDict` order is LRU -> MRU.
            So `popitem(last=False)` removes LRU live entry.
            Correct.
            
            One detail:
            In step 2, we iterate `self.cache.items()`.
            If we modify `self.cache` during iteration, it's bad.
            So we collect keys to remove in a list.
            
            Also, `__len__` needs to count live entries.
            It can use the same logic (scan).
            
            Let's refine `_enforce_capacity` to be efficient.
            Actually, if `len(self.cache)` is large, scanning is costly.
            But we only scan when `len > capacity`.
            And we reduce `len` significantly (remove expired, evict live).
            So it shouldn't happen too often.
            
            Wait, what if `ttl` is very large?
            Then entries don't expire.
            `len` will be at most `capacity + 1` (before eviction).
            So scan is O(capacity).
            If `capacity` is large, O(capacity) is O(N).
            But standard LRU cache operations are O(1).
            Here we have O(N) due to TTL check.
            Is there an O(1) way?
            Only if we maintain live count.
            But we decided maintaining live count is hard.
            However, maybe we can maintain it?
            Let's revisit maintaining `live_count`.
            
            If we maintain `live_count`:
Self-repair attempts
[
  {
    "total": 1,
    "answer": "",
    "passed": 0,
    "reasoning": "The user wants an implementation of an LRU Cache with TTL (Time To Live) in Python.\n\nKey requirements:\n1. Class `LRUCache` with `__init__`, `get`, `put`, `__len__`.\n2. `capacity`: max number of live entries.\n3. `ttl`: max age in seconds.\n4. `now`: callable for current time (default `time.monotonic`).\n5. `get(key)`: returns value or `None` if absent/expired. Updates recency (MRU), does NOT reset age.\n6. `put(key, value)`: inserts/updates. Resets age (timestamp = now). Updates recency. Evicts LRU if capacity exceeded.\n7. `__len__()`: count of live (non-expired) entries.\n8. Expired entries: never returned, don't count in `__len__`, can be purged lazily.\n9. Expiry condition: `now() - t0 >= ttl`.\n\nData structures needed:\n- To track LRU order: A doubly linked list or an `OrderedDict`. `OrderedDict` is convenient in Python as it maintains insertion order (which we can manipulate to represent usage order).\n- To store values and timestamps: A dictionary mapping `key` -> `(value, timestamp)`.\n\nLet's refine the approach using `OrderedDict`.\n`OrderedDict` keys will be the cache keys.\nThe values in `OrderedDict` will be `(value, timestamp)`.\n- When `put` is called:\n  - If key exists: update value and timestamp, move to end (MRU).\n  - If key doesn't exist:\n    - Check if capacity is full. If so, pop from beginning (LRU).\n    - Insert at end.\n- When `get` is called:\n  - Check if key exists.\n  - If exists, check expiry.\n    - If expired: remove from dict, return `None`.\n    - If live: move to end (MRU), return value.\n  - If not exists: return `None`.\n- When `__len__` is called:\n  - Need to count live entries.\n  - Since lazy purging is allowed, we might have expired entries sitting in the dict.\n  - We could iterate and count, or clean up expired entries during `get`/`put` and maybe `__len__`.\n  - Iterating `__len__` is O(N). If N is large, this is slow. However, for a standard interview-style problem, O(N) for `__len__` is often acceptable unless specified otherwise. But we can do better or just rely on lazy cleanup.\n  - Actually, `__len__` usually implies O(1). But with TTL, maintaining an exact count of live entries without scanning or a separate structure (like a min-heap for expiry times) is tricky.\n  - Let's look at the requirements again: \"Expired entries may be purged lazily.\"\n  - If we purge lazily on access (`get`/`put`), `__len__` might return a count including expired entries if they haven't been accessed yet.\n  - However, usually `__len__` should be accurate.\n  - Let's consider the constraints. If strict O(1) `__len__` is required, we need to track count. But if an entry expires, we need to decrement count. We can't know when it expires without checking time.\n  - Maybe `__len__` is allowed to be O(N) or we clean up expired entries during `__len__`?\n  - Or, we can just return `len(self._cache)` assuming we clean up aggressively?\n  - Let's re-read: \"Expired entries may be purged lazily.\" This suggests we don't need a background thread or strict O(1) expiry handling.\n  - If `__len__` returns the number of *live* entries, and we have expired entries in the map, we must filter them.\n  - Let's implement `__len__` by iterating and cleaning up expired entries. This ensures correctness.\n\nWait, `OrderedDict` move_to_end is O(1).\n`popitem(last=False)` is O(1).\n`__contains__` is O(1).\n\nAlgorithm details:\n- `self._cache`: `OrderedDict` mapping `key` -> `(value, timestamp)`.\n- `self._capacity`: int.\n- `self._ttl`: float.\n- `self._now`: callable.\n\n`put(key, value)`:\n1. `current_time = self._now()`\n2. If `key` in `self._cache`:\n   - Update entry: `self._cache[key] = (value, current_time)`\n   - Move to end: `self._cache.move_to_end(key)`\n   - Return.\n3. If `len(self._cache) >= self._capacity`:\n   - We need to evict.\n   - The LRU item is at the front (`popitem(last=False)`).\n   - However, the LRU item might be expired. If it's expired, we should remove it anyway.\n   - Actually, if we just pop the front, we remove the LRU. If it was expired, good. If it was live, we evicted a live entry.\n   - Is it possible that the LRU entry is expired but there are other expired entries? Yes.\n   - Does it matter? The requirement says \"evict the least-recently-used live entry first\".\n   - Wait, \"evict the least-recently-used **live** entry first\".\n   - This implies if the LRU entry is expired, it's not a \"live\" entry. Should we evict it?\n   - Expired entries don't count toward capacity?\n   - \"capacity is the maximum number of live entries.\"\n   - So if we have expired entries taking up space in the dict, they shouldn't count towards capacity.\n   - This complicates things. If `len(self._cache)` includes expired entries, checking `len >= capacity` is wrong.\n   - We need to know the number of *live* entries.\n   - Or, we can clean up expired entries before checking capacity.\n   - Let's refine:\n     - Before inserting a new key, ensure we have space for a *live* entry.\n     - While `count_live_entries >= capacity`:\n       - Remove LRU entry (front of OrderedDict).\n       - Decrement count if it was live.\n     - But counting live entries is O(N).\n     - Maybe we can maintain a count of live entries?\n     - When an entry expires, we decrement count. But we only detect expiry on access.\n     - If we rely on lazy purging, we might have \"ghost\" entries.\n     - Let's look at the wording: \"capacity is the maximum number of live entries.\"\n     - \"If inserting a new key would exceed capacity, evict the least-recently-used live entry first.\"\n     - This strongly suggests that expired entries do not consume capacity.\n     - So, if I have capacity 2, and I have 2 live entries, and 1 expired entry. Total items in dict = 3.\n     - If I put a new key, I must evict an LRU live entry. The expired entry should probably be cleaned up too, but strictly speaking, it doesn't block capacity.\n     - However, keeping expired entries forever is bad.\n     - Let's assume a simpler interpretation often used in these problems: The cache size limit applies to the internal storage, or we just evict LRU regardless of expiry status, but expired ones are effectively gone.\n     - But the text says \"maximum nu\n…[53988 chars elided]…\n \n            So `_enforce_capacity` logic:\n            1. If `len(self.cache) <= self.capacity`: return.\n            2. Scan to count live and collect expired keys.\n            3. Remove expired keys.\n            4. While `live_count > self.capacity`:\n               - Pop LRU (which is now the first item, guaranteed live since expired removed).\n               - `live_count -= 1`.\n            \n            Wait, step 4: \"Pop LRU (which is now the first item, guaranteed live)\".\n            Is it guaranteed live?\n            We removed all expired keys in step 3.\n            So yes, remaining keys are live.\n            And `OrderedDict` order is LRU -> MRU.\n            So `popitem(last=False)` removes LRU live entry.\n            Correct.\n            \n            One detail:\n            In step 2, we iterate `self.cache.items()`.\n            If we modify `self.cache` during iteration, it's bad.\n            So we collect keys to remove in a list.\n            \n            Also, `__len__` needs to count live entries.\n            It can use the same logic (scan).\n            \n            Let's refine `_enforce_capacity` to be efficient.\n            Actually, if `len(self.cache)` is large, scanning is costly.\n            But we only scan when `len > capacity`.\n            And we reduce `len` significantly (remove expired, evict live).\n            So it shouldn't happen too often.\n            \n            Wait, what if `ttl` is very large?\n            Then entries don't expire.\n            `len` will be at most `capacity + 1` (before eviction).\n            So scan is O(capacity).\n            If `capacity` is large, O(capacity) is O(N).\n            But standard LRU cache operations are O(1).\n            Here we have O(N) due to TTL check.\n            Is there an O(1) way?\n            Only if we maintain live count.\n            But we decided maintaining live count is hard.\n            However, maybe we can maintain it?\n            Let's revisit maintaining `live_count`.\n            \n            If we maintain `live_count`:\n",
    "test_error": "no code extracted from response"
  },
  {
    "total": 8,
    "answer": "```python\nimport time\nfrom collections import OrderedDict\n\n\nclass LRUCache:\n    def __init__(self, capacity: int, ttl: float, now=time.monotonic):\n        self.capacity = capacity\n        self.ttl = ttl\n        self.now = now\n        self.cache = OrderedDict()  # key -> (value, timestamp)\n\n    def _is_expired(self, timestamp: float) -> bool:\n        return self.now() - timestamp >= self.ttl\n\n    def _count_live(self) -> int:\n        current_time = self.now()\n        return sum(\n            1 for _, (_, timestamp) in self.cache.items()\n            if current_time - timestamp < self.ttl\n        )\n\n    def get(self, key):\n        if key not in self.cache:\n            return None\n\n        value, timestamp = self.cache[key]\n\n        if self._is_expired(timestamp):\n            del self.cache[key]\n            return None\n\n        # Move to end (MRU) — refreshes recency, not age\n        self.cache.move_to_end(key)\n        return value\n\n    def put(self, key, value):\n        if self.capacity <= 0:\n            return\n\n        current_time = self.now()\n\n        if key in self.cache:\n            # Update existing entry: reset age, mark as MRU\n            self.cache[key] = (value, current_time)\n            self.cache.move_to_end(key)\n        else:\n            # Lazy purge: remove expired entries from LRU end\n            while self.cache:\n                lru_key, (_, lru_timestamp) = next(iter(self.cache.items()))\n                if self._is_expired(lru_timestamp):\n                    del self.cache[lru_key]\n                else:\n                    break\n\n            # If live entries would exceed capacity, evict LRU live entry\n            if self._count_live() >= self.capacity:\n                self.cache.popitem(last=False)\n\n            # Insert new entry as MRU\n            self.cache[key] = (value, current_time)\n\n    def __len__(self) -> int:\n        return self._count_live()\n```",
    "passed": 8,
    "reasoning": "The user wants me to implement an LRU Cache with TTL in a file called `solution.py`. Let me carefully implement this based on the specifications:\n\n1. `LRUCache` class with `__init__`, `get`, `put`, and `__len__` methods\n2. Bounded by capacity (evicts LRU)\n3. Entries expire after `ttl` seconds\n4. `get(key)` returns value or None if absent/expired; successful get refreshes recency but NOT age\n5. `put(key, value)` inserts/updates, marks as MRU, resets age (timestamp)\n6. Expired entries don't count toward `__len__`\n7. Uses injectable `now` callable for time\n\nLet me implement this using a doubly-linked list for LRU ordering and a dict for O(1) lookups.\n\nKey design decisions:\n- Use OrderedDict or a custom doubly-linked list + dict\n- Store entries as (value, timestamp)\n- On `get`: check if expired, if not return value and move to MRU position\n- On `put`: if key exists, update value and timestamp, move to MRU; if new, add to MRU, evict LRU if needed\n- On `__len__`: count non-expired entries (lazy purge)\n\nLet me use `collections.OrderedDict` for simplicity - it maintains insertion order and we can move items to end on access.\n\nActually, let me think about the lazy purge more carefully:\n- Expired entries should be purged lazily\n- When we do `get`, we check expiration\n- When we do `put`, we might need to evict LRU entries (which might be expired)\n- `__len__` should return count of live entries\n\nFor eviction: when capacity is exceeded, we evict from the LRU end. But the LRU entry might be expired. We should skip expired entries when evicting? Or should expired entries be considered for eviction?\n\nRe-reading: \"If inserting a new key would exceed capacity, evict the least-recently-used live entry first.\"\n\nSo we evict the LRU **live** entry. Expired entries don't count toward capacity.\n\nLet me think about this more carefully:\n- Expired entries don't count toward `__len__`\n- Expired entries don't count toward capacity\n- When evicting, we evict the LRU live entry\n\nSo the approach:\n1. Store all entries in an OrderedDict (key -> (value, timestamp))\n2. On `get(key)`:\n   - If key not in dict, return None\n   - Check if expired: if now() - timestamp >= ttl, remove and return None\n   - Otherwise, move to end (MRU) and return value\n3. On `put(key, value)`:\n   - If key exists: update value and timestamp, move to end\n   - If key doesn't exist:\n     - First, clean up expired entries from LRU end (they don't count toward capacity)\n     - If still at capacity, evict LRU entry (pop from left)\n     - Add new entry at end\n4. On `__len__`:\n   - Count non-expired entries (or maintain a count)\n\nActually, for efficiency, let me think about maintaining the count. But lazy purge means we might have stale entries. Let me just count on `__len__` or maintain it carefully.\n\nActually, let me reconsider. The spec says \"Expired entries may be purged lazily.\" This suggests we don't need to eagerly purge them. But `__len__` must return the correct count of live entries.\n\nLet me implement with lazy purging:\n\n```python\nimport time\nfrom collections import OrderedDict\n\nclass LRUCache:\n    def __init__(self, capacity: int, ttl: float, now=time.monotonic):\n        self.capacity = capacity\n        self.ttl = ttl\n        self.now = now\n        self.cache = OrderedDict()  # key -> (value, timestamp)\n    \n    def _is_expired(self, timestamp):\n        return self.now() - timestamp >= self.ttl\n    \n    def _purge_expired_from_left(self):\n        \"\"\"Remove expired entries from the LRU end.\"\"\"\n        while self.cache:\n            key, (value, timestamp) = next(iter(self.cache.items()))\n            if self._is_expired(timestamp):\n                del self.cache[key]\n            else:\n                break\n    \n    def get(self, key):\n        if key not in self.cache:\n            return None\n        \n        value, timestamp = self.cache[key]\n        \n        if self._is_expired(timestamp):\n            del self.cache[key]\n            return None\n        \n        # Move to end (MRU)\n        self.cache.move_to_end(key)\n        return value\n    \n    def put(self, key, value):\n        current_time = self.now()\n        \n        if key in self.cache:\n            # Update existing entry\n            self.cache[key] = (value, current_time)\n            self.cache.move_to_end(key)\n        else:\n            # Remove expired entries from LRU end first\n            self._purge_expired_from_left()\n            \n            # If still at capacity, evict LRU\n            if len(self.cache) >= self.capacity:\n                self.cache.popitem(last=False)\n            \n            # Add new entry\n            self.cache[key] = (value, current_time)\n    \n    def __len__(self) -> int:\n        # Count live entries\n        count = 0\n        current_time = self.now()\n        for key, (value, timestamp) in self.cache.items():\n            if current_time - timestamp < self.ttl:\n                count += 1\n        return count\n```\n\nWait, there's an issue with `_purge_expired_from_left`. When we iterate with `next(iter(self.cache.items()))`, we're getting the first item, but if we delete it, the next call will get the new first item. But we're modifying the dict while iterating... Actually, we're not iterating with a for loop, we're using a while loop and calling `next(iter(...))` each time, so it should be fine.\n\nBut actually, there's a subtlety. The spec says \"evict the least-recently-used live entry first\". So when we're at capacity and need to evict, we should evict the LRU live entry, not just the LRU entry (which might be expired).\n\nLet me reconsider the `put` logic:\n1. If key exists: update and move to MRU\n2. If key doesn't exist:\n   - We need to make room. The capacity is for live entries.\n   - Expired entries don't count toward capacity.\n   - So we should first purge expired entries (they're not taking up capacity).\n   - Then if we still need room, evict the LRU live entry.\n\nActually, wait. Let me re-read: \"capacity is the maximum number of live entries\". So if we have 5 entries but 2 are expired, we only have 3 live entries. If capacity is 4, we can add one more without evicting.\n\nSo the logic should be:\n1. On `put\n…[25215 chars elided]…\ne immediately expired. That's probably the correct behavior.\n\nOK, I'm done. Let me write the final code.\n\n```python\nimport time\nfrom collections import OrderedDict\n\nclass LRUCache:\n    def __init__(self, capacity: int, ttl: float, now=time.monotonic):\n        self.capacity = capacity\n        self.ttl = ttl\n        self.now = now\n        self.cache = OrderedDict()  # key -> (value, timestamp)\n    \n    def _is_expired(self, timestamp):\n        return self.now() - timestamp >= self.ttl\n    \n    def _count_live(self):\n        current_time = self.now()\n        return sum(1 for _, (value, timestamp) in self.cache.items() \n                   if current_time - timestamp < self.ttl)\n    \n    def get(self, key):\n        if key not in self.cache:\n            return None\n        \n        value, timestamp = self.cache[key]\n        \n        if self._is_expired(timestamp):\n            del self.cache[key]\n            return None\n        \n        # Move to end (MRU)\n        self.cache.move_to_end(key)\n        return value\n    \n    def put(self, key, value):\n        if self.capacity <= 0:\n            return\n        \n        current_time = self.now()\n        \n        if key in self.cache:\n            # Update existing entry\n            self.cache[key] = (value, current_time)\n            self.cache.move_to_end(key)\n        else:\n            # Lazy purge: remove expired entries from LRU end\n            while self.cache:\n                lru_key, (lru_value, lru_timestamp) = next(iter(self.cache.items()))\n                if self._is_expired(lru_timestamp):\n                    del self.cache[lru_key]\n                else:\n                    break\n            \n            # If at capacity (counting live entries), evict LRU live entry\n            if self._count_live() >= self.capacity:\n                self.cache.popitem(last=False)\n            \n            # Add new entry\n            self.cache[key] = (value, current_time)\n    \n    def __len__(self) -> int:\n        return self._count_live()\n```\n\nThis should be correct. Let me submit it.\n",
    "test_error": ""
  }
]