Skip to content

Commit

Permalink
util: group array elements together
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
BridgeAR authored and addaleax committed Mar 1, 2019
1 parent 4500ed8 commit 6828fbb
Show file tree
Hide file tree
Showing 3 changed files with 341 additions and 36 deletions.
6 changes: 3 additions & 3 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,9 +451,9 @@ changes:
to be displayed on a new line. It will also add new lines to text that is
longer than `breakLength`. If set to a number, the most `n` inner elements
are united on a single line as long as all properties fit into
`breakLength`. Note that no text will be reduced below 16 characters, no
matter the `breakLength` size. For more information, see the example below.
**Default:** `true`.
`breakLength`. Short array elements are also grouped together. Note that no
text will be reduced below 16 characters, no matter the `breakLength` size.
For more information, see the example below. **Default:** `true`.
* `sorted` {boolean|Function} If set to `true` or a function, all properties
of an object, and `Set` and `Map` entries are sorted in the resulting
string. If set to `true` the [default sort][] is used. If set to a function,
Expand Down
178 changes: 145 additions & 33 deletions lib/internal/util/inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -785,8 +785,35 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
}
}

const combine = typeof ctx.compact === 'number' &&
ctx.currentDepth - recurseTimes < ctx.compact;
let combine = false;
if (typeof ctx.compact === 'number') {
// Memorize the original output length. In case the the output is grouped,
// prevent lining up the entries on a single line.
const entries = output.length;
// Group array elements together if the array contains at least six separate
// entries.
if (extrasType === kArrayExtrasType && output.length > 6) {
output = groupArrayElements(ctx, output);
}
// `ctx.currentDepth` is set to the most inner depth of the currently
// inspected object part while `recurseTimes` is the actual current depth
// that is inspected.
//
// Example:
//
// const a = { first: [ 1, 2, 3 ], second: { inner: [ 1, 2, 3 ] } }
//
// The deepest depth of `a` is 2 (a.second.inner) and `a.first` has a max
// depth of 1.
//
// Consolidate all entries of the local most inner depth up to
// `ctx.compact`, as long as the properties are smaller than
// `ctx.breakLength`.
if (ctx.currentDepth - recurseTimes < ctx.compact &&
entries === output.length) {
combine = true;
}
}

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

function groupArrayElements(ctx, output) {
let totalLength = 0;
let maxLength = 0;
let i = 0;
const dataLen = new Array(output.length);
// Calculate the total length of all output entries and the individual max
// entries length of all output entries. We have to remove colors first,
// otherwise the length would not be calculated properly.
for (; i < output.length; i++) {
const len = ctx.colors ? removeColors(output[i]).length : output[i].length;
dataLen[i] = len;
totalLength += len;
if (maxLength < len)
maxLength = len;
}
// Add two to `maxLength` as we add a single whitespace character plus a comma
// in-between two entries.
const actualMax = maxLength + 2;
// Check if at least three entries fit next to each other and prevent grouping
// of arrays that contains entries of very different length (i.e., if a single
// entry is longer than 1/5 of all other entries combined). Otherwise the
// space in-between small entries would be enormous.
if (actualMax * 3 + ctx.indentationLvl < ctx.breakLength &&
(totalLength / maxLength > 5 || maxLength <= 6)) {

const approxCharHeights = 2.5;
const bias = 1;
// Dynamically check how many columns seem possible.
const columns = Math.min(
// Ideally a square should be drawn. We expect a character to be about 2.5
// times as high as wide. This is the area formula to calculate a square
// which contains n rectangles of size `actualMax * approxCharHeights`.
// Divide that by `actualMax` to receive the correct number of columns.
// The added bias slightly increases the columns for short entries.
Math.round(
Math.sqrt(
approxCharHeights * (actualMax - bias) * output.length
) / (actualMax - bias)
),
// Limit array grouping for small `compact` modes as the user requested
// minimal grouping.
ctx.compact * 3,
// Limit the columns to a maximum of ten.
10
);
// Return with the original output if no grouping should happen.
if (columns <= 1) {
return output;
}
// Calculate the maximum length of all entries that are visible in the first
// column of the group.
const tmp = [];
let firstLineMaxLength = dataLen[0];
for (i = columns; i < dataLen.length; i += columns) {
if (dataLen[i] > firstLineMaxLength)
firstLineMaxLength = dataLen[i];
}
// Each iteration creates a single line of grouped entries.
for (i = 0; i < output.length; i += columns) {
// Calculate extra color padding in case it's active. This has to be done
// line by line as some lines might contain more colors than others.
let colorPadding = output[i].length - dataLen[i];
// Add padding to the first column of the output.
let str = output[i].padStart(firstLineMaxLength + colorPadding, ' ');
// The last lines may contain less entries than columns.
const max = Math.min(i + columns, output.length);
for (var j = i + 1; j < max; j++) {
colorPadding = output[j].length - dataLen[j];
str += `, ${output[j].padStart(maxLength + colorPadding, ' ')}`;
}
tmp.push(str);
}
output = tmp;
}
return output;
}

