Here’s the CustomQsort handler I mentioned earlier. Sorts work by comparing items in a list and repositioning them according to their relative values. But comparing items directly isn’t always possible or desirable. You might want to sort a list of lists, or of records, or of files, or even of mixed classes. Or the sort order might need to be modified in some way. Each contingency requires a different method for determining which items are “greater” or “less” than others, which means having either several specialised sort handlers or a customisable one such as CustomQsort. CustomQsort does the physical work of moving items around in the list, but the comparisons are done by short handlers in a script object that’s passed to CustomQsort as a parameter.
Arthur and I had slightly different preferences for what the script object should contain, so this is just my version:
13th September 2010: I’ve uploaded a new version of this sort with an improved customisation regime, as part of a collection, to ScriptBuilders. The new version, though, will accept customisation scripts written for the one below, which has been around for quite a while now.
on CustomQsort(theList, l, r, compObj)
script o
property cutoff : 10
property p : theList
on qsrt(l, r)
set i to l
set j to r
set v to my p's item ((l + r) div 2)
repeat while (j > i)
set u to my p's item i
repeat while (compObj's isLess(u, v))
set i to i + 1
set u to my p's item i
end repeat
set w to my p's item j
repeat while (compObj's isGreater(w, v))
set j to j - 1
set w to my p's item j
end repeat
if (i > j) then
else
set my p's item i to w
set my p's item j to u
-- Inform the script object of this swap.
compObj's swap(i, j)
set i to i + 1
set j to j - 1
end if
end repeat
if (j - l < cutoff) then
else
qsrt(l, j)
end if
if (r - i < cutoff) then
else
qsrt(i, r)
end if
end qsrt
on isrt(l, r)
set x to l
set z to l + cutoff - 1
if (z > r) then set z to r
set v to my p's item x
repeat with y from (x + 1) to z
if (compObj's isLess(my p's item y, v)) then
set x to y
set v to my p's item y
end if
end repeat
tell my p's item l
set my p's item l to v
set my p's item x to it
end tell
-- Inform the script object of this swap.
compObj's swap(l, x)
set u to my p's item (l + 1)
repeat with i from (l + 2) to r
set v to my p's item i
if (compObj's isLess(v, u)) then
set my p's item i to u
repeat with j from (i - 2) to l by -1
if (compObj's isLess(v, my p's item j)) then
set my p's item (j + 1) to my p's item j
else
set my p's item (j + 1) to v
-- Inform the script object of this insertion.
compObj's shift(j + 1, i)
exit repeat
end if
end repeat
else
set u to v
end if
end repeat
end isrt
end script
set listLen to (count theList)
if (listLen > 1) then -- otherwise the handler will error
-- Translate negative indices
if (l < 0) then set l to listLen + l + 1
if (r < 0) then set r to listLen + r + 1
if (r = l) then
-- No point in sorting just one item
else
-- Transpose transposed indices
if (l > r) then
set temp to l
set l to r
set r to temp
end if
if (r - l < o's cutoff) then
-- Skip the Quicksort if cutoff or less items
else
o's qsrt(l, r)
end if
o's isrt(l, r)
end if
end if
return -- nothing
end CustomQsort
The compObj parameter should be a script object containing these four handlers and anything else that may be required:
isLess(a, b): decides if the value of a is “less” than the value of b or not. (Returns ‘true’ or ‘false’.)
isGreater(a, b): decides if the value of a is “greater” than the value of b, ditto.
swap(a, b): called by CustomQsort after each swap performed in the list being sorted. a and b are the indices of the swapped items. This handler would normally be left empty, but it could be used to do the same swap in another list.
shift(a, b): called by CustomQsort after each “insertion” performed in the list being sorted. The parameters indicate that item b of the list has been moved to position a after items a thru (b - 1) have been shifted uplist. This handler would also normally be left empty, but could be used to echo the moves in another list.
The idea is that you write your own script objects to control how the sort is done. For a straightforward sort similar to qsort’s, isLess() and isGreater() would do simple comparisons and swap() and shift() would do nothing:
script standardSort
on isLess(a, b)
(a < b)
end isLess
on isGreater(a, b)
(a > b)
end isGreater
on swap(a, b)
end swap
on shift(a, b)
end shift
end script
CustomQsort(myList, 1, -1, standardSort)
If you wanted to sort one list and echo the moves in another ” say you had a list of files that had to be sorted in parallel with a list of corresponding modification dates ” you’d use the swap() and shift() handlers to perform the slave moves in the file list:
script slaveSort
property slaveList : missing value
on isLess(a, b)
(a < b)
end isLess
on isGreater(a, b)
(a > b)
end isGreater
-- Do the same swap in slaveList that CustomQsort has just done in the list it's sorting.
on swap(a, b)
tell item a of my slaveList
set item a of my slaveList to item b of my slaveList
set item b of my slaveList to it
end tell
end swap
-- Do the same shift and insertion in slaveList that CustomQsort has just done in the list it's sorting.
on shift(a, b)
tell item b of my slaveList
repeat with i from b - 1 to a by -1
set item (i + 1) of my slaveList to item i of my slaveList
end repeat
set item a of my slaveList to it
end tell
end shift
end script
set slaveSort's slaveList to myFileList
CustomQsort(myDateList, 1, -1, slaveSort)
--> myDateList sorted by the dates it contains
--> myFileList sorted so that the files still correspond to the dates
For a spreadsheet-style sort, sorting a list of lists on, say, the third item in each list:
script listSort
property i : missing value
on isLess(a, b)
(a's item i < b's item i)
end isLess
on isGreater(a, b)
(a's item i > b's item i)
end isGreater
on swap(a, b)
end swap
on shift(a, b)
end shift
end script
set listSort's i to 3
CustomQsort(myListOfLists, 1, -1, listSort)
For a reverse sort, simply lie to CustomQsort by swapping the contents of the isLess() and isGreater() handlers.
script reverseSort
on isLess(a, b)
(a > b)
end isLess
on isGreater(a, b)
(a < b)
end isGreater
on swap(a, b)
end swap
on shift(a, b)
end shift
end script
CustomQsort(myList, 1, -1, reverseSort)
And so on.
Within the same script, handlers and properties that are common to different script objects can be shared. For example, if there’s a standard sort and a reverse sort in the same script, the amount of code in one of the script objects could be reduced by making it a child of the other:
script standardSort
on isLess(a, b)
(a < b)
end isLess
on isGreater(a, b)
(a > b)
end isGreater
on swap(a, b)
end swap
on shift(a, b)
end shift
end script
script reverseSort
property parent : standardSort
on isLess(a, b)
(a > b)
end isLess
on isGreater(a, b)
(a < b)
end isGreater
end
With standardSort as its ‘parent’, reverseSort inherits all standardSort’s handlers (and would inherit its properties too, if it had any), so there’s no need to write out the swap() and shift() handlers in reverseSort itself. The other two handlers, though, have to be different and so are written out, which overrides the inheritance as far as they’re concerned. The two script objects thus have their own isLess() and isGreater() handlers, but share standardSort’s swap() and shift() handlers.
It’s possible to go even further with the above example, as reverseSort’s isLess() and isGreater() handlers are the same as standardSort’s isGreater() and isLess() handlers. These too can be shared by assigning them to the relevantly named variables in reverseSort. The full specification for the reverseSort script object would then be:
script reverseSort
property parent : standardSort
property isLess : standardSort's isGreater
property isGreater : standardSort's isLess
end script