Skip to content

Commit

Permalink
feat: add pairwiseTraversal to util
Browse files Browse the repository at this point in the history
union and diff functions both use pairwiseTraversal instead of
duplicating logic
  • Loading branch information
tabcat committed Apr 22, 2024
1 parent 75425f7 commit 231a9b7
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 92 deletions.
51 changes: 6 additions & 45 deletions src/difference.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { safeArrayAccess, dualTraversal, readArray } from "./util.js";
import { safeArrayAccess, dualTraversal, readArray, pairwiseTraversal } from "./util.js";

/**
* Yields the difference of two ordered sets.
Expand Down Expand Up @@ -48,7 +48,7 @@ export function* difference<T>(
export function* symmetric<T>(
minuend: T[],
subtrahend: T[],
comparator: (a: T, b: T) => number,
comparator: (a: T, b: T) => number
): Generator<T> {
for (const [s, t] of diff(minuend, subtrahend, comparator)) {
yield (s ?? t)!;
Expand All @@ -67,50 +67,11 @@ export type Diff<T> = [T, null] | [null, T];
export function* diff<T>(
source: T[],
target: T[],
comparator: (a: T, b: T) => number,
comparator: (a: T, b: T) => number
): Generator<Diff<T>> {
let pastSource = false;
let pastTarget = false;

for (const [i, j, order] of dualTraversal(source, target, comparator)) {
if (pastSource) {
for (const element of readArray(target, j)) {
yield [null, element];
}
break;
}

if (pastTarget) {
for (const element of readArray(source, i)) {
yield [element, null];
}
break;
}

if (order < 0) {
yield [safeArrayAccess(source, i), null];
}

if (order > 0) {
yield [null, safeArrayAccess(target, j)];
}

// see if source is exausted
if (order <= 0 && i === source.length - 1) {
pastSource = true;
// if order is not equal then also yield target[j]
if (order < 0 && j > 0) { // j could be -1
yield [null, safeArrayAccess(target, j)];
}
}

// see if target is exausted
if (order >= 0 && j === target.length - 1) {
pastTarget = true;
// if order is not equal then also yield source[i]
if (order > 0 && i > 0) { // i could be -1
yield [safeArrayAccess(source, i), null];
}
for (const [s, t] of pairwiseTraversal(source, target, comparator)) {
if (s === null || t === null) {
yield [s, t] as Diff<T>
}
}
}
50 changes: 4 additions & 46 deletions src/union.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { safeArrayAccess, dualTraversal, readArray } from "./util.js";
import { pairwiseTraversal } from "./util.js";

/**
* Yields the union of two ordered sets.
Expand All @@ -12,49 +12,7 @@ export function* union<T>(
target: T[],
comparator: (a: T, b: T) => number,
): Generator<T> {
let pastSource: boolean = false;
let pastTarget: boolean = false;

// logic is similar to diff, might be able to generalize this
for (const [i, j, order] of dualTraversal(source, target, comparator)) {
if (pastSource) {
yield* readArray(target, j);
break;
}

if (pastTarget) {
yield* readArray(source, i);
break;
}

if (order < 0) {
yield safeArrayAccess(source, i);
}

if (order > 0) {
yield safeArrayAccess(target, j);
}

if (order === 0) {
yield safeArrayAccess(source, i);
}

// see if source is exausted
if (order <= 0 && i === source.length - 1) {
pastSource = true;
// if order is not equal then also yield minuend[i]
if (order < 0 && j >= 0) { // j could be -1
yield safeArrayAccess(target, j);
}
}

// see if target is exausted
if (order >= 0 && j === target.length - 1) {
pastTarget = true;
// if order is not equal then also yield minuend[i]
if (order > 0 && i >= 0) { // i could be -1
yield safeArrayAccess(source, i);
}
}
}
for (const [s, t] of pairwiseTraversal(source, target, comparator)) {
yield (s ?? t)!
}
}
51 changes: 51 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,54 @@ export function* readArray<T>(
start++;
}
}

export type PairwiseElement<T> = [T, null] | [null, T] | [T, T]

export function* pairwiseTraversal <T>(source: T[], target: T[], comparator: (a: T, b: T) => number,): Generator<PairwiseElement<T>> {
for (const [i, j, order] of dualTraversal(source, target, comparator)) {
switch (true) {
case order < 0:
yield [safeArrayAccess(source, i), null];
break
case order > 0:
yield [null, safeArrayAccess(target, j)];
break
default: // order === 0
yield [safeArrayAccess(source, i), safeArrayAccess(target, j)]
}

// if source is exausted
if (order <= 0 && i === source.length - 1) {
if (j >= 0) { // j could be -1
if (order < 0) { // if order is not equal then also yield target[j]
yield [null, safeArrayAccess(target, j)];
}

if (j < target.length - 1) {
// yield the rest of target
for (const element of readArray(target, j + 1)) {
yield [null, element];
}
break;
}
}
}

// if target is exausted
if (order >= 0 && j === target.length - 1) {
if (i >= 0) { // i could be -1
if (order > 0) { // if order is not equal then also yield source[i]
yield [safeArrayAccess(source, i), null];
}

if (i < source.length - 1) {
// yield the rest of source
for (const element of readArray(source, i + 1)) {
yield [element, null];
}
break;
}
}
}
}
}
95 changes: 94 additions & 1 deletion test/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, test, expect } from "vitest";
import { numbers, even, odd, comparator } from "./helpers/sets.js";
import { isGenerator } from "./helpers/isGenerator.js";
import { testNames } from "./helpers/test-names.js";
import { safeArrayAccess, dualTraversal, readArray } from "../src/util.js";
import { safeArrayAccess, dualTraversal, readArray, pairwiseTraversal, PairwiseElement } from "../src/util.js";

describe("safeArrayAccess", () => {
test("returns array[index]", () => {
Expand Down Expand Up @@ -188,3 +188,96 @@ describe("readArray", () => {
});
});
});

describe("pairwiseTraversal", () => {
test(testNames.returnsGenerator, () => {
expect(isGenerator(pairwiseTraversal([], [], comparator))).toBe(true);
});

describe("finds ordered traversal of two sets", () => {
let g: Generator<PairwiseElement<number>>;
let u: PairwiseElement<number>[];

test(testNames.firstAndSecondEmpty, () => {
g = pairwiseTraversal([], [], comparator);
u = [];
for (const {} of g) {
expect.fail();
}
expect(u).toEqual([]);
});

test(testNames.firstEmpty, () => {
g = pairwiseTraversal([], numbers, comparator);
u = [];
for (const element of g) {
expect(element[0]).toBe(null);
expect(element[1]).toBeGreaterThanOrEqual(0);
u.push(element);
}
expect(u).toEqual(numbers.map(n => [null, n]));
});

test(testNames.secondEmpty, () => {
g = pairwiseTraversal(numbers, [], comparator);
u = [];
for (const element of g) {
expect(element[1]).toBe(null)
u.push(element);
}
expect(u).toEqual(numbers.map(n => [n, null]));
});

test(testNames.identicalSingle, () => {
g = pairwiseTraversal(numbers.slice(0, 1), numbers.slice(0, 1), comparator);
u = [];
for (const element of g) {
expect(element[0]).toEqual(element[1]);
u.push(element);
}
expect(u).toEqual(numbers.slice(0, 1).map(n => [n, n]));
});

test(testNames.identical, () => {
g = pairwiseTraversal(numbers, numbers, comparator);
u = [];
for (const element of g) {
expect(element[0]).toEqual(element[1]);
u.push(element);
}
expect(u).toEqual(numbers.map(n => [n, n]));
});

test(testNames.partialOverlap, () => {
g = pairwiseTraversal(numbers, even, comparator);
u = [];
for (const element of g) {
u.push(element);
}
expect(u).toEqual(numbers.map(n => [n, n % 2 === 0 ? n : null]));

g = pairwiseTraversal(numbers, odd, comparator);
u = [];
for (const element of g) {
u.push(element);
}
expect(u).toEqual(numbers.map(n => [n, n % 2 === 1 ? n : null]));
});

test(testNames.noOverlap, () => {
g = pairwiseTraversal(even, odd, comparator);
u = [];
for (const element of g) {
u.push(element)
}
expect(u).toEqual(numbers.map(n => [n % 2 === 0 ? n : null, n % 2 === 1 ? n : null]));

g = pairwiseTraversal(odd, even, comparator);
u = [];
for (const element of g) {
u.push(element)
}
expect(u).toEqual(numbers.map(n => [n % 2 === 1 ? n : null, n % 2 === 0 ? n : null]));
});
});
});

0 comments on commit 231a9b7

Please sign in to comment.