Last active
May 2, 2022 04:05
-
-
Save JavaScriptDude/dd112d69b082c089fe19e0b2545826da to your computer and use it in GitHub Desktop.
Python3 - Multi-Column Sorting of List of Dicts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import inspect, time | |
from random import randint | |
from functools import cmp_to_key | |
def main(): | |
perf_test() | |
# test_Comparator() | |
# test_student_comp() | |
students = [ | |
{'idx': 0, 'name': 'joh', 'grade': 'C', 'attend': 100} | |
,{'idx': 1, 'name': 'jan', 'grade': 'a', 'attend': 80} | |
,{'idx': 2, 'name': 'dav', 'grade': 'B', 'attend': 85} | |
,{'idx': 3, 'name': 'bob' , 'grade': 'C', 'attend': 85} | |
,{'idx': 4, 'name': 'jim' , 'grade': 'F', 'attend': 55} | |
,{'idx': 5, 'name': 'joe' , 'grade': None, 'attend': 55} | |
] | |
# Add an additional number of records for testing | |
for i in range(1,10000): | |
students.append({'idx': len(students), 'name':'rnd', 'grade': 'ABCDEF'[randint(0,5)], 'attend': randint(0,100)}) | |
# .: Comparator :. | |
# This was an experiment to make a more streamlined way of sorting | |
# Performance of this is reasonable and can be 'compiled' at init of application | |
# For example: | |
# stud_sorter = MultiSorter([ | |
# ('grade', True, lambda v: None if v is None else v.lower()) | |
# ,('attend', True) | |
# ], reverse=True) | |
# ... | |
# students_sorted = stud_sorter.sort(students) | |
# Note: The performance benefit of MultiSorter is only achieved with multiple calls. | |
# For Single call, use Comparator Example: | |
# students_sorted = sorted(students, key=Comparator.new( | |
# ('grade', True, lambda v: None if v is None else v.lower()) | |
# ,('attend', True) | |
# ), reverse=True) | |
# spec is a list one of the following | |
# <key> | |
# (<key>,) | |
# (<key>, <cleaner>) | |
# (<key>, <reverse>) | |
# (<key>, <reverse>, <cleaner>) | |
# where: | |
# <key> Property, Key or Index for 'column' in row | |
# <reverse>: opt - reversed sort (defaults to False) | |
# <cleaner>: opt - callback to clean / alter data in 'field' | |
class Comparator: | |
@classmethod | |
def new(cls, *args): | |
_c = Comparator(spec=args) | |
return cmp_to_key(_c._compare_a_b) | |
def __init__(self, spec): | |
a = [] | |
for s_c in spec: | |
if isinstance(s_c, (int, str)): | |
s_u = (s_c, False, None) | |
else: | |
assert isinstance(s_c, tuple) and len(s_c) in (1,2,3),\ | |
f"Invalid spec. Must have 1 to 3 params per record. Got: {s_c}" | |
if len(s_c) == 1: | |
s_u = (s_c[0], False, None) | |
elif len(s_c) == 2: | |
if inspect.isfunction(s_c[1]): | |
s_u = (s_c[0], False, s_c[1]) | |
else: | |
s_u = (s_c[0], s_c[1], None) | |
elif len(s_c) == 3: | |
s_u = s_c | |
assert inspect.isfunction(s_c[2]),\ | |
f"Third item in spec must be a function. Got: {s_c[2]}" | |
else: | |
raise Exception() | |
assert isinstance(s_u[1], bool),\ | |
f"Second item in spec row must be a boolean. Got: {s_u[1]}" | |
a.append(s_u) | |
self.spec = a | |
def _compare_a_b(self, a, b): | |
if a is None: return 1 | |
if b is None: return -1 | |
for k, desc, cleaner in self.spec: | |
try: | |
try: | |
va = a[k]; vb = b[k] | |
except Exception as ex: | |
va = getattr(a, k); vb = getattr(b, k) | |
except Exception as ex: | |
raise KeyError(f"Key {k} is not available in object(s) given a: {a.__class__.__name__}, b: {a.__class__.__name__}") | |
if cleaner: | |
va = cleaner(va) | |
vb = cleaner(vb) | |
if va != vb: | |
if va is None: return 1 | |
if vb is None: return -1 | |
if desc: | |
return -1 if va > vb else 1 | |
else: | |
return 1 if va > vb else -1 | |
return 0 | |
class MultiSorter(): | |
def __init__(self, spec, reverse:bool=False): | |
self._comp = Comparator(spec) | |
self.reverse = reverse | |
def sort(self, rows): | |
return sorted(rows, key=cmp_to_key(self._comp._compare_a_b), reverse=self.reverse) | |
class _rev: | |
def __init__(self, obj): | |
self.obj = obj | |
def __eq__(self, other): | |
return other.obj == self.obj | |
def __lt__(self, other): | |
return False if self.obj is None else \ | |
True if other.obj is None else \ | |
other.obj < self.obj | |
def perf_test(): | |
iterations = 2 | |
print(f"Starting tests. Iterations: {iterations}") | |
sw = StopWatch() | |
def student_comp(a,b): | |
k='grade'; va=a[k]; vb=b[k] | |
if va != vb: | |
if va is None: return -1 | |
if vb is None: return 1 | |
return -1 if va > vb else 1 | |
k='attend'; va=a[k]; vb=b[k]; | |
if va != vb: return -1 if va < vb else 1 | |
return 0 | |
for i in range(0,iterations): | |
students_sorted = sorted(students, key=cmp_to_key(student_comp), reverse=True) | |
print(f'student_comp: {sw.elapsed(prec=6)}s') | |
sw = StopWatch() | |
for i in range(0,iterations): | |
students_sorted = sorted(students, key=Comparator.new( | |
('grade', True, lambda v: None if v is None else v.lower()) | |
,('attend', True) | |
), reverse=True) | |
print(f'Comparator: {sw.elapsed(prec=6)}s') | |
sw = StopWatch() | |
stud_sorter = MultiSorter([ | |
('grade', True, lambda v: None if v is None else v.lower()) | |
,('attend', True) | |
], reverse=True) | |
for i in range(0,iterations): | |
students_sorted = stud_sorter.sort(students) | |
print(f'MultiSorter: {sw.elapsed(prec=6)}s') | |
sw.start() | |
for i in range(0,iterations): | |
students_sorted = sorted(students, key=lambda o: ( | |
_rev(None if o['grade'] is None else o['grade'].lower()) | |
,_rev(o['attend']) | |
), reverse=True) | |
print(f'BlackPanther: {sw.elapsed(prec=6)}s') | |
sw.start() | |
def _student_sort(o): | |
return ( _rev(None if o['grade'] is None else o['grade'].lower()) | |
,_rev(o['attend']) | |
) | |
for i in range(0,iterations): | |
students_sorted = sorted(students, key=_student_sort, reverse=True) | |
print(f'BlackPanther_optimized: {sw.elapsed(prec=6)}s') | |
def test_student_comp(): | |
def student_comp(a,b): | |
k='grade'; va=a[k]; vb=b[k] | |
if va != vb: | |
if va is None: return -1 | |
if vb is None: return 1 | |
return -1 if va.upper() > vb.upper() else 1 | |
k='attend'; va=a[k]; vb=b[k]; | |
if va != vb: return -1 if va < vb else 1 | |
return 0 | |
students_sorted = sorted(students, key=cmp_to_key(student_comp), reverse=True) | |
[print(r) for r in students_sorted] | |
def test_Comparator(): | |
students_sorted = sorted(students, key=Comparator.new( | |
('grade', False, lambda v: None if v is None else v.lower()) | |
,('attend', True) | |
)) | |
[print(r) for r in students_sorted] | |
class StopWatch: | |
def __init__(self): | |
self.start() | |
def start(self): | |
self._startTime = time.time() | |
def getStartTime(self): | |
return self._startTime | |
def elapsed(self, prec=3): | |
prec = 3 if prec is None or not isinstance(prec, int) else prec | |
diff= time.time() - self._startTime | |
return round(diff, prec) | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment