Associative Arrays

From AutoIt Wiki
Revision as of 09:40, 4 December 2017 by Skysnake (talk | contribs)
Jump to navigation Jump to search

In computer science, an associative array, map, symbol table, or dictionary is an abstract data type composed of a collection of (key, value) pairs, such that each possible key appears at most once in the collection. An associative array differs from a “normal” array in one major way: rather than being indexed numerically (i.e. 0, 1, 2, 3, …), it is indexed by a key, or a language-like word. A Scripting.Dictionary is a standard object provided by the Microsoft Scripting Runtime (scrrun.dll) dependency and is commonly used in VBScript, a reference to which needs to be added in order to create an object in your AutoIt project.

This discussion by Nutster explains the original implementation of Associative Arrays in AutoIt.

   An associative array is an array where the keys (index numbers) are string instead of integer, as they are in AutoIt. Linux / Unix awk and perl use associative arrays by default. The following functions can be used to manage a version of associative arrays in single AutoIt variables.  A hash table is used to store the values because it is faster to insert than other options I can think of and pretty much as fast as them to retrieve the data. The memory requirements are a little higher than other methods, but I think it is a reasonable trade-off. It uses a simple hash function that can probably be easily improved upon and while it can resize the hash table, the resize is slow so try to make your initial size large enough for all your elements.  Please see the Include file and an exerciser that demonstrates how to use the associative array functions. Read the comments at the top of each function for more details on their use.



Note:
AutoIt allows for at least two different implementations of Associative Arrays. The first method relies on a native AutoIt internal process handling named array elements, with no reference to the Scripting Dictionary. The second method relies on the Scripting Dictionary to create and manage associative arrays.



A few notes on Associative Arrays:

  • Think of an associative array as an array with named elements
  • Comparisons can be exact (binary) or case insensitive (alpha)



At the time of writing this - 28 November 2017 - a new map data type is under development for AutoIt. It is availabe in the current beta release.

Please also see this entry in the Snippets section of this Wiki _AssocArray

A dictionary object example by MHz

; Author Mhz
; Forum https://www.autoitscript.com/forum/topic/182334-scripting-dictionary-modified/
Global $vKey, $sItem, $sMsg, $oDictionary 

; Create dictionary object
$oDictionary = ObjCreate("Scripting.Dictionary")

If @error Then
    MsgBox(0, '', 'Error creating the dictionary object')
Else
    ; Add keys with items
    $oDictionary.Add ("One",    "Same"  )
    $oDictionary.Add ("Two",    "Car"   )
    $oDictionary.Add ("Three",  "House" )
    $oDictionary.Add ("Four",   "Boat"  )

    If $oDictionary.Exists('One') Then
        ; Display item
        MsgBox(0x0, 'Item One', $oDictionary.Item('One'), 2)
        ; Set an item
        $oDictionary.Item('One') = 'Changed'
        ; Display item
        MsgBox(0x20, 'Did Item One Change?', $oDictionary.Item('One'), 3)
        ; Remove key
        $oDictionary.Remove('One')
        ; 
        $oDictionary.Key ('Two') = 'Bike'
    EndIf

    Sleep(1000)

    ; Store items into a variable
    For $vKey In $oDictionary
       $sItem &= $oDictionary.Item($vKey) & @CRLF
    Next

    ; Display items
    MsgBox(0x0, 'Items Count: ' & $oDictionary.Count, $sItem, 3)

    ; Add items into an array
    $aItems = $oDictionary.Items

    ; Display items in the array
    For $i = 0 To $oDictionary.Count -1
        MsgBox(0x0, 'Items [ ' & $i & ' ]', $aItems[$i], 2)
    Next

    Sleep(1000)

    ; Add keys into an array
    $aKeys = $oDictionary.Keys

    ; Display keys in the array
    For $i = 0 To $oDictionary.Count -1
        MsgBox(0x0, 'Keys [ ' & $i & ' ]', $aKeys[$i], 2)
    Next
EndIf



The original AutoIt Scripting Dictionary implementation appears to have been by Gary Frost as far back as 2007. It has since been modified to the following:

