Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

util: add numericSeparator to util.inspect #41003

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,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 @@ -606,6 +609,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 @@ -754,6 +760,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 @@ -1370,13 +1375,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 @@ -1400,9 +1453,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 @@ -1519,8 +1572,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 @@ -1864,8 +1918,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 @@ -1888,6 +1942,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 @@ -1909,9 +1979,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 @@ -1931,11 +2001,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 @@ -1952,21 +2022,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'
);
}