-
-
Notifications
You must be signed in to change notification settings - Fork 419
PICARD-2103: Custom columns sorting tweaks #2735
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
2aea604
to
646a85e
Compare
646a85e
to
4c86cdf
Compare
This is unused and doesn't work with localized sorting anyway
4c86cdf
to
adb71b4
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well done. This makes it a lot clearer, especially from the user's perspective. This is the way it should have been done right from the start.
Also a "heads up" that the documentation changes in metabrainz/picard-docs#234 will need to be updated as well. |
It's good we have this PR already, but I think we should only merge it once we have fully finished the implementation of the new columns (including unifying the code for the predefined columns). There will probably be more changes to this coming. |
Yes, but sometimes it is easier to get a complex PR like #2714 merged first and make such small adjustments afterwards, where they are much easier to review. We already had 220 comments on the initial PR. |
I actual wonder if we need both @knguyen1 your thoughts? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, good move
|
||
# Build mapping dynamically from available class names in SORTING_ADAPTER_NAMES | ||
# This ensures we only try to map classes that are actually defined in the UI | ||
available_class_names = {name for name in SORTING_ADAPTER_NAMES.keys() if name} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make this a generator instead?
def available_class_names():
for name in SORTING_ADAPTER_NAMES.keys():
if name:
yield name
....
for class_name in available_class_names():
I agree completely. I'm in no rush to merge the documentation PRs for these large UI changes for exactly that reason. |
I know you didn't specifically ask for my thoughts on this, but here they are anyway. 😜 If we can remove one of the selection options and still get the same results from one of the other options, I think we should be doing exactly that. The simpler we can make this for the user the better. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me.
"NullsLastAdapter": SortingAdapterDisplayInfo( | ||
display_name=N_("Empty values first"), | ||
tooltip=N_("Empty/whitespace values sort first."), | ||
), | ||
"NullsFirstAdapter": SortingAdapterDisplayInfo( | ||
display_name=N_("Empty values last"), | ||
tooltip=N_("Empty/whitespace values sort last."), | ||
), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally, wouldn't it be better if these two (mutually exclusive) options could be applied to any of the other sorting options? Perhaps not in the selection list, but as a separate radio button as something like:
Show empty values: ○ first, ◉ last
Numeric and Natural are not interchangeable; they encode different semantics. Below is LLM claim. See #2735 (comment) for proof. What differs
Why keep both
Performance note
Recommendation
|
You can use this
#!/usr/bin/env python3
from __future__ import annotations
from typing import Callable
from picard.item import Item
from picard.ui.itemviews.custom_columns.sorting_adapters import (
NaturalSortAdapter,
NumericSortAdapter,
)
class _ValueItem(Item):
def __init__(self, value: str) -> None:
super().__init__()
self.value = value
class _Provider:
def __init__(self, value: str) -> None:
self._value = value
def evaluate(self, _obj: Item) -> str:
return self._value
def _sorted_values_with_adapter(
adapter_factory: Callable[[object], object], values: list[str]
) -> list[str]:
adapters = [adapter_factory(_Provider(v)) for v in values]
keys = [a.sort_key(_ValueItem(v)) for a, v in zip(adapters, values, strict=True)]
# Pair the original value with its key and sort by the produced key
pairs = sorted(zip(values, keys, strict=True), key=lambda p: p[1])
return [v for v, _ in pairs]
def _print_case(title: str, values: list[str]) -> None:
print(f"\n=== {title} ===")
nat = _sorted_values_with_adapter(lambda base: NaturalSortAdapter(base), values)
num = _sorted_values_with_adapter(lambda base: NumericSortAdapter(base), values)
print("Input: ", values)
print("Natural: ", nat)
print("Numeric: ", num)
def _print_case_with_parser(
title: str,
values: list[str],
parser: Callable[[str], float],
parser_label: str,
) -> None:
print(f"\n=== {title} ({parser_label}) ===")
nat = _sorted_values_with_adapter(lambda base: NaturalSortAdapter(base), values)
num = _sorted_values_with_adapter(lambda base: NumericSortAdapter(base, parser=parser), values)
print("Input: ", values)
print("Natural: ", nat)
print("Numeric*: ", num)
def _mmss_parser(s: str) -> float:
# e.g. "3:30" -> 210.0
total = 0
for part in s.split(":"):
total = total * 60 + int(part)
return float(total)
def _comma_number_parser(s: str) -> float:
# Handle localized/thousands separators like "1,200"
return float(s.replace(",", "").strip())
def main() -> None:
# 1) Mixed numerics, negatives, decimals, scientific notation, and text
# NaturalSortAdapter treats digits inside text as numbers, but not signs/exponents.
# NumericSortAdapter parses the entire value (float), so "-1" and "1e3" are truly numeric.
_print_case(
"Mixed: integers, decimals, negatives, scientific notation, and text",
["10", "2", "3.5", "x", "-1", "7a", "1e3", "3e2"],
)
# 2) Duration-like values need domain parsing. NumericSortAdapter with a custom parser
# sorts by actual time; NaturalSortAdapter only sees text + digit runs.
_print_case_with_parser(
"Durations requiring domain parsing",
["3:30", "2:05", "1:00", "x", "10:00", "0:59"],
parser=_mmss_parser,
parser_label="custom mm:ss parser",
)
# 3) Localized numbers with thousands separators. NumericSortAdapter can parse them
# (with a custom parser); NaturalSortAdapter compares as text with digits.
_print_case_with_parser(
"Localized numbers with commas",
["1,200", "300", "20", "1,050", "x"],
parser=_comma_number_parser,
parser_label="custom comma-aware parser",
)
# 4) Whitespace and formatting differences. float() handles surrounding whitespace;
# natural sorting treats whitespace as text.
_print_case(
"Whitespace and formatting",
[" 12 ", "2", " 10", "x", "000", "01", "1", "1.0"],
)
if __name__ == "__main__":
main() The Output$ uv run python test_sort.py
=== Mixed: integers, decimals, negatives, scientific notation, and text ===
Input: ['10', '2', '3.5', 'x', '-1', '7a', '1e3', '3e2']
Natural: ['-1', '1e3', '2', '3.5', '3e2', '7a', '10', 'x']
Numeric: ['-1', '2', '3.5', '10', '3e2', '1e3', '7a', 'x']
=== Durations requiring domain parsing (custom mm:ss parser) ===
Input: ['3:30', '2:05', '1:00', 'x', '10:00', '0:59']
Natural: ['0:59', '1:00', '2:05', '3:30', '10:00', 'x']
Numeric*: ['0:59', '1:00', '2:05', '3:30', '10:00', 'x']
=== Localized numbers with commas (custom comma-aware parser) ===
Input: ['1,200', '300', '20', '1,050', 'x']
Natural: ['1,050', '1,200', '20', '300', 'x']
Numeric*: ['20', '300', '1,050', '1,200', 'x']
=== Whitespace and formatting ===
Input: [' 12 ', '2', ' 10', 'x', '000', '01', '1', '1.0']
Natural: [' 12 ', ' 10', '000', '01', '1', '1.0', '2', 'x']
Numeric: ['000', '01', '1', '1.0', '2', ' 10', ' 12 ', 'x'] |
Summary
Problem
This applies some changes to the custom column sorting:
ReverseAdapter
. This does not work with locale aware sorting keys anyway.Solution
Action
Additional actions required: