Skip to content

Commit

Permalink
First commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Mr0grog committed May 12, 2012
0 parents commit 079a184
Show file tree
Hide file tree
Showing 10 changed files with 1,581 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -0,0 +1 @@
node_modules/*
87 changes: 87 additions & 0 deletions ES6StringFormat-tests.js
@@ -0,0 +1,87 @@
/**
* Tests for ES6 string formatting proposal:
* http://wiki.ecmascript.org/doku.php?id=strawman:string_format_take_two
*
* (c) 2012 Rob Brackett (rob@robbrackett.com)
* This code is free to use under the terms of the accompanying LICENSE.txt file
*/

// Util
this.assertLogger = this.assertLogger || function (pass, message) {
console[pass ? "log" : "error"](message);
};

var assertEquals = function (value, expected, message) {
if (expected === value) {
assertLogger(true, "PASS: " + (message || expected));
}
else {
assertLogger(false, "FAIL: " + (message || "") +
"\nExpected: " + expected +
"\nResult: " + value);
}
};


// The tests
assertEquals("{0}'s father has two sons, {0} and {1}.".format("tom", "jerry"),
"tom's father has two sons, tom and jerry.",
"Formatting with indices substitues the argument of the given index.");

assertEquals("The car is made by {brand} in year {make}.".format({brand: "Nissan", make: 2009}),
"The car is made by Nissan in year 2009.",
"Formatting with names substitutes the value of the given key from the first argument.");

var car = {brand: "Nissan", make: 2009};
assertEquals("The car is made by {0.brand} in year {0.make}.".format(car),
"The car is made by Nissan in year 2009.",
"Deep substitutions work with dot-delimited identifiers.");

assertEquals("The car is made by {0[brand]} in year {0[make]}.".format(car),
"The car is made by Nissan in year 2009.",
"Deep substitutions work with square-bracket-delimited identifiers that are names.");

assertEquals("This year’s top two Japanese brands are {0[0]} and {0[1]}.".format(["Honda", "Toyota"]),
"This year’s top two Japanese brands are Honda and Toyota.",
"Deep substitutions work with square-bracket-delimited identifiers that are numbers.");

assertEquals("This is a {very.very.deep} substitution.".format({very: {very: {deep: "very deep"}}}),
"This is a very deep substitution.",
"Substitutions work with a long series of dot-delimited identifiers.");

assertEquals("This is a {very[very][deep]} substitution.".format({very: {very: {deep: "very deep"}}}),
"This is a very deep substitution.",
"Substitutions work with long series of square-bracket-delimited identifiers.");

assertEquals("This is a {very.very[very]} {very[very].deep} substitution.".format({very: {very: {very: "very", deep: "deep"}}}),
"This is a very deep substitution.",
"Substitutions work with long series of mixed dot- and square-bracket-delimited identifiers.");

assertEquals("{0}".format(5), "5", "Positive numbers are replaced without a sign.");
assertEquals("{0}".format(-5), "-5", "Negative numbers are replaced with a sign.");
assertEquals("{0:+}".format(5), "+5", "The '+' specifier causes positive numbers to be replaced with a sign.");
assertEquals("{0:4}".format(5), " 5", "A width format specifier right-aligns the number.");
assertEquals("{0:04}".format(5), "0005", "A '0' flag and width format specifier pads with 0s instead of spaces.");
assertEquals("{0:-4}".format(5), "5 ", "A negative width format specifier left-aligns the number.");
assertEquals("{0:-04}".format(5), "5 ", "A negative width format specifier with a '0' flag still pads with spaces.");
assertEquals("{0:+4}".format(5), " +5", "A plus and width format specifier right-aligns the number with a plus sign.");
assertEquals("{0:+04}".format(5), "+005", "A plus with the '0' flag puts padding after the plus sign.");
assertEquals("{0:.4}".format(5), "5.0000", "A precision specifier results in at least that number of digits after the decimal.");
assertEquals("{0:.4}".format(5.14326), "5.14326", "A precision specifier does not truncate a number that is more precise.");
assertEquals("{0:b}".format(10), "1010", "The 'b' type converts a number to binary.")
assertEquals("{0:o}".format(10), "12", "The 'o' type converts a number to octal.")
assertEquals("{0:x}".format(10), "a", "The 'x' type converts a number to lower-case hexadecimal.")
assertEquals("{0:X}".format(10), "A", "The 'X' type converts a number to upper-case hexadecimal.")

