Simple Script...Long List Table Overflow?

Neither approach is correct and depends on mcsprodart’s requirements, but I looked at this from the opposite perspective. There seems to be four possibilities, and these are easily implemented in the following script, which took 12 milliseconds to run.


set searchStyle to "6405CVC"
set styleCode to getStyleCode(searchStyle) -- 0 is no match; 1 is Sanmar match; 2 is SSActivewear match; 3 is both Sanmar and SSActivewear

on getStyleCode(matchString)
	set matchFound to 0
	set allSanmarStyles to paragraphs of (read POSIX file "/Users/Robert/Desktop/allSanmarStyles.txt")
	if matchString is in allSanmarStyles then set matchFound to matchFound + 1
	set allSSStyles to paragraphs of (read POSIX file "/Users/Robert/Desktop/allSSStyles.txt")
	if matchString is in allSSStyles then set matchFound to matchFound + 2
	return matchFound
end getStyleCode

When working with large lists, I save them as AppleScript Lists in a text file ending with “.dat”
I use these two scripts to create and retrieve them

on get_Prefs(pFile as string)
	local cfile, show_list, ptype
	set cfile to (path to preferences as text) & "ScriptPrefs:" & pFile
	try
		cfile as alias
	on error
		display alert "\"" & pFile & "\" Doesn't exist!" giving up after 10
		return false
	end try
	set ptype to read (cfile as alias) for 5
	if 0 = id of (text 5 of ptype) then
		set ptype to text 1 thru 4 of ptype
	else
		set ptype to "text"
	end if
	try
		if ptype = "reco" then
			set show_list to read (cfile as alias) as record
		else if ptype = "list" then
			set show_list to read (cfile as alias) as list
		else if ptype = "text" then
			set show_list to read (cfile as alias) as text
		end if
	on error errMsg number errNum
		display alert errMsg giving up after 10
		set show_list to {}
	end try
	return show_list
end get_Prefs

on set_Prefs(pFile as string, pList)
	local cfile, flag
	set flag to false
	set cfile to (path to preferences as text)
	try
		(cfile & "ScriptPrefs:") as alias
	on error
		try
			tell application "System Events"
				make new folder at folder cfile with properties {name:"ScriptPrefs"}
			end tell
		on error
			display alert "Error creating folder \"ScriptPrefs\" in Preferences." giving up after 10
			return false
		end try
	end try
	set cfile to cfile & "ScriptPrefs:" & pFile
	try
		set cfile to open for access cfile with write permission
	on error eStr number eNum
		display alert "Error opening file " & pFile giving up after 10
		return false
	end try
	try
		set eof cfile to 0
		if class of pList = record then
			write pList to cfile as record
		else if class of pList = list then
			write pList to cfile as list
		else if class of pList = text then
			write pList to cfile as text
		else
			write pList to cfile
		end if
	on error
		display alert "Error! Can't write to preference file…" giving up after 10
	end try
	close access cfile
	return true
end set_Prefs

I’m enclosing the two dat files already created. You’ll need to put them in your user Preferences folder, in a subfolder names “ScriptPrefs”
allStyles.zip (31.5 KB)

and here is the modified script from above. It is very fast

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions

set wtf to "6405CVC"
set theResult to my testVendors(wtf)

on testVendors(testStyle)
	local allSSStyles, allSanmarStyles
	set vendorAPI to "none"
	
	---list one here needs more items
	set allSSStyles to get_Prefs("allSSStyles.dat")
	set allSanmarStyles to get_Prefs("allSanmarStyles.dat")
	
	if allSanmarStyles contains testStyle then
		set vendorAPI to "Sanmar"
	else if allSSStyles contains testStyle then
		set vendorAPI to "SSActivewear"
	end if
	return vendorAPI
end testVendors

on get_Prefs(pFile as string)
	local cfile, show_list, ptype
	set cfile to (path to preferences as text) & "ScriptPrefs:" & pFile
	try
		cfile as alias
	on error
		display alert "\"" & pFile & "\" Doesn't exist!" giving up after 10
		return false
	end try
	set ptype to read (cfile as alias) for 5
	if 0 = id of (text 5 of ptype) then
		set ptype to text 1 thru 4 of ptype
	else
		set ptype to "text"
	end if
	try
		if ptype = "reco" then
			set show_list to read (cfile as alias) as record
		else if ptype = "list" then
			set show_list to read (cfile as alias) as list
		else if ptype = "text" then
			set show_list to read (cfile as alias) as text
		end if
	on error errMsg number errNum
		display alert errMsg giving up after 10
		set show_list to {}
	end try
	return show_list
end get_Prefs

Here is a single line version of the commands to read in the lists. (i.e. no error checking)

	set allSSStyles to read alias ((path to preferences as text) & "ScriptPrefs:allSSStyles.dat") as list
	set allSanmarStyles to read alias ((path to preferences as text) & "ScriptPrefs:allSanmarStyles.dat") as list

