Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ examples/*.css
examples/*.html
examples/public
bower_components/*
*.swp
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

40 changes: 40 additions & 0 deletions docs/underscore.comparison.islike.js.md
Original file line number Diff line number Diff line change
@@ -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.
78 changes: 67 additions & 11 deletions index.html

Large diffs are not rendered by default.

82 changes: 82 additions & 0 deletions test/comparison.islike.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
2 changes: 2 additions & 0 deletions test/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<script src="../underscore.util.trampolines.js"></script>
<script src="../underscore.util.operators.js"></script>
<script src="../underscore.util.strings.js"></script>
<script src="../underscore.comparison.islike.js"></script>

<!-- contrib tests -->
<script src="array.builders.js"></script>
Expand All @@ -40,6 +41,7 @@
<script src="util.trampolines.js"></script>
<script src="util.operators.js"></script>
<script src="util.strings.js"></script>
<script src="comparison.islike.js"></script>
</head>
<body>
<div id="qunit"></div>
Expand Down
70 changes: 70 additions & 0 deletions underscore.comparison.islike.js
Original file line number Diff line number Diff line change
@@ -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") {
Comment on lines +18 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, while I personally prefer 4-space indents, the present convention in Underscore and Contrib is 2-space indents, so that's something that needs fixing as well.

return obj instanceof pattern;
}

if (typeof obj !== typeof pattern) return false;
if (pattern instanceof Array && !(obj instanceof Array)) return false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, I would suggest _.isArray if you keep this check, but another alternative is patter.constructor !== obj.constructor

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_.isArray is a good call. I don't think comparing constructors works because I'm only trying to reject arrays at this point.


var type = typeof pattern;

if (type == "object") {
if (pattern instanceof Array) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should use _.isArray instead, because the pattern might have used an Array constructor from a different frame or script context.

if (pattern.length > 0) {
var oTypes = _.uniq(_.map(obj, fTypeof));
var pTypes = _.uniq(_.map(pattern, fTypeof));
if (_.difference(oTypes, pTypes).length) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is sufficent. You should go key by key and ensure each key matches (recursive like).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I have misexplained the intention. It is not the idea that every element in the tested array is matched by an element in the pattern array, only that the set of types in the tested array matches the set of types in the pattern array. For example [1,2,3] matches [0] and [1, "hi", 2] matches [0, ""].

This type of test might be a bit weird though? Not what the user expects.

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)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just do a _.any

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I can't find _.any in underscore or contrib. I think I'm being blind, where is that?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, the official name for _.any is _.some (any is an alias)

On Fri, Jun 19, 2015 at 11:20 AM, Joe Bain [email protected] wrote:

In underscore.comparison.islike.js
#196 (comment)
:

  •            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)) {
    

Sorry, I can't find _.any in underscore or contrib. I think I'm being
blind, where is that?


Reply to this email directly or view it on GitHub
https://github.com/documentcloud/underscore-contrib/pull/196/files#r32838824
.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like _.any(pattern, function(p, k) { return !islike(obj[k], p); }) then?

I'm not sure if I prefer the for loop, it's slightly longer but a bit clearer and maybe quicker too since we don't need to create a lot of little closures.

Also, since _.any uses _.keys which iterates the whole object we do lose a bit of the short circuiting benefit of the loop.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree that it's more clear as a for in loop; and keys is often faster than the for ..in {hasOwn...}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also theres a potential bug currently if the user passes an object like {hasOwnProperty: []}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm of the opinion that if people start overriding hasOwnProperty then they get what they deserve :)

However, I take your point about the bug. We could also use _.has to fix that.

Regarding clarity, it's a bit of an taste thing I feel. Does the underscore style prefer one or the other?

I think the speed is probably negligible, but since _.keys does a for ... in {hasOwn...} under the covers it can't be faster. Unless I'm missing something there.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It uses Object.keys under the hood if it's present, which is generally faster than the for in loop

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right. I'll make some changes and update the pull request. Probably on the weekend at this point, it's almost 6pm.

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