views:

100

answers:

3

I'm trying to come up with an efficient way to overwrite 2 strings that look like this:

str1 = "width=800,height=600,resizable=no,titlebar=no";
str2 = "width=100,height=100";

In the above example, str2 should overwrite str1 to produce str3:

str3 = "width=100,height=100,resizable=no,titlebar=no";

In my tests, I turned str2 into an array and tested each key against a match in str1.

Can anyone think of a more efficient way to write this:

str1 = "width=800,height=600,resizable=no,titlebar=no";
str2 = "width=100,height=100";
sArray = str2.split(",");

for( var i = 0; i < sArray.length; i++ ) {
    var key = sArray[i].match(/(\w+)=/gi).toString().replace("=", ""),
        in_str1 = str1.search(key),
        replace_pattern = new RegExp(key+"=(\\w+)", "gi");

    if(in_str1 !== -1){                 
        str1 = str1.replace(replace_pattern, sArray[i]);
    } else {
        str1 = str1 + "," + sArray[i];
    }
}
+2  A: 

Didn't try running it, but this ought to work, created a utility function parseParamString that accepts a string of that format, and optionally an object to add the key-value pairs to.

function parseParamString(str, obj) {
    var pairs = str.split(","),
        i = 0, l = pairs.length,
        pair;
    obj || (obj = {});
    for (; l > i; ++i) {
        pair = pairs[i].split("=", 2);
        obj[pair[0]] = pair[1];
    }
    return obj;
}

function buildParamString(obj) {
    var pairs = [], i;
    for (i in obj) {
        if (obj.hasOwnProperty(i)) {
            pairs.push(i + "=" + obj[i]);
        }
    }
    return pairs.join(",");
}

var str1 = "width=800,height=600,resizable=no,titlebar=no",
    str2 = "width=100,height=100";

var resultObj = parseParamString(str2, parseParamString(str1));

var resultSTr = buildParamString(resultObj);

Edit: forgot about turning the object back into a string.

Ryan Tenney
Thanks Ryan. What do you think of my method vs yours?
Andres
It is not a bad solution, it's certainly more concise than Daniel's and my approach. However, if the purpose of your method weren't explained to me I would have had some difficulty figuring it out. The same can be said of Sean's extremely terse RegEx approach, but there is at the same time a certain beauty to it. Also, I am generally averse to dynamic regular expressions. There isn't anything inherently wrong with them, it's just a personal preference.
Ryan Tenney
+2  A: 

I came up with the following, but I'm not sure if it'll be faster than your method since there's a few more steps involved. Also it's a bit longer...

var toObj = function (str) {
   var arr = str.split(","), obj = {};
   for (var i = 0; i < arr.length; i++) {
      var keyval = arr[i].split("=", 2);
      obj[keyval[0]] = keyval[1];
   }
   return obj;
};

var objToString = function (obj) {
   var stringBuilder = [];
   for (var k in obj) {
      stringBuilder.push(k+"="+obj[k]);
   }
   return stringBuilder.join(",");
};

var merge  = function (obj1, obj2) { // merge obj1 into obj2.. obj2 will be modified.
   for (var k in obj1) {
      obj2[k] = obj2[k] || obj1[k];
   }
   return obj2;
};

