Skip to content

Instantly share code, notes, and snippets.

@JavaScriptDude
Last active May 2, 2022 04:05
Show Gist options
  • Save JavaScriptDude/dd112d69b082c089fe19e0b2545826da to your computer and use it in GitHub Desktop.
Save JavaScriptDude/dd112d69b082c089fe19e0b2545826da to your computer and use it in GitHub Desktop.
Python3 - Multi-Column Sorting of List of Dicts
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