Advanced Array functions

There are a number of useful array functions that have been around since EmacsScript5.1. This means that they are not available in some older browsers, but have been implemented in Google Apps Script. Here's a few examples showing how to use them. I first posted this on G+, and here's the explanations along with a few more examples. The Array.forEach() covered in Looping in Google Apps Script is also a function like this.

In each of these example, the array object is iterated, and you process each item in a function you pass as one of the arguments. On the one hand, these elegant functions are more self descriptive than the equivalent using if and loop, but you should be aware that performance is potentially slower than the more traditional looping. You should be familiar with anonymous functions from Looping in Google Apps Script

For an example of a Google Docs Application that uses these, see Sorting bookmarks in a document

Array.prototype.reduce()


reduce() eventually returns a single item which is some operation performed on each item of an array. 

Here's a very simple function to sum the values in an Array.
// sum values in an array
function sumArray(array) {
  return array.reduce (function(previous,current) {
      return previous+current;
  });

We'd call it like this
sumArray([1,2,3]);

explanation

The anonymous function given is called for each element in the array, with four  arguments. We're just dealing with the first two.

previous - the value returned from the previous function call
current - the latest array value

It's easy to see in this simple example, that previous will continue to be incremented until eventually the function returns the sum of every element in the array

Array.prototype.map()


.map() returns a copy of an array with each element transformed by your anonymous function. In this example we'll combine .map() .. producing an array of the squares of the elements of the original array .. with reduce() .. adding them up to  single value, as in the previous example. 

sum of the squares of the values in an array

function sumSquaresArray(array) {
  return array.map ( function(x) {
    return x*x;
  }).reduce (function(x,y) {
       return x+y;
  });
}

the .map() anonymous function receives each value of the original array, and returns a transformed value (in this case the value squared). These transformed values make up the contents of the array returned by .map(), and in this example are passed to .reduce() for summation.

Before we look at the example we need a quick recap on .apply(). With apply, you can make an array act like parameter arguments to a function. For example this function is expecting 3 arguments.

function xyz (a,b,c) {
}

let's say that we have our arguments in array a[1,2,3] - we could do this

xyz (a[0],a[1],a[2]);

but better, we can do this
xyz.apply (null, a);

For the moment, we'll ignore the first argument - null. I'll deal with that in a later example.

SImply using xyz(a) would not work, because the first argument would be an array, and the 2nd and 3rd would be undefined. 

Knowing how to use .apply(), means we can create Arrays of whatever length we want (watch out for performance of big arrays), to simulate a loop. 

new Array(n) will create an array of n elements, so Array.apply(null,Array(n)).reduce() will loop n times. In this case - calculating n factorial - we are not interested in the original array contents. We are mainly going to use the third argument that .reduce() passes to your function, namely the index of the array we are currently on. We also need to use the initialValue argument to .reduce(), since we need the starting reduction to be 1. 

function fact (n) {
  return Array.apply (null,Array(n))
    .reduce (function (current,previous,index) { 
      return current + current*index ;
     }, 1 );
}

calculating a factorial, lets say 4! - we need 4*3*2*1, so we simply multiply the current index by all that's gone before. 



Array.prototype.filter()

.filter() returns an array, similar to .map(), except in this case, it may not be the same length. Your function needs to return true or false to signal whether or not each individual element should be included in the final array. Here' we'll use .map() and .filter() together in order to first create an array of transformed values, then select only some of them. Specifically, an array of random numbers, which get filtered down to those that are even

function randomEvens(min, max, sampleSize) {
  return Array.apply(null,Array(sampleSize))
    .map (function (){
      return Math.floor(Math.random() * (max - min + 1)) + min ;})
    .filter(function (x) {
      return !(x % 2);
   });
}

Again we are using Array.apply() to provoke a .map() iteration. Notice that we don't even bother with the argument to the anonymous function called by map, since we are interested only in the length of the array, not its contents or even its index.

More examples


Fibonacci to n elements
function fib(n) {
  return Array.apply(0, Array(n)).
    reduce(function(current,previous,index) {
      return current.concat( (index < 2) ? 1 : current[index-1] + current[index-2]);
    }, []);
}

This one is very interesting, since we are returning and array from .reduce(), even though its normal usage is to return a single value based on some operations on array elements. We can do this because, the initial value to the reduce() function can be an empty array. Thereafter, we just need to add the calculated next value to the every increasing array.

Using .filter() with spreadsheet data
function singers() {
  var values= SpreadsheetApp
    .getActiveSpreadsheet()
    .getSheetByName("Sheet1")
    .getDataRange()
    .getValues()

  var singerCol = values[0].indexOf("singer");
  var songCol = values[0].indexOf("song");

  values.filter ( function (row) {
    return (row[singerCol] === "Madonna");
  })
  .forEach(function(row) {
    Logger.log (row[singerCol] + ":" + row[songCol]);
  });
}

This one was prompted by a G+ question. In this example, we have a sheet with various columns including 'singer' and 'song'. The objective is log the songs by a given filter. Once the column positions have been found, it's a simple .filter() checking that the singer matches the target, returning the whole row for each match. We can then iterate the resultant array with forEach() to print the two interesting columns in each row.

Using .filter() to create unique values
function uniqueValues() {
  var array = [1,2,2,3,4,1];
  var u = array.filter( function (v,i,a) {
    return a.indexOf(v) === i;
  });
  Logger.log (u);
}

In this case, we are using the characteristic of indexOf, where it will return the index of the first occurrence of a value in an array, so this will only be true for one instance of any given value.


Using .filter() and .map() to create unique values from two dimensional array
function uniqueValues2D() {
  var array = [[1,"a",2,3,4,1],[1,"b",2,3,4,1],[1,"b",2,3,4,1]];
  var col = 1;
  
  var u = array.map( function (v) {
    return v[col];
  })
  .filter( function (v,i,a) {
    return a.indexOf(v) === i;
  });
  Logger.log (u);
}

Here we are only interested in one column of an array - this would be like data returned from a spreadsheet. First we select out only the column interest with map, then filter that to get the unique values.


For more like this, see  From VBA to Google Apps Script . Why not join our forum,follow the blog or follow me on twitter to ensure you get updates when they are available. 




Comments