Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

_.islike(obj, pattern) tests objects are like patterns #196

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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.

62 changes: 62 additions & 0 deletions docs/underscore.comparison.islike.js.md
Original file line number Diff line number Diff line change
@@ -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: "" }
}
});
```
92 changes: 81 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
67 changes: 67 additions & 0 deletions underscore.comparison.islike.js
Original file line number Diff line number Diff line change
@@ -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") {
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 (_.isArray(pattern) && !_.isArray(obj)) return false;

var type = typeof pattern;
Comment on lines +23 to +26
Copy link
Contributor

Choose a reason for hiding this comment

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

Indentation is going astray here, that's something that needs fixing before we merge this.


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