assertEquals("Number {0} can be presented as decimal {0:d}, octex {0:o}, hex {0:x}".format(56),
"Number 56 can be presented as decimal 56, octex 70, hex 38",
"Basic format specifiers for numbers work.");

assertEquals("Number formatter: {0} formatted string: {1:{0}}".format("03d", 56),
"Number formatter: 03d formatted string: 056",
"Format specifiers can be identifiers themselves.");

assertEquals("Curly {0} can be {{ escaped }} by doubling }}".format("brackets"),
"Curly brackets can be { escaped } by doubling }",
"Curly brackets can be escaped by doubling them up.");

51 changes: 51 additions & 0 deletions ES6StringFormat.html
@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>ES6 String.format() Shim</title>
<script src="ES6StringFormat.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript" charset="utf-8">
this.assertLogger = function (pass, message) {
console[pass ? "log" : "error"](message);

var domResult = document.createElement("li");
domResult.innerHTML = message.split("\n").join("<br/>");
domResult.className = pass ? "pass" : "fail";
document.getElementById("results").appendChild(domResult);
};
</script>
<style type="text/css" media="screen">
body {
font-family: Helvetica, arial, sans-serif;
}

#results {
margin: 0;
padding: 0;
list-style-type: none;
font-family: Monaco, Courier, sans-serif;
}

#results li {
padding: 0.5em;
}

.pass {
background: #dfd;
}

.fail {
color: #fff;
background: #a00;
}
</style>
</head>
<body>

<h1>ECMAScript 6 <code>String.format()</code> Proposal Implementation</h1>
<p>See the proposal at: <a href="http://wiki.ecmascript.org/doku.php?id=strawman:string_format_take_two">http://wiki.ecmascript.org/doku.php?id=strawman:string_format_take_two</a></p>
<ul id="results"></ul>
<script src="ES6StringFormat-tests.js" type="text/javascript" charset="utf-8"></script>

</body>
</html>
242 changes: 242 additions & 0 deletions ES6StringFormat.js
@@ -0,0 +1,242 @@
/**
* An experimental (and propably very inefficient) implementation of
* the string formatting proposal from ECMAScript 6.
* Proposal: http://wiki.ecmascript.org/doku.php?id=strawman:string_format_take_two
*
* It's not quite complete; some number formatting isn't yet there:
* - The '#' flag isn't supported
* - e/E format specifier isn't supported
* - g/G format specifier isn't supported
* - Capitalization is incorrect with f/F
*
* (c) 2012 Rob Brackett (rob@robbrackett.com)
* This code is free to use under the terms of the accompanying LICENSE.txt file
*/

