Elaborating further my previous proposal and the interesting solution given by @Alexandr Shurigin, I'm now suggesting another option.
This new solution splits "signature" into two field:
- code: a variable length alfanumeric string
- weigth: a numeric value, possibly with leading 0s to be ignored
Given:
[
'X 1',
'XY 1',
'XYZ 1',
'BA 1',
'BA 10',
'BA 100',
'BA 2',
'BA 1002',
'BA 1000',
'BA 1001',
'BA 003',
]
the expected result is:
[
'BA 1',
'BA 2',
'BA 003',
'BA 10',
'BA 100',
'BA 1000',
'BA 1001',
'BA 1002',
'X 1',
'XY 1',
'XYZ 1',
]
All computations are delegated to the database in a generic way, thanks to django.db.models.functions module.
queryset = (
Item.objects.annotate(
split_index=StrIndex('signature', Value(' ')),
).annotate(
left=Substr('signature', Value(1), 'split_index', output_field=CharField()),
right=Substr('signature', F('split_index'), output_field=CharField()),
).annotate(
code=Trim('left'),
weight=Cast('right', output_field=IntegerField())
).order_by('code', 'weight')
)
A more compact, but also less readable solution, is this:
queryset = (
Item.objects.annotate(
split_index=StrIndex('signature', Value(' ')),
).annotate(
code=Trim(Substr('signature', Value(1), 'split_index', output_field=CharField())),
weight=Cast(Substr('signature', F('split_index'), output_field=CharField()), output_field=IntegerField())
).order_by('code', 'weight')
)
What I'm really missing here is a "IndexOf" function to compute "split_index" as the position of either the first space OR digit, thus giving a really Natural Sort behaviour (to accept, for example "BA123" as well as "BA 123")
Proof:
import django
#from django.db.models.expressions import RawSQL
from pprint import pprint
from backend.models import Item
from django.db.models.functions import Length, StrIndex, Substr, Cast, Trim
from django.db.models import Value, F, CharField, IntegerField
class ModelsItemCase(django.test.TransactionTestCase):
def test_item_sorting(self):
signatures = [
'X 1',
'XY 1',
'XYZ 1',
'BA 1',
'BA 10',
'BA 100',
'BA 2',
'BA 1002',
'BA 1000',
'BA 1001',
'BA 003',
]
for signature in signatures:
Item.objects.create(signature=signature)
print(' ')
pprint(list(Item.objects.all()))
print('')
expected_result = [
'BA 1',
'BA 2',
'BA 003',
'BA 10',
'BA 100',
'BA 1000',
'BA 1001',
'BA 1002',
'X 1',
'XY 1',
'XYZ 1',
]
queryset = (
Item.objects.annotate(
split_index=StrIndex('signature', Value(' ')),
).annotate(
code=Trim(Substr('signature', Value(1), 'split_index', output_field=CharField())),
weight=Cast(Substr('signature', F('split_index'), output_field=CharField()), output_field=IntegerField())
).order_by('code', 'weight')
)
pprint(list(queryset))
print(' ')
print(str(queryset.query))
self.assertSequenceEqual(
[row.signature for row in queryset],
expected_result
)
The resulting query, for sqlite3 is:
SELECT
"backend_item"."id",
"backend_item"."signature",
INSTR("backend_item"."signature", ) AS "split_index",
TRIM(SUBSTR("backend_item"."signature", 1, INSTR("backend_item"."signature", ))) AS "code",
CAST(SUBSTR("backend_item"."signature", INSTR("backend_item"."signature", )) AS integer) AS "weight"
FROM "backend_item"
ORDER BY "code" ASC, "weight" ASC
and for PostgreSQL:
SELECT
"backend_item"."id",
"backend_item"."signature",
STRPOS("backend_item"."signature", ) AS "split_index",
TRIM(SUBSTRING("backend_item"."signature", 1, STRPOS("backend_item"."signature", ))) AS "code",
(SUBSTRING("backend_item"."signature", STRPOS("backend_item"."signature", )))::integer AS "weight"
FROM "backend_item"
ORDER BY "code" ASC, "weight" ASC