var newStr = objToString(merge(toObj(str1), toObj(str2));

Benchmarking

I was bored, so I decided to do a quick benchmark of these 3 different solutions offered.

Benchmarking function/test:

function benchmark(name, fn, n) { 
   console.time(name); 
   for(var i = 0; i < n; i++) 
      fn(); 
   console.timeEnd(name);  
}

Results:

Andres (OP):

benchmark("Andres", function () { paramStrAndres("width=800,height=600,resizable=no,titlebar=no", "width=100,height=100"); }, 10000);
Andres: 505ms

Ryan/RWT:

benchmark("RWT", function () { paramStrRWT("width=800,height=600,resizable=no,titlebar=no", "width=100,height=100"); }, 10000);
RWT: 86ms

Daniel:

benchmark("Daniel", function () { paramStrDaniel("width=800,height=600,resizable=no,titlebar=no", "width=100,height=100"); }, 10000);
Daniel: 98ms

Sean:

benchmark("Sean", function () { paramStrSean("width=800,height=600,resizable=no,titlebar=no", "width=100,height=100"); }, 10000);
Sean: 40ms

Functions Tested:

Andres:

function paramStrAndres(str1, str2) {
    sArray = str2.split(",");

    for( var i = 0; i < sArray.length; i++ ) {
        var key = sArray[i].match(/(\w+)=/gi).toString().replace("=", ""),
        in_str1 = str1.search(key),
        replace_pattern = new RegExp(key+"=(\\w+)", "gi");

        if(in_str1 !== -1){                 
            str1 = str1.replace(replace_pattern, sArray[i]);
        } else {
            str1 = str1 + "," + sArray[i];
        }
    }
    return str1;
}

Ryan/RWT:

function paramStrRWT (str1, str2) {
 function parseParamString(str, obj) {
    var pairs = str.split(","),
        i = 0, l = pairs.length,
        pair;
    obj || (obj = {});
    for (; l > i; ++i) {
        pair = pairs[i].split("=", 2);
        obj[pair[0]] = pair[1];
    }
    return obj;
 }

 function buildParamString(obj) {
    var pairs = [], i;
    for (i in obj) {
        if (obj.hasOwnProperty(i)) {
            pairs.push(i + "=" + obj[i]);
        }
    }
    return pairs.join(",");
 }
 var resultObj = parseParamString(str2, parseParamString(str1));
 return buildParamString(resultObj);
}

Mine:

function paramStrDaniel(str1, str2) {
    var toObj = function (str) {
        var arr = str.split(","), obj = {};
        for (var i = 0; i < arr.length; i++) {
            var keyval = arr[i].split("=", 2);
            obj[keyval[0]] = keyval[1];
        }
        return obj;
    };

    var objToString = function (obj) {
        var stringBuilder = [];
        for (var k in obj) {
            stringBuilder.push(k+"="+obj[k]);
        }
        return stringBuilder.join(",");
    };

    var merge  = function (obj1, obj2) { // merge obj1 into obj2.. obj2 will be modified.
        for (var k in obj1) {
            obj2[k] = obj2[k] || obj1[k];
        }
        return obj2;
    };

    return objToString(merge(toObj(str1), toObj(str2)));
    }

Sean:

function paramStrSean(str1, str2) {
    var str3 = str2 + "," + str1, temp;

    while ((temp = str3.replace(/\b([a-z]+)(=.*)\b\1=[^,]*,?/, "$1$2")) != str3) {
        str3 = temp;
    }
    return str3;
    }

EDIT: I found it odd that my version was faster than Ryan's, considering they were similar. After looking into it, I see I made a boo-boo on the for loop (length -> arr.length). I've updated the benchmarks. That's what I get for not testing my code.

CD Sanchez
Thanks for the benchmarks! I'm amused that the time required by your method and my method were so different. I knew `.hasOwnProperty()` (the only appreciable difference between our solutions) is expensive, but I had no idea it was that expensive!
Ryan Tenney
@Ryan W Tenney: Actually, yours is faster by about 10ms! I made a mistake in my code which was making a for loop not do anything. I've updated the post.
CD Sanchez
+7  A: 

Here's a pretty terse regex-based solution:

str3 = str2 + "," + str1;

while ((temp = str3.replace(/\b([a-z]+)(=.*)\b\1=[^,]*,?/, "$1$2")) != str3) {
    str3 = temp;
}

It works by prepending the overriding string to the overridable string, then repeatedly stripping out duplicates that occur later in the joined string until there aren't any.

A little more care will be needed if either string could be empty, or if any kind of escaping is permitted, or if the keys can be named other than with letters only.

Sean
+1 Very nice approach to this problem.
Ryan Tenney
Thanks Sean. This is what I had envisioned. The regex concept is interesting, but is it more efficient?
Andres
@Andres: I suspect it probably is, but I couldn't say for sure without benchmarking it. Unless this is an operation that you're carrying out thousands of times, any but the most grossly inefficient approach should be fine.
Sean
@Andres: I added some benchmarks, but I would have to agree with Sean that a few milliseconds here and there won't make a difference. Although I personally would try to avoid this particular solution because I suck at regular expressions and it would be hard for me to maintain it if it came to it.
CD Sanchez
The only change I would make is replacing the word boundary `\b` before the `\1` with a comma `/\b([a-z]+)(=.*),\1=[^,]*,?/`
Ryan Tenney
It looks like, in this situation, the most terse solution is also the fastest solution.
CD Sanchez
What does the \1 mean here?
Andres
@Andres: The \1 is a backreference. It matches whatever the first parenthesized group matched. Here, the first parenthesized group is a word that comes before an equals sign.
Sean
Oh ok... It's so hard to find a tutorial for backreference in JavaScript
Andres
Javascript's backreferences work the same as any other regex engine's. Any good generic regular expression documentation should cover them in detail.
Sean
@sean any suggestions on a good tutorial. I've done a google search and found http://www.regular-expressions.info/javascript.html but it lacks examples.
Andres
I can't easily find a page with plentiful examples of backreference usage, but http://download.oracle.com/javase/tutorial/essential/regex/groups.html has a good simple example that might get you started.
Sean