Skip to content

Commit

Permalink
Add formatSpecifier.
Browse files Browse the repository at this point in the history
  • Loading branch information
mbostock committed Jun 14, 2015
1 parent 5db403a commit 5423f29
Show file tree
Hide file tree
Showing 19 changed files with 864 additions and 674 deletions.
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,33 @@ Equivalent to [*locale*.format](#locale_format) on the default U.S. English loca

Equivalent to [*locale*.formatPrefix](#locale_formatPrefix) on the default U.S. English locale. Use [localeFormat](#localeFormat) to specify a different locale.

<a name="formatSpecifier" href="#formatSpecifier">#</a> <b>formatSpecifier</b>(<i>specifier</i>)

Parses the specified *specifier*, returning an object with exposed fields that correspond to the [format specification mini-language](#locale_format). For example, `formatSpecifier("s")` returns:

```js
{
"fill": " ",
"align": ">",
"sign": "-",
"symbol": "",
"zero": false,
"width": undefined,
"comma": false,
"precision": 6,
"type": "s"
}
```

This method is useful for understanding how format specifiers are parsed, and for deriving new specifiers. For example, you might compute an appropriate precision based on the numbers you want to format, set the precision, and then create a new format:

```js
var s = formatSpecifier("f");
s.precision = 2;
var f = format(s);
f(42); // "42.00";
```

<a name="locale_format" href="#locale_format">#</a> <i>locale</i>.<b>format</b>(<i>specifier</i>)

Returns a new format function with the given string *specifier*. The returned function takes a number as the only argument, and returns a string representing the formatted number. The format specifier is modeled after Python 3’s [format specification mini-language](https://docs.python.org/3/library/string.html#format-specification-mini-language). The general form of a specifier is:
Expand Down
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import formatSpecifier from "./src/formatSpecifier";
import locale from "./src/format-en-US";
import localeFormat from "./src/localeFormat";

export var format = locale.format;
export var formatPrefix = locale.formatPrefix;

export {
formatSpecifier,
localeFormat
};
55 changes: 55 additions & 0 deletions src/formatSpecifier.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// [[fill]align][sign][symbol][0][width][,][.precision][type]
var re = /(?:(.)?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?([a-z%])?/i;

export default function(specifier) {
return new FormatSpecifier(specifier);
};

function FormatSpecifier(specifier) {
var match = re.exec(specifier),
fill = match[1] || " ",
align = match[2] || ">",
sign = match[3] || "-",
symbol = match[4] || "",
zero = !!match[5],
width = match[6] && +match[6],
comma = !!match[7],
precision = match[8] && +match[8].slice(1),
type = match[9] || "";

// The "n" type is an alias for ",g".
if (type === "n") comma = true, type = "g";

// Map invalid types to the default format.
else if (!/[%bcdefgoprsXx]/.test(type)) type = "";

// If zero fill is specified, padding goes after sign and before digits.
if (zero || (fill === "0" && align === "=")) zero = fill = "0", align = "=";

// Set the default precision if not specified.
// Note that we don’t clamp the precision here to the allowed range,
// mainly because of formatPrefix treating "s" precision as fixed.
if (precision == null) precision = type ? 6 : 12;

this.fill = fill;
this.align = align;
this.sign = sign;
this.symbol = symbol;
this.zero = zero;
this.width = width;
this.comma = comma;
this.precision = precision;
this.type = type;
}

FormatSpecifier.prototype.toString = function() {
return this.fill
+ this.align
+ this.sign
+ this.symbol
+ (this.zero ? "0" : "")
+ (this.width == null ? "" : this.width | 0)
+ (this.comma ? "," : "")
+ (this.precision == null ? "" : "." + (this.precision | 0))
+ this.type;
};
72 changes: 29 additions & 43 deletions src/localeFormat.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import formatGroup from "./formatGroup";
import formatRounded from "./formatRounded";
import formatRoundedPercentage from "./formatRoundedPercentage";
import formatSpecifier from "./formatSpecifier";
import {default as formatAutoPrefix, exponent} from "./formatAutoPrefix";

// [[fill]align][sign][symbol][0][width][,][.precision][type]
var re = /(?:(.)?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?([a-z%])?/i,
prefixes = ["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];
var prefixes = ["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];

var formatTypes = {
" ": function(x, p) { return x.toPrecision(p).replace(/(?:\.|(\.\d+?))0+(e|$)/, "$1$2"); },
"": function(x, p) { return x.toPrecision(p).replace(/(?:\.|(\.\d+?))0+(e|$)/, "$1$2"); },
"%": function(x, p) { return (x * 100).toFixed(p); },
"b": function(x) { return x.toString(2); },
"c": function(x) { return String.fromCharCode(x); },
Expand All @@ -34,39 +33,20 @@ export default function(locale) {
decimal = locale.decimal;

function format(specifier) {
var match = re.exec(specifier),
fill = match[1] || " ",
align = match[2] || ">",
sign = match[3] || "-",
symbol = match[4] || "",
zero = match[5],
width = +match[6],
comma = match[7],
precision = match[8],
type = match[9];

// The "n" type is an alias for ",g".
if (type === "n") comma = true, type = "g";

// Map invalid types to the default format.
else if (!(type in formatTypes)) type = " ";

// If zero fill is specified, padding goes after sign and before digits.
if (zero || (fill === "0" && align === "=")) zero = fill = "0", align = "=";

// Clamp the specified precision to the supported range.
// For significant precision, it must be in [1, 21].
// For fixed precision, it must be in [0, 20].
if (precision) {
precision = +precision.slice(1);
precision = /[gprs]/.test(type)
? Math.max(1, Math.min(21, precision))
: Math.max(0, Math.min(20, precision));
} else {
precision = type === " " ? 12 : 6;
}

// Compute the fixed prefix and suffix.
specifier = formatSpecifier(specifier);

var fill = specifier.fill,
align = specifier.align,
sign = specifier.sign,
symbol = specifier.symbol,
zero = specifier.zero,
width = specifier.width,
comma = specifier.comma,
precision = specifier.precision,
type = specifier.type;

// Compute the prefix and suffix.
// For SI-prefix, the suffix is lazily computed.
var prefix = symbol === "$" ? currency[0] : symbol === "#" && /[boxX]/.test(type) ? "0" + type.toLowerCase() : "",
suffix = symbol === "$" ? currency[1] : /[%p]/.test(type) ? "%" : "";

Expand All @@ -75,9 +55,16 @@ export default function(locale) {
// Can this type generate exponential notation?
var formatType = formatTypes[type],
integer = /[bcdoxX]/.test(type),
maybeExponent = /[ deg]/.test(type),
maybeExponent = !type || /[deg]/.test(type),
maybeDecimal = maybeExponent || /[fprs%]/.test(type);

// Clamp the specified precision to the supported range.
// For significant precision, it must be in [1, 21].
// For fixed precision, it must be in [0, 20].
precision = /[gprs]/.test(type)
? Math.max(1, Math.min(21, precision))
: Math.max(0, Math.min(20, precision));

return function(value) {
value = +value;

Expand Down Expand Up @@ -133,12 +120,11 @@ export default function(locale) {
}

function formatPrefix(specifier, prefix) {
var match = re.exec(specifier),
prefixIndex = prefixes.indexOf(prefix),
scale = Math.pow(10, (prefixIndex < 0 ? (prefix = "", 0) : 8 - prefixIndex) * 3),
f = (match[0] = match[2] = null, match[9] = "f", format(match.join("")));
var f = format((specifier = formatSpecifier(specifier), specifier.type = "f", specifier)),
i = prefixes.indexOf(prefix),
k = Math.pow(10, (i < 0 ? (prefix = "", 0) : 8 - i) * 3);
return function(value) {
return f(scale * value) + prefix;
return f(k * value) + prefix;
};
}

Expand Down
Loading

0 comments on commit 5423f29

Please sign in to comment.