8
8
# Licence: MIT
9
9
#########################################
10
10
from functools import cmp_to_key
11
+ from typing import Union
11
12
cmp_func = cmp_to_key
12
13
13
14
14
- # .: multisort :.
15
- # spec is a list one of the following
16
- # <key>
17
- # spec
18
- # spec options:
19
- # key Property, Key or Index for 'column' in row
20
- # reverse: opt - reversed sort (defaults to False)
21
- # clean: opt - callback to clean / alter data in 'field'
22
- # default: Value to default if None is found or required = False
23
- # required: Will not fail if key not found
24
- # Use mscol helper to ease passing of variables or just pass lists of args:
25
- # spec=mscol('colname1', reverse=True), mscol('colname2', reverse=True)]
26
- # -or-
27
- # spec=[('colname1', True),('colname2',True)]
28
-
29
- def mscol (key , reverse = False , clean = None , default = None , required = True ):
30
- return (key , reverse , clean , default , required )
31
-
32
- def multisort (rows , spec , reverse :bool = False ):
33
- rows_sorted = None
34
- if isinstance (spec , (int , str )): spec = [mscol (spec )]
15
+ # multisort - Non-destructive sorter with multi column support
16
+ # [rows] list of records to sort
17
+ # [spec] list/tuple one of <key> or <spec> or list/tuple(of <spec>)
18
+ # items by order in tuple:
19
+ # [key] Key or Index for 'column' in row
20
+ # [reverse] reversed sort (defaults to False) (opt)
21
+ # [clean] callback to clean / alter data in 'field' (opt)
22
+ # [default] Value to default if None is found or required = False (opt)
23
+ # [required] Will not fail if key not found (opt)
24
+ # [reverse] reverse the sort (defaults to False)
25
+ # Other:
26
+ # mscol: Helper to simplify construction of <spec> record(s) eg:
27
+ # multisort(rows, [mscol('colname1', reverse=True),
28
+ # mscol('colname2', reverse=True, default=1)]
29
+ # # as opposed to:
30
+ # multisort(rows, [('colname1', True),
31
+ # ('colname2', True, None, 1)]
32
+ def multisort (rows : list ,
33
+ spec : Union [int , str , list , tuple ] = None ,
34
+ reverse : bool = False ):
35
+
36
+ if spec is None :
37
+ _clone = rows [:]
38
+ _clone .sort (reverse = reverse )
39
+ return _clone
40
+
41
+ rows_sorted = None
42
+ if isinstance (spec , (int , str )):
43
+ spec = [mscol (spec )]
35
44
for spec_c in reversed (spec ):
36
45
spec_c_t = type (spec_c )
37
- if spec_c_t in (int , str ):
38
- (key , col_reverse , clean , default , required ) = (spec_c , False , None , None , True )
46
+ if spec_c_t in (int , str ):
47
+ (key , col_reverse , clean , default , required ) \
48
+ = (spec_c , False , None , None , True )
39
49
else :
40
- assert spec_c_t in (list , tuple ), f"Invalid spec. Got: { spec_c_t .__name__ } . See docs"
41
- if len (spec_c ) < 5 : spec_c = mscol (* spec_c )
50
+ assert spec_c_t in (list , tuple ), \
51
+ f"Invalid spec. Got: { spec_c_t .__name__ } . See docs"
52
+ if len (spec_c ) < 5 :
53
+ spec_c = mscol (* spec_c )
42
54
(key , col_reverse , clean , default , required ) = spec_c
43
- def _sort_column (row ): # Throws MSIndexError, MSKeyError
44
- ex1 = None
55
+
56
+ def _sort_column (row ): # Throws MSIndexError, MSKeyError
57
+ ex1 = None
45
58
try :
46
59
try :
47
- v = row [key ]
60
+ v = row [key ]
48
61
except Exception as ex :
49
62
ex1 = ex
50
63
v = getattr (row , key )
51
64
except Exception as ex2 :
52
- if isinstance (row , (list , tuple )): # failfast for tuple / list
65
+ if isinstance (row , (list , tuple )): # failfast for tuple / list
53
66
raise MSIndexError (ex1 .args [0 ], row , ex1 )
54
67
55
68
elif required :
56
69
raise MSKeyError (ex2 .args [0 ], row , ex2 )
57
70
58
71
else :
59
- if default is None :
72
+ if default is None :
60
73
v = None
61
74
else :
62
75
v = default
63
76
64
77
if default :
65
- if v is None : return default
78
+ if v is None :
79
+ return default
66
80
return clean (v ) if clean else v
67
81
else :
68
- if v is None : return True , None
69
- if clean : return False , clean (v )
82
+ if v is None :
83
+ return True , None
84
+ if clean :
85
+ return False , clean (v )
70
86
return False , v
71
87
72
88
try :
73
89
if rows_sorted is None :
74
- rows_sorted = sorted (rows , key = _sort_column , reverse = col_reverse )
90
+ rows_sorted = sorted (rows ,
91
+ key = _sort_column ,
92
+ reverse = col_reverse )
75
93
else :
76
94
rows_sorted .sort (key = _sort_column , reverse = col_reverse )
77
95
78
-
79
96
except Exception as ex :
80
- msg = None
81
- row = None
82
- key_is_int = isinstance (key , int )
97
+ sb = []
98
+ msg = None
99
+ row = None
100
+ key_is_int = isinstance (key , int )
83
101
84
102
if isinstance (ex , MultiSortBaseExc ):
85
103
row = ex .row
86
104
if isinstance (ex , MSIndexError ):
87
- msg = f"Invalid index for { row .__class__ .__name__ } row of length { len (row )} . Row: { row } "
88
- else : # MSKeyError
89
- msg = f"Invalid key/property for row of type { row .__class__ .__name__ } . Row: { row } "
105
+ sb .append (f"Invalid index for { row .__class__ .__name__ } " )
106
+ sb .append (f" row of length { len (row )} . Row: { row } " )
107
+ else : # MSKeyError
108
+ sb .append ("Invalid key/property for row of type" )
109
+ sb .append (f" { row .__class__ .__name__ } . Row: { row } " )
110
+ msg = ' ' .join (sb )
90
111
else :
91
112
msg = ex .args [0 ]
92
-
93
- raise MultiSortError (f"""Sort failed on key { "int" if key_is_int else "str '" } { key } { '' if key_is_int else "' " } . { msg } """ , row , ex )
94
113
114
+ msg = "Sort failed on key {0}{1}{2}. {3}" .format (
115
+ "int" if key_is_int else "str '" ,
116
+ key ,
117
+ '' if key_is_int else "' " ,
118
+ msg )
119
+ raise MultiSortError (msg , row , ex )
95
120
96
121
return reversed (rows_sorted ) if reverse else rows_sorted
97
122
123
+
124
+ def mscol (key , reverse = False , clean = None , default = None , required = True ):
125
+ return (key , reverse , clean , default , required )
126
+
127
+
98
128
class MultiSortBaseExc (Exception ):
99
129
def __init__ (self , msg , row , cause ):
100
130
self .message = msg
101
131
self .row = row
102
132
self .cause = cause
103
-
133
+
134
+
104
135
class MSIndexError (MultiSortBaseExc ):
105
136
def __init__ (self , msg , row , cause ):
106
137
super (MSIndexError , self ).__init__ (msg , row , cause )
107
138
139
+
108
140
class MSKeyError (MultiSortBaseExc ):
109
141
def __init__ (self , msg , row , cause ):
110
142
super (MSKeyError , self ).__init__ (msg , row , cause )
111
143
144
+
112
145
class MultiSortError (MultiSortBaseExc ):
113
146
def __init__ (self , msg , row , cause ):
114
147
super (MultiSortError , self ).__init__ (msg , row , cause )
148
+
115
149
def __str__ (self ):
116
150
return self .message
151
+
117
152
def __repr__ (self ):
118
153
return f"<MultiSortError> { self .__str__ ()} "
119
154
120
- # For use in the multi column sorted syntax to sort by 'grade' and then 'attend' descending
121
- # dict example:
122
- # rows_sorted = sorted(rows, key=lambda o: ((None if o['grade'] is None else o['grade'].lower()), reversor(o['attend'])), reverse=True)
123
- # object example:
124
- # rows_sorted = sorted(rows, key=lambda o: ((None if o.grade is None else o.grade.lower()), reversor(o.attend)), reverse=True)
125
- # list, tuple example:
126
- # rows_sorted = sorted(rows, key=lambda o: ((None if o[COL_GRADE] is None else o[COL_GRADE].lower()), reversor(o[COL_ATTEND])), reverse=True)
127
- # where: COL_GRADE and COL_ATTEND are column indexes for values
155
+
156
+ # reversor() For use in the multi column sorted
157
+ # syntax to sort by 'grade'and then 'attend' descending
158
+ # Dict example:
159
+ # rows_sorted = sorted(rows,
160
+ # key=lambda o: (\
161
+ # (None if o['grade'] is None else o['grade'].lower()),\
162
+ # reversor(o['attend'])), reverse=True)
163
+ # Object example:
164
+ # rows_sorted = sorted(rows,
165
+ # key = lambda o: ((None if o.grade is None else o.grade.lower()), \
166
+ # reversor(o.attend)),
167
+ # reverse = True)
168
+ # List, Tuple example:
169
+ # rows_sorted = sorted(rows, key=lambda o:\
170
+ # ((None if o[COL_GRADE] is None else o[COL_GRADE].lower()),
171
+ # reversor(o[COL_ATTEND])), reverse=True)
172
+ # where: COL_GRADE and COL_ATTEND are column indexes for values
173
+
174
+
128
175
class reversor :
129
176
def __init__ (self , obj ):
130
177
self .obj = obj
178
+
131
179
def __eq__ (self , other ):
132
180
return other .obj == self .obj
181
+
133
182
def __lt__ (self , other ):
134
183
return False if self .obj is None else \
135
184
True if other .obj is None else \
136
185
other .obj < self .obj
137
186
138
187
139
188
def getClassName (o ):
140
- return None if o == None else type (o ).__name__
141
-
189
+ return None if o is None else type (o ).__name__
0 commit comments