(function (exports) {

// regex for separating the various parts of an identifier from each other
var identifierIdentifier = /^(?:\[([^\.\[\]]+)\]|\.?([^\.\[\]]+))(.*)$/
// convert an identifier into the actual value that will be substituted
var findByPath = function (path, data, top) {
var identifiers = path.match(identifierIdentifier);
if (!identifiers) {
throw "Invalid identifier: " + path;
}
var key = identifiers[1] || identifiers[2];
// For the first identifier, named keys are a shortcut to "0.key"
if (top && !isFinite(key)) {
data = data[0];
}
var value = data[key];
// recurse as necessary
return identifiers[3] ? findByPath(identifiers[3], value) : value;
};

// the actual format function
var format = function (template, data) {
// NOTE: other versions of this algorithm are in performance/parsing-algorithms.js
// Generally, this version performs best on small-ish strings and a regex-based
// versions performs best on very large strings. Since people will probably use
// more complicated templating libraries for big strings, we use the small-string
// optimized version here.

var args = Array.prototype.slice.call(arguments, 1);

var outputBuffer = "";
var tokenBuffer = "";
// true if we are currently buffering a token that will be replaced
var bufferingToken = false;
// true if we've encountered a specifier that will be replaced
var specifierIsReplaced = false;
// track the {identifier:specifier} replacement parts
var identifier, specifier;

// walk the template
for (var i = 0, length = template.length; i < length; i++) {
var current = template.charAt(i);

if (bufferingToken) {
// ":" designates end of identifier, start of specifier
if (current === ":") {
identifier = tokenBuffer;
tokenBuffer = "";
// if the first character of the specifier is "{", assume it is replaced
if (template.charAt(i + 1) === "{") {
specifierIsReplaced = true;
i += 1;
}
}
// end of token
else if (current === "}") {
// if we've already captured an identifier, the buffer contains the specifier
// (see check for ":" above)
if (identifier) {
specifier = tokenBuffer;
}
else {
identifier = tokenBuffer;
}

var foundValue = findByPath(identifier, args, true);
// if a specifier is an identifier itself, do the replacement and
// if we're dealing with a replaced specifier, we should end with a "}}"
// so deal with the second "}"
if (specifierIsReplaced) {
specifier = findByPath(specifier, args, true);
i += 1;
}

// format the value
outputBuffer += formatValue(foundValue, specifier);

// cleanup
bufferingToken = false;
specifierIsReplaced = false;
specifier = identifier = null;
}
// non-special characters
else {
tokenBuffer += current;
}
}
// when not buffering a token
else if (current === "{") {
// doubled up {{ is an escape sequence for {
if (template.charAt(i + 1) === "{") {
outputBuffer += current;
i += 1;
}
// otherwise start buffering a new token
else {
tokenBuffer = "";
bufferingToken = true;
}
}
// doubled up }} is an escape sequence for }
// TODO: what should happen when encountering a single "}"?
else if (current === "}" && template.charAt(i + 1) === "}") {
outputBuffer += "}";
i += 1;
}
// non-special character
else {
outputBuffer += current;
}
}

return outputBuffer;
};

var formatValue = function (value, specifier) {
if (!value) return "";
if (value.toFormat) return value.toFormat(specifier);
if (typeof(value) === "number") return numberToFormat(value, specifier);
return value.toString();
};

var numberToFormat = function (value, specifier) {
if (!specifier) {
return value.toString();
}
var formatters = specifier.match(/^([\+\-#0]*)(\d*)(?:\.(\d+))?(.*)$/);
var flags = formatters[1],
width = formatters[2],
precision = formatters[3],
type = formatters[4];

var repeatCharacter = function (character, times) {
var result = "";
while (times--) {
result += character;
}
return result;
}

var applyPrecision = function (result) {
if (precision) {
var afterDecimal = result.split(".")[1];
var extraPrecision = precision - afterDecimal;
if (isNaN(extraPrecision)) {
extraPrecision = precision;
}
if (extraPrecision > 0) {
if (result.indexOf(".") === -1) {
result += ".";
}
for (; extraPrecision > 0; extraPrecision--) {
result += "0";
}
}
}
return result;
}

var result = "";
switch (type) {
case "d":
result = Math.round(value - 0.5).toString(10);
result = applyPrecision(result);
break;
case "x":
result = Math.round(value - 0.5).toString(16);
break;
case "X":
result = Math.round(value - 0.5).toString(16).toUpperCase();
break;
case "b":
result = Math.round(value - 0.5).toString(2);
break;
case "o":
result = Math.round(value - 0.5).toString(8);
break;
// TODO: e,E,g,G types
// not quite clear on whether g/G ignores the precision specifier
case "f":
case "F":
// TODO: proper case for NaN, Infinity
// proposal talks about INF and INFINITY, but not sure when each would be used :\
default:
result = value.toString(10);
result = applyPrecision(result);
}

if (~flags.indexOf("+")) {
if (value >= 0) {
result = "+" + result;
}
}

if (width && result.length < width) {
// "-" flag is right-fill
if (~flags.indexOf("-")) {
result += repeatCharacter(" ", width - result.length);
}
else {
var padding = repeatCharacter(~flags.indexOf("0") ? "0" : " ", width - result.length);
if (~flags.indexOf("0") && (result[0] === "+" || result[0] === "-")) {
result = result[0] + padding + result.slice(1);
}
else {
result = padding + result;
}
}
}
// TODO: # flag
return result;
};

// exports
exports.format = format;

// prototype modification
String.prototype.format = function (data) {
var args = [this].concat(Array.prototype.slice.call(arguments));
return format.apply(null, args);
};

Number.prototype.toFormat = function (specifier) {
return numberToFormat.call(null, this, specifier);
};

})(typeof(exports) !== "undefined" ? exports : this);

0 comments on commit 079a184

Please sign in to comment.