275 lines
8.7 KiB
TypeScript
275 lines
8.7 KiB
TypeScript
export const splitAt = (subject: string, index: number): readonly [string, string] => {
|
|
if (index < 0) {
|
|
return [subject, ''];
|
|
}
|
|
|
|
if (index > subject.length) {
|
|
return [subject, ''];
|
|
}
|
|
|
|
return [subject.slice(0, index), subject.slice(index + 1)];
|
|
};
|
|
export function* gen__split_by_filter(subject: string, filter: string): Generator<readonly [boolean, string], void, unknown> {
|
|
let lastIndex = 0;
|
|
|
|
for (const { 0: match, index } of subject.matchAll(new RegExp(filter, 'gmi'))) {
|
|
const end = index + match.length;
|
|
|
|
yield [false, subject.slice(lastIndex, index)];
|
|
yield [true, subject.slice(index, end)];
|
|
|
|
lastIndex = end;
|
|
}
|
|
|
|
yield [false, subject.slice(lastIndex, subject.length)];
|
|
}
|
|
export function split_by_filter(subject: string, filter: string): (readonly [boolean, string])[] {
|
|
return Array.from<readonly [boolean, string]>(gen__split_by_filter(subject, filter));
|
|
}
|
|
|
|
type Hex = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 'a' | 'b' | 'c' | 'd' | 'e' | 'f';
|
|
type EncodedChar = 't' | 'b' | 'n' | 'r' | 'f' | '\'' | '"' | `u${Hex}${Hex | ''}${Hex | ''}${Hex | ''}`
|
|
|
|
const decodeRegex = /(?<!\\)\\(t|b|n|r|f|'|"|u[0-9a-f]{1,4})/gi;
|
|
const decodeReplacer = (_: any, char: EncodedChar) => ({
|
|
t: '\t',
|
|
b: '\b',
|
|
n: '\n',
|
|
r: '\r',
|
|
f: '\f',
|
|
"'": '\'',
|
|
'"': '\"',
|
|
u: String.fromCharCode(Number.parseInt(`0x${char.slice(1)}`)),
|
|
}[char.charAt(0) as ('t' | 'b' | 'n' | 'r' | 'f' | '\'' | '"' | 'u')]);
|
|
export const decode = (subject: string): string => subject.replace(decodeRegex, decodeReplacer);
|
|
|
|
export const deepCopy = <T>(original: T): T => {
|
|
if (typeof original !== 'object' || original === null || original === undefined) {
|
|
return original;
|
|
}
|
|
|
|
if (original instanceof Date) {
|
|
return new Date(original.getTime()) as T;
|
|
}
|
|
|
|
if (original instanceof Array) {
|
|
return original.map(item => deepCopy(item)) as T;
|
|
}
|
|
|
|
if (original instanceof Set) {
|
|
return new Set(original.values().map(item => deepCopy(item))) as T;
|
|
}
|
|
|
|
if (original instanceof Map) {
|
|
return new Map(original.entries().map(([key, value]) => [key, deepCopy(value)])) as T;
|
|
}
|
|
|
|
return Object.assign(
|
|
Object.create(Object.getPrototypeOf(original)),
|
|
Object.fromEntries(Object.entries(original).map(([key, value]) => [key, deepCopy(value)]))
|
|
) as T;
|
|
}
|
|
|
|
export enum MutarionKind {
|
|
Create = 'created',
|
|
Update = 'updated',
|
|
Delete = 'deleted',
|
|
}
|
|
export type Created = { kind: MutarionKind.Create, key: string, value: any };
|
|
export type Updated = { kind: MutarionKind.Update, key: string, value: any, original: any };
|
|
export type Deleted = { kind: MutarionKind.Delete, key: string, original: any };
|
|
export type Mutation = Created | Updated | Deleted;
|
|
|
|
export function* deepDiff<T1 extends object, T2 extends object>(a: T1, b: T2, path: string[] = []): Generator<Mutation, void, unknown> {
|
|
if (!isIterable(a) || !isIterable(b)) {
|
|
console.error('Edge cases', a, b);
|
|
|
|
return;
|
|
}
|
|
|
|
for (const [[keyA, valueA], [keyB, valueB]] of zip(entriesOf(a), entriesOf(b))) {
|
|
if (keyA === undefined && keyB !== undefined) {
|
|
yield { key: path.concat(keyB.toString()).join('.'), kind: MutarionKind.Create, value: valueB };
|
|
|
|
continue;
|
|
}
|
|
|
|
if (keyA !== undefined && keyB === undefined) {
|
|
yield { key: path.concat(keyA.toString()).join('.'), kind: MutarionKind.Delete, original: valueA };
|
|
|
|
continue;
|
|
}
|
|
|
|
if (typeof valueA == 'object' && typeof valueB == 'object') {
|
|
yield* deepDiff(valueA, valueB, path.concat(keyA!.toString()));
|
|
|
|
continue;
|
|
}
|
|
|
|
if (valueA === valueB) {
|
|
continue;
|
|
}
|
|
|
|
const key = path.concat(keyA!.toString()).join('.');
|
|
|
|
yield ((): Mutation => {
|
|
if (valueA === null || valueA === undefined) {
|
|
return { key, kind: MutarionKind.Create, value: valueB };
|
|
}
|
|
|
|
if (valueB === null || valueB === undefined) {
|
|
return { key, kind: MutarionKind.Delete, original: valueA };
|
|
}
|
|
|
|
return { key, kind: MutarionKind.Update, value: valueB, original: valueA };
|
|
})();
|
|
}
|
|
};
|
|
|
|
const isIterable = (subject: object): subject is Iterable<any> => ['boolean', 'undefined', 'null', 'number'].includes(typeof subject) === false;
|
|
const entriesOf = (subject: object): Iterable<readonly [string | number, any]> => {
|
|
if (subject instanceof Array) {
|
|
return subject.entries();
|
|
}
|
|
|
|
if (subject instanceof Map) {
|
|
return subject.entries();
|
|
}
|
|
|
|
if (subject instanceof Set) {
|
|
return subject.entries();
|
|
}
|
|
|
|
if (subject === null || subject === undefined) {
|
|
return [];
|
|
}
|
|
|
|
return Object.entries(subject);
|
|
};
|
|
|
|
type ZippedPair =
|
|
| readonly [readonly [string | number, any], readonly [string | number, any]]
|
|
| readonly [readonly [undefined, undefined], readonly [string | number, any]]
|
|
| readonly [readonly [string | number, any], readonly [undefined, undefined]]
|
|
;
|
|
const zip = function* (a: Iterable<readonly [string | number, any]>, b: Iterable<readonly [string | number, any]>): Generator<ZippedPair, void, unknown> {
|
|
const iterA = bufferredIterator(a);
|
|
const iterB = bufferredIterator(b);
|
|
|
|
const EMPTY = [undefined, undefined] as const;
|
|
|
|
while (!iterA.done || !iterB.done) {
|
|
// if we have a match on the keys of a and b we can simply consume and yield
|
|
if (iterA.current.key === iterB.current.key) {
|
|
// When we match keys it could have happened that
|
|
// there are as many keys added as there are deleted,
|
|
// therefor we can now flush both a and b because we are aligned
|
|
yield* iterA.flush().map(entry => [entry, EMPTY] as const);
|
|
yield* iterB.flush().map(entry => [EMPTY, entry] as const);
|
|
|
|
yield [iterA.consume(), iterB.consume()];
|
|
}
|
|
|
|
// key of a aligns with last key in buffer b
|
|
// conclusion: a has key(s) that b does not
|
|
else if (iterA.current.key === iterB.top.key) {
|
|
yield* iterA.flush().map(entry => [entry, EMPTY] as const);
|
|
yield [iterA.consume(), iterB.consume()];
|
|
}
|
|
|
|
// the reverse case, key of b is aligns with the last key in buffer a
|
|
// conclusion: a is missing key(s) the b does have
|
|
else if (iterB.current.key === iterA.top.key) {
|
|
yield* iterB.flush().map(entry => [EMPTY, entry] as const);
|
|
yield [iterA.consume(), iterB.consume()];
|
|
}
|
|
|
|
else if (iterA.done && !iterB.done) {
|
|
yield [EMPTY, iterB.consume()];
|
|
}
|
|
|
|
else if (!iterA.done && iterB.done) {
|
|
yield [iterA.consume(), EMPTY];
|
|
}
|
|
|
|
// Neiter of the above cases are hit.
|
|
// conclusion: there still is no alignment.
|
|
else {
|
|
iterA.advance();
|
|
iterB.advance();
|
|
}
|
|
}
|
|
};
|
|
|
|
const bufferredIterator = <T extends readonly [string | number, any]>(subject: Iterable<T>) => {
|
|
const iterator = Iterator.from(subject);
|
|
const buffer: T[] = [];
|
|
let done = false;
|
|
|
|
const next = () => {
|
|
const res = iterator.next();
|
|
done = res.done!;
|
|
|
|
if (!done) {
|
|
buffer.push(res.value);
|
|
}
|
|
};
|
|
|
|
next();
|
|
|
|
return {
|
|
advance() {
|
|
next();
|
|
},
|
|
|
|
consume() {
|
|
const value = buffer.shift()!;
|
|
|
|
this.advance();
|
|
|
|
return value;
|
|
},
|
|
|
|
flush(): T[] {
|
|
const entries = buffer.splice(0, buffer.length - 1);
|
|
|
|
return entries;
|
|
},
|
|
|
|
get done() {
|
|
return done && buffer.length === 0;
|
|
},
|
|
|
|
get top() {
|
|
const [key = undefined, value = undefined] = buffer.at(0) ?? [];
|
|
|
|
return { key, value };
|
|
},
|
|
|
|
get current() {
|
|
const [key = undefined, value = undefined] = buffer.at(-1) ?? [];
|
|
|
|
return { key, value };
|
|
},
|
|
};
|
|
};
|
|
|
|
export interface filter {
|
|
<T, S extends T>(subject: AsyncIterableIterator<T>, predicate: (value: T) => value is S): AsyncGenerator<S, void, unknown>;
|
|
<T>(subject: AsyncIterableIterator<T>, predicate: (value: T) => unknown): AsyncGenerator<T, void, unknown>;
|
|
}
|
|
|
|
export const filter = async function*<T, S extends T>(subject: AsyncIterableIterator<T>, predicate: (value: T) => value is S): AsyncGenerator<S, void, unknown> {
|
|
for await (const value of subject) {
|
|
if (predicate(value)) {
|
|
yield value;
|
|
}
|
|
}
|
|
};
|
|
|
|
export const map = async function*<TIn, TResult>(subject: AsyncIterableIterator<TIn>, predicate: (value: TIn) => TResult): AsyncGenerator<TResult, void, unknown> {
|
|
for await (const value of subject) {
|
|
yield predicate(value);
|
|
}
|
|
};
|
|
|