A recursive extend function for Apps Script

If you've worked through JavaScript recursion primer and More recursion - parents and children you should be pretty comfortable with how recursion works by now. Now we're going to apply it to create a simple version of jQuery.extend(). If you are not familiar with it, its a useful function that allows you merge multiple objects - usually used for setting default properties. 


Let's say you have this as the default parameters to something like a charts API

var defaultOptions = { 
    width: 20 , 
    height:100, 
    title:"this is the default title", 
    type:"bar"
};

and you want to use most of those settings,  but make just a  couple of changes, with jQuery you can say

var options = jQuery.extend ( {
    height:200,
    type:"pie"
} , defaultOptions) ;

and you'll get back an object like this

{ width: 20 , 
  height:200, 
  title:"this is the default title",
  type:"pie"
};

Deeper

So far pretty simple .. but you may need to go deeper into the object if there are properties beyond the first level. 

var defaultOptions = { 
  dimensions: {
    width: 20 , 
    height:100 
  },
  title:"this is the default title", 
  type:"bar"
};

Still not too bad, but you'd have to do this. So you can imagine if you continue this out to multiple levels, we have a good candidate for a solution involving recursion

var options = cUseful.extend ( {
  dimensions: {
    height:200
  }
  type:"pie"
 } , defaultOptions) ;

The cUseful library

Many of the snippets in this section of the site are part of the cUseful library. You can find the details below. This extend function uses a couple of functions from that, and will be implemented there too if you want to use it from a library.

https://script.google.com/macros/s/AKfycbwZ2Hht93wTNzvRmYINYF7obaOHciBXWcP_wAiEtyGq70_x3cI/exec?list=cUseful

The extend function

You use it like this

var options = cUseful.extend ( yourOptions , default1, default2 .... etc);

You can have as many default objects as you like and the result will be the merger of all those defaults applied to your options, very much like jQuery.extend(). 


So how to do it ?

Here's the code. We'll do a walkthrough later on once we've done some testing

/**
 * a little like the jquery.extend() function
 * the first object is extended by the 2nd and subsequent objects - its always deep
 * @param {object} ob to be extended
 * @param {object...} repeated for as many objects as there are
 * @return {object} the first object extended
 */
function extend () {
  
    // we have a variable number of arguments
    if (!arguments.length) {
      // default with no arguments is to return undefined 
      return undefined;
    }
    
    // validate we have all objects
    var extenders = [],targetOb;
    for (var i = 0; i < arguments.length; i++) {
      if (!isObject(arguments[i])) {
        throw 'extend arguments must be objects';
      }
      if (i ===0 ) {
        targetOb = arguments[i];
      } 
      else {
        extenders.push (arguments[i]);
      }
    }
    
    // set defaults from extender objects
    extenders.forEach(function(d) {
        recurse(targetOb, d);
    });
    
    return targetOb;
   
    // run do a deep check
    function recurse(tob,sob) {
      Object.keys(sob).forEach(function (k) {
      
        // if target ob is completely undefined, then copy the whole thing
        if (isUndefined(tob[k])) {
          tob[k] = sob[k];
        }
        
        // if source ob is an object then we need to recurse to find any missing items in the target ob
        else if (isObject(sob[k])) {
          recurse (tob[k] , sob[k]);
        }
        
      });
    }
}

Some tests

See if you can figure out how these cases should be handled.

function textend() {

  // expect an empty object
  var ob = cUseful.extend ({});
  Logger.log(ob);

  // expect ob to look like cUseful.extended
  var ob = cUseful.extend (ob , {a:true,b:true});
  Logger.log(ob);
  
  // expect ob to look like cUseful.extended a/b/c/d/e
  var ob = cUseful.extend (ob , {c:true,d:true}, {e:true});
  Logger.log(ob);
  
  // none of these should change, but add f
  var ob = cUseful.extend (ob, {e:false,d:false}, {a:false} , {c:false}, {f:true});
  Logger.log(ob);
  
  // see that arrays work
  var ob = cUseful.extend (ob, {a1:[true,2,3]});
  Logger.log(ob);
  
  // now some recursing - should not cUseful.extend because a is not an object
  var ob = cUseful.extend (ob , {a:{aa:false}}, {a:{aa:false}});
  Logger.log(ob);
  
  // should add  first one because g is new object
  var ob = cUseful.extend (ob , {g:{gg:true}}, {g:{gg:false}});
  Logger.log(ob);
  
  // should add an extra property to g
  var ob = cUseful.extend (ob , {g:{ggg:true}}, {g:{gg:false}});
  Logger.log(ob);
  
  // should not add an extra property to g.ggg since that is not an object
  var ob = cUseful.extend (ob , {g:{ggg:{gggg:false}}}, {g:{gg:false}});
  Logger.log(ob);
  
}