#cs
	Scripting Dictionary UDF

	Made by GaryFrost <https://www.autoitscript.com/forum/topic/47048-scripting-dictionary/>
	Modified by Jefrey <jefrey[at]jefrey.ml>
	
	Forum https://www.autoitscript.com/forum/topic/182334-scripting-dictionary-modified/
	
#ce

Func _InitDictionary()
	Return ObjCreate("Scripting.Dictionary")
EndFunc   ;==>_InitDictionary

; Adds a key and item pair to a Dictionary object.
Func _AddItem($oDictionary, $v_key, $v_item)
	$oDictionary.ADD($v_key, $v_item)
	If @error Then Return SetError(1, 1, -1)
EndFunc   ;==>_AddItem

; Returns true if a specified key exists in the Dictionary object, false if it does not.
Func _ItemExists($oDictionary, $v_key)
	Return $oDictionary.Exists($v_key)
EndFunc   ;==>_ItemExists

; Returns an item for a specified key in a Dictionary object
Func _Item($oDictionary, $v_key)
	Return $oDictionary.Item($v_key)
EndFunc   ;==>_Item

; Sets an item for a specified key in a Dictionary object
Func _ChangeItem($oDictionary, $v_key, $v_item)
	$oDictionary.Item($v_key) = $v_item
EndFunc   ;==>_ChangeItem

; Sets a key in a Dictionary object.
Func _ChangeKey($oDictionary, $v_key, $v_newKey)
	$oDictionary.Key($v_key) = $v_newKey
EndFunc   ;==>_ChangeKey

; Removes a key, item pair from a Dictionary object.
Func _ItemRemove($oDictionary, $v_key)
	$oDictionary.Remove($v_key)
	If @error Then Return SetError(1, 1, -1)
EndFunc   ;==>_ItemRemove

; Returns the number of items in a collection or Dictionary object.
Func _ItemCount($oDictionary)
	Return $oDictionary.Count
EndFunc   ;==>_ItemCount

; Returns an array containing all the items in a Dictionary object
Func _GetItems($oDictionary)
	Return $oDictionary.Items
EndFunc   ;==>_GetItems

Func _Clear($oDictionary)
	Return $oDictionary.RemoveAll
EndFunc



Below are several examples that illustrate the usage of the Scripting Dictionary UDF in AutoIt.
Example 1:

; Author Jefrey
; Forum https://www.autoitscript.com/forum/profile/75137-jefrey/
#include 'scriptingdic.au3'
#include <Array.au3> ; Needed only for _ArrayDisplay, and not required by the lib

; Init the object
$oObj = _InitDictionary()

; Adding a single value
_AddItem($oObj, 0, "Test")
; And showing it
msgbox(0, '', _Item($oObj, 0))

; Adding an array
Dim $aArr[] = [0, -80, -49, -44, 80, 100, 8, 7, 6, 5, 4, 3, 2, 1]
_AddItem($oObj, 1, $aArr)
; And showing it
_ArrayDisplay(_Item($oObj, 1))

; We can also use this approach:
$oObj3 = _InitDictionary()
$oObj.Add("test", "foo")
MsgBox(0, '', $oObj.Item("test"))

Example 2 is a reply to a common question asked on the Forum, How to find duplicate lines/values...

; Author Malkey 
;        https://www.autoitscript.com/forum/profile/31256-malkey/
; Forum  https://www.autoitscript.com/forum/topic/116156-best-way-to-compare-strings-for-duplicates/?do=findComment&comment=810752
Local $sStr, $begin, $dif, $sMatches

; Create random test data
For $i = 1 To 1000
    $sStr &= Chr(Random(Asc("a"), Asc("z"), 1)) & Chr(Random(Asc("a"), Asc("z"), 1)) & _
            Chr(Random(Asc("a"), Asc("z"), 1)) & Chr(Random(Asc("a"), Asc("z"), 1)) & @CRLF ;
Next
;ConsoleWrite($sStr & @CRLF)

$begin = TimerInit()

Local $oDict = ObjCreate("Scripting.Dictionary") ; to create a dictionary object
Local $aArray = StringSplit(StringStripWS($sStr, 2), @CRLF, 3)
For $i = 0 To UBound($aArray) - 1
    If $oDict.Exists($aArray[$i]) Then
        $sMatches &= $i & " " & $aArray[$i] & @CRLF ; Record duplicates
    Else
        $oDict.add($aArray[$i], '') ; to store a string (you may set $value = '' if you have no use of it)
    EndIf
