12
\$\begingroup\$

I was bored over the last couple of days and wrote up a string interpolation library for JavaScript.

I'm very pleased with its functionality, it passes it 79 tests cross browser and the comments and README seem fine too.

My main concern here are the regular expressions. I'm not a real pro in that area so I suspect there could be some enhancement to them.

Another thing is readability of the code as well as how good the comments are. I'd like to have "unclear" sections of the code pointed out so I can improve the naming / comments.

The library brings with it the Formatter factory and the FormatError.

Basic usage

> format = Formatter()
> format('Good {} Sir {}.', 'morning', 'Lancelot')
'Good morning Sir Lancelot.'
> format('Good {time} Sir {name}.', 'morning', 'Lancelot')
'Good morning Sir Lancelot.'
> format('Good {0} Sir {1}.', ['morning', 'Lancelot'])
'Good morning Sir Lancelot.'

Source

(function(undefined) {
 'use strict';
 // Formatter factory
 function Formatter(formats) {
 function f() {
 return format(f.formats, arguments);
 }
 f.formats = formats || {};
 return f;
 }
 // Default formatters
 Formatter.formats = {
 repeat: repeat,
 join: function(value, str) {
 return value.join(str || ', ');
 },
 upper: function(value) {
 return value.toUpperCase();
 },
 lower: function(value) {
 return value.toLowerCase();
 },
 lpad: function(value, length, str) {
 return pad(value, length, str, 'l');
 },
 rpad: function(value, length, str) {
 return pad(value, length, str, 'r');
 },
 pad: function(value, length, str) {
 return pad(value, length, str);
 },
 surround: function(value, left, right) {
 return left + value + (right || left);
 },
 hex: function(value, lead) {
 return (lead ? '0x' : '') + value.toString(16);
 },
 bin: function(value, lead) {
 return (lead ? '0b' : '') + value.toString(2);
 }
 };
 function repeat(value, count) {
 return new Array((count || 0) + 1).join(value || ' ');
 }
 function pad(value, length, str, mode) {
 value = '' + value;
 str = str || ' ';
 var len = length - value.length;
 if (len < 0) {
 return value;
 } else if (mode === 'l') {
 return repeat(str, len) + value;
 } else if (mode === 'r') {
 return value + repeat(str, len);
 } else {
 return repeat(str, len - ~~(len / 2))
 + value
 + repeat(str, ~~(len / 2));
 }
 }
 // match {} placholders like {0}, {name}, {} and the inner "{{foo}}"
 // {} can be escaped with \
 var replaceExp = /([^\\]|^)\{([^\{\}]*[^\\^\}]|)\}/g,
 // match things like: foo[0].test["test"]['test]
 accessExp = /^\.?([^\.\[]+)|\[((-?\d+)|('|")(.*?[^\\])4円)\]/,
 // match :foo and :foo(.*?)
 formatExp = /\:([a-zA-Z]+)(\((.*?)\))?(\:|$)/,
 // match arguments: "test", 12, -12, 'test', true, false
 // strings can contain escaped characters like \"
 argumentsExp = /^(,|^)\s*?((true|false|(-?\d+))|('|")(.*?([^\\]|5円))5円)/;
 // Main formatting function
 function format(formatters, args) {
 // Setup magic!
 var string = args[0],
 first = args[1],
 argsLength = args.length - 2,
 type = first != null ? {}.toString.call(first).slice(8, -1) : '',
 arrayLength = first ? first.length - 1 : 0,
 autoIndex = 0;
 function replace(value, pre, form) {
 // Extract formatters
 var formats = [], format = null, id = form;
 while (format = form.match(formatExp)) {
 if (formats.length === 0) {
 id = form.substring(0, format.index);
 }
 form = form.substring(format[0].length - 1);
 formats.push(format);
 }
 // In case of a valid number use it for indexing
 var num = (isNaN(+id) || id === '') ? null : +id;
 // Handle objects
 if (type === 'Object' && id !== '') {
 // Handle plain keys
 if (id.indexOf('.') === -1 && id.indexOf('[') === -1) {
 if (first[id] !== undefined) {
 value = first[id];
 // fall back to obj.toString()
 } else {
 value = args[1 + autoIndex];
 }
 // Access properties
 } else {
 value = getProperty(first, id);
 }
 // Handle given array indexes
 } else if (type === 'Array' && num !== null) {
 value = first[num >= 0 ? num : arrayLength + num];
 // Handle given arguments indexes
 } else if (num !== null) {
 value = args[1 + (num >= 0 ? num : argsLength + num)];
 // Handle automatic arguments indexes
 } else {
 value = args[1 + autoIndex];
 }
 autoIndex++;
 // Apply formats
 while (format = formats.shift()) {
 var method = (formatters[format[1]] ? formatters : Formatter.formats)[format[1]];
 if (method) {
 value = method.apply(undefined,
 getArguments(value, format[3] || ''));
 } else {
 throw new FormatError(
 replace, 'Undefined formatter "{}".', format[1]
 );
 }
 }
 return pre + value;
 }
 return string.replace(replaceExp, replace);
 }
 // Get a specific peoperty of an object based on a accessor string
 function getProperty(obj, id) {
 var m, pos = 0;
 while (m = id.substring(pos).match(accessExp)) {
 // .name / [0] / ["test"]
 var prop = m[1] || (m[3] ? +m[3] : m[5].replace('\\' + m[4], m[4]));
 if (obj === undefined) {
 throw new FormatError(
 getProperty,
 'Cannot access property "{}" of undefined.', prop
 );
 } else {
 obj = obj[prop];
 }
 pos += m[0].length;
 }
 return obj;
 }
 // Convert a string like:
 // true, false, -1, 34, 'foo', "bla\" foo"
 //
 // Into a list of arguments:
 // [true, false, -1, 34, 'foo', 'bla" foo']
 function getArguments(value, string) {
 var m, pos = 0, args = [value];
 while (m = string.substring(pos).match(argumentsExp)) {
 // number
 args.push(m[4] ? +m[4]
 // boolean
 : (m[3] ? m[3] === 'true'
 // string
 : m[6].replace('\\' + m[5], m[5])));
 pos += m[0].length;
 }
 return args;
 }
 // Formatting error type
 function FormatError(func, msg, value) {
 this.name = 'FormatError';
 this.message = format(Formatter.formats, [msg, value]);
 if (Error.captureStackTrace) {
 Error.captureStackTrace(this, func);
 }
 }
 FormatError.prototype = new Error();
 // Exports
 var exp = typeof window === 'undefined' ? exports : window;
 exp.Formatter = Formatter;
 exp.FormatError = FormatError;
})();
Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Feb 19, 2011 at 19:06
\$\endgroup\$
0

1 Answer 1

3
\$\begingroup\$

I have not looked at your regexes in detail. Could you please explain them? The rest of the code could use better documentation as well.

I did find two serious code correctness issues:

  1. Your :join() formatter will not work with the empty string as the delimiter.

    return value.join(str || ', '); // Boolean('') === false
    
  2. Your :pad() formatter, when used for center padding, will not correctly add an odd number of padding characters, as the padding on either side is rounded down.

    } else {
     return repeat(str, len - ~~(len / 2))
     + value
     + repeat(str, ~~(len / 2));
    }
    

    In general, using tricks such as ~~ tends to reduce the code's clarity, and this is an excellent example of that. You should use the Math.floor() and Math.ceil() functions instead if that is what you intend.

Fix these issues, and of course add corresponding tests. Also document the fact that :pad() and the other padding functions are only intended to work with a single padding character.

answered Feb 22, 2011 at 23:16
\$\endgroup\$
3
  • \$\begingroup\$ Good catches. :pad() actually works with odd numbers, maybe I fixed that after I last updated the post. The :join() problem uncovered some bugs with the parsing of empty strings in arguments, that's also fixed and tested for now. I also replaced the ~~ floor shortcut. I'll add the note and try to document the regular expressions, JS unfortunately doesn't allow them to be split over multiple lines :( \$\endgroup\$ Commented Feb 23, 2011 at 12:48
  • \$\begingroup\$ @IvoWetzel You can have large comment blocks explaining how they work though: github.com/derobins/wmd/blob/master/showdown.js#L161 \$\endgroup\$ Commented Feb 25, 2011 at 9:01
  • \$\begingroup\$ Added the fixed / changed code to the question. There are also 7 new tests accompanying the changes. @YiJiang I gave the regex commenting a try, could you tell me whether the format is useful? \$\endgroup\$ Commented Feb 25, 2011 at 11:29

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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.