** EDIT ** - I changed the get_Prefs and set_Prefs scripts to newer version I had

The native AppleScrip approach is 21% faster using the strings provided by mcsprodart

@peavine can you suggest how I can use containsObject without case sensitivity? I have used filteredArrayUsingPredicate but not containsObject before.

mcsprodart. I don’t know of any way to make the containsObject method case insensitive, but a different approach is to lowercase all strings. With the text files you supplied, the timing result was 3 milliseconds.

use framework "Foundation"
use scripting additions

set searchStyle to "6405cVc" -- case insensitive

set theStyle to getStyle(searchStyle)

on getStyle(searchStyle)
	set searchStyle to (current application's NSString's stringWithString:searchStyle)'s lowercaseString()
	set fileOne to POSIX path of (path to desktop) & "allSSStyles.txt" -- user set to desired value
	set fileTwo to POSIX path of (path to desktop) & "allSanmarStyles.txt" -- user set to desired value
	set stringOne to (current application's NSString's stringWithContentsOfFile:fileOne encoding:(current application's NSUTF8StringEncoding) |error|:(missing value))'s lowercaseString()
	set arrayOne to (stringOne's componentsSeparatedByString:linefeed)
	if (arrayOne's containsObject:searchStyle) is true then return "SSActivewear"
	set stringTwo to (current application's NSString's stringWithContentsOfFile:fileTwo encoding:(current application's NSUTF8StringEncoding) |error|:(missing value))'s lowercaseString()
	set arrayTwo to (stringTwo's componentsSeparatedByString:linefeed)
	if (arrayTwo's containsObject:searchStyle) is true then return "Sanmar"
	return "No Match"
end getStyle

The issue raised in this thread has been resolved, and I’ve learned a lot, but I had one lingering question. Can the arrays be stored in a plist and is that better than a text file. To provide the answer up front, using a plist raises issues of complexity and memory usage with only a minuscule speed advantage.

The first question I had is whether large arrays can be stored in a plist, and the answer is a qualified yes. The following is from the Apple documentation:

Many applications require a mechanism for storing information that will be needed at a later time. For situations where you need to store small amounts of persistent data—say less than a few hundred kilobytes—property lists offer a uniform and convenient means of organizing, storing, and accessing the data.

So, I decided to investigate further. Creating a plist of arrays within the context of a script is easy but doing that for testing in this particular instance is a bit convoluted. In the end I read the contents of the text files supplied by the OP and wrote them to a plist. It should be noted that the following script creates a plist with the name “com.peavine.Test” in the user’s Preferences folder, but it is easily deleted after testing.

use framework "Foundation"
use scripting additions

-- create plists from text files for testing purposes only
set fileOne to POSIX path of (path to desktop) & "allSSStyles.txt" -- user set to desired value
set fileTwo to POSIX path of (path to desktop) & "allSanmarStyles.txt" -- user set to desired value
set theDefaults to current application's NSUserDefaults's alloc()'s initWithSuiteName:"com.peavine.Test"
set stringOne to (current application's NSString's stringWithContentsOfFile:fileOne encoding:(current application's NSUTF8StringEncoding) |error|:(missing value))'s lowercaseString()
set arrayOne to (stringOne's componentsSeparatedByString:linefeed)
theDefaults's setObject:arrayOne forKey:"allSSStyles"
set stringTwo to (current application's NSString's stringWithContentsOfFile:fileTwo encoding:(current application's NSUTF8StringEncoding) |error|:(missing value))'s lowercaseString()
set arrayTwo to (stringTwo's componentsSeparatedByString:linefeed)
theDefaults's setObject:arrayTwo forKey:"allSanmarStyles"

The script that retrieves the desired data from the arrays is:

use framework "Foundation"
use scripting additions

set searchStyle to "6405CVC"

set theStyle to getStyle(searchStyle)

on getStyle(searchStyle)
	set searchStyle to (current application's NSString's stringWithString:searchStyle)'s lowercaseString()
	set theDefaults to current application's NSUserDefaults's alloc()'s initWithSuiteName:"com.peavine.Test"
	set arrayOne to theDefaults's objectForKey:"allSSStyles"
	if (arrayOne's containsObject:searchStyle) is true then return "SSActivewear"
	set arrayTwo to theDefaults's objectForKey:"allSanmarStyles"
	if (arrayTwo's containsObject:searchStyle) is true then return "Sanmar"
	return "No Match"
end getStyle

The timing result with the above script was 0.4 millisecond, as compared with 3 milliseconds for the text-file-based solution, and this difference is not significant IMO. An issue that remains unresolved is how much memory does the plist use. The plist file itself is 104KB, but I don’t know what impact that has on actual memory usage. Anyways, a text-file solution seems best to me.

1 Like

I completely agree. I am already using plist for stored prefs and to me it doesn’t make sense to store in plist file unless the text files never changed. I learned a lot as well and plan on exploring speed changes on using a csv file with multiple columns and using filtered predicates on both list of lists and a simple array. Thank you for helping me sort out these methods.

This is one of the few valid situations to employ a try block in AppleScript to catch an error, however the reason why this could be necessary doesn’t get addressed by the above code.

When using open for access to read from or write to a file, the filesystem initialises access to the file and allocates memory to be used by buffers. It returns a file handle, which is an integer that essentially serves as a pointer to the resource, but additionally allows operations on the file to be tracked should other processes be accessing the file concurrently. When no longer required, the file handle should be closed so that the filesystem can finish transferring whatever’s in the buffer or flush it, free up resources, and update the system to allow other processes whose operations were queued to progress.

I imagine an open file handle will be automatically closed once execution of the script is completed. This won’t necessarily be the case when script execution terminates unexpectedly, such as when an exception is thrown. This is the purpose of the try block, which should enclose all code starting from the call to open for access up to and including the corresponding close access call, in order to catch any exception that arises whilst there’s an active file handle in use. If an exception is raised by any instruction contained within these lines of code, this will mean that execution has not yet reached the point at which the call to close access was to be made, so the file remains open and in use. Therefore, upon catching the error, one of the first things that ought to happen is a call to close access to the file handle, so it’s never left dangling.

Conversely, this try block is unnecessary, as by this point, you’ve already acquired file access with write permission. The only errors that could arise out of these lines would either be caused by insufficient memory or disk space, or the fact that you’re attempting to write to the same file to which you already have an open file handle that, for whatever reason, you’ve neglected. As far as I know, AppleScript doesn’t request exclusive access to files, so while the latter scenario is a little careless, it’s not likely to create any issues.

The write handler below (equivalent to your get_Prefs() handler) demonstrates how to make use of a file handle and ensure it gets released should the script terminate prematurely. The read handler is, likewise, a modified version of your set_Prefs() handler that is largely a refactoring to remove the try blocks and the sequential blocks of if...then...else.

Like yours, both of these modified handlers take a parameter that gets coerced into a string assigned to the variable pFile. This can either be a filename (which will be processed relative to the folder path ~/Library/Preferences/ScriptPrefs); or it can be an absolute file path or valid file reference, which will be used to read from (provided it exists) or write to. This provides the option of operating within the “ScriptPrefs” directory by default, while allowing the user to specify any other location of their choosing, e.g.

tell ScriptPrefs to write {"foo", "bar"} to "somefile.dat" --> Writes a list out to ~/Library/Preferences/ScriptPrefs/somefile.dat
read ScriptPrefs from "somefile.dat" --> Reads in the list from ~/Library/Preferences/ScriptPrefs/somefile.dat

tell ScriptPrefs to write {foo:"bar", bar:"foo"} to "~/Documents/somefile.dat" --> Writes a record out to ~/Documents/somefile.dat
read ScriptPrefs from "~/Documents/somefile.dat" --> Reads in the record from ~/Documents/somefile.dat

tell application id "com.apple.SystemEvents" to script ScriptPrefs
        property parent : it
        # System Events won't complain if the folder already exists
        property pFolder : make new folder with properties ¬
                {name:"ScriptPrefs"} at preferences folder
        
        to read from (pFile as text)
                local pPath, pType
                
                tell pFolder's file pFile to if (exists) ¬
                        then set pFile to the POSIX path
                tell file pFile to if not (exists) then ¬
                        error "File not found: " & pFile
                set pPath to file pFile's POSIX path
                
                tell the current application
                        # Reading in as "type" essentially reads in
                        # the first four bytes, then coerces this to
                        # an AppleScript class
                        set pType to read pPath as "type"
                        
                        # Note the "as..." class can be supplied by
                        # way of a stored variable, and can either
                        # be an AppleScript class or a four-byte
                        # type code, e.g. record or "reco"
                        #
                        # This avoids the if...then...else ballache
                        if pType is not in [record, list] ¬
                                then set pType to "utf8"
                        read pPath as pType from 0
                end tell
        end read
        
        to write PData to (pFile as text)
                local pPath, pType, fh
                
                tell pFile to if it starts with "~/" then set ¬
                        pFile to my home folder's POSIX path & ¬
                        text 2 thru -1
                
                using terms from scripting additions
                        tell the POSIX path of pFile to if ("/") ¬
                                is not in text 2 thru -1 then set ¬
                                pFile to pFolder's «class posx» & ¬
                                it
                        
                        set pPath to pFile's POSIX path
                end using terms from
                
                # Again, using a variable to facilitate
                # writing out of data as an appropriate
                # class without if...then...else-ing to
                # hell and back
                set pType to PData's class
                if pType = text then set pType to "utf8"
                
                tell the current application to try
                        open for access pPath with write permission
                        set fh to the result
                        set eof of fh to 0
                        write PData to fh as pType
                        close access fh
                on error E number N
                        close access fh
                        display alert ("" & N & ": " & E) ¬
                                giving up after 10
                        return false
                end try
                
                return true
        end write
end script
1 Like