and the results
[15-02-08 13:32:34:591 GMT] {}
[15-02-08 13:32:34:592 GMT] {b=true, a=true}
[15-02-08 13:32:34:593 GMT] {d=true, e=true, b=true, c=true, a=true}
[15-02-08 13:32:34:594 GMT] {f=true, d=true, e=true, b=true, c=true, a=true}
[15-02-08 13:32:34:595 GMT] {f=true, d=true, e=true, b=true, c=true, a=true, a1=[true, 2.0, 3.0]}
[15-02-08 13:32:34:596 GMT] {f=true, d=true, e=true, b=true, c=true, a=true, a1=[true, 2.0, 3.0]}
[15-02-08 13:32:34:597 GMT] {f=true, g={gg=true}, d=true, e=true, b=true, c=true, a=true, a1=[true, 2.0, 3.0]}
[15-02-08 13:32:34:597 GMT] {f=true, g={gg=true, ggg=true}, d=true, e=true, b=true, c=true, a=true, a1=[true, 2.0, 3.0]}
[15-02-08 13:32:34:598 GMT] {f=true, g={gg=true, ggg=true}, d=true, e=true, b=true, c=true, a=true, a1=[true, 2.0, 3.0]}

Let's check on our initial example.

  var defaultOptions = { 
    dimensions: {
      width: 20 , 
      height:100 
    },
    title:"this is the default title", 
    type:"bar"
  };

  Logger.log(cUseful.extend ( {
    dimensions: {
      height:200
    },
    type:"pie"
  } , defaultOptions)) ;

and the result

{
    "dimensions": {
        "height": 200,
        "width": 20
    },
    "type": "pie",
    "title": "this is the default title"
}

Walkthrough

  • Since we have a variable number of arguments, we use the special arguments array like object to pick the up from - returning undefined if we don't get any.
    // we have a variable number of arguments
    if (!arguments.length) {
      // default with no arguments is to return undefined 
      return undefined;
    }

  • Pick up each of the objects from the arguments array, check that they are in fact objects, and set up the target from the first one, and note all of the extenders from which to apply their default values.
    // validate we have all objects
    var extenders = [],targetOb;
    for (var i = 0; i < arguments.length; i++) {
      if (!isObject(arguments[i])) {
        throw 'extend arguments must be objects';
      }
      if (i ===0 ) {
        targetOb = arguments[i];
      } 
      else {
        extenders.push (arguments[i]);
      }
    }

  • For each of those extenders, apply their defaults. We have to do it recursively as each property of each extender may have sub properties.
    // set defaults from extender objects
    extenders.forEach(function(d) {
        recurse(targetOb, d);
    });
    
    return targetOb;

  • Here's the recursive function - it calls itself with the properties at the current level if any of them are an object so that matching properties from the default objects can be applied. These are analogous to the children in JavaScript recursion primer
     // run do a deep check
    function recurse(tob,sob) {
      Object.keys(sob).forEach(function (k) {
      
        // if target ob is completely undefined, then copy the whole thing
        if (isUndefined(tob[k])) {
          tob[k] = sob[k];
        }
        
        // if source ob is an object then we need to recurse to find any missing items in the target ob
        else if (isObject(sob[k])) {
          recurse (tob[k] , sob[k]);
        }
        
      });
    }


And that's all there is to it. Happy recursing. 

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

You want to learn Google Apps Script?

Learning Apps Script, (and transitioning from VBA) are covered comprehensively in my my book, Going Gas - from VBA to Apps script, available All formats are available now from O'Reilly,Amazon and all good bookshops. You can also read a preview on O'Reilly

If you prefer Video style learning I also have two courses available. also published by O'Reilly.
Google Apps Script for Developers and Google Apps Script for Beginners.





Comments