From 6247c67439c15e21a37e264b11849f09bf26cd16 Mon Sep 17 00:00:00 2001 From: Joe Bain Date: Fri, 19 Jun 2015 15:45:50 +0100 Subject: [PATCH 1/3] first commit of _.islike() with docs and tests --- .gitignore | 1 + docs/index.md | 1 + docs/underscore.comparison.islike.js.md | 40 ++++++++++++ index.html | 78 +++++++++++++++++++---- test/comparison.islike.js | 82 +++++++++++++++++++++++++ test/index.html | 2 + underscore.comparison.islike.js | 70 +++++++++++++++++++++ 7 files changed, 263 insertions(+), 11 deletions(-) create mode 100644 docs/underscore.comparison.islike.js.md create mode 100644 test/comparison.islike.js create mode 100644 underscore.comparison.islike.js diff --git a/.gitignore b/.gitignore index 9ee2555..6dd0cfb 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ examples/*.css examples/*.html examples/public bower_components/* +*.swp diff --git a/docs/index.md b/docs/index.md index 5d8cf3f..040eb73 100644 --- a/docs/index.md +++ b/docs/index.md @@ -74,6 +74,7 @@ The _.contrib library currently contains a number of related capabilities, aggre - [underscore.util.operators](#util.operators) - functions that wrap common (or missing) JavaScript operators - [underscore.util.strings](#util.strings) - functions to work with strings - [underscore.util.trampolines](#util.trampolines) - functions to facilitate calling functions recursively without blowing the stack + - [underscore.comparison.islike](#comparison.islike) - a function to test objects fit simple patterns The links above are to the annotated source code. Full-blown _.contrib documentation is in the works. Contributors welcomed. diff --git a/docs/underscore.comparison.islike.js.md b/docs/underscore.comparison.islike.js.md new file mode 100644 index 0000000..4d4e28c --- /dev/null +++ b/docs/underscore.comparison.islike.js.md @@ -0,0 +1,40 @@ +### comparison.islike + +This is a function to check things, and particularly complex objects, fit a certain pattern. It is useful when you want to check that an argument you have received has the properties you expect. + +**Signature:** `_.islike(object:Any, pattern:Any)` + +Returns `true` if the object is like the pattern. `false` otherwise. + +```javascript +_.islike( + {name: "James", age: 10, hobbies: ["football", "computer games", "baking"]}, + {name: "", age: 0, hobbies: [""]} +) + +``` + +To specify that a value should be a string you can put an empty string in the pattern `""`. For a number use `0` and for an array use an empty array `[]`. Nested objects are recursively checked. + + * `""` - stands for a string + * `0` - stands for a number + * `false` - stands for a boolean + * `[]` - stands for an array + * `Function` - stands for a function + +If you specify a type in the pattern then the value will be tested using `instanceof`. If you want to verify a function value (for instance a callback) you need to pass the `Function` type, since a normal `function() {}` is indistinguishable from type in Javascript. A more complex example using these follows: + +```javascript +_.islike(myArgument, { + title: "", count: "", owner: OwnerModel, success: Function, error: Function +}); +``` + +An array value can also be type checked by passing an array of types in the pattern. For example + + * `_.islike([ 1, 2, 3, "hello" ], [ 0 ])` - returns false + * `_.islike([ 1, 2, 3, "hello", function() {} ], [ 0, "" ])` - returns false + * `_.islike([ 1, 2, 3, "hello" ], [ 0, "" ]}` - returns true + * `_.islike([ 1, 2, 3, "hello", function() {} ], [ 0, "", Function ]}` - returns true + +`[""]` allows an array of only strings and `["",0]` allows strings and numbers. This check is done using `typeof` so objects and arrays will fall into the same category. diff --git a/index.html b/index.html index 4177872..9e8bf09 100644 --- a/index.html +++ b/index.html @@ -10,7 +10,7 @@ -
+

Underscore-contrib (0.3.0)

The brass buckles on Underscore's utility belt - a contributors' library for Underscore.

@@ -65,6 +65,7 @@

Sub-librari
  • underscore.util.operators - functions that wrap common (or missing) JavaScript operators
  • underscore.util.strings - functions to work with strings
  • underscore.util.trampolines - functions to facilitate calling functions recursively without blowing the stack
  • +
  • underscore.comparison.islike - a function to test objects fit simple patterns
  • The links above are to the annotated source code. Full-blown _.contrib documentation is in the works. Contributors welcomed.

    array.builders

    @@ -301,7 +302,7 @@

    nth

    If wrapping a function around _.nth is too tedious or you'd like to partially apply the index then Underscore-contrib offers any of _.flip2, _.fix or _.curryRight2 to solve this.


    partitionBy

    -

    Signature: _.keep(array:Array, fun:Function)

    +

    Signature: _.partitionBy(array:Array, fun:Function)

    Takes an array and partitions it into sub-arrays as the given predicate changes truth sense.

    _.partitionBy([1,2,2,3,1,1,5], _.isEven);
    @@ -359,6 +360,34 @@ 

    co pluck: function(obj, propertyName) pluckRec: function(obj, propertyName) _.walk.collect = _.walk.map;

    +

    comparison.islike

    +

    This is a function to check things, and particularly complex objects, fit a certain pattern. It is useful when you want to check that an argument you have received has the properties you expect.

    +

    Signature: _.islike(object:Any, pattern:Any)

    +

    Returns true if the object is like the pattern. false otherwise.

    +
    _.islike(
    +  {name: "James", age: 10, hobbies: ["football", "computer games", "baking"]},
    +  {name: "", age: 0, hobbies: [""]}
    +)
    +

    To specify that a value should be a string you can put an empty string in the pattern "". For a number use 0 and for an array use an empty array []. Nested objects are recursively checked.

    + +

    If you specify a type in the pattern then the value will be tested using instanceof. If you want to verify a function value (for instance a callback) you need to pass the Function type, since a normal function() {} is indistinguishable from type in Javascript. A more complex example using these follows:

    +
    _.islike(myArgument, {
    +    title: "", count: "", owner: OwnerModel, success: Function, error: Function
    +});
    +

    An array value can also be type checked by passing an array of types in the pattern. For example

    + +

    [""] allows an array of only strings and ["",0] allows strings and numbers. This check is done using typeof so objects and arrays will fall into the same category.

    function.arity

    Functions which manipulate the way functions work with their arguments.

    @@ -1427,10 +1456,23 @@

    frequencies

    merge

    Signature: _.merge(obj1:Object[, obj:Object...])

    -

    Merges two or more objects starting with the left-most and applying the keys -rightward.

    -
    _.merge({ a: "alpha" }, { b: "beta" });
    -// => { a: "alpha", b: "beta" }
    +

    Returns a new object resulting from merging the passed objects. Objects +are processed in order, so each will override properties of the same +name occurring in earlier arguments.

    +

    Returns null if called without arguments.

    +
    var a = {a: "alpha"};
    +var b = {b: "beta"};
    +
    +var threeGreekLetters = _.merge(a, b, {g: "gamma"});
    +
    +a;
    +// => {a: "alpha"}
    +
    +b;
    +// => {b: "beta"}
    +
    +threeGreekLetters;
    +// => { a: "alpha", b: "beta", g: "gamma" }

    renameKeys

    Signature: _.renameKeys(obj:Object, keyMap:Object)

    @@ -1444,9 +1486,21 @@

    setPath

    Sets the value of a property at any depth in obj based on the path described by the ks array. If any of the properties in the ks path don't exist, they will be created with defaultValue.

    -

    See _.updatePath about obj not being mutated in the process by cloning it.

    -
    _.setPath({}, "Plotinus", ["Platonism", "Neoplatonism"], {});
    -// => { Platonism: { Neoplatonism: "Plotinus" } }
    +

    Note that the original object will not be mutated. Instead, obj will +be cloned deeply.

    +
    
    +var obj = {};
    +
    +var plotinusObj = _.setPath(obj, "Plotinus", ["Platonism", "Neoplatonism"], {});
    +
    +obj;
    +// => {}
    +
    +plotinusObj;
    +// => { Platonism: { Neoplatonism: "Plotinus" } }
    +
    +obj === plotinusObj;
    +// => false;

    snapshot

    Signature: _.snapshot(obj:Object)

    @@ -1459,6 +1513,7 @@

    snapshot

    schools === _.snapshot(schools); // => false
    +

    updatePath

    Signature: _.updatePath(obj:Object, fun:Function, ks:Array, defaultValue:Any)

    Updates the value at any depth in a nested object based on the path described by the ks array. The function fun is called with the current value and is @@ -1486,6 +1541,7 @@

    snapshot

    obj === imperialObj; // => false +

    object.selectors

    Functions to select values from an object.

    @@ -1619,7 +1675,7 @@

    omitWhen

    shakespere: "England" }; -_.omitWhen(obj, function (country) { return country == "England" }); +_.omitWhen(playwrights, function (country) { return country == "England" }); // => { euripedes: "Greece" }

    pickWhen

    @@ -1632,7 +1688,7 @@

    pickWhen

    shakespere: "England" }; -_.pickWhen(obj, function (country) { return country == "England" }); +_.pickWhen(playwrights, function (country) { return country == "England" }); // => { shakespeare: "England" }

    selectKeys

    diff --git a/test/comparison.islike.js b/test/comparison.islike.js new file mode 100644 index 0000000..7a26487 --- /dev/null +++ b/test/comparison.islike.js @@ -0,0 +1,82 @@ +$(document).ready(function() { + + module("underscore.comparison.islike"); + + test("string islike string", function() { + ok(_.islike("hello, world", "")); + }); + + test("number islike number", function() { + ok(_.islike(32.4, 0)); + }); + + test("boolean islike boolean", function() { + ok(_.islike(true, true)); + }); + + test("string is not like number", function() { + equal(_.islike("hello", 0), false); + }); + + test("boolean is not like number", function() { + equal(_.islike(false, 0), false); + }); + + test("array is like array", function() { + ok(_.islike([1,2,3], [])); + }); + + test("number array is typed like array", function() { + ok(_.islike([1,2,3], [0])); + }); + + test("string array is typed like array", function() { + ok(_.islike(["hello", "world"], [""])); + }); + + test("string array is not typed like number array", function() { + equal(_.islike(["hello", "world"], [0]), false); + }); + + test("object is like object", function() { + ok(_.islike( + {name: "James", age: 10, hobbies: ["football", "computer games", "baking"]}, + {name: "", age: 0, hobbies: [""]} + )); + }); + + test("object is not like object", function() { + equal(_.islike( + {name: "James", age: 10, hobbies: ["football", "computer games", "baking"]}, + {name: "", age: 0, hometown: "", hobbies: [""]} + ), false); + }); + + test("object is like type", function() { + var Type = function(){}; + + ok(_.islike(new Type, Type)); + }); + + test("function is like Function", function() { + ok(_.islike(function(){}, Function)); + }); + + test("function is not like function", function() { + equal(_.islike(function(){}, function(){}), false); + }); + + test("object with functions is like object", function() { + ok(_.islike( + {name: "James", age: 10, hobbies: ["football", "computer games", "baking"], done: function() { console.log("done");} }, + {name: "", age: 0, hobbies: [""], done: Function} + )); + }); + + test("object with functions is not like object", function() { + equal(_.islike( + {name: "James", age: 10, hobbies: ["football", "computer games", "baking"], done: true}, + {name: "", age: 0, hobbies: [""], done: Function} + ), false); + }); +}); diff --git a/test/index.html b/test/index.html index 7cfe9b5..178713e 100644 --- a/test/index.html +++ b/test/index.html @@ -24,6 +24,7 @@ + @@ -40,6 +41,7 @@ +
    diff --git a/underscore.comparison.islike.js b/underscore.comparison.islike.js new file mode 100644 index 0000000..d902ac6 --- /dev/null +++ b/underscore.comparison.islike.js @@ -0,0 +1,70 @@ +/* +* Tests if an object is like another. This means objects should follow the same +* structure and arrays should contain the same types. +* +* E.g. +* +* _.islike( +* {name: "James", age: 10, hobbies: ["football", "computer games", "baking"]}, +* {name: "", age: 0, hobbies: [""]} +* ) +*/ +(function() { + // Establish the root object, `window` in the browser, or `require` it on the server. + if (typeof exports === 'object') { + _ = module.exports = require('underscore'); + } + + var islike = function(obj, pattern) { + if (typeof pattern === "function") { + return obj instanceof pattern; + } + + if (typeof obj !== typeof pattern) return false; + if (pattern instanceof Array && !(obj instanceof Array)) return false; + + var type = typeof pattern; + + if (type == "object") { + if (pattern instanceof Array) { + if (pattern.length > 0) { + var oTypes = _.uniq(_.map(obj, fTypeof)); + var pTypes = _.uniq(_.map(pattern, fTypeof)); + if (_.difference(oTypes, pTypes).length) { + return false; + } + } + } else { // object + if (pattern.constructor === pattern.constructor.prototype.constructor) { + // for 'simple' objects we enumerate + for (var k in pattern) { + if (!pattern.hasOwnProperty(k)) { + continue; + } + var p = pattern[k]; + var o = obj[k]; + if (!islike(o, p)) { + return false; + } + } + } else { + // for 'types' we just check the inheritance chain + if (!(obj instanceof pattern.constructor)) { + return false; + } + } + } + } + + return true; + }; + + var fTypeof = function(o) { + return typeof o; + } + + _.mixin({islike: islike}); +}).call(this); + + + From cc17ec0897116fc19d7c23d8b910bc566c69e5ea Mon Sep 17 00:00:00 2001 From: Joe Bain Date: Fri, 19 Jun 2015 15:54:38 +0100 Subject: [PATCH 2/3] added semi-colon to appeas jslint --- underscore.comparison.islike.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/underscore.comparison.islike.js b/underscore.comparison.islike.js index d902ac6..c6d80d4 100644 --- a/underscore.comparison.islike.js +++ b/underscore.comparison.islike.js @@ -61,7 +61,7 @@ var fTypeof = function(o) { return typeof o; - } + }; _.mixin({islike: islike}); }).call(this); From 9ff12677c42d0cf1a45d5d35a52311a5e61e1391 Mon Sep 17 00:00:00 2001 From: Joe Bain Date: Mon, 22 Jun 2015 23:03:43 +0100 Subject: [PATCH 3/3] Made some cosmetic changes to the implementation of _.islike Functionaly the same but using more underscore-ish style and making use of underscore methods for type checking where it makes sense. Also updated the documentation for _.islike to be clearer and better explain arrays and nested objects. --- docs/underscore.comparison.islike.js.md | 24 +++++++++++++++++++++++- index.html | 18 ++++++++++++++++-- underscore.comparison.islike.js | 15 ++++++--------- 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/docs/underscore.comparison.islike.js.md b/docs/underscore.comparison.islike.js.md index 4d4e28c..eabdf16 100644 --- a/docs/underscore.comparison.islike.js.md +++ b/docs/underscore.comparison.islike.js.md @@ -14,7 +14,9 @@ _.islike( ``` -To specify that a value should be a string you can put an empty string in the pattern `""`. For a number use `0` and for an array use an empty array `[]`. Nested objects are recursively checked. +#### Basic types + +To specify that a value should be a string you can put an empty string in the pattern `""`. For a number use `0` and for an array use an empty array `[]`. * `""` - stands for a string * `0` - stands for a number @@ -30,6 +32,8 @@ _.islike(myArgument, { }); ``` +#### Array types + An array value can also be type checked by passing an array of types in the pattern. For example * `_.islike([ 1, 2, 3, "hello" ], [ 0 ])` - returns false @@ -38,3 +42,21 @@ An array value can also be type checked by passing an array of types in the patt * `_.islike([ 1, 2, 3, "hello", function() {} ], [ 0, "", Function ]}` - returns true `[""]` allows an array of only strings and `["",0]` allows strings and numbers. This check is done using `typeof` so objects and arrays will fall into the same category. + +#### Complex nested objects + +Nested objects are recursively checked, so you just need to nest your pattern. + +This is a very complex example, probably more complex than `_.islike` is suited for, but is shows the nesting. In the example the object has a `process` property with two callback functuons and an array of numbers. It also has an `author` property which has another nested `location` property. + +```javascript +_.islike(myComplexArgument, + title: "", age: 0, popularity: 0, available: false, + process: { + success: Function, error: Function, values: [0] + }, + author: { + name: "", location: { country: "", city: "", postcode: "" } + } +}); +``` diff --git a/index.html b/index.html index 9e8bf09..0d60009 100644 --- a/index.html +++ b/index.html @@ -10,7 +10,7 @@ - +

    Underscore-contrib (0.3.0)

    The brass buckles on Underscore's utility belt - a contributors' library for Underscore.

    @@ -368,7 +368,8 @@

    -

    To specify that a value should be a string you can put an empty string in the pattern "". For a number use 0 and for an array use an empty array []. Nested objects are recursively checked.

    +

    Basic types

    +

    To specify that a value should be a string you can put an empty string in the pattern "". For a number use 0 and for an array use an empty array [].

    • "" - stands for a string
    • 0 - stands for a number
    • @@ -380,6 +381,7 @@

      _.islike(myArgument, { title: "", count: "", owner: OwnerModel, success: Function, error: Function }); +

      Array types

      An array value can also be type checked by passing an array of types in the pattern. For example

      [""] allows an array of only strings and ["",0] allows strings and numbers. This check is done using typeof so objects and arrays will fall into the same category.

      +

      Complex nested objects

      +

      Nested objects are recursively checked, so you just need to nest your pattern.

      +

      This is a very complex example, probably more complex than _.islike is suited for, but is shows the nesting. In the example the object has a process property with two callback functuons and an array of numbers. It also has an author property which has another nested location property.

      +
      _.islike(myComplexArgument,
      +    title: "", age: 0, popularity: 0, available: false,
      +    process: {
      +        success: Function, error: Function, values: [0]
      +    },
      +    author: {
      +        name: "", location: { country: "", city: "", postcode: "" }
      +    }
      +});

      function.arity

      Functions which manipulate the way functions work with their arguments.

      diff --git a/underscore.comparison.islike.js b/underscore.comparison.islike.js index c6d80d4..a5810c3 100644 --- a/underscore.comparison.islike.js +++ b/underscore.comparison.islike.js @@ -21,7 +21,7 @@ } if (typeof obj !== typeof pattern) return false; - if (pattern instanceof Array && !(obj instanceof Array)) return false; + if (_.isArray(pattern) && !_.isArray(obj)) return false; var type = typeof pattern; @@ -37,15 +37,12 @@ } else { // object if (pattern.constructor === pattern.constructor.prototype.constructor) { // for 'simple' objects we enumerate - for (var k in pattern) { - if (!pattern.hasOwnProperty(k)) { - continue; - } - var p = pattern[k]; + var anyUnlike = _.any(pattern, function(p, k) { var o = obj[k]; - if (!islike(o, p)) { - return false; - } + return !islike(o, p); + }); + if (anyUnlike) { + return false; } } else { // for 'types' we just check the inheritance chain