Skip to content

Commit

Permalink
util: add numericSeparator to util.inspect
Browse files Browse the repository at this point in the history
This adds the `numericSeparator` option to util.inspect. Using it
separates numbers by thousands adding the underscore accordingly.

Signed-off-by: Ruben Bridgewater <ruben@bridgewater.de>

PR-URL: #41003
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Rich Trott <rtrott@gmail.com>
  • Loading branch information
BridgeAR authored and danielleadams committed Feb 1, 2022
1 parent 5f01d03 commit 45a3e24
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 23 deletions.
21 changes: 21 additions & 0 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,9 @@ stream.write('With ES6');
<!-- YAML
added: v0.3.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/41003
description: The `numericSeparator` option is supported now.
- version:
- v14.6.0
- v12.19.0
Expand Down Expand Up @@ -604,6 +607,9 @@ changes:
set to `'set'`, only getters with a corresponding setter are inspected.
This might cause side effects depending on the getter function.
**Default:** `false`.
* `numericSeparator` {boolean} If set to `true`, an underscore is used to
separate every three digits in all bigints and numbers.
**Default:** `false`.
* Returns: {string} The representation of `object`.

The `util.inspect()` method returns a string representation of `object` that is
Expand Down Expand Up @@ -752,6 +758,21 @@ assert.strict.equal(
);
```

The `numericSeparator` option adds an underscore every three digits to all
numbers.

```js
const { inspect } = require('util');

const thousand = 1_000;
const million = 1_000_000;
const bigNumber = 123_456_789n;
const bigDecimal = 1_234.123_45;