Next
$dif = Round(TimerDiff($begin) / 1000, 3) & " secs"

;Display results
If $sMatches <> "" Then
    MsgBox(0, "Results", StringTrimRight($sMatches, 2) & @CRLF & "Time taken = " & $dif)
Else
    MsgBox(0, "Results", "No duplicates found" & @CRLF & "Time taken = " & $dif)
EndIf





__________________________________________________________________________
While the examples above use the Scripting Dictionary method, the example below by guinness does not use the Scripting Dictionary. Also see this discussion by Nutster, with Associative Array management tools.

The next example does not use the scripting dictionary UDF, but makes use of Global Enumerators. This allows access to data through named elements and uses an Object Oriented Programming-like approach to encapsulate data.

; Author   guinness
; Forum    https://www.autoitscript.com/forum/topic/173803-oop-like-approach-in-autoit/
#include <MsgBoxConstants.au3>

; ---- Start of Person Class

; Stored in the 'object' to verify it's our 'object' and not some random array
Global Const $PERSON_GUID = '4197B285-6AB1-489B-8585-08C852E33F3D'

; Friendly names for 0, 1, 2 and 3
Global Enum $PERSON_AGE, $PERSON_NAME, $PERSON_ID, $PERSON_MAX

; Constructor
Func Person($sName, $iAge)
    Local $hPerson[$PERSON_MAX]

    ; Set the GUID, so as the verification will work
    $hPerson[$PERSON_ID] = $PERSON_GUID

    Person_SetAge($hPerson, $iAge)
    Person_SetName($hPerson, $sName)

    ; Return the Person 'object'
    Return $hPerson
EndFunc   ;==>Person

; Getter for the age property
Func Person_GetAge(ByRef $hPerson)
    Return _Person_IsObject($hPerson) ? $hPerson[$PERSON_AGE] : Null
EndFunc   ;==>Person_GetAge

; Setter for the age property
Func Person_SetAge(ByRef $hPerson, $iAge)
    ; If not a valid 'object' or integer then return
    If Not _Person_IsObject($hPerson) Or Not IsInt($iAge) Then Return

    ; Set the age
    $hPerson[$PERSON_AGE] = $iAge
EndFunc   ;==>Person_SetAge

; Getter for the name property
Func Person_GetName(ByRef $hPerson)
    Return _Person_IsObject($hPerson) ? $hPerson[$PERSON_NAME] : Null
EndFunc   ;==>Person_GetName

; Setter for the name property
Func Person_SetName(ByRef $hPerson, $sName)
    ; If not a valid 'object' then return
    If Not _Person_IsObject($hPerson) Then Return

    ; Set the name
    $hPerson[$PERSON_NAME] = $sName
EndFunc   ;==>Person_SetName

; ToString() for the 'object'
Func Person_ToString(ByRef $hPerson)
    Return _Person_IsObject($hPerson) ? StringFormat('Name: %s, Age: %i', $hPerson[$PERSON_NAME], $hPerson[$PERSON_AGE]) : Null
EndFunc   ;==>Person_ToString

; Check if it's a valid 'object' and not some random array. "NTERNAL ONLY!
Func _Person_IsObject(ByRef $hPerson)
    Return UBound($hPerson) = $PERSON_MAX And $hPerson[$PERSON_ID] == $PERSON_GUID
EndFunc   ;==>_Person_IsObject

; ---- End of Person Class

Example()

Func Example()
    ; Store the Person 'object', which is just a glorified array
    Local $hP1 = Person('John', 30)

    ; Display the 'object'
    MsgBox($MB_SYSTEMMODAL, 'Person 1', Person_ToString($hP1))

    ; Create a new person

    ; Store the Person 'object', which is just a glorified array
    Local $hP2 = Person('James', 36)

    ; Display the 'object'
    MsgBox($MB_SYSTEMMODAL, 'Person 2', Person_ToString($hP2))

    ; Set the age for Person 2
    Person_SetAge($hP2, 45)

    ; Display the 'object'
    MsgBox($MB_SYSTEMMODAL, 'Person 2 - Revised', Person_ToString($hP2))
EndFunc   ;==>Example

Please see the current beta for more information on map functions.