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 @@
-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 use0
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 functionIf 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 theFunction
type, since a normalfunction() {}
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
++
+- +
_.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 usingtypeof
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 aprocess
property with two callback functuons and an array of numbers. It also has anauthor
property which has another nestedlocation
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:
@@ -1444,9 +1500,21 @@_.renameKeys(obj:Object, keyMap:Object)
setPath
Sets the value of a property at any depth in
-obj
based on the path described by theks
array. If any of the properties in theks
path don't exist, they will be created withdefaultValue
.See
-_.updatePath
aboutobj
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:
@@ -1459,6 +1527,7 @@_.snapshot(obj:Object)
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 functionfun
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); + + +