Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
10 changes: 2 additions & 8 deletions picard/ui/itemviews/custom_columns/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,12 @@
CachedSortAdapter,
CasefoldSortAdapter,
CompositeSortAdapter,
DescendingCasefoldSortAdapter,
DescendingNaturalSortAdapter,
DescendingNumericSortAdapter,
LengthSortAdapter,
LocaleAwareSortAdapter,
NaturalSortAdapter,
NullsFirstAdapter,
NullsLastAdapter,
NumericSortAdapter,
ReverseAdapter,
)


Expand All @@ -66,16 +63,13 @@
"_create_custom_column",
# Sorting adapters
"CasefoldSortAdapter",
"DescendingCasefoldSortAdapter",
"NumericSortAdapter",
"DescendingNumericSortAdapter",
"NaturalSortAdapter",
"DescendingNaturalSortAdapter",
"LengthSortAdapter",
"LocaleAwareSortAdapter",
"ArticleInsensitiveAdapter",
"CompositeSortAdapter",
"NullsLastAdapter",
"NullsFirstAdapter",
"CachedSortAdapter",
"ReverseAdapter",
]
5 changes: 1 addition & 4 deletions picard/ui/itemviews/custom_columns/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,13 +296,10 @@ def generate_new_key() -> str:

# Mapping of user-friendly names to sorting adapter class names
SORTING_ADAPTER_NAMES: dict[str, str] = {
N_("Default"): "", # No adapter (use default sorting)
N_("Default"): "LocaleAwareSortAdapter",
N_("Case Insensitive"): "CasefoldSortAdapter",
N_("Case Insensitive - Descending"): "DescendingCasefoldSortAdapter",
N_("Numeric"): "NumericSortAdapter",
N_("Numeric - Descending"): "DescendingNumericSortAdapter",
N_("Natural"): "NaturalSortAdapter",
N_("Natural - Descending"): "DescendingNaturalSortAdapter",
N_("By Value Length"): "LengthSortAdapter",
N_("Article Insensitive"): "ArticleInsensitiveAdapter",
N_("Empty Values Last"): "NullsLastAdapter",
Expand Down
96 changes: 8 additions & 88 deletions picard/ui/itemviews/custom_columns/sorting_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,27 +66,20 @@ def __repr__(self) -> str: # pragma: no cover - debug helper
return f"{self.__class__.__name__}(base={self._base!r})"


class CasefoldSortAdapter(_AdapterBase):
class LocaleAwareSortAdapter(_AdapterBase):
"""Provide case-insensitive sort using `str.casefold()` on evaluated value."""

def sort_key(self, obj: Item): # pragma: no cover - thin wrapper
"""Return case-insensitive sort key for item."""
return (self._base.evaluate(obj) or "").casefold()

return _sort_key(self._base.evaluate(obj) or "")

class DescendingCasefoldSortAdapter(_AdapterBase):
"""Provide descending case-insensitive sort by inverting code points."""

@staticmethod
def _invert_string(s: str) -> str:
# Map code points to inverted order to simulate descending in ascending sort
# Use BMP range for practicality; characters outside will still order consistently
return "".join(chr(0x10FFFF - ord(c)) for c in s)
class CasefoldSortAdapter(_AdapterBase):
"""Provide case-insensitive sort using `str.casefold()` on evaluated value."""

def sort_key(self, obj: Item): # pragma: no cover - thin wrapper
"""Return descending case-insensitive sort key for item."""
v = (self._base.evaluate(obj) or "").casefold()
return self._invert_string(v)
"""Return case-insensitive sort key for item."""
return _sort_key((self._base.evaluate(obj) or "").casefold())


class NumericSortAdapter(_AdapterBase):
Expand Down Expand Up @@ -116,26 +109,6 @@ def sort_key(self, obj: Item): # pragma: no cover - thin wrapper
return (0, parsed_value)


class DescendingNumericSortAdapter(NumericSortAdapter):
"""Provide descending numeric sort by negating parsed value."""

def sort_key(self, obj: Item): # pragma: no cover - thin wrapper
"""Return descending numeric-first composite sort key for item.

- (0, -number) when numeric (numbers first, descending)
- (1, inverted_natural_key) when non-numeric (fallback, descending)
"""
value_str: str = self._base.evaluate(obj) or ""
try:
parsed_value = self._parser(value_str)
except (ValueError, TypeError):
natural_key = _sort_key(value_str, numeric=True)
inverted = DescendingNaturalSortAdapter._invert_string(str(natural_key))
return (1, inverted)
else:
return (0, -parsed_value)


class LengthSortAdapter(_AdapterBase):
"""Provide sort by string length of evaluated value."""

Expand Down Expand Up @@ -174,8 +147,8 @@ def sort_key(self, obj: Item): # pragma: no cover - thin wrapper
for art in self._articles:
prefix = f"{art} "
if lv.startswith(prefix):
return (lv[len(prefix) :], lv)
return (lv, lv)
return (_sort_key(lv[len(prefix) :]), _sort_key(prefix))
return (_sort_key(lv), _sort_key(""))


class CompositeSortAdapter(_AdapterBase):
Expand Down Expand Up @@ -299,37 +272,6 @@ def sort_key(self, obj: Item): # pragma: no cover - thin wrapper
return key


class ReverseAdapter(_AdapterBase):
"""Provide reversed sorting for string or numeric sort keys."""

@staticmethod
def _invert_string(s: str) -> str:
return "".join(chr(0x10FFFF - ord(c)) for c in s)

def sort_key(self, obj: Item): # pragma: no cover - thin wrapper
"""Return reversed sort key for item.

Always return a normalized tuple key to avoid cross-type comparisons:
- Numbers -> (0, -float(value))
- Strings -> (1, inverted string)
- Tuples -> keep as-is (assumed already normalized)
- Other -> (1, lowercased string)
"""
if isinstance(self._base, SortKeyProvider):
base_key = self._base.sort_key(obj)
else:
base_key = self._base.evaluate(obj) or ""

if isinstance(base_key, tuple):
return base_key
if isinstance(base_key, (int, float)):
return (0, -float(base_key))
if isinstance(base_key, str):
return (1, self._invert_string(base_key))
text = "" if base_key is None else str(base_key)
return (1, text.casefold())


class NaturalSortAdapter(_AdapterBase):
"""Provide natural (alphanumeric) sort using locale-aware natural ordering."""

Expand All @@ -340,25 +282,3 @@ def sort_key(self, obj: Item): # pragma: no cover - thin wrapper
For example: "Track 1", "Track 2", "Track 10" instead of "Track 1", "Track 10", "Track 2".
"""
return _sort_key(self._base.evaluate(obj) or "", numeric=True)


class DescendingNaturalSortAdapter(_AdapterBase):
"""Provide descending natural (alphanumeric) sort using string inversion."""

@staticmethod
def _invert_string(s: str) -> str:
"""Invert string for descending order (same as ReverseAdapter)."""
return "".join(chr(0x10FFFF - ord(c)) for c in s)

def sort_key(self, obj: Item): # pragma: no cover - thin wrapper
"""Return descending natural sort key for item.

Gets the natural sort key, converts to string, then inverts character order.
"""

# Get the natural sort key and convert to string
natural_key = _sort_key(self._base.evaluate(obj) or "", numeric=True)
key_str = str(natural_key)

# Apply string inversion for proper descending order
return self._invert_string(key_str)
10 changes: 2 additions & 8 deletions picard/ui/itemviews/custom_columns/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,30 +263,24 @@ def _apply_sorting_adapter(base_provider: ColumnValueProvider, sorting_adapter_n
from picard.ui.itemviews.custom_columns.sorting_adapters import (
ArticleInsensitiveAdapter,
CasefoldSortAdapter,
DescendingCasefoldSortAdapter,
DescendingNaturalSortAdapter,
DescendingNumericSortAdapter,
LengthSortAdapter,
LocaleAwareSortAdapter,
NaturalSortAdapter,
NullsFirstAdapter,
NullsLastAdapter,
NumericSortAdapter,
ReverseAdapter,
)

# Mapping of class names to actual classes
adapter_classes = {
'CasefoldSortAdapter': CasefoldSortAdapter,
'DescendingCasefoldSortAdapter': DescendingCasefoldSortAdapter,
'NumericSortAdapter': NumericSortAdapter,
'DescendingNumericSortAdapter': DescendingNumericSortAdapter,
'NaturalSortAdapter': NaturalSortAdapter,
'DescendingNaturalSortAdapter': DescendingNaturalSortAdapter,
'LengthSortAdapter': LengthSortAdapter,
'LocaleAwareSortAdapter': LocaleAwareSortAdapter,
'ArticleInsensitiveAdapter': ArticleInsensitiveAdapter,
'NullsLastAdapter': NullsLastAdapter,
'NullsFirstAdapter': NullsFirstAdapter,
'ReverseAdapter': ReverseAdapter,
}

adapter_class = adapter_classes.get(sorting_adapter_name)
Expand Down
Loading
Loading