Skip to content

Commit 91df200

Browse files
BridgeARdanielleadams
authored andcommitted
util: add numericSeparator to util.inspect
This adds the `numericSeparator` option to util.inspect. Using it separates numbers by thousands adding the underscore accordingly. Signed-off-by: Ruben Bridgewater <[email protected]> PR-URL: #41003 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Michaël Zasso <[email protected]> Reviewed-By: Rich Trott <[email protected]>
1 parent 6f0ec98 commit 91df200

File tree

4 files changed

+194
-23
lines changed

4 files changed

+194
-23
lines changed

doc/api/util.md

+21
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,9 @@ stream.write('With ES6');
485485
<!-- YAML
486486
added: v0.3.0
487487
changes:
488+
- version: REPLACEME
489+
pr-url: https://github.com/nodejs/node/pull/41003
490+
description: The `numericSeparator` option is supported now.
488491
- version:
489492
- v14.6.0
490493
- v12.19.0
@@ -606,6 +609,9 @@ changes:
606609
set to `'set'`, only getters with a corresponding setter are inspected.
607610
This might cause side effects depending on the getter function.
608611
**Default:** `false`.
612+
* `numericSeparator` {boolean} If set to `true`, an underscore is used to
613+
separate every three digits in all bigints and numbers.
614+
**Default:** `false`.
609615
* Returns: {string} The representation of `object`.
610616

