What is Best Method for Sorting List of Objects by Object Property?

What is Best Method for Sorting List of Objects by Object Property?

For example, I’d like to sort a list of Note objects, from an Evernote search or selection, by one of the date properties of the Note.


EDIT: Mon, Feb 22, 2016 at 10:44 PM CST
See answer and final script here:

The below script does this by:

  1. Looping through the original Note list, putting the index and date into an AppleScript record.
  2. Sorting this list of records using the ASObjC Runner “rearrange records” command
  3. To process the Notes in sorted order, I use the record “index” property to access the actual Note object in the original Note list.

I am a little concerned about performance, since this took 17.2 seconds to sort 2,563 notes/records.

Is there a better method to do this?

@Shane Stanley:
¢ Is there a replacement for the ASObjC Runner “rearrange records” command in one of your script libraries?
¢ I looked but did not find one.
¢ You have previously stated that Runner may not work post El Capitan

I apologize for the sloppiness of this code, but it is just a quick-and-dirty script to identify the best method for sorting objects.



set cmdStr to "perl -e 'use Time::HiRes qw(time); print time'"
set timeStart to do shell script cmdStr

set noteRecList to {}

--- GET LIST OF NOTES FROM EVERNOTE ---

tell application "Evernote"
	
	--set selNotes to selection
	--      OR --
	set selNotes to (find notes "tag:.NB.Personal")
	
	set numNotes to count of selNotes
	log "Num of Notes: " & numNotes
	display notification "Number of Notes: " & numNotes
	
	set noteIndex to 0
	
	repeat with oNote in selNotes
		
		set noteIndex to noteIndex + 1
		set noteTitleStr to title of oNote
		set creationDate to creation date of oNote
		set modificationDate to modification date of oNote
		
		--- ADD TO LIST OF RECORDS SO NOTES CAN BE SORTED ON DATE ---
		
		copy {index:noteIndex, modDate:modificationDate, noteTitle:noteTitleStr} to the end of noteRecList
		--noteObj:oNote, --> when I included the oNote object, the rearrange records got error.
		
	end repeat
	
end tell

set noteRec to item 1 of noteRecList
log "~~~ INFO OF FIRST NOTE PRIOR TO SORT ~~~"
log modDate of noteRec
log noteTitle of noteRec

--- SORT THE NOTE RECORD LIST ---

tell application "ASObjC Runner"
	set sortedList to rearrange records noteRecList by keys "modDate" with ascending orders
end tell

log "
~~~~ AFTER SORT ~~~~
"
--- GET THE 1st NOTE IN THE SORTED LIST, WHICH SHOULD BE THE OLDEST ---

set noteRec to item 1 of sortedList
log modDate of noteRec
log noteTitle of noteRec
log index of noteRec
set noteIndex to (index of noteRec)

--- NOW GET THE SAME NOTE OBJECT FROM THE EVERNOTE NOTE LIST ---

tell application "Evernote"
	
	set oNote to (item noteIndex of selNotes)
	log "~~~ CONFIRM SELECTION OF NOTE OBJECT ~~~"
	log (modification date of oNote as string)
	log (title of oNote as string)
	
end tell

set timeStop to do shell script cmdStr
set executionTime to (round ((timeStop - timeStart) * 1000)) / 1000.0
log "Execution Time:  " & executionTime & " sec"

LOG PANEL


(*Num of Notes: 2563*)
(*~~~ INFO OF FIRST NOTE PRIOR TO SORT ~~~*)
(*date Sat, Feb 20, 2016 at   11:02 PM*)
(*New Books Dec 26*)
(*
~~~~ AFTER SORT ~~~~
*)
(*date Fri, Feb 10, 1950 at   11:10 PM*)
(*TEST Script Sort*)
(*2563*)
(*~~~ CONFIRM SELECTION OF NOTE OBJECT ~~~*)
(*Fri, Feb 10, 1950 at   11:10 PM*)
(*TEST Script Sort*)
(*Execution Time:  17.206 sec*)

No, but it’s easy enough to do in ASObjC. So the equivalent of this:

would, in El Capitan, and assuming you have a use framework “Foundation” statement at the beginning of the script, be:

