Skip to content

Commit 6828fbb

Browse files
BridgeARaddaleax
authored andcommitted
util: group array elements together
When using `util.inspect()` with `compact` mode set to a number, all array entries exceeding 6 are going to be grouped together into logical parts. PR-URL: #26269 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Anna Henningsen <anna@addaleax.net>
1 parent 4500ed8 commit 6828fbb

File tree

3 files changed

+341
-36
lines changed

3 files changed

+341
-36
lines changed

doc/api/util.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -451,9 +451,9 @@ changes:
451451
to be displayed on a new line. It will also add new lines to text that is
452452
longer than `breakLength`. If set to a number, the most `n` inner elements
453453
are united on a single line as long as all properties fit into
454-
`breakLength`. Note that no text will be reduced below 16 characters, no
455-
matter the `breakLength` size. For more information, see the example below.
456-
**Default:** `true`.
454+
`breakLength`. Short array elements are also grouped together. Note that no
455+
text will be reduced below 16 characters, no matter the `breakLength` size.
456+
For more information, see the example below. **Default:** `true`.
457457
* `sorted` {boolean|Function} If set to `true` or a function, all properties
458458
of an object, and `Set` and `Map` entries are sorted in the resulting
459459
string. If set to `true` the [default sort][] is used. If set to a function,

lib/internal/util/inspect.js

+145-33
Original file line numberDiff line numberDiff line change
@@ -785,8 +785,35 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
785785
}
786786
}
787787

788-
const combine = typeof ctx.compact === 'number' &&
789-
ctx.currentDepth - recurseTimes < ctx.compact;
788+
let combine = false;
789+
if (typeof ctx.compact === 'number') {
790+
// Memorize the original output length. In case the the output is grouped,
791+
// prevent lining up the entries on a single line.
792+
const entries = output.length;
793+
// Group array elements together if the array contains at least six separate
794+
// entries.
795+
if (extrasType === kArrayExtrasType && output.length > 6) {
796+
output = groupArrayElements(ctx, output);
797+
}
798+
// `ctx.currentDepth` is set to the most inner depth of the currently
799+
// inspected object part while `recurseTimes` is the actual current depth
800+
// that is inspected.
801+
//
802+
// Example:
803+
//
804+
// const a = { first: [ 1, 2, 3 ], second: { inner: [ 1, 2, 3 ] } }
805+
//
806+
// The deepest depth of `a` is 2 (a.second.inner) and `a.first` has a max
807+
// depth of 1.
808+
//
809+
// Consolidate all entries of the local most inner depth up to
810+
// `ctx.compact`, as long as the properties are smaller than
811+
// `ctx.breakLength`.
812+
if (ctx.currentDepth - recurseTimes < ctx.compact &&
813+
entries === output.length) {
814+
combine = true;
815+
}
816+
}
790817

791818
const res = reduceToSingleString(ctx, output, base, braces, combine);
792819
const budget = ctx.budget[ctx.indentationLvl] || 0;
@@ -805,6 +832,83 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
805832
return res;
806833
}
807834