611617
The `util.inspect()` method returns a string representation of `object` that is
@@ -754,6 +760,21 @@ assert.strict.equal(
754760
);
755761
```
756762

763+
The `numericSeparator` option adds an underscore every three digits to all
764+
numbers.
765+
766+
```js
767+
const { inspect } = require('util');
768+
769+
const thousand = 1_000;
770+
const million = 1_000_000;
771+
const bigNumber = 123_456_789n;
772+
const bigDecimal = 1_234.123_45;
773+
774+
console.log(thousand, million, bigNumber, bigDecimal);
775+
// 1_000 1_000_000 123_456_789n 1_234.123_45
776+
```
777+
757778
`util.inspect()` is a synchronous method intended for debugging. Its maximum
758779
output length is approximately 128 MB. Inputs that result in longer output will
759780
be truncated.

lib/internal/util/inspect.js

+93-23
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ const {
2626
MathMin,
2727
MathRound,
2828
MathSqrt,
29+
MathTrunc,
2930
Number,
31+
NumberIsFinite,
3032
NumberIsNaN,
3133
NumberParseFloat,
3234
NumberParseInt,
@@ -168,7 +170,8 @@ const inspectDefaultOptions = ObjectSeal({
168170
breakLength: 80,
169171
compact: 3,
170172
sorted: false,
171-
getters: false
173+
getters: false,
174+
numericSeparator: false,
172175
});
173176

174177
const kObjectType = 0;
@@ -244,6 +247,7 @@ function getUserOptions(ctx, isCrossContext) {
244247
compact: ctx.compact,
245248
sorted: ctx.sorted,
246249
getters: ctx.getters,
250+
numericSeparator: ctx.numericSeparator,
247251
...ctx.userOptions
248252
};
249253

@@ -301,7 +305,8 @@ function inspect(value, opts) {
301305
breakLength: inspectDefaultOptions.breakLength,
302306
compact: inspectDefaultOptions.compact,
303307
sorted: inspectDefaultOptions.sorted,
304-
getters: inspectDefaultOptions.getters
308+
getters: inspectDefaultOptions.getters,
309+
numericSeparator: inspectDefaultOptions.numericSeparator,
305310
};
306311
if (arguments.length > 1) {
307312
// Legacy...
@@ -949,7 +954,7 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
949954
formatter = formatArrayBuffer;
950955
} else if (keys.length === 0 && protoProps === undefined) {
951956
return prefix +
952-
`{ byteLength: ${formatNumber(ctx.stylize, value.byteLength)} }`;
957+
`{ byteLength: ${formatNumber(ctx.stylize, value.byteLength, false)} }`;
953958
}
954959
braces[0] = `${prefix}{`;
955960
ArrayPrototypeUnshift(keys, 'byteLength');
@@ -1434,13 +1439,61 @@ function handleMaxCallStackSize(ctx, err, constructorName, indentationLvl) {
14341439
assert.fail(err.stack);
14351440
}
14361441

1437-
function formatNumber(fn, value) {
1438-
// Format -0 as '-0'. Checking `value === -0` won't distinguish 0 from -0.
1439-
return fn(ObjectIs(value, -0) ? '-0' : `${value}`, 'number');
1442+
function addNumericSeparator(integerString) {
1443+
let result = '';
1444+
let i = integerString.length;
1445+
const start = integerString.startsWith('-') ? 1 : 0;
1446+
for (; i >= start + 4; i -= 3) {
1447+
result = `_${integerString.slice(i - 3, i)}${result}`;
1448+
}
1449+
return i === integerString.length ?
1450+
integerString :
1451+
`${integerString.slice(0, i)}${result}`;
1452+
}
1453+
1454+
function addNumericSeparatorEnd(integerString) {
1455+
let result = '';
1456+
let i = 0;
1457+
for (; i < integerString.length - 3; i += 3) {
1458+
result += `${integerString.slice(i, i + 3)}_`;
1459+
}
1460+
return i === 0 ?
1461+
integerString :
1462+
`${result}${integerString.slice(i)}`;
1463+
}
1464+
1465+
function formatNumber(fn, number, numericSeparator) {
1466+
if (!numericSeparator) {
1467+
// Format -0 as '-0'. Checking `number === -0` won't distinguish 0 from -0.
1468+
if (ObjectIs(number, -0)) {
1469+
return fn('-0', 'number');
1470+
}
1471+
return fn(`${number}`, 'number');
1472+
}
1473+
const integer = MathTrunc(number);
1474+
const string = String(integer);
1475+
if (integer === number) {
1476+
if (!NumberIsFinite(number) || string.includes('e')) {
1477+
return fn(string, 'number');
1478+
}
1479+
return fn(`${addNumericSeparator(string)}`, 'number');
1480+
}
1481+
if (NumberIsNaN(number)) {
1482+
return fn(string, 'number');
1483+
}
1484+
return fn(`${
1485+
addNumericSeparator(string)
1486+
}.${
1487+
addNumericSeparatorEnd(String(number).slice(string.length + 1))
1488+
}`, 'number');
14401489
}
14411490

1442-
function formatBigInt(fn, value) {
1443-
return fn(`${value}n`, 'bigint');
1491+
function formatBigInt(fn, bigint, numericSeparator) {
1492+
const string = String(bigint);
1493+
if (!numericSeparator) {
1494+
return fn(`${string}n`, 'bigint');
1495+
}
1496+
return fn(`${addNumericSeparator(string)}n`, 'bigint');
14441497
}
14451498

14461499
function formatPrimitive(fn, value, ctx) {
@@ -1464,9 +1517,9 @@ function formatPrimitive(fn, value, ctx) {
14641517
return fn(strEscape(value), 'string') + trailer;
14651518
}
14661519
if (typeof value === 'number')
1467-
return formatNumber(fn, value);
1520+
return formatNumber(fn, value, ctx.numericSeparator);
14681521
if (typeof value === 'bigint')
1469-
return formatBigInt(fn, value);
1522+
return formatBigInt(fn, value, ctx.numericSeparator);
14701523
if (typeof value === 'boolean')
14711524
return fn(`${value}`, 'boolean');
14721525
if (typeof value === 'undefined')
@@ -1583,8 +1636,9 @@ function formatTypedArray(value, length, ctx, ignored, recurseTimes) {
15831636
const elementFormatter = value.length > 0 && typeof value[0] === 'number' ?
15841637
formatNumber :
15851638
formatBigInt;
1586-
for (let i = 0; i < maxLength; ++i)
1587-
output[i] = elementFormatter(ctx.stylize, value[i]);
1639+
for (let i = 0; i < maxLength; ++i) {
1640+
output[i] = elementFormatter(ctx.stylize, value[i], ctx.numericSeparator);
1641+
}
15881642
if (remaining > 0) {
15891643
output[maxLength] = `... ${remaining} more item${remaining > 1 ? 's' : ''}`;
15901644
}
@@ -1928,8 +1982,8 @@ function tryStringify(arg) {
19281982
if (!CIRCULAR_ERROR_MESSAGE) {
19291983
try {
19301984
const a = {}; a.a = a; JSONStringify(a);
1931-
} catch (err) {
1932-
CIRCULAR_ERROR_MESSAGE = firstErrorLine(err);
1985+
} catch (circularError) {
1986+
CIRCULAR_ERROR_MESSAGE = firstErrorLine(circularError);
19331987
}
19341988
}
19351989
if (err.name === 'TypeError' &&
@@ -1952,6 +2006,22 @@ function formatWithOptions(inspectOptions, ...args) {
19522006
return formatWithOptionsInternal(inspectOptions, args);
19532007
}
19542008

2009+
function formatNumberNoColor(number, options) {
2010+
return formatNumber(
2011+
stylizeNoColor,
2012+
number,
2013+
options?.numericSeparator ?? inspectDefaultOptions.numericSeparator
2014+
);
2015+
}
2016+
2017+
function formatBigIntNoColor(bigint, options) {
2018+
return formatBigInt(
2019+
stylizeNoColor,
2020+
bigint,
2021+
options?.numericSeparator ?? inspectDefaultOptions.numericSeparator
2022+
);
2023+
}
2024+
19552025
function formatWithOptionsInternal(inspectOptions, args) {
19562026
const first = args[0];
19572027
let a = 0;
@@ -1973,9 +2043,9 @@ function formatWithOptionsInternal(inspectOptions, args) {
19732043
case 115: // 's'
19742044
const tempArg = args[++a];
19752045
if (typeof tempArg === 'number') {
1976-
tempStr = formatNumber(stylizeNoColor, tempArg);
2046+
tempStr = formatNumberNoColor(tempArg, inspectOptions);
19772047
} else if (typeof tempArg === 'bigint') {
1978-
tempStr = `${tempArg}n`;
2048+
tempStr = formatBigIntNoColor(tempArg, inspectOptions);
19792049
} else if (typeof tempArg !== 'object' ||
19802050
tempArg === null ||
19812051
!hasBuiltInToString(tempArg)) {
@@ -1995,11 +2065,11 @@ function formatWithOptionsInternal(inspectOptions, args) {
19952065
case 100: // 'd'
19962066
const tempNum = args[++a];
19972067
if (typeof tempNum === 'bigint') {
1998-
tempStr = `${tempNum}n`;
2068+
tempStr = formatBigIntNoColor(tempNum, inspectOptions);
19992069
} else if (typeof tempNum === 'symbol') {
20002070
tempStr = 'NaN';
20012071
} else {
2002-
tempStr = formatNumber(stylizeNoColor, Number(tempNum));
2072+
tempStr = formatNumberNoColor(Number(tempNum), inspectOptions);
20032073
}
20042074
break;
20052075
case 79: // 'O'
@@ -2016,21 +2086,21 @@ function formatWithOptionsInternal(inspectOptions, args) {
20162086
case 105: // 'i'
20172087
const tempInteger = args[++a];
20182088
if (typeof tempInteger === 'bigint') {
2019-
tempStr = `${tempInteger}n`;
2089+
tempStr = formatBigIntNoColor(tempInteger, inspectOptions);
20202090
} else if (typeof tempInteger === 'symbol') {
20212091
tempStr = 'NaN';
20222092
} else {
2023-
tempStr = formatNumber(stylizeNoColor,
2024-
NumberParseInt(tempInteger));
2093+
tempStr = formatNumberNoColor(
2094+
NumberParseInt(tempInteger), inspectOptions);
20252095
}
20262096
break;
20272097
case 102: // 'f'
20282098
const tempFloat = args[++a];
20292099
if (typeof tempFloat === 'symbol') {
20302100
tempStr = 'NaN';
20312101
} else {
2032-
tempStr = formatNumber(stylizeNoColor,
2033-
NumberParseFloat(tempFloat));
2102+
tempStr = formatNumberNoColor(
2103+
NumberParseFloat(tempFloat), inspectOptions);
20342104
}
20352105
break;
20362106
case 99: // 'c'

test/parallel/test-util-format.js

+31
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,37 @@ assert.strictEqual(
7474
'1180591620717411303424n 12345678901234567890123n'
7575
);
7676

77+
{
78+
const { numericSeparator } = util.inspect.defaultOptions;
79+
util.inspect.defaultOptions.numericSeparator = true;
80+
81+
assert.strictEqual(
82+
util.format('%d', 1180591620717411303424),
83+
'1.1805916207174113e+21'
84+
);
85+
86+
assert.strictEqual(
87+
util.format(
88+
'%d %s %i', 118059162071741130342, 118059162071741130342, 123_123_123),
89+
'118_059_162_071_741_140_000 118_059_162_071_741_140_000 123_123_123'
90+
);
91+
92+
assert.strictEqual(
93+
util.format(
94+
'%d %s',
95+
1_180_591_620_717_411_303_424n,
96+
12_345_678_901_234_567_890_123n
97+
),
98+
'1_180_591_620_717_411_303_424n 12_345_678_901_234_567_890_123n'
99+
);
100+
101+
assert.strictEqual(
102+
util.format('%i', 1_180_591_620_717_411_303_424n),
103+
'1_180_591_620_717_411_303_424n'
104+
);
105+
106+
util.inspect.defaultOptions.numericSeparator = numericSeparator;
107+
}
77108
// Integer format specifier
78109
assert.strictEqual(util.format('%i'), '%i');
79110
assert.strictEqual(util.format('%i', 42.0), '42');

test/parallel/test-util-inspect.js

+49
Original file line numberDiff line numberDiff line change
@@ -3104,3 +3104,52 @@ assert.strictEqual(
31043104
"{ ['__proto__']: { a: 1 } }"
31053105
);
31063106
}
3107+
3108+
{
3109+
const { numericSeparator } = util.inspect.defaultOptions;
3110+
util.inspect.defaultOptions.numericSeparator = true;
3111+
3112+
assert.strictEqual(
3113+
util.inspect(1234567891234567891234),
3114+
'1.234567891234568e+21'
3115+
);
3116+
assert.strictEqual(
3117+
util.inspect(123456789.12345678),
3118+
'123_456_789.123_456_78'
3119+
);
3120+
3121+
assert.strictEqual(util.inspect(10_000_000), '10_000_000');
3122+
assert.strictEqual(util.inspect(1_000_000), '1_000_000');
3123+
assert.strictEqual(util.inspect(100_000), '100_000');
3124+
assert.strictEqual(util.inspect(99_999.9), '99_999.9');
3125+
assert.strictEqual(util.inspect(9_999), '9_999');
3126+
assert.strictEqual(util.inspect(999), '999');
3127+
assert.strictEqual(util.inspect(NaN), 'NaN');
3128+
assert.strictEqual(util.inspect(Infinity), 'Infinity');
3129+
assert.strictEqual(util.inspect(-Infinity), '-Infinity');
3130+
3131+
assert.strictEqual(
3132+
util.inspect(new Float64Array([100_000_000])),
3133+
'Float64Array(1) [ 100_000_000 ]'
3134+
);
3135+
assert.strictEqual(
3136+
util.inspect(new BigInt64Array([9_100_000_100n])),
3137+
'BigInt64Array(1) [ 9_100_000_100n ]'
3138+
);
3139+
3140+
assert.strictEqual(
3141+
util.inspect(123456789),
3142+
'123_456_789'
3143+
);
3144+
assert.strictEqual(
3145+
util.inspect(123456789n),
3146+
'123_456_789n'
3147+
);
3148+
3149+
util.inspect.defaultOptions.numericSeparator = numericSeparator;
3150+
3151+
assert.strictEqual(
3152+
util.inspect(123456789.12345678, { numericSeparator: true }),
3153+
'123_456_789.123_456_78'
3154+
);
3155+
}

0 commit comments

Comments
 (0)