console.log(thousand, million, bigNumber, bigDecimal);
// 1_000 1_000_000 123_456_789n 1_234.123_45
```

`util.inspect()` is a synchronous method intended for debugging. Its maximum
output length is approximately 128 MB. Inputs that result in longer output will
be truncated.
Expand Down
116 changes: 93 additions & 23 deletions lib/internal/util/inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ const {
MathMin,
MathRound,
MathSqrt,
MathTrunc,
Number,
NumberIsFinite,
NumberIsNaN,
NumberParseFloat,
NumberParseInt,
Expand Down Expand Up @@ -168,7 +170,8 @@ const inspectDefaultOptions = ObjectSeal({
breakLength: 80,
compact: 3,
sorted: false,
getters: false
getters: false,
numericSeparator: false,
});

const kObjectType = 0;
Expand Down Expand Up @@ -244,6 +247,7 @@ function getUserOptions(ctx, isCrossContext) {
compact: ctx.compact,
sorted: ctx.sorted,
getters: ctx.getters,
numericSeparator: ctx.numericSeparator,
...ctx.userOptions
};

Expand Down Expand Up @@ -301,7 +305,8 @@ function inspect(value, opts) {
breakLength: inspectDefaultOptions.breakLength,
compact: inspectDefaultOptions.compact,
sorted: inspectDefaultOptions.sorted,
getters: inspectDefaultOptions.getters
getters: inspectDefaultOptions.getters,
numericSeparator: inspectDefaultOptions.numericSeparator,
};
if (arguments.length > 1) {
// Legacy...
Expand Down Expand Up @@ -949,7 +954,7 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
formatter = formatArrayBuffer;
} else if (keys.length === 0 && protoProps === undefined) {
return prefix +
`{ byteLength: ${formatNumber(ctx.stylize, value.byteLength)} }`;
`{ byteLength: ${formatNumber(ctx.stylize, value.byteLength, false)} }`;
}
braces[0] = `${prefix}{`;
ArrayPrototypeUnshift(keys, 'byteLength');
Expand Down Expand Up @@ -1434,13 +1439,61 @@ function handleMaxCallStackSize(ctx, err, constructorName, indentationLvl) {
assert.fail(err.stack);
}

function formatNumber(fn, value) {
// Format -0 as '-0'. Checking `value === -0` won't distinguish 0 from -0.
return fn(ObjectIs(value, -0) ? '-0' : `${value}`, 'number');
function addNumericSeparator(integerString) {
let result = '';
let i = integerString.length;
const start = integerString.startsWith('-') ? 1 : 0;
for (; i >= start + 4; i -= 3) {
result = `_${integerString.slice(i - 3, i)}${result}`;
}
return i === integerString.length ?
integerString :
`${integerString.slice(0, i)}${result}`;
}

function addNumericSeparatorEnd(integerString) {
let result = '';
let i = 0;
for (; i < integerString.length - 3; i += 3) {
result += `${integerString.slice(i, i + 3)}_`;
}
return i === 0 ?
integerString :
`${result}${integerString.slice(i)}`;
}

function formatNumber(fn, number, numericSeparator) {
if (!numericSeparator) {
// Format -0 as '-0'. Checking `number === -0` won't distinguish 0 from -0.
if (ObjectIs(number, -0)) {
return fn('-0', 'number');
}
return fn(`${number}`, 'number');
}
const integer = MathTrunc(number);
const string = String(integer);
if (integer === number) {
if (!NumberIsFinite(number) || string.includes('e')) {
return fn(string, 'number');
}
return fn(`${addNumericSeparator(string)}`, 'number');
}
if (NumberIsNaN(number)) {
return fn(string, 'number');
}
return fn(`${
addNumericSeparator(string)
}.${
addNumericSeparatorEnd(String(number).slice(string.length + 1))
}`, 'number');
}

function formatBigInt(fn, value) {
return fn(`${value}n`, 'bigint');
function formatBigInt(fn, bigint, numericSeparator) {
const string = String(bigint);
if (!numericSeparator) {
return fn(`${string}n`, 'bigint');
}
return fn(`${addNumericSeparator(string)}n`, 'bigint');
}

function formatPrimitive(fn, value, ctx) {
Expand All @@ -1464,9 +1517,9 @@ function formatPrimitive(fn, value, ctx) {
return fn(strEscape(value), 'string') + trailer;
}
if (typeof value === 'number')
return formatNumber(fn, value);
return formatNumber(fn, value, ctx.numericSeparator);
if (typeof value === 'bigint')
return formatBigInt(fn, value);
return formatBigInt(fn, value, ctx.numericSeparator);
if (typeof value === 'boolean')
return fn(`${value}`, 'boolean');
if (typeof value === 'undefined')
Expand Down Expand Up @@ -1583,8 +1636,9 @@ function formatTypedArray(value, length, ctx, ignored, recurseTimes) {
const elementFormatter = value.length > 0 && typeof value[0] === 'number' ?
formatNumber :
formatBigInt;
for (let i = 0; i < maxLength; ++i)
output[i] = elementFormatter(ctx.stylize, value[i]);
for (let i = 0; i < maxLength; ++i) {
output[i] = elementFormatter(ctx.stylize, value[i], ctx.numericSeparator);
}
if (remaining > 0) {
output[maxLength] = `... ${remaining} more item${remaining > 1 ? 's' : ''}`;
}
Expand Down Expand Up @@ -1928,8 +1982,8 @@ function tryStringify(arg) {
if (!CIRCULAR_ERROR_MESSAGE) {
try {
const a = {}; a.a = a; JSONStringify(a);
} catch (err) {
CIRCULAR_ERROR_MESSAGE = firstErrorLine(err);
} catch (circularError) {
CIRCULAR_ERROR_MESSAGE = firstErrorLine(circularError);
}
}
if (err.name === 'TypeError' &&
Expand All @@ -1952,6 +2006,22 @@ function formatWithOptions(inspectOptions, ...args) {
return formatWithOptionsInternal(inspectOptions, args);
}

function formatNumberNoColor(number, options) {
return formatNumber(
stylizeNoColor,
number,
options?.numericSeparator ?? inspectDefaultOptions.numericSeparator
);
}

function formatBigIntNoColor(bigint, options) {
return formatBigInt(
stylizeNoColor,
bigint,
options?.numericSeparator ?? inspectDefaultOptions.numericSeparator
);
}

function formatWithOptionsInternal(inspectOptions, args) {
const first = args[0];
let a = 0;
Expand All @@ -1973,9 +2043,9 @@ function formatWithOptionsInternal(inspectOptions, args) {
case 115: // 's'
const tempArg = args[++a];
if (typeof tempArg === 'number') {
tempStr = formatNumber(stylizeNoColor, tempArg);
tempStr = formatNumberNoColor(tempArg, inspectOptions);
} else if (typeof tempArg === 'bigint') {
tempStr = `${tempArg}n`;
tempStr = formatBigIntNoColor(tempArg, inspectOptions);
} else if (typeof tempArg !== 'object' ||
tempArg === null ||
!hasBuiltInToString(tempArg)) {
Expand All @@ -1995,11 +2065,11 @@ function formatWithOptionsInternal(inspectOptions, args) {
case 100: // 'd'
const tempNum = args[++a];
if (typeof tempNum === 'bigint') {
tempStr = `${tempNum}n`;
tempStr = formatBigIntNoColor(tempNum, inspectOptions);
} else if (typeof tempNum === 'symbol') {
tempStr = 'NaN';
} else {
tempStr = formatNumber(stylizeNoColor, Number(tempNum));
tempStr = formatNumberNoColor(Number(tempNum), inspectOptions);
}
break;
case 79: // 'O'
Expand All @@ -2016,21 +2086,21 @@ function formatWithOptionsInternal(inspectOptions, args) {
case 105: // 'i'
const tempInteger = args[++a];
if (typeof tempInteger === 'bigint') {
tempStr = `${tempInteger}n`;
tempStr = formatBigIntNoColor(tempInteger, inspectOptions);
} else if (typeof tempInteger === 'symbol') {
tempStr = 'NaN';
} else {
tempStr = formatNumber(stylizeNoColor,
NumberParseInt(tempInteger));
tempStr = formatNumberNoColor(
NumberParseInt(tempInteger), inspectOptions);
}
break;
case 102: // 'f'
const tempFloat = args[++a];
if (typeof tempFloat === 'symbol') {
tempStr = 'NaN';
} else {
tempStr = formatNumber(stylizeNoColor,
NumberParseFloat(tempFloat));
tempStr = formatNumberNoColor(
NumberParseFloat(tempFloat), inspectOptions);
}
break;
case 99: // 'c'
Expand Down
31 changes: 31 additions & 0 deletions test/parallel/test-util-format.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,37 @@ assert.strictEqual(
'1180591620717411303424n 12345678901234567890123n'
);

{
const { numericSeparator } = util.inspect.defaultOptions;
util.inspect.defaultOptions.numericSeparator = true;

assert.strictEqual(
util.format('%d', 1180591620717411303424),
'1.1805916207174113e+21'
);

assert.strictEqual(
util.format(
'%d %s %i', 118059162071741130342, 118059162071741130342, 123_123_123),
'118_059_162_071_741_140_000 118_059_162_071_741_140_000 123_123_123'
);

assert.strictEqual(
util.format(
'%d %s',
1_180_591_620_717_411_303_424n,
12_345_678_901_234_567_890_123n
),
'1_180_591_620_717_411_303_424n 12_345_678_901_234_567_890_123n'
);

assert.strictEqual(
util.format('%i', 1_180_591_620_717_411_303_424n),
'1_180_591_620_717_411_303_424n'
);

util.inspect.defaultOptions.numericSeparator = numericSeparator;
}
// Integer format specifier
assert.strictEqual(util.format('%i'), '%i');
assert.strictEqual(util.format('%i', 42.0), '42');
Expand Down
49 changes: 49 additions & 0 deletions test/parallel/test-util-inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -3104,3 +3104,52 @@ assert.strictEqual(
"{ ['__proto__']: { a: 1 } }"
);
}

{
const { numericSeparator } = util.inspect.defaultOptions;
util.inspect.defaultOptions.numericSeparator = true;

assert.strictEqual(
util.inspect(1234567891234567891234),
'1.234567891234568e+21'
);
assert.strictEqual(
util.inspect(123456789.12345678),
'123_456_789.123_456_78'
);

assert.strictEqual(util.inspect(10_000_000), '10_000_000');
assert.strictEqual(util.inspect(1_000_000), '1_000_000');
assert.strictEqual(util.inspect(100_000), '100_000');
assert.strictEqual(util.inspect(99_999.9), '99_999.9');
assert.strictEqual(util.inspect(9_999), '9_999');
assert.strictEqual(util.inspect(999), '999');
assert.strictEqual(util.inspect(NaN), 'NaN');
assert.strictEqual(util.inspect(Infinity), 'Infinity');
assert.strictEqual(util.inspect(-Infinity), '-Infinity');

assert.strictEqual(
util.inspect(new Float64Array([100_000_000])),
'Float64Array(1) [ 100_000_000 ]'
);
assert.strictEqual(
util.inspect(new BigInt64Array([9_100_000_100n])),
'BigInt64Array(1) [ 9_100_000_100n ]'
);

assert.strictEqual(
util.inspect(123456789),
'123_456_789'
);
assert.strictEqual(
util.inspect(123456789n),
'123_456_789n'
);

util.inspect.defaultOptions.numericSeparator = numericSeparator;

assert.strictEqual(
util.inspect(123456789.12345678, { numericSeparator: true }),
'123_456_789.123_456_78'
);
}

0 comments on commit 45a3e24

Please sign in to comment.