835+
function groupArrayElements(ctx, output) {
836+
let totalLength = 0;
837+
let maxLength = 0;
838+
let i = 0;
839+
const dataLen = new Array(output.length);
840+
// Calculate the total length of all output entries and the individual max
841+
// entries length of all output entries. We have to remove colors first,
842+
// otherwise the length would not be calculated properly.
843+
for (; i < output.length; i++) {
844+
const len = ctx.colors ? removeColors(output[i]).length : output[i].length;
845+
dataLen[i] = len;
846+
totalLength += len;
847+
if (maxLength < len)
848+
maxLength = len;
849+
}
850+
// Add two to `maxLength` as we add a single whitespace character plus a comma
851+
// in-between two entries.
852+
const actualMax = maxLength + 2;
853+
// Check if at least three entries fit next to each other and prevent grouping
854+
// of arrays that contains entries of very different length (i.e., if a single
855+
// entry is longer than 1/5 of all other entries combined). Otherwise the
856+
// space in-between small entries would be enormous.
857+
if (actualMax * 3 + ctx.indentationLvl < ctx.breakLength &&
858+
(totalLength / maxLength > 5 || maxLength <= 6)) {
859+
860+
const approxCharHeights = 2.5;
861+
const bias = 1;
862+
// Dynamically check how many columns seem possible.
863+
const columns = Math.min(
864+
// Ideally a square should be drawn. We expect a character to be about 2.5
865+
// times as high as wide. This is the area formula to calculate a square
866+
// which contains n rectangles of size `actualMax * approxCharHeights`.
867+
// Divide that by `actualMax` to receive the correct number of columns.
868+
// The added bias slightly increases the columns for short entries.
869+
Math.round(
870+
Math.sqrt(
871+
approxCharHeights * (actualMax - bias) * output.length
872+
) / (actualMax - bias)
873+
),
874+
// Limit array grouping for small `compact` modes as the user requested
875+
// minimal grouping.
876+
ctx.compact * 3,
877+
// Limit the columns to a maximum of ten.
878+
10
879+
);
880+
// Return with the original output if no grouping should happen.
881+
if (columns <= 1) {
882+
return output;
883+
}
884+
// Calculate the maximum length of all entries that are visible in the first
885+
// column of the group.
886+
const tmp = [];
887+
let firstLineMaxLength = dataLen[0];
888+
for (i = columns; i < dataLen.length; i += columns) {
889+
if (dataLen[i] > firstLineMaxLength)
890+
firstLineMaxLength = dataLen[i];
891+
}
892+
// Each iteration creates a single line of grouped entries.
893+
for (i = 0; i < output.length; i += columns) {
894+
// Calculate extra color padding in case it's active. This has to be done
895+
// line by line as some lines might contain more colors than others.
896+
let colorPadding = output[i].length - dataLen[i];
897+
// Add padding to the first column of the output.
898+
let str = output[i].padStart(firstLineMaxLength + colorPadding, ' ');
899+
// The last lines may contain less entries than columns.
900+
const max = Math.min(i + columns, output.length);
901+
for (var j = i + 1; j < max; j++) {
902+
colorPadding = output[j].length - dataLen[j];
903+
str += `, ${output[j].padStart(maxLength + colorPadding, ' ')}`;
904+
}
905+
tmp.push(str);
906+
}
907+
output = tmp;
908+
}
909+
return output;
910+
}
911+
808912
function handleMaxCallStackSize(ctx, err, constructor, tag, indentationLvl) {
809913
if (isStackOverflowError(err)) {
810914
ctx.seen.pop();
@@ -1196,50 +1300,58 @@ function formatProperty(ctx, value, recurseTimes, key, type) {
11961300
return `${name}:${extra}${str}`;
11971301
}
11981302

1303+
function isBelowBreakLength(ctx, output, start) {
1304+
// Each entry is separated by at least a comma. Thus, we start with a total
1305+
// length of at least `output.length`. In addition, some cases have a
1306+
// whitespace in-between each other that is added to the total as well.
1307+
let totalLength = output.length + start;
1308+
if (totalLength + output.length > ctx.breakLength)
1309+
return false;
1310+
for (var i = 0; i < output.length; i++) {
1311+
if (ctx.colors) {
1312+
totalLength += removeColors(output[i]).length;
1313+
} else {
1314+
totalLength += output[i].length;
1315+
}
1316+
if (totalLength > ctx.breakLength) {
1317+
return false;
1318+
}
1319+
}
1320+
return true;
1321+
}
1322+
11991323
function reduceToSingleString(ctx, output, base, braces, combine = false) {
1200-
const breakLength = ctx.breakLength;
1201-
let i = 0;
12021324
if (ctx.compact !== true) {
12031325
if (combine) {
1204-
const totalLength = output.reduce((sum, cur) => sum + cur.length, 0);
1205-
if (totalLength + output.length * 2 < breakLength) {
1206-
let res = `${base ? `${base} ` : ''}${braces[0]} `;
1207-
for (; i < output.length - 1; i++) {
1208-
res += `${output[i]}, `;
1209-
}
1210-
res += `${output[i]} ${braces[1]}`;
1211-
return res;
1326+
// Line up all entries on a single line in case the entries do not exceed
1327+
// `breakLength`. Add 10 as constant to start next to all other factors
1328+
// that may reduce `breakLength`.
1329+
const start = output.length + ctx.indentationLvl +
1330+
braces[0].length + base.length + 10;
1331+
if (isBelowBreakLength(ctx, output, start)) {
1332+
return `${base ? `${base} ` : ''}${braces[0]} ${join(output, ', ')} ` +
1333+
braces[1];
12121334
}
12131335
}
1336+
// Line up each entry on an individual line.
12141337
const indentation = `\n${' '.repeat(ctx.indentationLvl)}`;
1215-
let res = `${base ? `${base} ` : ''}${braces[0]}${indentation} `;
1216-
for (; i < output.length - 1; i++) {
1217-
res += `${output[i]},${indentation} `;
1218-
}
1219-
res += `${output[i]}${indentation}${braces[1]}`;
1220-
return res;
1338+
return `${base ? `${base} ` : ''}${braces[0]}${indentation} ` +
1339+
`${join(output, `,${indentation} `)}${indentation}${braces[1]}`;
12211340
}
1222-
if (output.length * 2 <= breakLength) {
1223-
let length = 0;
1224-
for (; i < output.length && length <= breakLength; i++) {
1225-
if (ctx.colors) {
1226-
length += removeColors(output[i]).length + 1;
1227-
} else {
1228-
length += output[i].length + 1;
1229-
}
1230-
}
1231-
if (length <= breakLength)
1232-
return `${braces[0]}${base ? ` ${base}` : ''} ${join(output, ', ')} ` +
1233-
braces[1];
1341+
// Line up all entries on a single line in case the entries do not exceed
1342+
// `breakLength`.
1343+
if (isBelowBreakLength(ctx, output, 0)) {
1344+
return `${braces[0]}${base ? ` ${base}` : ''} ${join(output, ', ')} ` +
1345+
braces[1];
12341346
}
1347+
const indentation = ' '.repeat(ctx.indentationLvl);
12351348
// If the opening "brace" is too large, like in the case of "Set {",
12361349
// we need to force the first item to be on the next line or the
12371350
// items will not line up correctly.
1238-
const indentation = ' '.repeat(ctx.indentationLvl);
12391351
const ln = base === '' && braces[0].length === 1 ?
12401352
' ' : `${base ? ` ${base}` : ''}\n${indentation} `;
1241-
const str = join(output, `,\n${indentation} `);
1242-
return `${braces[0]}${ln}${str} ${braces[1]}`;
1353+
// Line up each entry on an individual line.
1354+
return `${braces[0]}${ln}${join(output, `,\n${indentation} `)} ${braces[1]}`;
12431355
}
12441356

12451357
module.exports = {

0 commit comments

Comments
 (0)