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..eabdf16 --- /dev/null +++ b/docs/underscore.comparison.islike.js.md @@ -0,0 +1,62 @@ +### 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: [""]} +) + +``` + +#### 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 + * `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 +}); +``` + +#### 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 + * `_.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. + +#### 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 4177872..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.

@@ -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,48 @@ 

    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: [""]}
    +)
    +

    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 [].

    + +

    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
    +});
    +

    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.

    @@ -1427,10 +1470,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 +1500,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 +1527,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 +1555,7 @@

    snapshot

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

    object.selectors

    Functions to select values from an object.

    @@ -1619,7 +1689,7 @@

    omitWhen

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

    pickWhen

    @@ -1632,7 +1702,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..a5810c3 --- /dev/null +++ b/underscore.comparison.islike.js @@ -0,0 +1,67 @@ +/* +* 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 (_.isArray(pattern) && !_.isArray(obj)) 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 + var anyUnlike = _.any(pattern, function(p, k) { + var o = obj[k]; + return !islike(o, p); + }); + if (anyUnlike) { + 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); + + +