Real programming with JavaScript - Associative Arrays

Making a hash of JavaScript arrays

In the previous article we looked at creating, manipulating and extracting values from JavaScript arrays. Arrays allow us to store lists of related data using a numbered index.

But in JavaScript, the index of an array need not be a number, it can also be a word or key as listing 1 demonstrates.

Listing 1.

var primeMinister = new Object();

primeMinister["barton"] = "Sir Edmund BARTON";
primeMinister["bruce"] = "Stanley Melbourne BRUCE";
primeMinister["chifley"] = "Joseph Benedict CHIFLEY";
primeMinister["cook"] = "Joseph COOK";
primeMinister["curtin"] = "John Joseph CURTIN";
primeMinister["deakin"] = "Alfred DEAKIN";
primeMinister["fadden"] = "Arthur William FADDEN";
primeMinister["fisher"] = "Andrew FISHER";
primeMinister["forde"] = "Francis Michael FORDE";
primeMinister["fraser"] = "John Malcolm FRASER";
primeMinister["gorton"] = "John Grey GORTON";
primeMinister["hawke"] = "Robert James Lee HAWKE";
primeMinister["holt"] = "Harold Edward HOLT";
primeMinister["howard"] = "John Winston HOWARD";
primeMinister["hughes"] = "William Morris HUGHES";
primeMinister["keating"] = "Paul John KEATING";
primeMinister["lyons"] = "Joseph Aloysius LYONS";
primeMinister["mcewen"] = "John McEWEN";
primeMinister["mcmahon"] = "William McMAHON";
primeMinister["menzies"] = "Robert Gordon MENZIES";
primeMinister["page"] = "Earle Christmas Grafton PAGE";
primeMinister["reid"] = "George Houstoun REID";
primeMinister["scullin"] = "James Henry SCULLIN";
primeMinister["watson"] = "John Christian WATSON";
primeMinister["whitlam"] = "Edward Gough WHITLAM";

var selectedPM = prompt( "Which PM?","" );

var outputPM = primeMinister[ selectedPM ];

alert( outputPM );

(Live example currently deactivated)

This program uses the surnames of prime ministers as the array index. Firstly, define a perfectly ordinary array

var primeMinister = new Object();

(Hang on! Why isn’t that a new Array() object? See my comment below for the explanation.)

To the array add the twenty-five prime ministers using surname (not a number) as the key and the full name as the value:

primeMinister["barton"] = "Sir Edmund BARTON";
...
primeMinister["whitlam"] = "Edward Gough WHITLAM";

In the line

var selectedPM = prompt( "Which PM?","" );

the user’s response to the question “Which PM?” (that is, a prime minister’s surname - see Figure 1) is stored in the variable selectedPM.

Figure 1 - Request your PM by surname

Figure 1

Then the corresponding value is retrieved from the array and saved in outputPM

var outputPM = primeMinister[ selectedPM ];

And finally this value is printed to the screen in an alert box (figure 2)

alert( outputPM );

Figure 2 - Billy Hughes name in full

Figure 2

Key-Value

This sort of array is called an associative array (or a hash table if you’re coming from the general direction of Perl). Presumably associative arrays get the name because rather than being numerically indexed, values are associated with a specific key. This leads us to think of associative arrays as conceptually different structures: whereas a normal array is a list of values ordered by index, an associative array is a bunch of key-value pairs. There is no inherent order in the keys of an associative array.

Traps & Tricks

There are always a few traps and tricks, so let’s look at the common ones with associative arrays.

Firstly, each key must be unique but keys are case-sensitive. So the words “hughes”, “Hughes”, “HUGHES”, and “HuGhEs” could all be unique keys - that’s potentially very confusing! In listing 1 you can see that I’ve made all keys lower case to be consistent.

But there’s a further complication - of course! Because keys are case sensitive a value will only be retrieved if a perfect match is found. For example, if a user that wants to retrieve Billy Hughes’s record they might quite reasonably enter “Hughes” in response to the question “Which PM?”.

Figure 3 - A reasonable request …

Figure 3

Unfortunately the program will not find the record because the input word “Hughes” is not a match for the key “hughes” or any other key in the array.

Figure 4 - … rejected!

Figure 4

This is similar to the problem of zero-indexed arrays we had in the previous article - either we can instruct the users to enter their requests only in lower case or we can do some processing of the input to force input into lower case. Inevitably, the most reliable method is to give the user a free hand and make the program do the work. This solution is remarkably easy - change the line

var outputPM = primeMinister[ selectedPM ];

to read

var outputPM = primeMinister[ selectedPM.toLowerCase() ];

Applying the toLowerCase() function converts the value in selectedPM to lower case. In the example above, the user’s input “Hughes” is converted to “hughes” by toLowerCase(), making a match possible and Billy Hughes’s record is found.

More information please

This program is OK as far as it goes, but it would be good if we could include some other information about each prime minister - their terms in the top job for instance. Listing 2 has a possible approach.

Listing 2.

primeMinister = new Object();

primeMinister["barton"] = ["Sir Edmund BARTON","1901-03"];
primeMinister["bruce"] = ["Stanley Melbourne BRUCE","1923-29"];
primeMinister["chifley"] = ["Joseph Benedict CHIFLEY","1945-49"];
primeMinister["cook"] = ["Joseph COOK","1913-14"];
primeMinister["curtin"] = ["John Joseph CURTIN","1941-45"];
primeMinister["deakin"] = ["Alfred DEAKIN","1903-04","1905-08","1909-10"];
primeMinister["fadden"] = ["Arthur William FADDEN","1941-41"];
primeMinister["fisher"] = ["Andrew FISHER","1908-09","1910-13","1914-15"];
primeMinister["forde"] = ["Francis Michael FORDE","1945-45"];
primeMinister["fraser"] = ["John Malcolm FRASER","1975-83"];
primeMinister["gorton"] = ["John Grey GORTON","1968-71"];
primeMinister["hawke"] = ["Robert James Lee HAWKE","1983-91"];
primeMinister["holt"] = ["Harold Edward HOLT","1966-67"];
primeMinister["howard"] = ["John Winston HOWARD","1996-"];
primeMinister["hughes"] = ["William Morris HUGHES","1915-23"];
primeMinister["keating"] = ["Paul John KEATING","1991-96"];
primeMinister["lyons"] = ["Joseph Aloysius LYONS","1932-39"];
primeMinister["mcewen"] = ["John McEWEN","1967-68"];
primeMinister["mcmahon"] = ["William McMAHON","1971-72"];
primeMinister["menzies"] = ["Robert Gordon MENZIES","1939-41","1949-66"];
primeMinister["page"] = ["Earle Christmas Grafton PAGE","1939-39"];
primeMinister["reid"] = ["George Houstoun REID","1904-05"];
primeMinister["scullin"] = ["James Henry SCULLIN","1929-32"];
primeMinister["watson"] = ["John Christian WATSON","1904-04"];
primeMinister["whitlam"] = ["Edward Gough WHITLAM","1972-75"];

var selectedPM = prompt( "Which PM?","" );

var outputPM = primeMinister[ selectedPM.toLowerCase() ];

alert( outputPM.join("\n") );

(Live example currently deactivated)

The key difference here is that the values in the associative array are not plain strings - they are actually array literals. So what we have here is an associative array with values that are arrays, or an array of arrays!

But why?

Why an array of arrays? Why didn’t I just pile everything up into a string? The first reason is given by the last line

alert( outputPM.join("\n") );

Remember each value in the primeMinister associative array is itself an array - so all of the handy hidden extras are available. By using the join() function on outputPM (that is, one of the arrays selected from primeMinister) I can alter the presentation of the information. In this case I have joined each element from the prime minister’s array with the \n new line character, giving the output a columnar feel:

Figure 5 - Billy Hughes and his time as PM

Figure 5

But beyond that, structuring the data in this way provides me with a very simple database that can be queried.

Enquiries

Suppose you were more interested in finding out what prime ministers have had more than one separate term in the top job (i.e. were voted out then voted in again). Oh sure, you could just run your eye down the list but it’s more fun to alter the program to get the answer for you. See listing 3.

Listing 3

primeMinister = new Object();

primeMinister["barton"] = ["Sir Edmund BARTON","1901-03"];
...  as in listing 2 ...
primeMinister["whitlam"] = ["Edward Gough WHITLAM","1972-75"];

var numberTerms = numberPrompt( "How many terms served?", 1 );

var recordsFound = "";

for ( testKey in primeMinister )
{
    if ( primeMinister[ testKey ].length > numberTerms )
    {
    recordsFound += primeMinister[ testKey ][0] 
    + "\nTerm " 
    + numberTerms 
    + ": " 
    + primeMinister[ testKey ][ numberTerms ] 
    + "\n";
    }
}

if ( recordsFound == "" )
{
    alert( "No prime ministers have served for " 
           + numberTerms 
           + " separate terms." );
}
else
{
    alert( recordsFound );
}

(Live example currently deactivated)

This time instead of asking for a prime ministers name, the user is asked to nominate how many terms in office (figure 6):

var numberTerms = numberPrompt( "How many terms served?", 1 );

Figure 6 - How many PMs have had three separate goes at the job?

Figure 6

Note that this uses my numberPrompt() function (not included in the listing - see the introductory article) but the program works fine with an ordinary prompt() provided the user enters digits only.

Things start getting interesting in the line

for ( testKey in primeMinister )

This is a looping command which in English means: “for each key in the primeMinister associative array, assign that value to the variable testKey and use it in the following block of code”.

Inside the loop, the line

   if ( primeMinister[ testKey ].length > numberTerms )

we use testKey to retrieve the value (an array) from primeMinister and test its length against the number of terms served. How does this work? Each PM will have at least two elements in their array: element 0 (their name) and element 1 (their first term in office). So if the user is foolish enough to request all prime ministers who have served one term (by definition that’s all of them), this test will always succeed because the length of primeMinister[ testKey ] is 2, which is greater than 1 - the number of terms served. To extrapolate, all prime ministers who have served two terms will have three elements: element 0 (name), element 1 (first term), and element 2 (second term), and an array of length 3 is greater than numberTerms of 2. Hopefully it’s clear that the length of each prime minister’s array will always be longer than the number of terms served if that PM has served at least that many terms.

The next line

recordsFound += primeMinister[ testKey ][0] 
+ "\nTerm " 
+ numberTerms 
+ ": " 
+ primeMinister[ testKey ][ numberTerms ] 
+ "\n";

builds formatted output for the record including the prime minister’s name, which is extracted from the array using primeMinister[ testKey ][0]. Note the double use of array index format: first primeMinister[ testKey ] is used to retrieve the array for the appropriate prime minister, then the [0] element (which happens to be the PM’s name) is retrieved from the array that was just retrieved.

This is repeated again at primeMinister[ testKey ][ numberTerms ] where details of the prime minister’s term in office is retrieved. Note that this part will be different depending on what number is held in numberTerms.

And the rest of the line is simply concatenation - remembering that \n is the special character for a new line. The resulting string is appended to recordsFound - notice the use of the += operator, which means “add this onto the end of what’s already in recordsFound”.

Once out of the loop, we check the contents of recordsFound

if ( recordsFound == "" )

and if this test returns true (that is, recordsFound is empty) this means no prime ministers matched the question so we print an informative message to that effect:

Figure 7 - No one has had the longevity to do it five times

Figure 7

Otherwise at least one prime minister has been found by the query, so just print our prepared output in recordsFound:

Figure 8 - Politicians had real staying power back then

Figure 8

Wrap up

In this episode we covered the following:

  • an associative array doesn’t use a numeric index, it uses a word or key.
  • values held in an associative array can be any legal value - including other arrays
  • use of the for (… in …) construct to retrieve all of the keys in an associative array
  • associative arrays shouldn’t be Array() type objects - they can be any object, even the plain-vanilla Object() type.

I’ve used and abused prompt() and alert() well beyond their intended usage and abusage, so next time we’ll look at some other output options.

Sources

Information about the Prime Ministers was obtained from the excellent site Australia’s Prime Ministers published by the National Archives of Australia.

tech.thingoid Gosbell First published: PC Update August 2004 (online version updated)

12 Responses to “Real programming with JavaScript - Associative Arrays”

  1. tech.thingoid Says:

    Q. When is an array not an Array()?

    A. When it’s an associative array.

    In JavaScript, an associative array does not actually need to be an Array() type object. In fact, it could (and should) be any type of object - even the most basic Object() type - rather than an Array().

    Although it’s perfectly legal to do so it would be a bit misleading of me to use Array() here. Even if the primeMinister object is of the Array() type, all of the specialised Array() functions like length, join(), and sort() do not work for associative array data. Data for arrays and associative arrays are stored completely separately in an Array() object, which makes it particularly unsuitable for the purpose.

    I was aware of the above when, in the published version of this article, I used the Array() type rather than Object() type in the examples in the mistaken belief that it would simplify the conceptual connection between Array() objects and associative arrays. Of course there is no actual connection between the two and in making such a link this was more confusing that it need have been.

  2. jehiah Says:

    I think an easier way to write/store that data is like this

    primeMinister = {”barton” : [”Sir Edmund BARTON”,”1901-03″],
    “bruce”:[”Stanley Melbourne BRUCE”,”1923-29″],
    “chifley”:”Joseph Benedict CHIFLEY”,”1945-49″],
    “cook”:”Joseph COOK”,”1913-14″}

    Then you use it like

    alert(primeMinister[”barton”]);

  3. tech.thingoid Says:

    Hi Jehiah,

    I agree that the “Perl-style” usage that you suggest here is both easier typing and reading, indeed that is my preferred style. However I don’t think it’s particularly clear what’s going on for people who are just learning about associative arrays. As this series of articles is targeted at beginning programmers I deliberately chose to be more verbose than I normally would be in my own programming.

    A further advantage in using the
    primeMinister["barton"] = "Sir Edmund BARTON";
    format is that without any further information the reader knows how to add more entries to a hash, since that’s exactly what this does (an empty hash is first created then each successive line adds one element).

    On the other hand, in the format you recommend:
    primeMinister = {"barton" : ["Sir Edmund BARTON". "1901-03", ... "whitlam" : ["Edward Gough WHITLAM","1972-75"]}
    the hash is created and populated effectively simultaneously. This isn’t “complete” because it leaves the reader uninformed about how to extend the hash later in the program.

    But I consider that a side-effect, my main intention is clarity for the learner programmer. That said, I appreciate your comment because it helps to remind people that there are often several ways to achieve the same result in programming.

    Cheers,

    tech.thingoid.

  4. Beren Says:

    One thing that I am unclear on - if none of the specialised Array() functions work for associative arrays, how do you remove an item? Simply saying primeMinister[”barton”] = null will not remove that key from the list, so when you iterate through the keys, there is an empty element!

    Cheers,

    Beren

  5. tech.thingoid Says:

    Removing elements from a hash or array

    Hi Beren,

    Thanks for your question.

    The command you’re looking for is delete. See the Core JavaScript Guide for a complete rundown, but basically what you need to do is something like the following. If you wamted to remove Prime Minister Keating (as some did at the time!):

    delete primeMinister["keating"];

    And that’s it.

    This also works for array elements of Array() objects too.

    Cheers,

    tech.thingoid.

  6. Bruno Says:

    Hi!

    What about getting an associative array’s size, without using a loop to count all possible values?

  7. tech.thingoid Says:

    Getting the size of a JavaScript associative array

    Hi Bruno,

    The Array() object provides the length property, which you use like so:

    someArray.length

    But to the best of my knowledge that option isn’t available when using associative arrays. The elements in associative arrays don’t have a logical sequence, so I guess counting them doesn’t necessarily make sense.

    I’m afraid you’re stuck with running a counter inside a loop, something like this:


    var counter = 0;
    for (var i in primeMinister)
    {
    counter++;
    }

    Sorry ’bout that.

    tech.thingoid.

  8. Eugene Says:

    well … how can i create multidimensional hash ?
    eg:

    varName[1]["z"][33] = 1;
    varName[1]["y"][34] = 2;
    varName[2]["aa"]['a'] = 3;
    varName[2]["aa"]['b'] = 4;
    varName[2]["aa"]['c'] = 5;

    without varName[1] = new Object();

  9. tech.thingoid Says:

    Faking a multidimensional hash

    Hi Eugene,

    As you suggest, a multidimensional hash can be made by making a series of objects inside the first hash.

    varName[1] = new Object();
    varName[2] = new Object();
    ... and so on

    That’s a perfectly acceptable way to do it.

    However, if you find that’s a bit of overkill you can also fake it just by concatenating the key values:

    var varName = new Object()

    var i = 1 + "z" + 33;

    varName[i] = 1;
    varName[1 + "y" + 34] = 2;
    varName[2 + "aa" + 'a'] = 3;
    varName[2 + "aa" + 'b'] = 4;
    varName[2 + "aa" + 'c'] = 5;

    i = "2aaa";
    alert(varName["1z33"]);
    alert(varName[i]);
    alert(varName[2 + "aa" + 'c']);

    If you prefer your multiple dimensions separated, say by a comma, just include it in the concatenation:

    varName[1 + "," + "z" + "," + 33] = 1;

    alert(varName["1,z,33"]);

    I’d recommend this both for readability and especially if you’re using numeric keys (remember that 1 + 2 + 3 is not the concatenated value “123″, it’s the numeric value 6).

    Sure it’s a hack, but it’s legit: it doesn’t matter how you make your hash indexes as long as each one is unique.

    Cheers,

    tech.thingoid.

  10. Ed Says:

    Hey tech.thingoid, thanks for the helpful articles. You said, “The elements in associative arrays don’t have a logical sequence, so I guess counting them doesn’t necessarily make sense.”

    What if you WANT your elements to have a logical sequence (say, alphabetical). For example,

    var map = new Object();

    var foo = [x, y, z];
    var bar = [v, w, z];
    var baz = [a, b, c];

    I want to be able to add foo to map, and then bar, and have bar be “before” foo (because it comes first alphabetically). If I later add baz, I want it to be “between” bar and foo for the same reason.

    I get why associative arrays don’t have any logical sequence, and so adding ‘in sequence’ doesn’t really make sense. What I’m describing (and wanting!) is more like the Java TreeMap-like objects, which maintains the keys in order and guarantees quick access (I need both).

    What would solve this for me is a way to sort() the keys of an associative array/object (maintaining the associated element relationships of course).

    I’ve searched for both these solutions but haven’t found one yet. I thought I’d give you a try and see if you had any ideas.

    Thanks in advance!

    e.

  11. tech.thingoid Says:

    Hi Ed,

    An intriguing problem, although I’m not sure I understand what you’re trying to do so I hope my answer makes some sense.

    I don’t think JavaScript has this functionality built-in. It may be possible to build a user-defined function that would do this but the simplest solution I can think of is to keep an ordinary array of the hash table keys. Something like this:

    var keyIndex = new Array();
    var hashTable = new Object();

    hashTable[”z”] = “the last item”;
    keyIndex.push(”z”);

    hashTable[”x”] = “the first item”;
    keyIndex.push(”x”);

    hashTable[”y”] = “the middle item”;
    keyIndex.push(”y”);

    You can then use the Array() object’s built-in sort() function to order the keys and loop through the keyIndex array to give you your keys in order:

    keyIndex.sort();

    var i;
    for (i in keyIndex)
    {
    document.write(hashTable[ keyIndex[i] ] + “<br />”);
    }

    This should produce the output:

    the first item
    the middle item
    the last item
    

    There’s a bit of overhead in managing both the keyIndex and hashTable objects so I don’t think it will be particularly fast. And I don’t think it’s a very elegant solution but at least it’s an option for you. I’m sorry if it’s not really what you’re looking for.

    Please note: I haven’t tested this example code, so no guarantees that it’ll work!

    Cheers,

    tech.thingoid.

  12. Simon Norris Says:

    This may just be a lazy hack but it works for me. I’ve been unable to sort an associative array or find a script which works so…

    Lazy, lazy, lazy: concatenate it in an ordinary array, sort that and split the values into a new associative array. Probably not recommended for the phone book but like a say - works for me.

    Also removes duplicates

    function sortAssoc(inputArray) {
    var tempArray = new Array();
    var outputArray = new Array();

    i=0;
    
    for (keyVar in inputArray) {
        tempArray[i] = inputArray[keyVar] + '//--string_divider--//' + keyVar;
        i++;
    }
    
    tempArray = tempArray.sort();
    
    for (i=0; i < tempArray.length; i++) {
        if (tempArray[i]!=tempArray[i-1]) {
            temp = tempArray[i].split('//--string_divider--//');
            tempIndex = temp[1];
            outputArray[tempIndex] = temp[0];
        }
    }
    
    return outputArray;
    

    }