I've revised this answer several times, and people can look at the wiki if there interested in the revisions, but this is the final solution I've come up with. It's very similar to many of the others posted here, the key difference being that I've extended the Array.prototype
wrapped the Array.prototype.filter
function such that the test statements are evaluated in the scope of the array element instead of the array.
The main advantage I see with this solution rather than simply using the filter
method directly is that it allows you to write generic tests that test the value of this[key]
. Then you can create a generic form elements associated with a specific tests, and use metadata from the objects to be filtered (houses in this case) to associate specific instances of the form elements with keys. This not only makes the code more reusable, but I think it makes actually constructing the queries programmatically a bit more straightfoward. Also, since you can define generic form elements (say a multi-select for distinct values, or a set of dropdowns for a range), you could also create a more powerful query interface where additional form elements could be injected dynamically allowing users to create complex and customized queries. Here is what the code breaks down into:
Before doing anything you should first check to see if the filter
function exists and extend the Array.prototype
if it doesn't as mentioned in other solutions. Then extend the Array.prototype
to wrap the filter
function. I've made it so that the arguments are optional in case someone wants to employ a test function that doesn't take any for some reason, I've also tried to include errors to help you implement the code:
Array.prototype.filterBy = function(testFunc, args)
{
if(args == null || typeof args != 'object') args = [args];
if(!testFunc || typeof testFunc != 'function') throw new TypeError('argument 0 must be defined and a function');
return this.filter(function(elem)
{
return testFunc.apply(elem, args);
});
};
This takes a test function, and an argument array, and defines an inline function as the callback for the Array.prototype.filter
function which calls function.prototype.apply
to execute the test function in the scope of the array element being tested with the specified arguments. You can then write a suite of generic testing functions like these:
testSuite = {
range : function(key, min, max)
{
var min = parseFloat(min);
var max = parseFloat(max);
var keyVal = parseFloat(this[key]);
if(!min || !max|| !keyVal) return false;
else return keyVal >= min && keyVal <= max;
},
distinct : function(key, values)
{
if(typeof key != 'string') throw new TypeError('key must be a string');
if(typeof values != 'object') values = [values];
var keyVal = this[key];
if(keyVal == undefined) return false;
for(var i in values)
{
var value = values[i];
if(typeof value == 'function') continue;
if(typeof value == 'string')
{
if(keyVal.toString().toLowerCase() == value.toLowerCase()) return true;
else continue;
}
else
{
keyVal = parseFloat(keyVal);
value = parseFloat(value);
if(keyVal&&value&&(keyVal==value)) return true;
else continue;
}
}
return false;
}
};
These are just the test functions associated with the requirements in your question, however you could create additional ones that do more complex things like test string values against a regular expression, or test key values that aren't simple datatypes.
Then you extend the object containing the houses array like so (I've called it housesData, call it whatever it is in your code):
housesData.filterBy = function(tests)
{
ret = this.homes.slice(0);
if(tests)
{
for(var i in tests)
{
var test = tests[i];
if(typeof test != 'object') continue;
if(!test.func || typeof test.func != 'function') throw new TypeError('argument 0 must be an array or object containing test objects, each with a key "func" of type function');
else ret = ret.filterBy(test.func, test.args ? test.args : []);
}
}
return ret;
}
You could then call this function like so to get a result set using the generic functions defined above:
result = housesData.filterBy([{func:range,args:['price','150000','400000'],
{func:distinct,args:['num_of_bedsf',[1, 2, 3]]}]);
The way I would actually forsee using this however is to serialize the generic form elements I mentioned earlier into an array or hashmap of the test objects. Here's a simple example I used to test this code (I used jQuery, because it's easier, all the previous code is in example.js, along with @artlung's dummy homes array):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:myNS="http://uri.for.your.schema" xml:lang="en" lang="en">
<head>
<script src="jquery-1.3.2.js" type="text/javascript" language="javascript"></script>
<script src="example.js" type="text/javascript" language="javascript"></script>
<script type="text/javascript" language="javascript">
runQuery = function(event)
{
var tests = [];
$('#results').attr('value', '');;
controls = $('#theForm > fieldset');
for(var i = 0; i < controls.length ; i++)
{
var func;
var args = [];
control = controls.eq(i);
func = testSuite[control.attr('myNS:type')];
args.push(control.attr('myNS:key'));
var inputs = $('input', control);
for(var j=0; j< inputs.length; j++)
{
args.push(inputs[j].value);
}
tests.push({func:func,args:args});
}
result = housesData.filterBy(tests);
resultStr = '';
for(var i = 0; i < result.length; i++)
{
resultStr += result[i]['home_id'];
if(i < (result.length -1)) resultStr += ', ';
}
$('#results').attr('value', resultStr);
}
</script>
</head>
<body>
<form id="theForm" action="javascript:null(0)">
<fieldset myNS:type="range" myNS:key="price">
<legend>Price:</legend>
min: <input type="text" myNS:type="min"></input>
max: <input type="text" myNS:type="max"></input>
</fieldset>
<fieldset myNS:type="distinct" myNS:key="num_of_beds">
<legend>Bedrooms</legend>
bedrooms: <input type="text" myNS:type="value"></input>
</fieldset>
<button onclick="runQuery(event);">Submit</button>
</form>
<textarea id="results"></textarea>
</body>
</html>