41 lines
1.6 KiB
Python
41 lines
1.6 KiB
Python
"""Ranking rules for a product line.
|
||
|
||
Kept out of ``app.py`` so it can be imported and tested without executing the
|
||
Streamlit page. Deciding which 10 of 99 products to spend an hour analyzing is
|
||
real logic, not presentation.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
# Label shown in the picker → (field to sort on, descending?).
|
||
# Ordered by how often each is the right question to ask of a line.
|
||
LINE_SORTS: dict[str, tuple[str, bool]] = {
|
||
"Sales velocity (30d)": ("avg_30d", True),
|
||
"Inventory on hand": ("inventory", True),
|
||
"Cover days — lowest first": ("cover_days", False),
|
||
"Reorder size (min PO)": ("min_po_quantity", True),
|
||
"A–Z": ("sku", False),
|
||
}
|
||
|
||
|
||
def line_sort_key(product: dict, field: str):
|
||
"""Sort key tolerating both text and numeric fields, and missing values.
|
||
|
||
Two things it has to get right:
|
||
|
||
* **Mixed types.** A single numeric key raises ``TypeError`` the moment the
|
||
user picks A–Z, because it compares ``str`` against ``0``.
|
||
* **Missing values sort last, in either direction.** A SKU with no cover-days
|
||
reading is not the most urgent item in the line — but a plain ``or 0``
|
||
makes it exactly that when sorting ascending.
|
||
"""
|
||
value = product.get(field)
|
||
if isinstance(value, str):
|
||
return (False, value.upper())
|
||
return (value is None, value if value is not None else 0)
|
||
|
||
|
||
def rank(products: list[dict], sort_label: str) -> list[dict]:
|
||
"""Order a line by one of `LINE_SORTS`, falling back to the first rule."""
|
||
field, reverse = LINE_SORTS.get(sort_label, next(iter(LINE_SORTS.values())))
|
||
return sorted(products, key=lambda p: line_sort_key(p, field), reverse=reverse)
|