Script Objects

Basic Script Object

Applescript is object-oriented by it’s very nature, but objects are limited to the application being worked on or records, which are essentially property-only objects. Script objects allow custom methods and more functionality. Only use Script Object if methods are needed, otherwise always use records.

script scriptName
	property property1 : "property1 value"
	property property2 : "property2 value"
	property property3 : "property3 value"

	on handler1(param1, param2, param3)
		-- your code goes here
    	end handler1

	on handler2(param1, param2, param3)
		-- your code goes here
	end handler2
end script

How to use properly

A very important distinction needs to be made about creating script object. Always use a method to that returns a script object because using “copy” to create the object actually causes a “deep copy” that duplicates the child AND parent (in the example, the whole Applescript itself) which will use so much memory and processor cycles as to crash to the Mac.

on MakeScriptObject()
    script
        property Vegetable : ""
    end script
--no return statement is necessary for script objects.
end MakeScript Object

set John to MakeScriptObject() of me
set Vegetable of John to "Swiss chard"
return Vegetable of myScriptObject
--result: "Swiss chard"

OCDictionary Data Storage Class


N.B.:
 This class is now in active development at my GitHub account asASDictionary. While this class works as writ, no more updated will be made here, and the latest version is much faster and easier to use. Really, you should just go there now.

About

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. This simple dictionary class gives all of that functionality in native Applescript form.

The caveats

The problem, though, is that because we are looping through a list to find items, things can go slow, even for Applescript. There is a checkDataIntegrity flag that can be turned on and off, and that increases the speed (everything being relative in Applescript) but all data and dictionary integrity checks are switched on by default. What this means is that if you turn off the checking, then you can end up with a dictionary with mismatched key-value pairs because one item may be null and the other not.

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

Possible future plans

Right now, the accessor methods only go by the key and not the value, but that can easily be added. I would like to see more of the functionality that I get in REALbasic and Cocoa’s NSDictionary class.
I would like to see this put aside and have access to the NSDictionary class within Cocoa with all of its features, but that would be an add-on and this, at least, does away with the dependencies (and land mines) that go along with that.

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.

This uses the Script Object features of Applescript, which is essentially allows custom OOP in Applescript complete with inheritance. The caveat is that doing this can be ”very” resource and memory intensive. 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