UNPKG

locutus

Version:

Locutus other languages' standard libraries to JavaScript for fun and educational purposes

448 lines (447 loc) 15.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ndiff = ndiff; class SequenceMatcher { a = []; b = []; b2j = new Map(); matchingBlocksCache; opcodesCache; isJunk; constructor(isJunk) { this.isJunk = isJunk; } setSeqs(a, b) { this.setSeq1(a); this.setSeq2(b); } setSeq1(a) { this.a = [...a]; this.matchingBlocksCache = undefined; this.opcodesCache = undefined; } setSeq2(b) { this.b = [...b]; this.b2j = new Map(); this.matchingBlocksCache = undefined; this.opcodesCache = undefined; for (let index = 0; index < this.b.length; index++) { const value = this.b[index]; const existing = this.b2j.get(value); if (existing) { existing.push(index); continue; } this.b2j.set(value, [index]); } if (this.isJunk) { for (const [value] of this.b2j) { if (this.isJunk(value)) { this.b2j.delete(value); } } } if (this.b.length >= 200) { const popularThreshold = Math.floor(this.b.length / 100) + 1; for (const [value, indexes] of this.b2j) { if (indexes.length > popularThreshold) { this.b2j.delete(value); } } } } getOpcodes() { if (this.opcodesCache) { return this.opcodesCache; } const out = []; let aIndex = 0; let bIndex = 0; for (const block of this.getMatchingBlocks()) { const aStart = block.aIndex; const bStart = block.bIndex; let tag = null; if (aIndex < aStart && bIndex < bStart) { tag = 'replace'; } else if (aIndex < aStart) { tag = 'delete'; } else if (bIndex < bStart) { tag = 'insert'; } if (tag) { out.push([tag, aIndex, aStart, bIndex, bStart]); } if (block.size > 0) { out.push(['equal', aStart, aStart + block.size, bStart, bStart + block.size]); } aIndex = aStart + block.size; bIndex = bStart + block.size; } this.opcodesCache = out; return out; } ratio() { const total = this.a.length + this.b.length; if (total === 0) { return 1; } const matches = this.getMatchingBlocks().reduce((sum, block) => sum + block.size, 0); return (2 * matches) / total; } quickRatio() { const total = this.a.length + this.b.length; if (total === 0) { return 1; } const bCounts = new Map(); for (const value of this.b) { bCounts.set(value, (bCounts.get(value) ?? 0) + 1); } let matches = 0; for (const value of this.a) { const count = bCounts.get(value) ?? 0; if (count > 0) { matches += 1; bCounts.set(value, count - 1); } } return (2 * matches) / total; } realQuickRatio() { const total = this.a.length + this.b.length; if (total === 0) { return 1; } return (2 * Math.min(this.a.length, this.b.length)) / total; } getMatchingBlocks() { if (this.matchingBlocksCache) { return this.matchingBlocksCache; } const queue = [[0, this.a.length, 0, this.b.length]]; const out = []; while (queue.length > 0) { const next = queue.pop(); if (!next) { continue; } const [alo, ahi, blo, bhi] = next; const match = this.findLongestMatch(alo, ahi, blo, bhi); if (match.size === 0) { continue; } out.push(match); if (alo < match.aIndex && blo < match.bIndex) { queue.push([alo, match.aIndex, blo, match.bIndex]); } const aMatchEnd = match.aIndex + match.size; const bMatchEnd = match.bIndex + match.size; if (aMatchEnd < ahi && bMatchEnd < bhi) { queue.push([aMatchEnd, ahi, bMatchEnd, bhi]); } } out.sort((left, right) => left.aIndex - right.aIndex || left.bIndex - right.bIndex); const merged = []; for (const block of out) { const previous = merged.at(-1); if (previous && previous.aIndex + previous.size === block.aIndex && previous.bIndex + previous.size === block.bIndex) { previous.size += block.size; continue; } merged.push({ ...block }); } merged.push({ aIndex: this.a.length, bIndex: this.b.length, size: 0 }); this.matchingBlocksCache = merged; return merged; } findLongestMatch(alo, ahi, blo, bhi) { let bestI = alo; let bestJ = blo; let bestSize = 0; let j2len = new Map(); for (let i = alo; i < ahi; i++) { const newj2len = new Map(); const indexes = this.b2j.get(this.a[i]) ?? []; for (const j of indexes) { if (j < blo) { continue; } if (j >= bhi) { break; } const size = (j2len.get(j - 1) ?? 0) + 1; newj2len.set(j, size); if (size > bestSize) { bestI = i - size + 1; bestJ = j - size + 1; bestSize = size; } } j2len = newj2len; } while (bestI > alo && bestJ > blo && !this.isJunk?.(this.b[bestJ - 1]) && this.a[bestI - 1] === this.b[bestJ - 1]) { bestI -= 1; bestJ -= 1; bestSize += 1; } while (bestI + bestSize < ahi && bestJ + bestSize < bhi && !this.isJunk?.(this.b[bestJ + bestSize]) && this.a[bestI + bestSize] === this.b[bestJ + bestSize]) { bestSize += 1; } while (bestI > alo && bestJ > blo && !!this.isJunk?.(this.b[bestJ - 1]) && this.a[bestI - 1] === this.b[bestJ - 1]) { bestI -= 1; bestJ -= 1; bestSize += 1; } while (bestI + bestSize < ahi && bestJ + bestSize < bhi && !!this.isJunk?.(this.b[bestJ + bestSize]) && this.a[bestI + bestSize] === this.b[bestJ + bestSize]) { bestSize += 1; } return { aIndex: bestI, bIndex: bestJ, size: bestSize }; } } class Differ { linejunk; charjunk; constructor(linejunk, charjunk) { this.linejunk = linejunk; this.charjunk = charjunk; } compare(a, b) { const out = []; const cruncher = new SequenceMatcher(this.linejunk); cruncher.setSeqs(a, b); for (const [tag, alo, ahi, blo, bhi] of cruncher.getOpcodes()) { if (tag === 'replace') { out.push(...this.fancyReplace(a, alo, ahi, b, blo, bhi)); continue; } if (tag === 'delete') { out.push(...this.dump('-', a, alo, ahi)); continue; } if (tag === 'insert') { out.push(...this.dump('+', b, blo, bhi)); continue; } out.push(...this.dump(' ', a, alo, ahi)); } return out; } dump(tag, source, lo, hi) { const out = []; for (let index = lo; index < hi; index++) { out.push(`${tag} ${source[index]}`); } return out; } plainReplace(a, alo, ahi, b, blo, bhi) { const first = bhi - blo < ahi - alo ? this.dump('+', b, blo, bhi) : this.dump('-', a, alo, ahi); const second = bhi - blo < ahi - alo ? this.dump('-', a, alo, ahi) : this.dump('+', b, blo, bhi); return [...first, ...second]; } fancyReplace(a, alo, ahi, b, blo, bhi) { let bestRatio = 0.74; const cutoff = 0.75; const cruncher = new SequenceMatcher(this.charjunk); let equalI; let equalJ; let bestI = alo; let bestJ = blo; for (let j = blo; j < bhi; j++) { const right = b[j] ?? ''; cruncher.setSeq2(Array.from(right)); for (let i = alo; i < ahi; i++) { const left = a[i] ?? ''; if (left === right) { if (equalI === undefined) { equalI = i; equalJ = j; } continue; } cruncher.setSeq1(Array.from(left)); if (cruncher.realQuickRatio() > bestRatio && cruncher.quickRatio() > bestRatio) { const ratio = cruncher.ratio(); if (ratio > bestRatio) { bestRatio = ratio; bestI = i; bestJ = j; } } } } if (bestRatio < cutoff) { if (equalI === undefined || equalJ === undefined) { return this.plainReplace(a, alo, ahi, b, blo, bhi); } bestI = equalI; bestJ = equalJ; } else { equalI = undefined; equalJ = undefined; } const out = this.fancyHelper(a, alo, bestI, b, blo, bestJ); const leftLine = a[bestI] ?? ''; const rightLine = b[bestJ] ?? ''; if (equalI === undefined || equalJ === undefined) { let leftTags = ''; let rightTags = ''; cruncher.setSeqs(Array.from(leftLine), Array.from(rightLine)); for (const [tag, ai1, ai2, bj1, bj2] of cruncher.getOpcodes()) { const leftLength = ai2 - ai1; const rightLength = bj2 - bj1; if (tag === 'replace') { leftTags += '^'.repeat(leftLength); rightTags += '^'.repeat(rightLength); continue; } if (tag === 'delete') { leftTags += '-'.repeat(leftLength); continue; } if (tag === 'insert') { rightTags += '+'.repeat(rightLength); continue; } leftTags += ' '.repeat(leftLength); rightTags += ' '.repeat(rightLength); } out.push(...this.qformat(leftLine, rightLine, leftTags, rightTags)); } else { out.push(` ${leftLine}`); } out.push(...this.fancyHelper(a, bestI + 1, ahi, b, bestJ + 1, bhi)); return out; } fancyHelper(a, alo, ahi, b, blo, bhi) { if (alo < ahi) { if (blo < bhi) { return this.fancyReplace(a, alo, ahi, b, blo, bhi); } return this.dump('-', a, alo, ahi); } if (blo < bhi) { return this.dump('+', b, blo, bhi); } return []; } qformat(aline, bline, atags, btags) { const leftTags = rstripTrailingWhitespace(keepOriginalWhitespace(aline, atags)); const rightTags = rstripTrailingWhitespace(keepOriginalWhitespace(bline, btags)); const out = [`- ${aline}`]; if (leftTags) { out.push(`? ${leftTags}\n`); } out.push(`+ ${bline}`); if (rightTags) { out.push(`? ${rightTags}\n`); } return out; } } function ndiff(a, b, linejunk = null, charjunk = isCharacterJunk) { // discuss at: https://locutus.io/python/difflib/ndiff/ // parity verified: Python 3.12 // original by: Python Software Foundation (https://www.python.org/) // original by: Kevin van Zonneveld (https://kvz.io) // note 1: Returns a Differ-style delta with '-', '+', ' ', and '?' prefixed lines. // note 2: The default char junk filter ignores spaces and tabs, matching Python's difflib. // example 1: ndiff(['one', 'two', 'three'], ['ore', 'too', 'tree']) // returns 1: ['- one', '- two', '+ ore', '+ too', '- three', '? -\n', '+ tree'] // example 2: ndiff(['spam\n', 'ham\n', 'eggs\n'], ['spam\n', 'jam\n', 'eggs\n']) // returns 2: [' spam\n', '- ham\n', '? ^\n', '+ jam\n', '? ^\n', ' eggs\n'] // example 3: ndiff([], ['x']) // returns 3: ['+ x'] const left = normalizeLineSequence(a, 'a'); const right = normalizeLineSequence(b, 'b'); const lineFilter = normalizeJunkPredicate(linejunk, false); const charFilter = normalizeJunkPredicate(charjunk, true); return new Differ(lineFilter, charFilter).compare(left, right); } function normalizeLineSequence(value, paramName) { if (!Array.isArray(value)) { throw new TypeError(`ndiff(): ${paramName} must be an array of strings`); } for (const line of value) { if (typeof line !== 'string') { throw new TypeError(`ndiff(): ${paramName} must be an array of strings`); } } return [...value]; } function isCharacterJunk(char) { return char === ' ' || char === '\t'; } function normalizeJunkPredicate(predicate, useDefault) { if (typeof predicate === 'undefined') { return useDefault ? isCharacterJunk : undefined; } if (predicate === null) { return undefined; } if (typeof predicate === 'function') { return predicate; } if (isPythonFalsy(predicate)) { return undefined; } throw new TypeError("'int' object is not callable"); } function isPythonFalsy(value) { if (value === null || typeof value === 'undefined') { return true; } if (typeof value === 'boolean') { return value === false; } if (typeof value === 'number') { return value === 0; } if (typeof value === 'bigint') { return value === 0n; } if (typeof value === 'string') { return value.length === 0; } if (Array.isArray(value)) { return value.length === 0; } if (typeof value === 'object') { const prototype = Object.getPrototypeOf(value); if (prototype === Object.prototype || prototype === null) { return Object.keys(value).length === 0; } } return false; } function keepOriginalWhitespace(line, tags) { const lineChars = Array.from(line); const tagChars = Array.from(tags); const out = []; for (let index = 0; index < Math.min(lineChars.length, tagChars.length); index++) { const lineChar = lineChars[index] ?? ''; const tagChar = tagChars[index] ?? ''; out.push(tagChar === ' ' && /\s/u.test(lineChar) ? lineChar : tagChar); } return out.join(''); } function rstripTrailingWhitespace(value) { return value.replace(/\s+$/u, ''); }