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

Thanks Nigel! That is perfect! I think that will be most useful for AppleScript users that have a more limited knowledge/understanding of AppleScript than you guys. I work with a lot of AppleScript users that are more interested in running the scripts than in developing them. So having a simple interface to sorting will be much more understandable to them.

Thank you so much for taking the time to build the “makeRecordComparer” handler. It would have been a big challenge for me. But having your example will teach me a lot.

[delurk]

Relevant to your interests: Over the last few weeks I’ve been quietly putting together an AppleScript ‘standard library’ which I’m going to upload to Radar as a ‘bug patch’ in the vague hope that someone at Apple with more sense than Sal’s lot has the wits to stick it into 10.12/10.13 as it’s ridiculous that after 20+ years AS still doesn’t have a stdlib worth squat. I’m just about through the features list and am currently getting caught up on unit tests and documentation. A lot of it’s still underdocumented and buggy as anthills, but the List library now has pretty good test coverage and I’ve shaken a lot of bugs out of it over the weekend, so you may want to give it a go. Download the zip file from github, and drop the libraries into your Script Libraries folder. Touch wood you should then be able to sort your list of records as follows:


script ModDateComparator
  -- orders a list of {index:., modDate:., noteTitle:.} records by modDate property
    
  property parent : (date comparator)
    
  to makeKey(aRecord)
    return continue makeKey(modDate of aRecord)
  end makeKey
    
end script

sort list noteRecList using ModDateComparator

More complex ordering requirements, e.g. to order on multiple fields, can be composed as needed. (You’ll find it easier if you start with a list of lists rather than a list of records, but you can adapt it to extract record properties if necessary.) See the List library’s dictionary for more (rough) examples, e.g. see the list comparator constructor for examples of sorting on multiple fields. Any problems, requests for documentation improvements, etc, please message me via GitHub.

BTW, I’ve never used Evernote so don’t know its dictionary, but retrieving thousands of property values from it one one by one is liable to impact performance no matter what else you do, due to the overhead in sending and processing thousands of Apple events. If possible, you should retrieve each property from all elements at once:


tell app "SomeApp"
  tell every element whose <whatever>
    set allTitles to its title
    set allModDates to its modification date
    -- etc.
  end tell
end tell

Bear in mind that Apple event IPC is RPC plus simple first-class relational queries, not OOP, so it’s often quicker to send a small number of AEs containing complex queries that process lots of elements at once, rather than retrieving a list of element specifiers then iterating over it to manipulate one element at a time. How well this works in practice depends a lot on how well the app’s Apple Event Object Model is implemented, so if a command like find notes returns a list of specifiers then you’re SOOL unless you can replace it with an every note whose . clause that performs the same filtering operation.

Caveat: Always performance profile a slow script before trying to optimize it, so that you accurately identify the real bottlenecks and aren’t just wasting your time randomly “optimizing” code that isn’t actually causing the performance problem. The TestTools library includes a basic timer object you can use to time portions of your script manually. (I’d really like to provide an osaprofile tool that times all handlers automatically, but the OSA API and its documentation is woefully inadequate on the details needed to do this, and I’m pretty much out of time.)

[/delurk]

@hhas, thank you very much for opening my eyes and pointing out this very important approach.
I have a test script using this approach which processes 4,108 notes in 0.3 seconds!!! Amazing!!!

Perhaps you, or someone, can help me with the next step:
¢ How do I associate multiple lists so I can sort on one list, and get the corresponding item in the other list?

For example, I have two lists created using the “tell every note…” command:
¢ modDateList
¢ noteLinkList

I want to sort on the modDateList, and then process the notes in that order. So for each date in the modDateList, I need to get the corresponding note link in the noteLinkList.

How do I do this?

Here’s my test script which works very, very fast getting the lists:


use scripting additions
use framework "Foundation"

timer("start")

tell application "Evernote"
  
  tell every note in notebook ".ToBeFiled"
    
    set titleList to its title
    set modDateList to its modification date
    set noteLinkList to its note link
    
  end tell -- note
  
  set numNotes to count of titleList
  log numNotes
  
  set noteLinkStr to item 1 of noteLinkList
  set oNote to find note noteLinkStr
  set titleStr to title of oNote
  log titleStr
  
end tell -- Evernote

### QUESTION:  
#     How do I associate the modDateList with noteLinkList
#     so I can sort the modDateList and get the noteLink that goes 
#     with each date in the sorted list?


timer("STOP")

###””””””””””””””””””””””””””””””””””””””””””””””
#      timer()    Calculate and Log Execution Time
#
#      Ver 1.0    2016-02-21
#
#      REF:  The base ASObjC code was provided by Shane Stanley
###””””””””””””””””””””””””””””””””””””””””””””””

