Last active
April 11, 2025 07:08
-
-
Save kalgon/de96dd9474af3a3fa0ad898d7bfe2b50 to your computer and use it in GitHub Desktop.
Typescript Diff
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {byField, differ} from './diff'; | |
export type Address = { | |
id: number | null; | |
street: string; | |
city: string; | |
state: string; | |
zip: string; | |
}; | |
export type President = { | |
firstNames: Array<string>; | |
lastName: string; | |
birthYear: number; | |
party?: string; | |
electedYears: Array<number>; | |
addresses: Array<Address>; | |
}; | |
export const president: President = { | |
lastName: 'Bush', | |
birthYear: 1946, | |
firstNames: ['George', 'Walker'], | |
electedYears: [2001, 2005], | |
addresses: [ | |
{ | |
id: 1, | |
street: '742 Evergreen Terrace', | |
city: 'Springfield', | |
state: 'U.S.', | |
zip: '99999' | |
}, | |
{ | |
id: 2, | |
street: '11228 W Sunset Blvd', | |
city: 'Los Angeles', | |
state: 'U.S.', | |
zip: '90049' | |
} | |
] | |
}; | |
export const addressDiffer = differ<Address>().object({ | |
id: differ(), | |
street: differ(), | |
city: differ(), | |
zip: differ(), | |
state: differ() | |
}); | |
export const presidentDiffer = differ<President>().object({ | |
birthYear: differ(), | |
party: differ(), | |
firstNames: differ().array(), | |
lastName: differ(), | |
electedYears: differ().array(), | |
addresses: addressDiffer.array(byField('id')) | |
}); | |
const diff = presidentDiffer.diff(president, president); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {Pipe, PipeTransform} from '@angular/core'; | |
import {ArrayDiffItem, Diff, left, leftIndex, modified, right, rightIndex} from './diff'; | |
@Pipe({name: 'left'}) | |
export class LeftPipe implements PipeTransform { | |
transform<T>(diff: Diff<T>) { | |
return left(diff); | |
} | |
} | |
@Pipe({name: 'right'}) | |
export class RightPipe implements PipeTransform { | |
transform<T>(diff: Diff<T>) { | |
return right(diff); | |
} | |
} | |
@Pipe({name: 'modified'}) | |
export class Modified implements PipeTransform { | |
transform(diff: Diff<any>) { | |
return modified(diff); | |
} | |
} | |
@Pipe({name: 'leftIndex'}) | |
export class LeftIndexPipe implements PipeTransform { | |
transform(diff: ArrayDiffItem) { | |
return leftIndex(diff); | |
} | |
} | |
@Pipe({name: 'rightIndex'}) | |
export class RightIndexPipe implements PipeTransform { | |
transform(diff: ArrayDiffItem) { | |
return rightIndex(diff); | |
} | |
} | |
@Pipe({ | |
name: 'diffClass' | |
}) | |
export class DiffClassPipe implements PipeTransform { | |
transform(diff: Diff<any>) { | |
if (!modified(diff)) { | |
return 'unchanged'; | |
} else if (left(diff) === null && right(diff) !== null) { | |
return 'added'; | |
} else if (left(diff) !== null && right(diff) === null) { | |
return 'deleted'; | |
} else { | |
return 'changed'; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export type Equivalence<T> = (left: T | null, right: T | null, leftIndex: number, rightIndex: number) => boolean; | |
export function byValue<T>(): Equivalence<T> { | |
return (left, right) => left === right; | |
} | |
export function byFunction<T, V>(extractor: (target: T | null) => V, equivalence: Equivalence<V> = byValue()): Equivalence<T> { | |
return (left, right, leftIndex, rightIndex) => equivalence(extractor(left), extractor(right), leftIndex, rightIndex); | |
} | |
export function byField<T, K extends keyof T>(key: K, equivalence: Equivalence<T[K] | null> = byValue()): Equivalence<T> { | |
return byFunction(target => target?.[key] ?? null, equivalence); | |
} | |
export function byIndex<T>(): Equivalence<T> { | |
return (_left, _right, leftIndex, rightIndex) => leftIndex === rightIndex; | |
} | |
const LEFT: unique symbol = Symbol('left'); | |
const RIGHT: unique symbol = Symbol('right'); | |
const MODIFIED: unique symbol = Symbol('modified'); | |
const LEFT_INDEX: unique symbol = Symbol('leftIndex'); | |
const RIGHT_INDEX: unique symbol = Symbol('rightIndex'); | |
export type Diff<T> = { | |
readonly [LEFT]: T | null; | |
readonly [RIGHT]: T | null; | |
readonly [MODIFIED]: boolean; | |
}; | |
export type ArrayDiffItem = { | |
[LEFT_INDEX]: number | null; | |
[RIGHT_INDEX]: number | null; | |
}; | |
export const left = <T>(diff: Diff<T>) => diff[LEFT]; | |
export const right = <T>(diff: Diff<T>) => diff[RIGHT]; | |
export const modified = (diff: Diff<any>) => diff[MODIFIED]; | |
export const leftIndex = (diff: ArrayDiffItem) => diff[LEFT_INDEX]; | |
export const rightIndex = (diff: ArrayDiffItem) => diff[RIGHT_INDEX]; | |
type Properties<T> = { | |
readonly [K in keyof T]?: Differ<T[K], any>; | |
}; | |
type PropertiesDiff<P> = { | |
readonly [K in keyof P]: P[K] extends Differ<any, infer D> ? D : never; | |
}; | |
export type Differ<T, D extends Diff<T>> = { | |
diff(left: T | null, right: T | null): D; | |
}; | |
const array = <T, D extends Diff<T>>(differ: Differ<T, D>, equivalence: Equivalence<T>) => ({ | |
diff: (left: Array<T> | null, right: Array<T> | null) => { | |
const array = arrayDiff(left ?? [], right ?? [], differ, equivalence); | |
const modified = array.some(({[MODIFIED]: modified}) => modified); | |
return Object.assign(array, {[LEFT]: left, [RIGHT]: right, [MODIFIED]: modified}); | |
} | |
}); | |
const object = <T, P extends Properties<T>>(props: P) => ({ | |
diff(left: T | null, right: T | null) { | |
// TODO check if we can make all `any` go away | |
const properties = Object.entries(props).reduce((result, [property, differ]) => ({ | |
...result, | |
[property]: (differ as Differ<any, any>).diff((left as any)?.[property], (right as any)?.[property]) | |
}), {}) as PropertiesDiff<P>; | |
const modified = Object.entries<Diff<any>>(properties).some(([_property, {[MODIFIED]: modified}]) => modified); | |
return {[LEFT]: left, [RIGHT]: right, [MODIFIED]: modified, ...properties}; | |
}, | |
array(equivalence: Equivalence<T> = byValue()) { | |
return array(this, equivalence); | |
} | |
}); | |
export const differ = <T>() => ({ | |
diff(left: T | null, right: T | null) { | |
return {[LEFT]: left, [RIGHT]: right, [MODIFIED]: left !== right}; | |
}, | |
array(equivalence: Equivalence<T> = byValue()) { | |
return array(this, equivalence); | |
}, | |
object<P extends Properties<T>>(props: keyof P extends keyof T ? P : never) { | |
return object<T, P>(props); | |
} | |
}); | |
function arrayDiff<T, D extends Diff<T>>(left: Array<T>, right: Array<T>, differ: Differ<T, D>, equivalence: Equivalence<T>) { | |
const f = (leftIndex: number | null, rightIndex: number | null) => ({ | |
...differ.diff(leftIndex === null ? null : left[leftIndex], rightIndex === null ? null : right[rightIndex]), | |
[LEFT_INDEX]: leftIndex, | |
[RIGHT_INDEX]: rightIndex | |
}); | |
const e = (leftIndex: number, rightIndex: number) => equivalence(left[leftIndex], right[rightIndex], leftIndex, rightIndex); | |
const n = left?.length ?? 0; | |
const m = right?.length ?? 0; | |
const z = n + m; | |
const v = Array<number>(z + 1 + z).fill(0); | |
const trace: Array<typeof v> = []; | |
const result: Array<D & ArrayDiffItem> = []; | |
for (let d = 0; d <= z; d++) { | |
trace.push([...v]); | |
for (let k = -d; k <= d; k += 2) { | |
let x = k === -d || k !== d && v[z + k - 1] < v[z + k + 1] ? v[z + k + 1] : v[z + k - 1] + 1; | |
let y = x - k; | |
while (x < n && y < m && e(x, y)) { | |
x++; | |
y++; | |
} | |
v[z + k] = x; | |
if (x === n && y === m) { | |
do { | |
const v = trace[d]; | |
const k = x - y; | |
const pk = k === -d || k !== d && v[z + k - 1] < v[z + k + 1] ? k + 1 : k - 1; | |
const px = v[z + pk]; | |
const py = px - pk; | |
while (x > px && y > py) { | |
result.unshift(f(--x, --y)) | |
} | |
if (d > 0) { | |
result.unshift(f(x === px ? null : x = px, y === py ? null : y = py)); | |
} | |
} while (--d >= 0); | |
return result; | |
} | |
} | |
} | |
return result; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment