The CreatorCon Call for Content is officially open! Get started here.

treycarroll
Giga Guru

I come from a Java and C# background so, as much as I love the power of Javascript, there are some significant gaps that leave me longing for those other languages.   One of the "biggies" is the lack of data structures.     Javascript arrays are these odd "everything" structures.   After programming in Javascript for a while, we become accustomed to using arrays as: arrays (of course), lists, queues, stacks and sets.     But implementations of those abstract data-types have some very specific characteristics and therefore should provide certain required operations, while very intentionally eschewing other operations.

But the real question is, "Are we missing out as Javascript programmers because our language doesn't natively support these ADTs?"       I would give a pragmatic answer: "It depends."   I don't really mind using the array functions to simulate Stacks and Queues, but some of the other ADTs are problematic.   Let's walk through some examples to see how this plays out.

To create a Javascript Stack we can very easily do the following:

var stack = [];

stack.push('vanilla');

stack.push('chocolate');

stack.push('strawberry');

var sb = stack.pop();

alert(sb);   //this will output 'strawberry'

Stacks are LIFO (Last-in First-out) data structures, so pop() gives us back strawberry, which was added last.     This works great!   Even the names of the methods, "push" and "pop" are conceptually correct.     We'll just overlook the fact that you could do this.

alert( stack[0] );//This should not be possible on a true stack

To create a Javascript Queue we can do this:

var queue = [];

queue.push('vanilla');

queue.push('chocolate');

queue.push('strawberry');

var v = queue.shift();

alert(v);   //this will output 'vanilla'

Queues are FIFO (First-in First-out) data structures, so shift() gives us back vanilla, which was added first.     So it works, but this is not quite as nice as when we used an array as a Stack.     The correct name for the operation for adding an element to a Queue should be enqueue(), not push(). (This is not a Stack!)     Similarly, Javascript arrays' way of getting something out of a queue is called shift(), but it should be called dequeue().

And again, we can violate the structure's conceptual accessors and grab something off of the back end using code like this:

var evilAccessElement = queue[queue.length - 1];

Once we get to the Set data type, however, things really break down.   The characteristics of a Set mean that it:

  • Must not allow duplicate elements,
  • Must only support insert() and delete() with no provision for adding or removing at an index,
  • Must offer union(), difference() and intersection() operations.

Our humble Javascript array was doing fairly well up to this point, but now it is hopelessly inadequate to mimic the desired data type.   But do we really need Sets for ServiceNow coding?   I would argue yes.

How many times have you had to remove duplicates from an array?   Have you ever loaded a user's roles from sys_user_has_roles?     Sure we can write a GlideAggregate query that groups by the role's sys_id to eliminate the duplicates, but what if the data structure could just quietly disregard the addition of redundant elements while we iterated over the elements after running the simple query?     And what about the power of intersections, unions and differences?     If we take the example of working with groups of approvers (this is a frequent use-case in our development of catalog items), the power becomes clear.     Storing the approvers' ids in arrays forces you to repeatedly, painstakingly re-implement comparison code which could have been written (and tested!) once as a part of the data structure itself.

As Michael Ritchie has pointed out, ServiceNow provides the ArrayUtil to support the use of Javascript's array type in situations where Set-like behaviors are required.   I have used the ArrayUtil and I am sincerely grateful to our friends at ServiceNow for providing us with tools, but I am not a huge fan of the ArrayUtil/array-as-a-Set paradigm for a few reasons:

1)   I want the data structure to throw an exception if I accidentally add an element of the wrong type or attempt to use it incorrectly.   But even if I do something catastrophically wrong, such as providing a string in place of the required array-type first parameter of indexOf(), the ArrayUtil does not throw an exception.   Instead, it returns the -1 (not found) flag.   That behavior leads me to believe that my call was well formed and that the desired element just isn't there.   But, how are we going log and handle errors, if we can't detect them?     The human who raises an INC becomes our only "catch" mechanism!

var x = [1, 2, 3];

var util = new ArrayUtil();

try{    

    gs.print(util.indexOf("This should be an array not a string", "1"));//This outputs -1!

} catch(e){

    gs.print('Error:'+e);

}

Similarly, the concat method is oddly tolerant when I do this:

//Outputs *** Script: This should be an array not a string

//Does not throw an error

var x = [1, 2, 3];

var util = new ArrayUtil();

try{

    gs.print(util.concat("This should be an array not a string", x));

} catch(e){

    gs.print('Error:'+e);

}

2) It allows you to perform set operations on arrays of wildly different type elements:

var arrayUtil = new ArrayUtil();

var a1 = ["1","2","3","4","5"];

var a2 = [{foo:'bar'}];

var a3 = [undefined,null,3,'string'];

gs.print(arrayUtil.diff(a1, a2, a3)); //*** Script: 1,2,3,4,5

While this is not an error per se, it silently (complicitly?) tolerates meaningless comparisons.

3) The implementation of the indexOf() method returns inconsistent results when the array contains string and number versions of the same value.

var x = [1, "1"];

var util = new ArrayUtil();

gs.print(util.indexOf(x, "1"));//This returns 0, even though the string   "1" is at index 1!

4) The wiki article documenting its use contains errors and omissions.

  • indexOf

indexOf (Array array, [int startIndex]);

arrayUtil.indexOf(a1, "b"));//The parameter "b" doesn't look much like a start index!

 

  • ensureArray and convertArray

There is no documentation for the ensureArray or convertArray methods.   Given that objects can have complex, nested properties, how would theses conversions work?

5) It blurs the lines between the list and set data types.

  • It supports retrieval of indices suggesting a List data type.
  • It supports intersection|difference|union operations suggesting a Set data type.
  • You can change conceptual data structures suddenly.   The structure (an array) tolerates duplicates, but then removes them when you call unique.   What was the data-type before the call to unique?   What is it after the call?

6) It violates Solid OO Design principles.

  • Putting the state into one object and behaviors into another just doesn't make sense.

7) Lastly, and perhaps most importantly: The ArrayUtil is not available on the client.

  • No client scripts can make immediate use of unique(), intersection(), diff(), union(), indexOf(), etc.   If you include this code in a client script, it will error out:

var arrayUtil = new ArrayUtil();

  • It is possible to use the ArrayUtil methods by creating a client-callable Script Include to wrap the operations and make them available on the client, however, the inherent network latency involved in making an ajax call means that there will always be a noticeable lag while the result is retrieved.

For these reasons I found that it made sense to create a separate, new u_GenericSet data type.   The structure is "generic" in the sense of Java generics, in which collections are permanently typed at construction, and which throw exceptions when passed parameters of an incorrect type.

// Wrap code that uses this in a try/catch block and write code to handle exceptions.

  1. var u_GenericSet = Class.create();  
  2. u_GenericSet.prototype = {  
  3.  
  4.       // Constructor creates a deep copy of the array parameter to avoid pass by reference mayhem.      
  5.       // Array parameter is optional  
  6.       initialize: function (elementType, array) {  
  7.  
  8.               if (elementType == undefined || elementType == null || elementType == '') {  
  9.                       throw "Mandatory constructor parameter 'elementType' was omitted when constructing object of type u_GenericSet";  
  10.               }  
  11.  
  12.               if (!/^(number|string|object|boolean)$/i.test(elementType)) {  
  13.                       throw "Constructor parameter 'elementType' must be number|string|object|boolean when constructing object of type u_GenericSet";  
  14.               }  
  15.  
  16.               this.elementType = elementType.toLowerCase();  
  17.               this.ary = [];  
  18.  
  19.               if ( array && array.length ) {  
  20.                       for (var i = 0; i < array.length; i++) {  
  21.                               if ((typeof array[i]) != this.elementType) {  
  22.                                       throw "Could not construct the u_GenericSet object because array parameter elements were not all of type " + this.elementType;  
  23.                               }  
  24.                               this.add(array[i]);  
  25.                       }  
  26.               }  
  27.       },  
  28.  
  29.       // Ensures no duplicates.   Param must be of type set at construction  
  30.       add: function (element) {  
  31.  
  32.               var passedType = (typeof element).toLowerCase();  
  33.  
  34.               if (passedType != this.elementType) {  
  35.                       throw "u_GenericSet was constructed to contain only elements of type:" + this.elementType + " Attempted to add element of type:" + typeof element;  
  36.               }  
  37.  
  38.               //First element  
  39.               if (this.ary.length == 0) {  
  40.                       this.ary[0] = element;  
  41.                       return;  
  42.               }  
  43.  
  44.               //If the element is bigger than last element, stick it at the end                
  45.               if (element > this.ary[this.ary.length - 1]) {  
  46.                       this.ary[this.ary.length] = element;  
  47.                       return;  
  48.               }  
  49.  
  50.               //If the element is smaller than first element, stick it at the beginning  
  51.               if (element < this.ary[0]) {  
  52.                       this.ary.splice(0, 0, element);  
  53.                       return;  
  54.               }  
  55.  
  56.               var finder = this._binaryIndexOf(element);  
  57.               if (finder.foundIt) {  
  58.                       return;  
  59.               } else {  
  60.                       this.ary.splice(finder.ndx, 0, element);  
  61.               }  
  62.       },  
  63.  
  64.       //Returns object with 2 properties: 1) foundIt (boolean), 2) ndx (integer)  
  65.       _binaryIndexOf: function (searchElement) {  
  66.  
  67.               var minIndex = 0;  
  68.               var maxIndex = (this.ary.length == 0) ? 0 : this.ary.length - 1;  
  69.               var currentIndex;  
  70.               var currentElement;  
  71.  
  72.               while (minIndex <= maxIndex) {  
  73.                       currentIndex = (minIndex + maxIndex) / 2 | 0;  
  74.                       currentElement = this.ary[currentIndex];  
  75.  
  76.                       if (currentElement < searchElement) {  
  77.                               minIndex = currentIndex + 1;  
  78.                       }  
  79.                       else if (currentElement > searchElement) {  
  80.                               maxIndex = currentIndex - 1;  
  81.                       }  
  82.                       else if (currentElement == searchElement) {  
  83.                               return { foundIt: true, ndx: currentIndex };  
  84.                       }  
  85.               }  
  86.               return { foundIt: false, ndx: minIndex };  
  87.       },  
  88.  
  89.       //Returns a boolean  
  90.       _binaryExists: function (searchElement) {  
  91.  
  92.               var minIndex = 0;  
  93.               var maxIndex = (this.ary.length == 0) ? 0 : this.ary.length - 1;  
  94.               var currentIndex;  
  95.               var currentElement;  
  96.  
  97.               while (minIndex <= maxIndex) {  
  98.                       currentIndex = (minIndex + maxIndex) / 2 | 0;  
  99.                       currentElement = this.ary[currentIndex];  
  100.  
  101.                       if (currentElement < searchElement) {  
  102.                               minIndex = currentIndex + 1;  
  103.                       } else if (currentElement > searchElement) {  
  104.                               maxIndex = currentIndex - 1;  
  105.                       } else {  
  106.                               return true;  
  107.                       }  
  108.               }  
  109.               return false;  
  110.       },  
  111.          
  112.       //Returns a boolean  
  113.       contains: function (element) {  
  114.  
  115.               var passedType = (typeof element).toLowerCase();  
  116.  
  117.               if (passedType != this.elementType) {  
  118.                       throw "u_GenericSet was constructed to contain only elements of type:" + this.elementType + " Attempted to call contains for element of type:" + typeof element;  
  119.               }  
  120.  
  121.               return this._binaryExists(element);  
  122.       },  
  123.  
  124.       //Removes a value, not a value at an index  
  125.       remove: function (element) {  
  126.  
  127.  
  128.               var passedType = (typeof element).toLowerCase();  
  129.  
  130.               if (passedType != this.elementType) {  
  131.                       throw "u_GenericSet was constructed to contain only elements of type:" + this.elementType + " Attempted to call remove for element of type:" + typeof element;  
  132.               }  
  133.  
  134.               var finder = this._binaryIndexOf(element);  
  135.               if (finder.foundIt) {  
  136.                       this.ary.splice(finder.ndx, 1);  
  137.                       return true;  
  138.               }  
  139.               return false;  
  140.       },  
  141.  
  142.       // Returns an int  
  143.       size: function () { return this.ary.length; },  
  144.  
  145.       //Returns a new u_GenericSet containing the elements from both sets  
  146.       union: function (otherSet) {  
  147.  
  148.               if (otherSet.type != "u_GenericSet") {  
  149.                       throw "Called union() on u_GenericSet with a parameter that was not a u_GenericSet type object.";  
  150.               }  
  151.  
  152.  
  153.               var otherAsArray = otherSet.toArray();  
  154.               var oReturn = new u_GenericSet(this.elementType, this.ary);  
  155.               for (var i = 0; i < otherAsArray.length; i++) {  
  156.                       oReturn.add(otherAsArray[i]);  
  157.               }  
  158.               return oReturn;  
  159.       },  
  160.  
  161.       //Returns a new u_GenericSet containg the elements in this set that are not in the other  
  162.       difference: function (otherSet) {  
  163.  
  164.  
  165.               if (otherSet.type != "u_GenericSet") {  
  166.                       throw "Called difference() on u_GenericSet with a parameter that was not a u_GenericSet type object.";  
  167.               }  
  168.  
  169.               var inThisSetButNotInOtherSet = [];  
  170.               for (var i = 0; i < this.ary.length; i++) {  
  171.                       if (!otherSet.contains(this.ary[i])) {  
  172.                               inThisSetButNotInOtherSet.push(this.ary[i]);  
  173.                       }  
  174.               }  
  175.  
  176.               return new u_GenericSet(this.elementType, inThisSetButNotInOtherSet);  
  177.       },  
  178.  
  179.       //Returns a new u_GenericSet containing elements that are members of both sets  
  180.       intersection: function (otherSet) {  
  181.  
  182.               if (otherSet.type != "u_GenericSet") {  
  183.                       throw "Called intersection() on u_GenericSet with a parameter that was not a u_GenericSet type object.";  
  184.               }  
  185.  
  186.               var inBothSets = [];  
  187.               var baseSet;  
  188.               var foreignSet;  
  189.  
  190.               //Use the smallest set as the base set to minimize number of comparisons/iterations required  
  191.               if (this.ary.size() < otherSet.size()) {  
  192.                       baseSet = this;  
  193.                       foreignSet = otherSet;  
  194.               } else {  
  195.                       baseSet = otherSet;  
  196.                       foreignSet = this;  
  197.               }  
  198.  
  199.               var baseIter = baseSet.getIterator();  
  200.               while (baseIter.hasNext()) {  
  201.                       var val = baseIter.getValue();  
  202.                       if (foreignSet.contains(val)) {  
  203.                               inBothSets.push(val);  
  204.                       }  
  205.                       baseIter.next();  
  206.               }  
  207.  
  208.               return new u_GenericSet(this.elementType, inBothSets);  
  209.       },  
  210.  
  211.       // Returns a boolean  
  212.       isEmpty: function () {  
  213.               return this.ary.length == 0;  
  214.       },  
  215.  
  216.       // Empties all elements  
  217.       purgeAll: function () {  
  218.               this.ary = [];  
  219.       },  
  220.  
  221.       // Returns a comma separated list in a String  
  222.       toString: function () {  
  223.               return this.ary.toString();  
  224.       },  
  225.  
  226.       //Creates a deep copy to avoid inadvertent modification of the original  
  227.       toArray: function () {  
  228.               var ret = [];  
  229.               for (var i = 0; i < this.ary.length; i++) {  
  230.                       ret.push(this.ary[i]);  
  231.               }  
  232.               return ret;  
  233.       },  
  234.   //Returns an object with functions: next(), hasNext(), and getValue();    
  235.     getIterator: function () {  
  236.              
  237.               var current = undefined;  
  238.               var store = this.ary;  
  239.               function hasNext() {  
  240.                       if (store.length == 0) {  
  241.                               return false;  
  242.                       }  
  243.                       if (current == undefined) {  
  244.                               current = 0;  
  245.                       }  
  246.                       if (current < store.length) {  
  247.                               return true;  
  248.                       } else {  
  249.                               return false;  
  250.                       }  
  251.               }  
  252.               function next() {  
  253.                       var val = store[current];  
  254.                       current++;  
  255.                       return val;  
  256.               }  
  257.               function getValue() {  
  258.                       return store[current];  
  259.               }  
  260.               return { next: next, getValue: getValue, hasNext: hasNext };  
  261.       },  
  262.       type: 'u_GenericSet'  
  263. }  

Intersection:

var a = new u_GenericSet('number',[2002,2004,2006]);

for(var i = 0; i < 1000; i++){

    a.add(i);

}

var b = new u_GenericSet('number', [3,6,9]);

for(var j = 2000; j < 5000; j++){

    b.add(j);

}

var res = a.intersection(b);

gs.print(res.toString());

There are thousand of elements in these sets, but they only share these few elements:

find_real_file.png

Union:

//If we ever need to find the set of elements that are in set a OR in set b we can take the set union of the two sets

var a = new u_GenericSet('number');

for(var i = 0; i < 10; i++){

    a.add(i);

}

var b = new u_GenericSet('number');

for(var j = 5; j < 15; j++){

    b.add(j);

}

var res = a.union(b);

gs.print(res.toString());

Notice that even though 5-10 were in both, they only appear in the resulting Set once.

find_real_file.png

Difference:

//If we ever need to find elements that are in set a, but not in set b we can take the set difference

var a = new u_GenericSet('number');

for(var i = 0; i < 10; i++){

    a.add(i);

}

var b = new u_GenericSet('number');

for(var j = 7; j < 20; j++){

    b.add(j);

}

var res = a.difference(b);

gs.print(res.toString());

Only 0-6 were in Set a and not in Set b.   7-10 were in Set b, so they were removed from the result set.   11-19 were never even checked; since they were not in Set a, we automatically discarded them.

find_real_file.png

So there you have it: a Generic Set data structure that you can use for your Service-Now development.   Enjoy!     Make sure that you wrap your code in try/catch blocks and handle the exceptions that it throws when you pass the wrong type elements into it.   The really nice thing about this class is that it will work both from client code and from server code.   I have it loaded as a global UI Script so that I can use it from any client script.   I am also using it as a server-side script include.   I really like the result, since I can now use a u_GenericSet from any script without having to change gears between client and server scripts.

If you like this article, make sure that you check out my post on a Map/Dictionary for Client and Server side use in ServiceNow code: A Map Data Type for ServiceNow

Happy coding. We work on an amazing platform. Enjoy it!

var u_GenericMap = Class.create();   u_GenericMap.prototype = {                       //Constructor.               //@param map is optional.   If provided, it must be of type u_GenericMap and its elements will be put() into the new Map.           initialize: function (keyType, valType, map) {                           if (keyType == undefined || keyType == null || keyType == '') {                           throw "Mandatory constructor parameter 'keyType' was omitted when constructing object of type u_GenericMap";                   }                           if (!/^(number|string|object|boolean)$/i.test(keyType)) {                           throw "Constructor parameter 'keyType' must be number|string|object|boolean when constructing object of type u_GenericMap";                   }                           if (valType == undefined || valType == null || valType == '') {                           throw "Mandatory constructor parameter 'valType' was omitted when constructing object of type u_GenericMap";                   }                           if (!/^(number|string|object|boolean)$/i.test(valType)) {                           throw "Constructor parameter 'valType' must be number|string|object|boolean when constructing object of type u_GenericMap";                   }                           this.keyType = keyType.toLowerCase();                   this.valType = valType.toLowerCase();                   this.obj = {};                   this.length = 0;                           if (map && map.type == 'u_GenericMap') {                           var iterator = map.getIterator();                           while (iterator.next()) {                                   this.obj[iterator.getCurrent().key] = iterator.getCurrent().value;                           }                   }           },                       //Adds an element to the map using he key/value params           put: function (key, val) {                           var passedKeyType = (typeof key).toLowerCase();                   var passedValType = (typeof val).toLowerCase();                           if (passedKeyType != this.keyType) {                           throw "u_GenericMap was constructed to contain only keys of type:" + this.keyType + " Attempted to put element using key of type:" + passedKeyType;                   }                           if (passedValType != this.valType) {                           throw "u_GenericMap was constructed to contain only values of type:" + this.valType + " Attempted to put value of type:" + passedValType;                   }                                         this.length++;                   this.obj[key] = val;           },                   //Returns true if the map contains this key           containsKey: function (key) {                           var passedKeyType = (typeof key).toLowerCase();                           if (passedKeyType != this.keyType) {                           throw "u_GenericMap was constructed to contain only keys of type:" + this.keyType + " Attempted to call containsKey with parameter of type:" + passedKeyType;                   }                           return this.obj.hasOwnProperty(key) ;           },                   //Returns undefined if the key does not exist, otherwise returns the associated value           getValue: function (key) {                           var passedKeyType = (typeof key).toLowerCase();                           if (passedKeyType != this.keyType) {                           throw "u_GenericMap was constructed to contain only keys of type:" + this.keyType + " Attempted to call getValue with parameter of type:" + passedKeyType;                   }                           return this.obj[key];           },                   //Remove an element at a key           remove: function (key) {                           var passedKeyType = (typeof key).toLowerCase();                           if (passedKeyType != this.keyType) {                           throw "u_GenericMap was constructed to contain only keys of type:" + this.keyType + " Attempted to call remove with parameter of type:" + passedKeyType;                   }                           if (!this.obj.hasOwnProperty(key)) {                           return false;                   }                           this.length--;                   delete this.obj[key];                           return true;           },                   //Returns the size of the structure           size: function () { return this.length; },                   //Determines if map is empty           isEmpty: function () {                   return this.obj.length == 0;           },                   //Purge all elements           purgeAll: function () {                   this.length = 0;                   this.obj = {};           },                   //Returns a JSON encoded string representation of the map           toString: function () {                   try {//try client first.   This will throw if executing on the server                           if (window) {                                   return JSON.stringify(this.obj);                           }                   } catch (e) {//Server side                           try {                                   return new JSON().encode(this.obj);                           } catch (e) {                                   throw "u_GenericMap->toString() Error:" + e.message;                           }                   }           },                   //Returns an array of the keys           getKeys: function () {                   var keys = [];                   for (var k in this.obj) {                           keys.push(k);                   }                   return keys;           },                   //Returns an array of the values           getValues: function () {                   var vals = [];                   for (var k in this.obj) {                           vals.push(this.obj[k]);                   }                   return vals;           },                   //Returns an iterator with functions for next(), hasNext() and getCurrent().   getCurrent() returns an obj with keys: key, value           getIterator: function () {                           var store = [];                   var currentVal;                   var currentIndex;                                       for (var k in this.obj) {                           store.push({ 'key': k, 'value': this.obj[k] });                   }                   currentIndex = 0;                                           //sets var "currentVal", returns true if there are more elements, false otherwise                   function hasNext() {                           if (currentIndex < store.length) {                                   currentVal = store[currentIndex];                                   return true;                           } else {                                   return false;                           }                   }                           //Returns the currentValue, set the var "currentVal", increments the internal index                   function next() {                           currentVal = store[currentIndex];                           return store[currentIndex++];                   }                           //Returns an object with keys: key, value                   function getCurrent() {                           return currentVal;                   }                   return { next: next, getCurrent: getCurrent, hasNext: hasNext };           },           type: 'u_GenericMap'   }

3 Comments