on timer(pAction)
  global gTimerStartDate
  if (pAction = "start") then
    set gTimerStartDate to current application's NSDate's |date|()
    log "START: " & ((current date) as text)
  else
    log pAction & ":  
    ¢ " & ((current date) as text) & "
    ¢ EXECUTION TIME: " & (round (-(gTimerStartDate's timeIntervalSinceNow())) * 1000) / 1000.0 & " sec"
  end if
end timer

LOG PANEL


(*START: Mon, Feb 22, 2016 at   5:11 PM*)
(*4108*)
(*EWT:  GOLD ALERT - Bullish > 127.15*)
(*STOP:  
    ¢ Mon, Feb 22, 2016 at   5:11 PM
    ¢ EXECUTION TIME: 0.306 sec*)


Here’s a lazy way, using BridgePlus:

use theLib : script "BridgePlus"

tell application "Finder"
	set theNames to name of files of desktop
	set theDates to modification date of files of desktop
end tell
set listPairs to theLib's colsToRowsIn:{theDates, theNames}
set listPairs to theLib's sublistsIn:listPairs sortedByIndexes:{1, 2} ascending:{true} sortTypes:{}
set {theDates, theNames} to theLib's colsToRowsIn:listPairs

Thanks, Shane. That looks easy.

Unfortunately I’m getting the same error in my Evernote script as in your script.
Running Yosemite 10.10.5.
BridgePlus.scptd Ver 1.3.1 dated Nov 25, 2015

Any suggestions on how to fix?

Here’s the Results Panel running your script:


tell application "Finder"
	get name of every file of desktop
		--> {"Scavenge Outlook Messages.app", "Win7 Pro x64"}
	get modification date of every file of desktop
		--> {date "Sun, Jan 15, 2012 at   7:17 PM", date "Sat, Apr 19, 2014 at   4:14 PM"}
Result:
error "-[NSAppleEventDescriptor compare:]: unrecognized selector sent to instance 0x61000062fd20" number -10000


I keep forgetting about dates and 10.10:

use theLib : script "BridgePlus"

tell application "Finder"
	set theNames to name of files of desktop
	set theDates to modification date of files of desktop
end tell
load framework
set theDates to Cocoaify theDates
set listPairs to theLib's colsToRowsIn:{theDates, theNames}
set listPairs to theLib's sublistsIn:listPairs sortedByIndexes:{1, 2} ascending:{true} sortTypes:{}
set {theDates, theNames} to theLib's colsToRowsIn:listPairs

Simplest is just to rearrange the lists. There’s a transpose list command in the List library that will do this:

set notesList to transpose list {modDateList, noteLinkList} --> {{date1,link1}, {date2,link2}, ...}

Which you can then order by date using the sort list command:

set sortedNotesList to sort list notesList using (list comparator for {itemIndex:1, itemComparator:date comparator})

Thanks. It now runs without error, but how do I get the normal date list back?


log item 1 of theDates

--> (*«class ocid» id «data optr000000000D00002CB2C3B441»*)

I assumed you were only after the other values, but it would be:

set theDates to ASify from theDates

The answer is YES, there is a MUCH better way to do this.

My undying gratitude to all of you who have helped me so much, have been so patient, and have helped me design a process for getting and sorting Evernote Notes that will be of huge help to everyone in the Evernote Mac community.

In particular, I’d like to thank:
@Shane Stanley
@hhas
@Nigel Garvey

My original script took 17.2 seconds to get and sort 2,563 notes.
My final script takes only 0.8 seconds to get and sort 4,108 notes!!!
¢ 0.3 sec to get the Note data
¢ 0.5 sec to sort the Note data

I plan to clean up this script some more, and add more comments, and then I’ll publish to my Gists.
For now, here is the final script:

Script has been published, and will be maintained, at my GitHub Gist:
EN Mac Get & Sort Notes by Property AS.applescript


(*
===============================================================================
	[EN] Get and Sort Notes by Note Property Using "every note" Method & ASObJC [AS]
===============================================================================

VER: 	2.0		LAST UPDATE:   2016-02-22

PURPOSE:
	¢ Get and Sort Notes by Note Property Using "every note" Method & ASObJC

AUTHOR:		JMichaelTX
					with a GIANT assist from @Shane Stanley and @hhas
					
					All errors are mine.  Credit for the core approach and code goes
					to the above.
					
					Find any bugs/issues or have suggestions for improvement?
					Contact me via PM or at blog.jmichaeltx.com/contact/

REQUIRED:
	1.	Mac OS X Yosemite 10.10.5+
	2.	Mac Applications
				¢ Evernote Mac 6.4+
				
	3.	EXTERNAL OSAX Additions/LIBRARIES/FUNCTIONS
	
				¢ BridgePlus 1.3.1 Script Library by Shane Stanley
				  https://www.macosxautomation.com/applescript/apps/BridgePlus.html
					
	4.	INTERNAL FUNCTIONS:
				¢ timer()		Calculate and Log Execution Time  (non-essential)

REF:  The following were essential in the writing of this script.

	1.	Using the "every note" method pointed out by @hhas at MacScripter.net
	    http://macscripter.net/viewtopic.php?pid=184925#p184925
			
	2.  Using the ASOBjC sort facility in BridgePlus by Shane Stanley
	    http://macscripter.net/viewtopic.php?pid=184936#p184936
			
===============================================================================
   
*)

