Thursday, July 15, 2010, 06:39 PM -
Programming & ComputersPosted by Administrator
I've been clocking a lot of hours in Applescript lately. The company I work for is making the push to CS4 and Snow Leopard, and by result a whole host of crucial scripts need to be upgraded. After having spent some time in other, more robust, technically-oriented languages, I've been frustrated by Applescript's lack of various classes and data types easily found on REALbasic and Cocoa. In particular, I've been missing dictionaries.
The problem I am currently trying to solve is that I am, more or less, combining the functionality of two monolithic scripts that weigh in at about 2000+ lines each. Their core functionality is different—they create different layouts in different applications—but the content model and the surrounding subroutines that handle files, data manipulation are pretty much the same. So, the idea is to merge the duplicate code and route accordingly. There are differences between the two, and that is where the dictionary came in: a place to put the content as needed for a particular product and send it to the script that needs that content.
The record class in Applescript does a lot of the same work as a dictionary, but it isn't dynamic in any way other than changing values for keys. Keys for key-value pairs are hard-coded, keys can only be strings (because they are hard-coded), and there is no way to iterate through the keys (and thus the values) once a record is created. But in my current problem, if I used the native record class, I would have a lot of key-value pairs that would hang out empty, and that would cause more error-handling than I would care for. This simple class gives all of the core functionality of a dictionary in native Applescript form. The code at the bottom of the page is copy-and-paste ready into your own scripts.
Documentation
Introduction
Essentially, the class is a wrapper for a private list of records with the format
{key:data, value:data}. When a key is accessed, it is essentially going down the list looking for the first record with the given key and acting accordingly based on whether it finds one or not.
Because it is iterating through a list, using this class can get expensive with large amounts of data. There is some data checking that can be toggled on and off, but Applescript is generally slow to begin with so there isn't too much that can be done about it. But, as I see it, the benefits in functionality outweigh any speed issues. Otherwise, this is light, flexible, and designed to be extensible when need be.
Applescript does a lot heavy-lifting for you; you don't get access to specific data comparison methods (either something equals something else or it doesn't), data is loosely-typed (almost too much), and coersion is done for you for the most part. By result, there aren't a lot of tools at our disposal for creating something like this, but there is enough to create a modest yet functioning dictionary.
Also, convenience methods are easy to create as well:
on MakeDictionaryWithValuesAndKeys(someValues, someKeys) -- (list, list) as OCDictionary
set newDictionary to MakeDictionary() of me
tell newDictionary
set valuesAdded to addValuesForKeys(someValues, someKeys)
end tell
return newDictionary
end MakeDictionaryWithValuesAndKeys
-- simply call with:
-- set newDictionary to MakeDictionaryWithValuesAndKeys({"ack", "greeble", "ponies"}, {"ACK", "GREEBLE", "PONIES"}) of me
This class uses the Script Object features of Applescript, which is essentially allowing custom OOP-style objects in Applescript complete with inheritance. The caveat is that doing this can be ''very'' resource and memory intensive (but, then, what
isn't in Applescript). If you aren't familiar with Script Objects, then I highly suggest you read the Applescript Language Guide, but essentially all of the action, the class in of itself, is held within the
script OCDictionary...end script block within the
MakeDictionary() subroutine.
run{}
This subroutine offers examples of syntax and functionality. This combined with the
MakeDictionary() subroutine makes for a fully working script.
MakeDictionary() -- as OCDictionary
If Script Objects are to be used, they need to be declared and returned, and that is all this subroutine does. This contains the entire OCDictionary declaration, so just copy and paste into your script and call with the simple
set testDictionary to MakeDictionary() of me
hasKey(aKey) -- (object) as boolean
Simple function that will return whether a key exists or not.
toggleDataIntegrityChecks() -- as boolean
This toggles the data integrity checks. Data integrity is checked on keys and values being sent to the class. That check is very simple—only checking for null or empty list values—so it's very fast, but when compiled over time, it can add up.
getKeys() -- as list
Returns a list of all the keys found in all the records. If there are no records, it will return an empty list
setValueForKey(aValue, aKey) -- (object, object) as boolean
False can be returned if data integrity checking is enabled and either the key or the value is invalid. Otherwise, this always returns true because of a key doesn't exist, it will create a new key-value pair.
valueForKey(aKey) -- (object) as object or (kOCDictionary_ValueNotFound as string)
This returns the given value for a key, regardless if that value is null or not. If it cannot find a value for that key because the key does not exist, it returns the internal error
"kOCDictionary_ValueNotFound".
addValuesForKeys(someValues, someKeys) -- (list, list) -- as boolean
This allows the addition of multiple keys and values as lists. If data integrity checking is on, then the subroutine checks to make sure there are no empty lists and that they have a one-to-one relationship with each other (i.e., both list lengths are the same. The order of both is entirely up to you). Any error along those lines returns false and nothing is added to the dictionary. If data integrity is off, the lists are added "as is" and can result in null values in the dictionary.
dictionaryIntegrityCheck(verboseFlag) -- (boolean) as boolean
This is added as a convenience method to check is any values are either null or have empty lists. The verbose flag will send basic information about key-value pairs to the Applescript log when errors are found.
The Code
on run {}
set testDictionary to MakeDictionary() of me
tell testDictionary
(* Basic Operations *)
(* Add a value and Key *)
log "setValueForKey(oop, OOP)"
set valueForKeySet to setValueForKey("oop", "OOP")
log valueForKeySet
(* Add a list of values and keys *)
log "addValuesForKeys({ack, greeble, ponies}, {ACK, GREEBLE, PONIES})"
set valuesAddedForKeys to addValuesForKeys({"ack", "greeble", "ponies"}, {"ACK", "GREEBLE", "PONIES"})
log valuesAddedForKeys
(* Get and set values for keys *)
log "setValueForKey(\"Luc Teyssier\", OOP)"
set valueSetForKey to setValueForKey("Luc Teyssier", "OOP")
log valueSetForKey
log "set myValueForKey to valueForKey(OOP)"
set myValueForKey to valueForKey("OOP")
log myValueForKey
(* Get all of the keys and iterate through the pairs *)
log "set theKeys to getKeys()"
set theKeys to getKeys()
log theKeys
log "iterate through all keys"
set lastKey to (count theKeys)
repeat with k from 1 to lastKey
set theKey to item k of theKeys
set theValue to valueForKey(theKey)
log {theKey, theValue}
end repeat
(* Operations That Will Cause Errors *)
log "toggleDataIntegrityChecks()"
log toggleDataIntegrityChecks()
log "setValueForKey(emptyValueList, emptyKeyList)"
log setValueForKey({}, {})
-- nothing should be added to keys or values,
-- but since we turned off data integrity checks,
-- we get it added but the report catches it
log "addValuesForKeys(unmatchedValueList, unmatchedKeyList)"
log addValuesForKeys({"Kate", "Charlie"}, {"Jean-Paul Cardon", "Bob", "Juliette", "Concierge"})
-- we should see errors in the log and nothing added
log "keyFound to hasKey(supercalifrajilisticexpialidocious)"
set keyFound to hasKey("supercalifrajilisticexpialidocious")
log keyFound
-- we should get back a false here
log "set theValueForKey to valueForKey(supercalifrajilisticexpialidocious)"
set theValueForKey to valueForKey("supercalifrajilisticexpialidocious")
log theValueForKey
-- we should get back the kOCDictionary_ValueNotFound error message here
(* Check to make sure our data is clean so we don't mess up operations later *)
log "set dictionaryIsSafe to dictionaryIntegrityCheck(true)"
set dictionaryIsSafe to dictionaryIntegrityCheck(true)
log dictionaryIsSafe
end tell
end run
on MakeDictionary() -- as OCDictionary
script OCDictionary
(* Public properties *)
property kOCDictionary_ValueNotFound : "kOCDictionary_ValueNotFound"
(* Private properties *)
property __keyValuePairs : {}
property __kOCDictionary_NoStoredKeys : -1
property __kOCDictionary_KeyNotFound : -2
property __kOCDictionary_InvalidKey : -3
property __kKeyIndexErrors : {__kOCDictionary_NoStoredKeys, __kOCDictionary_KeyNotFound, __kOCDictionary_InvalidKey}
property __checkDataIntegrity : true
(* Public SubRoutines *)
to hasKey(aKey) -- (object) as boolean
set keyValueIndex to __indexOfKey(aKey) of me
if keyValueIndex is in __kKeyIndexErrors then
return false
end if
return true
end hasKey
to toggleDataIntegrityChecks() -- as boolean
if __checkDataIntegrity = true then
set __checkDataIntegrity to false
else
set __checkDataIntegrity to true
end if
return __checkDataIntegrity
end toggleDataIntegrityChecks
to getKeys() -- as list
set keyList to {}
set keyValuePairCount to (count __keyValuePairs)
if keyValuePairCount = 0 then
return keyList
end if
repeat with thisKeyValuePair from 1 to keyValuePairCount
set theKeyValuePair to item thisKeyValuePair of __keyValuePairs
set theKey to key of theKeyValuePair
set end of keyList to theKey
end repeat
return keyList
end getKeys
to setValueForKey(aValue, aKey) -- (object, object) as boolean
if __checkDataIntegrity then
set aValuePassed to __dataIntegrityCheck(aValue)
set aKeyPassed to __dataIntegrityCheck(aKey)
if not (aValuePassed) or not aKeyPassed then return false
end if
set keyValueIndex to __indexOfKey(aKey) of me
if keyValueIndex is in {__kOCDictionary_NoStoredKeys, __kOCDictionary_KeyNotFound} then
set newKeyValuePair to __makeKeyValuePairWithKeyAndValue(aKey, aValue) of me
set end of __keyValuePairs to newKeyValuePair
else
set theKeyValuePair to item keyValueIndex of __keyValuePairs
set value of theKeyValuePair to aValue
end if
return true
end setValueForKey
to valueForKey(aKey) -- (object) as object or (kOCDictionary_ValueNotFound as string)
set keyValueIndex to __indexOfKey(aKey) of me
if keyValueIndex is in __kKeyIndexErrors then
return kOCDictionary_ValueNotFound
end if
set theKeyValuePair to item keyValueIndex of __keyValuePairs
set theValue to value of theKeyValuePair
return theValue
end valueForKey
to addValuesForKeys(someValues, someKeys) -- (list, list) -- as boolean
set keysCount to (count someKeys)
set valuesCount to (count someValues)
if __checkDataIntegrity then
if keysCount ≠ valuesCount then return false
if keysCount = 0 and valuesCount ≠ 0 then return false
if keysCount ≠ 0 and valuesCount = 0 then return false
end if
set keysCount to (count someKeys)
repeat with thisKey from 1 to keysCount
try
set theKey to item thisKey of someKeys
set theValue to item thisKey of someValues
set newKeyValuePair to __makeKeyValuePairWithKeyAndValue(theKey, theValue) of me
set end of __keyValuePairs to newKeyValuePair
on error
-- fail silently
end try
end repeat
return true
end addValuesForKeys
to dictionaryIntegrityCheck(verboseFlag) -- (boolean) as boolean
set dictionaryIsClean to true
set keyValuePairCount to (count __keyValuePairs)
if keyValuePairCount = 0 then return (dictionaryIsClean = true)
repeat with thisKeyValuePair from 1 to keyValuePairCount
set theKeyValuePair to item thisKeyValuePair of __keyValuePairs
set theKey to key of theKeyValuePair
set theValue to value of theKeyValuePair
set theKeyPassed to __dataIntegrityCheck(theKey)
set theValuePassed to __dataIntegrityCheck(theValue)
if not theKeyPassed or not theValuePassed then
set dictionaryIsClean to false
if verboseFlag then
set recordErrors to {}
set end of recordErrors to "__keyValuePair(" & thisKeyValuePair & ")"
set end of recordErrors to theKeyValuePair
if not theKeyPassed then
set end of recordErrors to "key is null or is an empty list"
end if
if not theValuePassed then
set end of recordErrors to "value is null or is an empty list"
end if
log recordErrors
end if
end if
end repeat
return dictionaryIsClean
end dictionaryIntegrityCheck
(*
Private Subroutines
All error checking is done before we get to these methods, so these should not be called directly.
*)
to __makeKeyValuePairWithKeyAndValue(aKey, aValue) -- (object, object) as record
(* Factory method for key-value pair records *)
set keyValuePair to {key:aKey, value:aValue}
return keyValuePair
end __makeKeyValuePairWithKeyAndValue
to __indexOfKey(aKey) -- (object) as integer
(* This subroutine combined with the __keyValuePairs property is really the crux of the whole class *)
if aKey = null then return __kOCDictionary_InvalidKey
set keyValuePairCount to (count __keyValuePairs)
if keyValuePairCount = 0 then
return __kOCDictionary_NoStoredKeys
else
repeat with thisKeyValuePair from 1 to keyValuePairCount
set theKeyValuePair to item thisKeyValuePair of __keyValuePairs
set theKey to key of theKeyValuePair
if aKey = theKey then
return thisKeyValuePair
end if
end repeat
end if
return __kOCDictionary_KeyNotFound
end __indexOfKey
to __dataIntegrityCheck(newData) -- (object) as boolean
(* This offers only very basic checks: null or empty lists *)
if newData = null then
return false
end if
try
set itemCount to (count newData)
if itemCount = 0 then
return false
end if
on error
-- fail silently
end try
return true
end __dataIntegrityCheck
end script
return OCDictionary
end MakeDictionary