From dd093920a37e29c629c28d7184b512c95c9bdcae Mon Sep 17 00:00:00 2001 From: Giovanni Date: Fri, 14 Mar 2025 15:56:45 +0100 Subject: [PATCH] util: expose diff function used by the assertion errors fix: https://github.com/nodejs/node/issues/51740 --- benchmark/util/diff.js | 43 +++++++++++++++++ doc/api/util.md | 65 +++++++++++++++++++++++++ lib/internal/assert/myers_diff.js | 37 +++++++------- lib/internal/util/diff.js | 42 ++++++++++++++++ lib/util.js | 6 +++ test/parallel/test-diff.js | 80 +++++++++++++++++++++++++++++++ 6 files changed, 257 insertions(+), 16 deletions(-) create mode 100644 benchmark/util/diff.js create mode 100644 lib/internal/util/diff.js create mode 100644 test/parallel/test-diff.js diff --git a/benchmark/util/diff.js b/benchmark/util/diff.js new file mode 100644 index 00000000000000..3ec024c24df6d0 --- /dev/null +++ b/benchmark/util/diff.js @@ -0,0 +1,43 @@ +'use strict'; + +const util = require('util'); +const common = require('../common'); + +const bench = common.createBenchmark(main, { + n: [1e3], + length: [1e3, 2e3], + scenario: ['identical', 'small-diff', 'medium-diff', 'large-diff'], +}); + +function main({ n, length, scenario }) { + const actual = Array.from({ length }, (_, i) => `${i}`); + let expected; + + switch (scenario) { + case 'identical': // 0% difference + expected = Array.from({ length }, (_, i) => `${i}`); + break; + + case 'small-diff': // ~5% difference + expected = Array.from({ length }, (_, i) => { + return Math.random() < 0.05 ? `modified-${i}` : `${i}`; + }); + break; + + case 'medium-diff': // ~25% difference + expected = Array.from({ length }, (_, i) => { + return Math.random() < 0.25 ? `modified-${i}` : `${i}`; + }); + break; + + case 'large-diff': // ~100% difference + expected = Array.from({ length }, (_, i) => `modified-${i}`); + break; + } + + bench.start(); + for (let i = 0; i < n; i++) { + util.diff(actual, expected); + } + bench.end(n); +} diff --git a/doc/api/util.md b/doc/api/util.md index 8210f33a2a664b..8da882b5e27267 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -325,6 +325,70 @@ The `--throw-deprecation` command-line flag and `process.throwDeprecation` property take precedence over `--trace-deprecation` and `process.traceDeprecation`. +## `util.diff(actual, expected)` + + + +> Stability: 1 - Experimental + +* `actual` {Array|string} The first value to compare + +* `expected` {Array|string} The second value to compare + +* Returns: {Array} An array of difference entries. Each entry is an array with two elements: + * Index 0: {number} Operation code: `-1` for delete, `0` for no-op/unchanged, `1` for insert + * Index 1: {string} The value associated with the operation + +* Algorithm complexity: O(N\*D), where: + +* N is the total length of the two sequences combined (N = actual.length + expected.length) + +* D is the edit distance (the minimum number of operations required to transform one sequence into the other). + +[`util.diff()`][] compares two string or array values and returns an array of difference entries. +It uses the Myers diff algorithm to compute minimal differences, which is the same algorithm +used internally by assertion error messages. + +If the values are equal, an empty array is returned. + +```js +const { diff } = require('node:util'); + +// Comparing strings +const actualString = '12345678'; +const expectedString = '12!!5!7!'; +console.log(diff(actualString, expectedString)); +// [ +// [0, '1'], +// [0, '2'], +// [1, '3'], +// [1, '4'], +// [-1, '!'], +// [-1, '!'], +// [0, '5'], +// [1, '6'], +// [-1, '!'], +// [0, '7'], +// [1, '8'], +// [-1, '!'], +// ] +// Comparing arrays +const actualArray = ['1', '2', '3']; +const expectedArray = ['1', '3', '4']; +console.log(diff(actualArray, expectedArray)); +// [ +// [0, '1'], +// [1, '2'], +// [0, '3'], +// [-1, '4'], +// ] +// Equal values return empty array +console.log(diff('same', 'same')); +// [] +``` + ## `util.format(format[, ...args])`