One of the things that I often end up doing is using JavaScript’s replace function with a regular expression and a callback function. Recently, though, I have been thinking about redeveloping an HTML application (HTA) that I made a long time ago for PCs. This HTA gave me the ability to write regular expressions and replacement strings which could alter the matched groups in ways that normal string substitution doesn’t allow without using callback functions. Let’s take the following file names as examples:

  • 001-the-nephews-come-to-town.mpg
  • 002-scroogey-scroogers.mp4
  • 32-hewi’s-lost-pet.mp4
  • 109-copped-by-the-coppers.mpeg

The file renamer that I use to have would allow these files to be renamed as the following by using /^(\d+)(.+)(?=\.\w+$)/ as the regular expression and "Episode ${1,RLZ} -${2,D2S,PROPER}" as the replacement:

  • Episode 1 – The Nephews Come To Town.mpg
  • Episode 2 – Scroogey Scroogers.mp4
  • Episode 32 – Hewi’s Lost Pet.mp4
  • Episode 109 – Copped By The Coppers.mpeg

As the first step towards achieving my goal, I wrote the following sub function which can accomplish this:

(function() {
  var functions = {};
  var hasOwnProperty = functions.hasOwnProperty;
  this.sub = function(subject, reTarget, strReplacement, objFns) {
    if(!reTarget && !strReplacement && !objFns) {
      for(var key in subject) {
        if(hasOwnProperty.call(subject, key)) {
          functions[key] = subject[key];
        }
      }
    }
    else {
      return subject.replace(reTarget, function(match) {
        var args = arguments;
        var reGroups = [];
        var i = args.length - 2;
        while(--i > 0) {
          reGroups.push(i);
        }
        reGroups = '(' + reGroups.join('|') + ')';
        reGroups = new RegExp('\\$(?:' + reGroups + '|\\{' + reGroups + '((?:,\\w+)*)\\})', 'g');
        return strReplacement.replace(reGroups, function(match, index, index2, fnsToUse) {
          fnsToUse = fnsToUse ? fnsToUse.slice(1).split(',') : [];
          var ret = args[index || index2];
          for(var i = 0, len = fnsToUse.length; i < len; i++) {
            var fnName = fnsToUse[i];
            var fn;
            if((objFns && hasOwnProperty.call(objFns, fnName) && (fn = objFns[fnName]))
                || (hasOwnProperty.call(functions, fnName) && (fn = functions[fnName]))) {
              ret = fn(ret, args);
            }
          }
          return ret;
        });
      });
    }
  };
})();

This function actually acts differently depending on the parameters that are supplied. The first way in which the function can be called is with four parameters:

  1. subject – string:
    The string that is to be modified.
  2. reTarget – RegExp:
    The regular expression used to capture substrings.
  3. strReplacement – string:
    The replacement string which can have the normal captured group expressions (eg. $1, $3, etc.), the new type of captured group expressions (eg. ${1}, ${3,RLZ}, etc.) and/or normal substrings.
  4. objFns – Object:
    Optional object whose keys correspond to functions referenced in the new type of group expressions. Each value should be a function where the first parameter passed to it will be the matched group and the second will be the arguments object which is normally passed to the callback function. The functions should return the string replacement for the captured group expression.
var filename = sub(
  "023-we-are-the-tigers.jpg",
  /^(\d+)(.+)(?=\.\w+$)/,
  "Episode ${1,RLZ} -${2,D2S,PROPER}",
  {
    RLZ: function(match) {
      return match.replace(/^0+(?!$)/, '');
    },
    D2S: function(match) {
      return match.replace(/-/g, ' ');
    },
    PROPER: function(match) {
      return match.toProperCase();
    },
    UPPER: function(match) {
      return match.toUpperCase();
    },
    CHARCODE: function(match) {
      return '<' + match + '=' + match.charCodeAt(0) + '>';
    }
  }
);
alert(filename);  // "Episode 23 - We Are The Tigers.jpg"

var str = sub(
  "abcdefghijklmnopqrstuvwxyz",
  /([aeiou])/g,
  "<${1,UPPER}>",
  {
    UPPER: function(match) {
      return match.toUpperCase();
    }
  }
);
alert(str);  // "<A>bcd<E>fgh<I>jklmn<O>pqrst<U>vwxyz" 

The reason the fourth parameter is optional is because those captured group expression replacement functions could be predefined for all calls to this sub function. If the function is just called with one parameter, that parameter should be the same as the fourth parameter outlined previously. The captured group expression replacement functions will remain persistent for the remainder of the page/program session:

sub({
  RLZ: function(match) {
    return match.replace(/^0+(?!$)/, '');
  },
  D2S: function(match) {
    return match.replace(/-/g, ' ');
  },
  PROPER: function(match) {
    return match.toProperCase();
  }
});
var filenames = [
  "001-the-nephews-come-to-town.mpg",
  "002-scroogey-scroogers.mp4",
  "32-hewi's-lost-pet.mp4",
  "109-copped-by-the-coppers.mpeg"
];
var exp = /^(\d+)(.+)(?=\.\w+$)/;
var replacement = "Episode ${1,RLZ} -${2,D2S,PROPER}";
for(var i = 0; i < filenames.length; i++) {
  filenames[i] = sub(filenames[i], exp, replacement);
}
alert(filenames.join('\n'));

The above code will result in the following filenames:

Episode 1 - The Nephews Come To Town.mpg
Episode 2 - Scroogey Scroogers.mp4
Episode 32 - Hewi's Lost Pet.mp4
Episode 109 - Copped By The Coppers.mpeg

Well, now that I have this code out of the way, hopefully the next step will be to actually make the File Renamer. When I do finish creating it, you can be sure to find it on this blog. 8)


Leave a Reply

Your email address will not be published. Required fields are marked *