Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 39 additions & 25 deletions app/bounty_sorting.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

from collections.abc import Callable
from decimal import Decimal
from typing import Any

Expand All @@ -15,9 +16,11 @@
}
BOUNTY_SORT_OPTIONS = tuple(BOUNTY_SORT_LABELS)
BOUNTY_SORT_ERROR = f"sort must be one of: {', '.join(BOUNTY_SORT_OPTIONS)}"
BountySortKey = Callable[[dict[str, Any]], Any]


def normalize_bounty_sort(sort: str | None) -> str:
"""Return a supported bounty sort mode, defaulting blank values to newest."""
raw_sort = sort or ""
if contains_control_character(raw_sort):
raise ValueError("sort must not contain control characters")
Expand All @@ -29,30 +32,41 @@ def normalize_bounty_sort(sort: str | None) -> str:
return normalized_sort


def _newest_sort_key(bounty: dict[str, Any]) -> int:
"""Sort newest bounties first by descending internal id."""
return int(bounty["id"])


def _reward_sort_key(bounty: dict[str, Any]) -> tuple[Decimal, int]:
"""Sort by per-award reward, then newest bounty id."""
return Decimal(str(bounty["reward_mrwk"])), int(bounty["id"])


def _available_sort_key(bounty: dict[str, Any]) -> tuple[Decimal, int]:
"""Sort by effective remaining MRWK pool, then newest bounty id."""
return (
Decimal(str(bounty.get("effective_available_mrwk", bounty["available_mrwk"]))),
int(bounty["id"]),
)


def _awards_sort_key(bounty: dict[str, Any]) -> tuple[int, int]:
"""Sort by effective remaining award slots, then newest bounty id."""
return (
int(bounty.get("effective_awards_remaining", bounty["awards_remaining"])),
int(bounty["id"]),
)


BOUNTY_SORT_KEYS: dict[str, BountySortKey] = {
"newest": _newest_sort_key,
"reward": _reward_sort_key,
"available": _available_sort_key,
"awards": _awards_sort_key,
}


def sort_bounties(bounties: list[dict[str, Any]], sort: str | None) -> list[dict[str, Any]]:
"""Return bounties ordered by the normalized public sort mode."""
normalized_sort = normalize_bounty_sort(sort)
if normalized_sort == "newest":
return sorted(bounties, key=lambda bounty: int(bounty["id"]), reverse=True)
if normalized_sort == "reward":
return sorted(
bounties,
key=lambda bounty: (Decimal(str(bounty["reward_mrwk"])), int(bounty["id"])),
reverse=True,
)
if normalized_sort == "available":
return sorted(
bounties,
key=lambda bounty: (
Decimal(str(bounty.get("effective_available_mrwk", bounty["available_mrwk"]))),
int(bounty["id"]),
),
reverse=True,
)
return sorted(
bounties,
key=lambda bounty: (
int(bounty.get("effective_awards_remaining", bounty["awards_remaining"])),
int(bounty["id"]),
),
reverse=True,
)
return sorted(bounties, key=BOUNTY_SORT_KEYS[normalized_sort], reverse=True)
90 changes: 90 additions & 0 deletions tests/test_bounty_sorting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from __future__ import annotations

import pytest

from app.bounty_sorting import BOUNTY_SORT_ERROR, normalize_bounty_sort, sort_bounties


def _bounty_row(
bounty_id: int,
*,
reward_mrwk: str,
available_mrwk: str,
awards_remaining: int,
effective_available_mrwk: str | None = None,
effective_awards_remaining: int | None = None,
) -> dict[str, object]:
return {
"id": bounty_id,
"reward_mrwk": reward_mrwk,
"available_mrwk": available_mrwk,
"effective_available_mrwk": effective_available_mrwk or available_mrwk,
"awards_remaining": awards_remaining,
"effective_awards_remaining": (
effective_awards_remaining
if effective_awards_remaining is not None
else awards_remaining
),
}


def _ids(rows: list[dict[str, object]]) -> list[int]:
return [int(row["id"]) for row in rows]


@pytest.mark.parametrize(
("raw_sort", "expected"),
[
(None, "newest"),
("", "newest"),
(" reward ", "reward"),
("AVAILABLE", "available"),
("awards", "awards"),
],
)
def test_normalize_bounty_sort(raw_sort: str | None, expected: str) -> None:
assert normalize_bounty_sort(raw_sort) == expected


def test_normalize_bounty_sort_rejects_invalid_values() -> None:
with pytest.raises(ValueError, match=BOUNTY_SORT_ERROR):
normalize_bounty_sort("oldest")


def test_normalize_bounty_sort_rejects_control_characters() -> None:
with pytest.raises(ValueError, match="sort must not contain control characters"):
normalize_bounty_sort("\x85reward")


def test_sort_bounties_preserves_supported_orders() -> None:
rows = [
_bounty_row(
1,
reward_mrwk="10",
available_mrwk="100",
awards_remaining=10,
effective_available_mrwk="20",
effective_awards_remaining=2,
),
_bounty_row(
2,
reward_mrwk="25",
available_mrwk="25",
awards_remaining=1,
effective_available_mrwk="25",
effective_awards_remaining=1,
),
_bounty_row(
3,
reward_mrwk="25",
available_mrwk="75",
awards_remaining=3,
effective_available_mrwk="75",
effective_awards_remaining=3,
),
]

assert _ids(sort_bounties(rows, None)) == [3, 2, 1]
assert _ids(sort_bounties(rows, "reward")) == [3, 2, 1]
assert _ids(sort_bounties(rows, "available")) == [3, 2, 1]
assert _ids(sort_bounties(rows, "awards")) == [3, 1, 2]
Loading