So I read a great article about underscore-contrib, and thought I’d try some fun with functions while working with a editors tool for Digeshops.com. The tool is for matching category strings in product feeds to those of Digeshops, which can be interesting at times. What I needed was a script that highlighted strings so I could see if a search needle would work in MySQL, and filtering the strings so that only matching strings are shown.
So simply put, I wanted the following lines
Monkey apple hat
Money apple orange
Orange Hat
to be displayed as
Money apple orange
for the search %ple%ange%, with ple and nge highlighted.
Oh, and since I’m playaing with functions, lets start out doing it really function intense. Lets start with getting an array of needles (to search for):
// Simple function splitting a variable at % and removing strings of length 0 var split = function(val){ return _.reduce(val.split('%'), function(memo, val){ if(val.length) memo.push(val); return memo; },[]); }
Ok, one fun function. Nothing fancy, but I used undescore’s _.reduce function, reducing an array. I could just as well have used _.filter, but I didn’t. Oh, my strings to be filtered, they are just plain strings stored in an array.
Since I’m working with a search, I decide to use a tree hierarchy for storing my results.
// creating an object with type, content and children. var treeNode = function(content, type, childA, childB){ if(type==undefined) type = 'normal'; return {type:type, content:content, childA:childA, childB:childB}; }
The only fun with this is that I can call it lazy with treeNode(‘hello’) just to create a “normal” node. That’s always nice.
Now, for the main highlighting function, I play a little with Javascript’s closures:
/** * This function returns a function that looks for a divider in a string. * The returned object is a treenode. If the divider is found in the string, * a treenode of type "highlight" will be returned, with left and right * substrings as child nodes. **/ var highLight = function(needle){ return function(oVal){ // Save the anonymous function, since this is a recursive function var p = arguments.callee; var val = oVal; // Check if it's treenode or not, so that the top call can be done // by just sending in a string if(oVal.type == undefined){ val = treeNode(oVal); } // Only look for the needle in "normal" nodes. if(val.type == 'normal'){ var str = toLower(val.content); if(str.indexOf(needle)>=0){ var i = str.indexOf(needle); var a = val.content.substr(0,i); // Left substring var b = val.content.substr(i, needle.length); // needle itself var c= val.content.substr(i+needle.length); // Right substring // Return a node of type "highlight". // Don't look for the needle in the childnodes, since it's not // that kind of search (find once per needle) return treeNode(b, 'highlight', treeNode(a), treeNode(c)); } }else{ // For nodes that are already highlighted, check for the needle in // the right child only. val.childB = p(val.childB); } return val; } };
When looking at a function like this, there are some few things that are quite interesting. For starters, the resulting function will be pure, i.e. not have any side effects. It will also follow the single responsibility pattern, since it is an encapsulated function with the sole purpose of finding a specific needle in a object tree.
Since both highLight and the anonymous function takes on parameter, they are made to work with map.
Moreover, since I’m working with a tree hierarchy, I will use the anonymous function recursively, so I make use of the arguments.callee parameter.
Next up I need a function for taking several functions in an array, calling them one after another, each taking the returned parameter from the previous function as their input parameter.
/** * Almost like the pipeline from underscore-contrib, but takes an array of * functions instead of just functions as parameters. * Calls all functions using reduce, starting with the seed parameter, then * using the returned value as parameter for the next. */ var pipeline2 = function(funs){ return function(seed) { return _.reduce(funs, function(l,r) { return r(l); }, seed); }; };
This is almost exactly like underscore-contrib’s pipeline function. Using this, I can call first
var highLighters = _.map(needles, highLight); var process = _.pipeline(treeNode, pipeline2(highLighters));
where of course, needles is an array of strings. The process function will hence first fall treeNode, to create an treeNode object from the string, and then subsequently all the highLight-functions. It doesn’t matter if I have one, two or ten needles, these two lines takes care of the string to highlighted tree object just fine.
Putting this together in a function I get the following:
/** * Function for updating a result div with strings that contain a highlight * search needle * The needle is to work for needle on the form "needle1%needle2%needle3" * that is, as a needle for mysql like. */ var processStringWithNeedle = function(needles){ // Create an array of functions that highlight their needle using the // highlight function above. // This is a good example of usage of _.map. var highLighters = _.map(needles, highLight); // Now pipeline the hightLighters to one function var process = _.pipeline(treeNode, pipeline2(highLighters)); // And of course, we return this as a function return function(stringToFilter){ // Empty filter, just output all strings. if(!needles.length){ return treeNode(stringToFilter); } // Return the pipeline of functions return process(stringToFilter); } };
Yup, that’s right. I encapsule it in a function so it’s easy to make multiple calls to it. The blog post is called Fun with Javascript Functions, after all.
And here is the function that actually does the calling, and updates the ui:
var updateUnconnected = function(stringsToFilter){ // Clear the result div $("#unconnected").html(""); // lowercase the search needle. Calling it filter since it filters out // strings without the filter. var needle = $("#testrule").val().toLowerCase(); // Now use the split above and get an array of filters (needles) var needles = split(needle); // Get the filter function var processString = processStringWithNeedle(needles); // Now, process all the strings (which resides in the unconnectedRows var nodes = _.map(stringsToFilter, processString); // Filter those with too few highlights var filteredNodes = _.filter(nodes, function(node){ return countHighlights(node,0)==needles.length; }) // And put them on display _.each(filteredNodes, function(node){ $("#unconnected").append("<div>"+render(node)+"</div>"); }); };
The render function is a very simple recursive rendering function for the object tree:
/** * recursive function for rendering the object tree provided to us by the * highLight function **/ var render = function(obj){ var r = arguments.callee; if(obj.type=='normal'){ return obj.content; }else if(obj.type=='highlight'){ return r(obj.childA) + "<div class='highlight'>" + obj.content +"</div>"+ r(obj.childB); } }
Fun, ha? Ok, let me know what you think about this in the comments!