set noteRecList to current application's NSArray's arrayWithArray:noteRecList
set sortDescriptor to current application's NSSortDescriptor's sortDescriptorWithKey:"modDate" ascending:true
set noteRecList to (noteRecList's sortedArrayUsingDescriptors:{sortDescriptor}) as list

For 10.10 you could use BridgePlus to bridge the dates:

use script "BridgePlus"
load framework
set noteRecList to Cocoaify noteRecList
set sortDescriptor to current application's NSSortDescriptor's sortDescriptorWithKey:"modDate" ascending:true
set noteRecList to ASify from (noteRecList's sortedArrayUsingDescriptors:{sortDescriptor})

That’s correct.

OK, that looks great! Many thanks. I’ll just refactor that into a handler in my standard AppleScript Script Lib.
One question: Runner handles multiple sort keys.
Can I just replace:
sortDescriptorWithKey:“modDate”

with:
sortDescriptorWithKey:{“key1”, “key2”}

I’m still on Yosemite, but will make a note to convert from Runner to this when I upgrade.

Other than that, do you see any better method to sort objects than my script?

No, you do it like this:

set noteRecList to current application's NSArray's arrayWithArray:noteRecList
set sortDescriptor to current application's NSSortDescriptor's sortDescriptorWithKey:"modDate" ascending:true
set sortDescriptor2 to current application's NSSortDescriptor's sortDescriptorWithKey:"otherKey" ascending:true
set noteRecList to (noteRecList's sortedArrayUsingDescriptors:{sortDescriptor, sortDescriptor2}) as list

You can have as many key as you want/need.

You could try one of Nigel’s native sorting routines.

To sort upon several keys you may use this code borrowed to Shane’s Everyday AppleScriptObjC 3ed.pdf

use AppleScript version "2.4"
use framework "Foundation"
use scripting additions
on sortTheList:theList byKeys:keyList ascendingOrder:ascendList sortSelector:sortSelector
	set descriptorList to {}
	repeat with i from 1 to count of keyList
		set end of descriptorList to (current application's NSSortDescriptor's sortDescriptorWithKey:(item i of keyList) ascending:(item i of ascendList) selector:sortSelector)
	end repeat
	set theArray to current application's NSArray's arrayWithArray:theList
	return (theArray's sortedArrayUsingDescriptors:descriptorList) as list
end sortTheList:byKeys:ascendingOrder:sortSelector:

set theList to {{firstName:"Jenny", lastName:"Smith"}, {firstName:"Robert", lastName:"Andrews"}, {firstName:"Ann", lastName:"Smith"}, {firstName:"Brian", lastName:"Smith"}, {firstName:"Ann", lastName:"Andrews"}, {firstName:"Gail", lastName:"Frost"}}

its sortTheList:theList byKeys:{"lastName", "firstName"} ascendingOrder:{true, true} sortSelector:"localizedCaseInsensitiveCompare:"

The book gives a simpler code for a sort using two keys but the given one may be used for different cases.

Yvan KOENIG running El Capitan 10.11.3 in French (VALLAURIS, France) dimanche 21 février 2016 09:50:41

Thanks.

That makes it tough to refactor into handler/function that allows for a variable number of sort keys.
How did you handle that in your Runner app?

Where do I find “Nigel’s native sorting routines”?

If you mean from this post:
http://macscripter.net/viewtopic.php?pid=59545#p59545

The http://scriptbuilders.net/ site is not responding.

But, to be honest, just looking at his posts in the above thread, it looks very complex, with many lines of code (makes distribution harder).
The ASObjC Runner app and the El Capitan ASObjC code are much simpler. Unless there is a huge advantage to Nigel’s code, I’m happy with what I have now.

Thanks.

Thanks, Yvan and Shane. That’s very nice code, nice handler.
I just bought Shane’s book, but haven’t had a chance to read it yet.

The code Yvan posted covers that pretty well.

Nigel has posted quite a few over the years. Maybe he’ll pop in here.

That’s what libraries are for :slight_smile:

Really, it depends how important performance is to you. I suspect a native sort would be quicker for a small number of items, but the only way to tell is to compare. Whether it’s worth it, only you can really say.

Yep, Yvan’s post did the trick.

I run ran his code, and the results were instantaneous. That’s fast enough for me. :wink:

Looks like I’m all set now.

Thanks for everyone’s help.

ScriptBuilders was discontinued a couple of weeks after I uploaded the sorts, unfortunately!

My vanilla sorts (the Quicksort originally a project of Arthur Knapp’s on which I collaborated) were written before there was any built-in way to sort things in AppleScript. If you needed to sort AppleScript lists, you had to write your own sort code or use someone else’s. All the sort handlers being bandied around at the time were literal transcriptions of low-level pseudocode gleaned from elsewhere and were horribly inefficient in a high-level language like AppleScript. Arthur and I used every trick we could think of to wring the last ounce of speed from our AppleScript code ” including sorting “in place”, exploiting AppleScript’s quirk of faster list accesses through “referenced” list variables, using variables where practical to minimise the number of list accesses, avoiding the use of ‘not’ and ‘less/greater than or equal to’, etc.

The resulting handlers were quite long, but very fast. Although some people were intimidated by their length, they weren’t meant to be read but to be put in libraries and used. The ScriptBuilders versions of the customisable sorts were simpler to use as they required fewer customisation handlers and contained their own default handlers for when none were provided.

I still use (and occasionally post) them myself as they were interesting and enjoyable to research and write and I’m quite proud of them. But they have few advantages over the built-in sorts now accessible through Shane’s ASObjC scripts. ASObjC sorts will generally be faster, since they’re written in lower-level code. My handlers actually sort the passed lists; ASObjC necessarily returns sorted copies.

One important advantage is that they can sort lists containing items other than the basic bridgeable AppleScript classes.

By the way, this is how the situation handled by Yvan’s script might look using one of my vanilla sorts. It’s assumed here that the system’s Yosemite or El Capitan and that my Custom Insertion Sort script is in a “Script Libraries” folder.

-- An insertion sort's good with a short list.
use sorter : script "Custom Insertion Sort"

set theList to {{firstName:"Jenny", lastName:"Smith"}, {firstName:"Robert", lastName:"Andrews"}, {firstName:"Ann", lastName:"Smith"}, {firstName:"Brian", lastName:"Smith"}, {firstName:"Ann", lastName:"Andrews"}, {firstName:"Gail", lastName:"Frost"}}

-- Comparison customiser.
-- Record a is "greater" than record b if its lastName is lexically greater than b's or if their lastNames are the same and a's firstName is lexically greater than b's.
script byLastnameThenFirstname
	on isGreater(a, b)
		set aln to a's lastName
		set bln to b's lastName
		return ((aln > bln) or ((aln = bln) and (a's firstName > b's firstName)))
	end isGreater
end script

tell sorter to sort(theList, 1, -1, {comparer:byLastnameThenFirstname})

theList
--> {{firstName:"Ann", lastName:"Andrews"}, {firstName:"Robert", lastName:"Andrews"}, {firstName:"Gail", lastName:"Frost"}, {firstName:"Ann", lastName:"Smith"}, {firstName:"Brian", lastName:"Smith"}, {firstName:"Jenny", lastName:"Smith"}}

Nigel, thanks for sharing the detailed explanation and the example code.
Both were very helpful.
Where would I find your sort script today?

What’s your definition of “short”?
My immediate use case needs to sort between 500 and 6,000 records.

Here is a plain vanilla code which I use from time to time when I need really specific criterias.


(*
Tri par classement
-----------------
Implémentation: L. Sebilleau & D. Varlet
*)

on classort(lista, fcmp)
	if (count of lista) < 2 then return lista
	
	script listb
		property liste : lista
		property Compare : fcmp
	end script
	
	repeat with i from 2 to count of listb's liste
		set cle to item i of listb's liste
		repeat with j from i - 1 to 1 by -1
			if listb's Compare(cle, item j of listb's liste) then
				set item (j + 1) of listb's liste to item j of listb's liste
			else
				set j to j + 1
				exit repeat
			end if
		end repeat
		set item j of listb's liste to cle
	end repeat
	return listb's liste
end classort
----------- les fonctions de comparaison ------------

on cmpasc(n1, n2) -- pour le tri ascendant de nombres ou de chaînes
	return n1 < n2
end cmpasc

on cmpdesc(n1, n2) -- tri descendant de nombres ou de chaînes
	return n1 > n2
end cmpdesc

on cmpBothAsc(n1, n2) -- pour le tri ascendant de listes de nombres et de chaînes
	return n1 as text < n2 as text
end cmpBothAsc

on cmpBothDesc(n1, n2) -- tri descendant de de listes de nombres et de chaînes
	return n1 as text > n2 as text
end cmpBothDesc

on cmpNumAsc(n1, n2) -- pour le tri ascendant des nombres et des chaînes
	considering numeric strings
		n1 < n2
	end considering
	return result
end cmpNumAsc

on cmpNumDesc(n1, n2) -- tri descendant des nombres et des chaînes
	considering numeric strings
		n1 > n2
	end considering
	return result
end cmpNumDesc

on cmpLengthAsc(n1, n2) -- pour le tri ascendant des nombres et des chaînes
	return (count (n1 as text)) < (count (n2 as text))
end cmpLengthAsc

on cmpLengthDesc(n1, n2) -- tri descendant des nombres et des chaînes
	return (count (n1 as text)) > (count (n2 as text))
end cmpLengthDesc

# Sort lists of strings abc ⇥ def ⇥ ghi upon first substring before tab
on cmpLengthFirstDesc(t1, t2) -- USED HERE
	set {oTids, text item delimiters} to {text item delimiters, tab}
	set tt1 to text item 1 of t1
	set tt2 to text item 1 of t2
	set text item delimiters to oTids
	return (count tt1) > (count tt2)
end cmpLengthFirstDesc

# Sort list of lists of several items upon length of the first one
on cmpLengthFirstItemInList(L1, L2)
	return (count L1's item 1) > (count L2's item 1)
end cmpLengthFirstItemInList

---------------- Pour tester ----------------
(*
set aList to {5, 1, 15, 2, 7, 3, 23, 125, 4, 15, 8, 7, 6, 5, 27, 35, 12, 22, 160}
set aList to {"5", "1", "15", "2", "7", "3", " 23", " 125", " 4", " 15", " 8", " 7", " 6", " 05", " 27", " 35", " 12", " 22", " 160"}
*)
set aList to {"123aze", "0024er", "987qs", "12fr", "012k"}

# classsort sorts the list on itself
(*
classort(aList, my cmpdesc)
classort(result, my cmpLengthDesc)
*)
classort(aList, cmpNumAsc)
log aList (*12fr, 012k, 0024er, 123aze, 987qs*)

my sortAListNumeric:{"123aze", "0024er", "987qs", "12fr", "012k"}

I apologize for the French comments but as some of you already know, French is my first language.

Yvan KOENIG running El Capitan 10.11.3 in French (VALLAURIS, France) dimanche 21 février 2016 16:58:52

There are various sorts from the collection dotted around this forum and Code Exchange. I’ve looked out the one below which I think would serve your needs if you didn’t like the ASObjC solution.

About 6. :wink:

Here’s a merge sort. If I remember correctly, merge sorts tend to do fewer comparisons than quicksorts (in my implementations, at least) and so compare favourably for speed in situations where comparisons take comparatively long to do. But there isn’t really much in it now, given the speed of today’s machines.

Save as a compiled script in the “Script Libraries” folder (which you’ll have to create if it doesn’t already exist) in your user “Library” folder. If like me you save it as “Custom Iterative Merge Sort.scpt”, the ‘use’ line in any script which uses it will have to refer to it simply as script “Custom Iterative Merge Sort”. eg.

use sorter: script "Custom Iterative Merge Sort"

Otherwise its use is exactly the same as in the script three posts up. Although the handler’s called ‘CustomNRMergeSort’ (in case I ever decide to put all the sorts into one script), the property ‘sort’ set to it immediately below allows ‘sort’ to be used instead of ‘CustomNRMergeSort’ in the calling script.

(* Iterative merge sort ” customisable version
Merge sort algorithm: John von Neumann, 1945.
AppleScript implementation: Nigel Garvey, 2007. Iterative (non-recursive) version 2012.

Parameters: (list to be sorted, sort range index 1, sort range index 2, customisation record).

The customisation record may have a 'comparer' property and/or a 'slave' property. Both are optional. Any other properties are ignored.
The 'comparer' property controls how items in the list are compared. Its value should be a script object containing an isGreater(a, b) handler which determines whether or nor object a is "greater" than object b. If this property's omitted, items are compared directly as in a normal sort.
The 'slave' property is principally to allow sort moves to be repeated in other lists so that they're sorted in parallel with the first. The value should be a script object containing (for this iterative merge sort) an extract(a, b) handler (called when the sort has extracted a copy of range a thru b of the main list to act as a mutual merge destination), a merge(a, b) handler (called when the sort has moved item a of the current source to position b in the current destination), a swap(a, b) handler (called when items a and b of the main list have been swapped), and a switch() handler (called when the source and destination switch roles). Writing these handlers obviously requires a knowledge of how each sort works, but a universal slave script would be distributed with the collection allowing any of the sorts to sort one other list in parallel with the first.
*)

on CustomNRMergeSort(theList, l, r, customiser) -- Sort items l thru r of theList.
	script o
		property comparer : me
		property slave : me
		
		-- The main list and its sort range indices.
		property l : missing value
		property r : missing value
		property lst : theList
		
		-- Default comparison and slave handlers for an ordinary sort.
		on isGreater(a, b)
			(a > b)
		end isGreater
		
		on extract(a, b)
		end extract
		
		on merge(a, b)
		end merge
		
		on swap(a, b)
		end swap
		
		on switch()
		end switch
	end script
	
	script aux -- Separate script object for an auxiliary list and its start and end indices.
		property l : missing value
		property r : missing value
		property lst : missing value
	end script
	
	-- Process the input parameters.
	set listLen to (count theList)
	if (listLen < 2) then return
	
	-- Negative and/or transposed range indices.
	if (l < 0) then set l to listLen + l + 1
	if (r < 0) then set r to listLen + r + 1
	if (l > r) then set {l, r} to {r, l}
	
	-- Supplied or default customisation scripts.
	if (customiser's class is record) then set {comparer:o's comparer, slave:o's slave} to (customiser & {comparer:o, slave:o})
	
	-- Do the sort.
	-- The first traversal simply orders pairs of adjacent items in place.
	repeat with mergeR from (l + 1) to r by 2
		set mergeL to mergeR - 1
		set lv to item mergeL of o's lst
		set rv to item mergeR of o's lst
		if (o's comparer's isGreater(lv, rv)) then
			set item mergeL of o's lst to rv
			set item mergeR of o's lst to lv
			o's slave's swap(mergeL, mergeR)
		end if
	end repeat
	
	set rangeLen to r - l + 1
	if (rangeLen < 3) then return -- Sort complete if only two items.
	
	-- Set the range indices for the original and auxiliary lists and create the auxiliary list with the items of the sort range as ordered so far. Items will be merged from one list to the other and back on alternate traversals.
	set o's l to l
	set o's r to r
	
	set aux's l to 1
	set aux's r to rangeLen
	set aux's lst to o's lst's items l thru r
	o's slave's extract(l, r)
	
	-- Work out how many more passes are needed and set the initial merge direction so that the last pass will merge back to the original list.
	set traversalsToDo to 0
	set pairLen to 2
	repeat while (pairLen < rangeLen)
		set traversalsToDo to traversalsToDo + 1
		set pairLen to pairLen + pairLen
	end repeat
	set {srce, dest} to item (traversalsToDo mod 2 + 1) of {{o, aux}, {aux, o}}
	if (srce is o) then o's slave's switch()
	
	-- Do the remaining passes, each merging pairs of merged pairs from the pass before. The last "pair" in the range will usually be truncated by the range boundary.
	set pairLen to 2
	repeat traversalsToDo times
		set previousPairLen to pairLen
		set pairLen to pairLen + pairLen
		
		-- Traverse the source range a "pair" section at a time, merging the already sorted halves of each into the equivalent section of the destination list.
		set dx to (dest's l) - 1 -- Destination tracking index.
		repeat with leftL from srce's l to srce's r by pairLen
			set mergeR to dx + pairLen -- Destination section end index.
			if (mergeR > dest's r) then set mergeR to dest's r
			
			set lx to leftL -- Left half tracking index.
			set leftR to leftL + previousPairLen - 1 -- Left half end index.
			if (leftR < srce's r) then
				-- Two "halves" in this pair. Merge them by repeatedly comparing the lowest remaining value from each and assigning the lower of the two (or the left if equal) to the next slot in the destination list.
				set rx to leftL + previousPairLen -- Right half tracking index.
				set rightR to leftR + previousPairLen -- Right half end index.
				if (rightR > srce's r) then set rightR to srce's r
				
				set lv to item lx of srce's lst -- First (lowest) value from left half.
				set rv to item rx of srce's lst -- Ditto right half.
				repeat with dx from (dx + 1) to mergeR
					if (o's comparer's isGreater(lv, rv)) then
						-- The right value's the lower. Assign it to the destination slot.
						set item dx of dest's lst to rv
						o's slave's merge(rx, dx)
						-- If no more right-half values, exit to the "one half" repeat below.
						if (rx is rightR) then exit repeat
						-- Otherwise get the next right-half value.
						set rx to rx + 1
						set rv to item rx of srce's lst
					else
						-- The left value's less than or equal to the right.
						set item dx of dest's lst to lv
						o's slave's merge(lx, dx)
						-- If no more left values, recast the right half as the left and exit to the "one half" repeat below.
						if (lx is leftR) then
							set lx to rx
							exit repeat
						end if
						-- Otherwise get the next left-half value.
						set lx to lx + 1
						set lv to item lx of srce's lst
					end if
				end repeat
			end if
			
			-- Only one remaining "half" in this section. Copy it over as it is.
			repeat with dx from (dx + 1) to mergeR
				set item dx of dest's lst to item lx of srce's lst
				o's slave's merge(lx, dx)
				set lx to lx + 1
			end repeat
		end repeat
		
		-- Switch the source and destination roles for the next traversal.
		tell srce
			set srce to dest
			set dest to it
		end tell
		o's slave's switch()
	end repeat
	
	return -- nothing
end CustomNRMergeSort

property sort : CustomNRMergeSort

Nigel, thank you very much for sharing your sort script, and for recommending the best method for my use case.

May I suggest that you post your scripts somewhere they are easy to find and reference, and easy for you to maintain?
I really like GitHub Gists. They are simple to create, and simple for the users to get the code.
There is also a web app called GistBox which makes it very easy for you to manage all of your Gists.

One more follow-on question, if you don’t mind.

Is there a way to incorporate the customizer script into the main sort script, so that the coder using the sort script does not have to make any code changes, but simply calls the sort script with the sort keys to use?

I’m referring to this customizer script:


-- Comparison customiser.
-- Record a is "greater" than record b if its lastName is lexically greater than b's or if their lastNames are the same and a's firstName is lexically greater than b's.
script byLastnameThenFirstname
	on isGreater(a, b)
		set aln to a's lastName
		set bln to b's lastName
		return ((aln > bln) or ((aln = bln) and (a's firstName > b's firstName)))
	end isGreater
end script

Thanks again for all of your kind help.

May I vote for rosettacode.org? I already share code there and in different languages :slight_smile:

but Nigel’s sort algorithm can easily be found using the search feature on MS

Of course you can vote for anything you like.

Personally, I don’t find Rosetta Code very helpful or friendly. I do a lot of google searches on “AppleScript”, and I don’t remember ever getting one hit on rosettacode.org. In the rosettacode.org site, I searched on both AppleScript and your name. Didn’t get any hits on your name. “AppleScript” returned a TOC style list. The few links I clicked on didn’t provide much depth.

I’m sure if I used it more, I would get better results. But the deal-breaker for me is that rosettacode code blocks don’t seem to be found by Google search.

My real name is not DJ Bazzie Wazzie, it was a nick I used a lot back in 2004.

Well the whole idea with a customisable sort is that the sort itself isn’t specialised. It’s the customising scripts passed to it which are supposed to be written to order.

But what you want can be done. The script below contains a handler which takes a list of keys (or rather the text equivalents of keys) and builds the source code for the required script object from them. The code’s then compiled and returned for use with the sort. The key list passed to it can contain any number of keys and should be in the order of the required sort priority ” that is, the records will be sorted on the first key, subsorted on the second, sub-subsorted on the third, and so on.

use sorter : script "Custom Iterative Merge Sort" -- or whatever you've called it.
use scripting additions

on run
	set theList to {{firstName:"Jenny", middleName:"", lastName:"Smith"}, {firstName:"Robert", middleName:"Ironside", lastName:"Andrews"}, {firstName:"Ann", middleName:"Zoella", lastName:"Smith"}, {firstName:"Brian", middleName:"Blenkinsop", lastName:"Smith"}, {firstName:"Ann", middleName:"Verisimilitude", lastName:"Andrews"}, {firstName:"Gail", middleName:"Warning", lastName:"Frost"}, {firstName:"Aloysius", middleName:"Q", lastName:"Aardvark"}, {firstName:"Aloysius", middleName:"Lazenby", lastName:"Aardvark"}}
	
	-- Say we want to sort the records on lastName, then firstName, then middleName. Construct the necessary custom comparison script.
	set comparer to makeRecordComparer({"lastName", "firstName", "middleName"})
	tell sorter to sort(theList, 1, -1, {comparer:comparer})
	
	return theList
	--> {{firstName:"Aloysius", middleName:"Lazenby", lastName:"Aardvark"}, {firstName:"Aloysius", middleName:"Q", lastName:"Aardvark"}, {firstName:"Ann", middleName:"Verisimilitude", lastName:"Andrews"}, {firstName:"Robert", middleName:"Ironside", lastName:"Andrews"}, {firstName:"Gail", middleName:"Warning", lastName:"Frost"}, {firstName:"Ann", middleName:"Zoella", lastName:"Smith"}, {firstName:"Brian", middleName:"Blenkinsop", lastName:"Smith"}, {firstName:"Jenny", middleName:"", lastName:"Smith"}}	
end run

-- Build a record comparer for a customised sort.
-- Parameter: a list of the relevant keys in text form, in the order they'll be compared in the sort.
on makeRecordComparer(prioritisedKeyList)
	set keyCount to (count prioritisedKeyList)
	if (keyCount is 0) then error -- Make up some error message for a bad parameter.
	
	-- Start building the source code for the script object, inserting the first key in the relevant places. The isGreater() handler in the script will return 'true' if and as soon as it determines that record a should come after record b. Otherwise it returns 'false'.
	set thisKey to beginning of prioritisedKeyList
	set scriptText to "script
on isGreater(a, b)
if (a's " & thisKey & " > b's " & thisKey & ") then
return true"
	
	-- Continue by nesting 'if' statements for the remaining keys, if any.
	repeat with i from 2 to keyCount
		set lastKey to thisKey
		set thisKey to item i of prioritisedKeyList
		set scriptText to scriptText & "
else if (a's " & lastKey & " = b's " & lastKey & ") then
if (a's " & thisKey & " > b's " & thisKey & ") then
return true"
	end repeat
	
	-- Append enough 'end ifs' to complete the nested 'if' statements.
	repeat keyCount times
		set scriptText to scriptText & "
end if"
	end repeat
	
	-- Finish off with the default 'false' return and the ends of the handler and script object.
	set scriptText to scriptText & "
return false
end isGreater
end script"
	
	-- Compile the script and return the result.
	return (run script scriptText)
end makeRecordComparer

The source code built above for the comparison customiser looks like this:

[format]“script
on isGreater(a, b)
if (a’s lastName > b’s lastName) then
return true
else if (a’s lastName = b’s lastName) then
if (a’s firstName > b’s firstName) then
return true
else if (a’s firstName = b’s firstName) then
if (a’s middleName > b’s middleName) then
return true
end if
end if
end if
return false
end isGreater
end script”[/format]

If you don’t like having the building handler in your main script code, you could save it as a library in its own right and just ‘use’ it in the same way as the merge sort script.