use AppleScript version "2.4" -- Yosemite 10.10.5+
use scripting additions
use framework "Foundation"
use BPLib : script "BridgePlus"
load framework


timer("start")

--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
--	GET LISTS OF Evernote NOTE PROPERTIES TO SEARCH ON --
--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

tell application "Evernote"
	
	tell every note in notebook ".ToBeFiled" -- change to your Notebook
		
		set titleList to its title
		set modDateList to its modification date -- will be main sort key in this example
		set noteLinkList to its note link -- will be used to get actual Note after sort
		
	end tell -- note
	
	set numNotes to count of titleList
	log numNotes
	
	--- LOG RESULTS BEFORE SORT FOR COMPARISION ---
	
	log "~~~~~ 	BEFORE SORT ~~~~~~~"
	set indexList to 1
	set modDate to item indexList of modDateList
	set noteLink to item indexList of noteLinkList
	set oNote to find note noteLink -- GET REF TO ACTUAL NOTE OBJECT
	set titleStr to title of oNote
	
	log "Mod Date: " & modDate & "
     Title: " & titleStr
	
end tell -- Evernote

timer("AFTER GET EN LISTS")

--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
--	SORT EVERNOTE NOTE LISTS --
--  (requires BridgePlus Script Lib)
--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

--- Convert Date List To Cocoa Format ---
--	(Needed ONLY for Yosemite 10.10)
set modDateList to Cocoaify modDateList -- needed for Yosemite 10.10

--- Setup the Sort ---
set listPairs to BPLib's colsToRowsIn:{modDateList, noteLinkList}

--- Do the Sort
set listPairs to BPLib's sublistsIn:listPairs sortedByIndexes:{1, 2} ascending:{true} sortTypes:{}

--- Get Sort Results ---
set {modDateList, noteLinkList} to BPLib's colsToRowsIn:listPairs

--- De-Cocoaify Date List For Yosemite 10.10 ---
set modDateList to ASify from modDateList

tell application "Evernote"
	
	log "~~~~~ 	AFTER SORT ~~~~~~~"
	
	set indexList to 1 -- should be oldest Note
	
	set modDate to item indexList of modDateList
	set noteLink to item indexList of noteLinkList
	set oNote to find note noteLink -- GET REF TO ACTUAL NOTE OBJECT
	set titleStr to title of oNote
	
	log "Mod Date: " & modDate & "
    Title: " & titleStr
	
end tell -- Evernote

timer("STOP")
--~~~~~~~~~~~~~~~~~~~~~ END OF MAIN SCRIPT ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

###””””””””””””””””””””””””””””””””””””””””””””””
#			timer()		Calculate and Log Execution Time
#
#			Ver 1.0		2016-02-21
#
#			REF:  The base ASObjC code was provided by Shane Stanley
###””””””””””””””””””””””””””””””””””””””””””””””

on timer(pAction)
	(*
	### Requires these two statements at top of main script: ###
		 	use scripting additions
		 	use framework "Foundation"
*)
	global gTimerStartDate
	if (pAction = "start") then
		set gTimerStartDate to current application's NSDate's |date|()
		log "START: " & ((current date) as text)
	else
		log pAction & ":  
    ¢ " & ((current date) as text) & "
    ¢ EXECUTION TIME: " & (round (-(gTimerStartDate's timeIntervalSinceNow())) * 1000) / 1000.0 & " sec"
	end if
end timer

LOG OUTPUT


(*START: Mon, Feb 22, 2016 at   10:32 PM*)
(*4108*)
(*~~~~~ 	BEFORE SORT ~~~~~~~*)
(*Mod Date: Tue, May 15, 2012 at   8:17 PM
     Title: AT&T Data Usage Jan 2011 - Apr 2012.pdf*)
(*AFTER GET EN LISTS:  
    ¢ Mon, Feb 22, 2016 at   10:32 PM
    ¢ EXECUTION TIME: 0.306 sec*)
(*~~~~~ 	AFTER SORT ~~~~~~~*)
(*Mod Date: Wed, Jun 1, 1977 at   12:00 AM
    Title: Bob -- Rockwell Engineer -- 1977*)
(*STOP:  
    ¢ Mon, Feb 22, 2016 at   10:32 PM
    ¢ EXECUTION TIME: 0.777 sec*)

Hi JMichaelTX.

Glad you’ve got your solution. Sorry I didn’t appreciate you wanted to sort a list of application objects. I’d gathered from the discussion that it was a list of records. Otherwise I too would have recommended extracting the relevant data first and sorting them. Good job hhas was on the ball!