1

This function loops through JavaScript nested arrays (recursively) and replaces the strings inside them:

function replaceRecur(tree, str, newStr) {
  for (var i = 1; i < tree.length; i++) {
    if (Array.isArray(tree[i])) {
      replaceRecur(tree[i], str, newStr)
    } else {
      tree[i] = tree[i].replace(str, newStr)
    }
  }
}

Usage example:

function replaceQuotes(tree, callback) {
  var str1 = /"(?=\b)/g
    , str2 = /"(?!\b)/g
    , newStr1 = '“'
    , newStr2 = '”'

  replaceRecur(tree, str1, newStr1)
  replaceRecur(tree, str2, newStr2)

  callback(null, tree) 
}

How should I modify replaceRecur so I allow two values per argument?

Example:

function replaceQuotes(tree, callback) {
  var str = ['/"(?=\b)/g/', '"(?!\b)/g']
    , newStr = '“ ”' // not sure whether to use arrays or strings
                     // what's more common?

  replaceRecur(tree, str, newStr)

  callback(null, tree) 
}

(The reason is, I don't want to repeat replaceRecur, str, and newStr twice. I want to keep the code DRY.)

EDIT:

Example input (just in case):

[ 'markdown',
  [ 'para', '“a paragraph”' ],
  [ 'hr' ],
  [ 'para', '\'another paragraph\'' ],
  [ 'para', 'test--test' ],
  [ 'para', 'test---test' ],
  [ 'bulletlist',
    [ 'listitem', '“a list item”' ],
    [ 'listitem', '“another list item”' ] ] ]
3
  • 2
    I would think that your code is already dry. You are not repeating yourself, you call a function multiple times with different arguments? If at all use a loop in replaceQuotes. Commented Mar 10, 2015 at 16:19
  • 1
    I'm with @Bergi, just calling a function twice doesn't mean you're repeating yourself. And adding complexity to a simple function to avoid it isn't necessarily a good idea. Commented Mar 10, 2015 at 16:20
  • @Bergi Could you please provide an example? Commented Mar 10, 2015 at 16:24

6 Answers 6

3

Create a function that performs the recursive traversal of the structure and invokes callbacks for values. Then you can write your replacement functions as callbacks you pass in to that:

function traverse(tree, callback) {
  for (var i = 0; i < tree.length; ++i) {
    if (Array.isArray(tree[i]))
      traverse(tree[i], callback);
    else
      tree[i] = callback(tree[i]);
  }
}

function replaceTwo(tree, s1from, s1to, s2from, s2to) {
  traverse(tree, function(element) {
    return element.replace(s1from, s1to).replace(s2from, s2to);
  });
}

You can now write all sorts of different functions to transform tree contents without having to rewrite the recursion part.

Sign up to request clarification or add additional context in comments.

1 Comment

plus1 - traverse is much more useful than a replaceRecur that goes deep.
1

Unfortunately there is no overloading in JavaScript.

One clean solution would to use replaceRecur but internally check the typeof arguments to determine whether you want to use _replaceRecur2 or _replaceRecur1

function replaceRecur(tree, arg1, arg2) {
  if(typeof(arg1) === "function"){
    _replaceRecur2(tree, arg1);
  } else {
    _replaceRecur1(tree, arg1, arg2);
  }
}

function _replaceRecur1(tree, str, newStr) {
  for (var i = 1; i < tree.length; i++) {
    if (Array.isArray(tree[i])) {
      _replaceRecur1(tree[i], str, newStr)
    } else {
      tree[i] = tree[i].replace(str, newStr)
    }
  }
}

function _replaceQuotes2(tree, callback) {
  var str1 = /"(?=\b)/g
    , str2 = /"(?!\b)/g
    , newStr1 = '“'
    , newStr2 = '”'

  _replaceRecur1(tree, str1, newStr1)
  _replaceRecur1(tree, str2, newStr2)

  callback(null, tree) 
}

Comments

1

not sure whether to use arrays or strings

Use arrays, you might want to replace multiple things. And you wouldn't alter replaceRecur, it is fine as it is. Rather introduce a new function

function replaceMultipleRecur(tree, strArr, newStrArr) {
    … // (in the simplest occasion a loop over the arrays with calls to replaceRecur)
}
function replaceQuotes(tree) {
    return replaceMultipeRecur([/"(?=\b)/g, /"(?!\b)/g], ['“', '”']);
}

I don't want to repeat replaceRecur, str, and newStr twice

You can simply use your exsting function, by passing a regex that matches all your cases and a replacer callback instead of strings.

function replaceQuotes(tree) {
  replaceRecur(tree, /("\b)|("\B)/g, function(m, l, r) {
      return l ? '“' : '”';
  });
  return tree
}

Comments

1

If you want to abstract your code a bit more, here's one possible way:

function maprec(x, callback) {
    return x.map ? x.map(function(x) {
        return maprec(x, callback);
    }) : callback(x);
}

function pipe() {
    var fns = arguments;
    return function(x) {
        return [].reduce.call(fns, function(a, f) { return f(a) }, x);
    }
}

// test/demo:

tree = ['foo "bar" !', ['baz', ['ccc "hi" ', 'd']], 'eee', ['"foo?"']];

converted = maprec(tree, pipe(
    function(x) { return x.replace(/"(?=\b)/g, '{') },
    function(x) { return x.replace(/"(?=\B)/g, '}') }
));

document.write("<pre>" + JSON.stringify(converted));

Ok, what we did here? First, we define maprec, a recursive mapper, which is the same as map, but respects nested structures. The second utility, pipe, is the function composer that takes a bunch of functions and returns a new function that applies these functions, in order, to the argument, in way similar to unix pipelines like grep | sort | uniq (hence the name). Note that this is different from the usual compose, which is right-associative. Finally, we use maprec(tree, pipe(replacer1, replacer2)) to do the actual job.

(I use {}'s instead of fancy quotes to make them look more obvious in the console window).

To illustrate the power of pipelining, here's a more advanced example:

fancyQuotes = pipe(
    function(x) { return x.replace(/"(?=\b)/g, '&laquo;') },
    function(x) { return x.replace(/"(?=\B)/g, '&raquo;') }
);

trim = "".trim.call.bind("".trim);

wrap = function(x) { return this.replace(/\$/g, x)};
wrap.in = wrap.bind;

converted = maprec(tree, pipe(
    fancyQuotes,
    trim,
    wrap.in("<p>$</p>")));

Comments

0

just alter the else to apply each regex in the array.

else {
    //array is the array of regex's passed
    array.forEach(function(item) {
        tree[i] = tree[i].replace(item, newStr);
    });
}

2 Comments

Perhaps you could put this in context in some way, as is it's fairly unclear and replaces all of the "source" strings with the full target string. If this is meant to follow on from the OP's partial attempt, it fails to do so.
Its not following on, its pseudocode and a step in the right direction
0

Preface: I'm with Bergi re his comment above, just calling a function twice doesn't mean you're repeating yourself. And adding complexity to a simple function to avoid calling it twice isn't necessarily a good idea.


But to your question: You could just add two arguments to the function, ignoring them if not given:

function replaceRecur(tree, str1, newStr1, str2, newStr2) {
  for (var i = 1; i < tree.length; i++) {
    if (Array.isArray(tree[i])) {
      replaceRecur(tree[i], str1, newStr1, str2, newStr2);
    } else {
      tree[i] = tree[i].replace(str1, newStr1);
      if (typeof str2 !== "undefined") {
        tree[i] = tree[i].replace(str2, newStr2);
      }
    }
  }
}

In fact, you can just keep doing this endlessly using a loop.

function replaceRecur(tree, str, newStr/*, str2, newStr2, ...*/) {
  for (var i = 1; i < tree.length; i++) {
    if (Array.isArray(tree[i])) {
      replaceRecur.apply(this, arguments);
    } else {
      for (var arg = 1; arg + 1 < arguments.length; arg += 2) {
        tree[i] = tree[i].replace(arguments[arg], arguments[arg + 1]);
      }
    }
  }
}

Note that on some JavaScript engines using the arguments pseudo-array has a negative impact on performance. On modern engines it doesn't matter at all.

Usage:

replaceRecur(tree, /"(?=\b)/g, '“', /"(?!\b)/g, '”');

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.