function handleMaxCallStackSize(ctx, err, constructor, tag, indentationLvl) {
if (isStackOverflowError(err)) {
ctx.seen.pop();
Expand Down Expand Up @@ -1196,50 +1300,58 @@ function formatProperty(ctx, value, recurseTimes, key, type) {
return `${name}:${extra}${str}`;
}

function isBelowBreakLength(ctx, output, start) {
// Each entry is separated by at least a comma. Thus, we start with a total
// length of at least `output.length`. In addition, some cases have a
// whitespace in-between each other that is added to the total as well.
let totalLength = output.length + start;
if (totalLength + output.length > ctx.breakLength)
return false;
for (var i = 0; i < output.length; i++) {
if (ctx.colors) {
totalLength += removeColors(output[i]).length;
} else {
totalLength += output[i].length;
}
if (totalLength > ctx.breakLength) {
return false;
}
}
return true;
}

function reduceToSingleString(ctx, output, base, braces, combine = false) {
const breakLength = ctx.breakLength;
let i = 0;
if (ctx.compact !== true) {
if (combine) {
const totalLength = output.reduce((sum, cur) => sum + cur.length, 0);
if (totalLength + output.length * 2 < breakLength) {
let res = `${base ? `${base} ` : ''}${braces[0]} `;
for (; i < output.length - 1; i++) {
res += `${output[i]}, `;
}
res += `${output[i]} ${braces[1]}`;
return res;
// Line up all entries on a single line in case the entries do not exceed
// `breakLength`. Add 10 as constant to start next to all other factors
// that may reduce `breakLength`.
const start = output.length + ctx.indentationLvl +
braces[0].length + base.length + 10;
if (isBelowBreakLength(ctx, output, start)) {
return `${base ? `${base} ` : ''}${braces[0]} ${join(output, ', ')} ` +
braces[1];
}
}
// Line up each entry on an individual line.
const indentation = `\n${' '.repeat(ctx.indentationLvl)}`;
let res = `${base ? `${base} ` : ''}${braces[0]}${indentation} `;
for (; i < output.length - 1; i++) {
res += `${output[i]},${indentation} `;
}
res += `${output[i]}${indentation}${braces[1]}`;
return res;
return `${base ? `${base} ` : ''}${braces[0]}${indentation} ` +
`${join(output, `,${indentation} `)}${indentation}${braces[1]}`;
}
if (output.length * 2 <= breakLength) {
let length = 0;
for (; i < output.length && length <= breakLength; i++) {
if (ctx.colors) {
length += removeColors(output[i]).length + 1;
} else {
length += output[i].length + 1;
}
}
if (length <= breakLength)
return `${braces[0]}${base ? ` ${base}` : ''} ${join(output, ', ')} ` +
braces[1];
// Line up all entries on a single line in case the entries do not exceed
// `breakLength`.
if (isBelowBreakLength(ctx, output, 0)) {
return `${braces[0]}${base ? ` ${base}` : ''} ${join(output, ', ')} ` +
braces[1];
}
const indentation = ' '.repeat(ctx.indentationLvl);
// If the opening "brace" is too large, like in the case of "Set {",
// we need to force the first item to be on the next line or the
// items will not line up correctly.
const indentation = ' '.repeat(ctx.indentationLvl);
const ln = base === '' && braces[0].length === 1 ?
' ' : `${base ? ` ${base}` : ''}\n${indentation} `;
const str = join(output, `,\n${indentation} `);
return `${braces[0]}${ln}${str} ${braces[1]}`;
// Line up each entry on an individual line.
return `${braces[0]}${ln}${join(output, `,\n${indentation} `)} ${braces[1]}`;
}

module.exports = {
Expand Down
Loading

0 comments on commit 6828fbb

Please sign in to comment.