union-find-ts
Version:
An immutable Union-Find structure.
503 lines (502 loc) • 13.2 kB
JavaScript
import { findPath, find, link, linkAll, toConnectedGroups, unionFind, linkItem, findItem } from '../src';
import { concat, flatten, groupBy, lift, map } from 'ramda';
import { Maybe } from 'purify-ts';
expect.extend({
toBeJust: (actual, expected) => {
if (!Maybe.isMaybe(actual)) {
return { message: () => `Expected maybe but result was ${typeof actual}`, pass: false };
}
if (!actual.isJust()) {
return {
message: () => 'Expected Just but received Nothing.',
pass: false
};
}
if (!expected) {
return {
message: () => 'Expected Nothing but received Just.',
pass: true
};
}
if (actual.extract() === expected) {
return {
message: () => `Expected Just to not equal ${String(expected)} but it did.}`,
pass: true
};
}
return {
message: () => `Expected Just to equal ${String(expected)} but got ${String(actual)}`,
pass: false
};
}
});
describe('Union-Find', () => {
describe('Building object', () => {
it('It should be able to build an object', () => {
const sorkin = unionFind(20, x => x);
expect(sorkin.items).toHaveLength(21);
expect(sorkin.ranks).toHaveLength(21);
expect(sorkin.roots).toHaveLength(21);
});
it('Should create the object when given a list', () => {
const uf = unionFind(allItems, ({ num }) => num);
expect(uf.items).toHaveLength(65);
});
const mappings = () => [
[40, 37],
[40, 21],
[37, 6],
[6, 59],
[34, 57],
[57, 20],
[6, 55],
[55, 39],
[59, 34]
].map(pair => pair.map(findItemByNum));
it('Should be able to create links', () => {
let uf = unionFind(allItems, ({ num }) => num);
expect(uf.items).toHaveLength(65);
uf = mappings().reduce((uf1, [left, right]) => linkItem(uf1, left, right), uf);
const [head, ...tail] = map(it => findItem(uf, it), flatten(mappings()));
tail.forEach(it => {
expect(head).toBe(it);
});
});
it('Should be able to create links during construction', () => {
let uf = unionFind(allItems, ({ num }) => num, (item) => {
return mappings().filter(it => it.includes(item)).map(([l, r]) => (l === item ? r : l));
});
uf = mappings().reduce((uf1, [left, right]) => linkItem(uf1, left, right), uf);
const [head, ...tail] = map(it => findItem(uf, it), flatten(mappings()));
tail.forEach(t => {
expect(head).toBe(t);
});
});
describe('Component Joining', () => {
it('items in each component should now have the same root.', () => {
let uf = unionFind(allItems, gateNum, [
[1, 2],
[1, 3],
[1, 4],
[1, 5],
[6, 7],
[6, 8],
[6, 9],
[6, 10]
].map(pair => pair.map(findItemByNum)));
const _find = (item) => find(uf, item);
expect(_find(2)).toBe(_find(5));
expect(_find(7)).toBe(_find(10));
expect(_find(2)).not.toBe(_find(7));
uf = link(uf, 1, 6);
expect(_find(2)).toBe(_find(7));
uf = linkAll(uf, 11, [12, 13, 14, 15, 16, 17, 18, 19, 20]);
});
});
});
const gateNum = ({ num }) => num;
describe('grouping', () => {
describe('Trivial Cases', () => {
it('Gets the right answer if components already connected', () => {
const uf = unionFind(3, x => x, [[2, 3]]);
const solution = findPath(uf, () => [1, 2, 3], 1, 2);
expect(solution.isJust()).toBeTruthy();
});
});
it('Connects one bigger.', () => {
const gates = [
20, 34, 63, 64, 55, 24, 53, 20, 17, 6, 52, 57, 7, 59, 55, 37, 40, 52, 40, 64, 31, 21, 46, 39,
57, 4
];
const gatesByCenter = groupBy(item => item.center.toString(), allItems);
const gateNumbers = new Set(gates);
const defined = gateNumbers.has.bind(gateNumbers);
const uf = unionFind(allItems, gateNum,
/**
* This is where we define which gates each one is connected to.
* Each gate is connected to the other items on its center,
* as well as the channel connection.
* @param param0
* @returns
*/
(gate) => !defined(gate.num)
? []
: [...gate.connected.map(findItemByNum), ...gatesByCenter[gate.center.toString()]].filter(it => defined(it.num)));
const groups = toConnectedGroups(uf);
expect(groups.length).toBe(2);
const findItemArray = lift(findItemByNum);
function candidates(item) {
return concat(findItemArray(item.connected), gatesByCenter[item.center.toString()]);
}
const solution = findPath(uf, candidates, groups[1][0], groups[0][0]);
const result = [[allItems.find(it => it.num === 62)]];
expect(solution.isJust()).toBeTruthy();
expect(solution.extract()).toEqual(result);
});
it('Connects another larger example.', () => {
const gates = [
54, 53, 18, 17, 41, 11, 54, 31, 15, 29, 1, 26, 57, 51, 57, 18, 17, 42, 27, 56, 15, 4
];
const gatesByCenter = groupBy(item => item.center.toString(), allItems);
const gateNumbers = new Set(gates);
const defined = gateNumbers.has.bind(gateNumbers);
const uf = unionFind(allItems, gateNum,
/**
* This is where we define which gates each one is connected to.
* Each gate is connected to the other items on its center,
* as well as the channel connection.
* @param param0
* @returns
*/
(gate) => !defined(gate.num)
? []
: [...gate.connected.map(findItemByNum), ...gatesByCenter[gate.center.toString()]].filter(it => defined(it.num)));
const groups = toConnectedGroups(uf);
expect(groups.length).toBe(5);
const findItemArray = lift(findItemByNum);
function candidates(item, uf) {
return concat(findItemArray(item.connected), gatesByCenter[`${item.center}`]);
}
const solution = findPath(uf, candidates, findItemByNum(42), findItemByNum(56));
expect(solution.isJust()).toBeTruthy();
if (solution.isJust()) {
expect(solution.extract()[0].length).toBe(2);
}
});
});
});
export var Center;
(function (Center) {
Center[Center["Head"] = 1] = "Head";
Center[Center["Ajna"] = 2] = "Ajna";
Center[Center["Throat"] = 3] = "Throat";
Center[Center["Identity"] = 4] = "Identity";
Center[Center["Sacral"] = 5] = "Sacral";
Center[Center["Root"] = 6] = "Root";
Center[Center["Spleen"] = 7] = "Spleen";
Center[Center["Will"] = 8] = "Will";
Center[Center["ESP"] = 9] = "ESP";
})(Center || (Center = {}));
const findItemByNum = (itemNum) => allItems.find(({ num }) => num === itemNum) ?? allItems[0];
const allItems = [
{
num: 41,
center: Center.Root,
connected: [30]
},
{
num: 19,
center: Center.Root,
connected: [49]
},
{
num: 13,
center: Center.Identity,
connected: [33]
},
{
num: 49,
center: Center.ESP,
connected: [19]
},
{
num: 30,
center: Center.ESP,
connected: [41]
},
{
num: 55,
center: Center.ESP,
connected: [39]
},
{
num: 37,
center: Center.ESP,
connected: [40]
},
{
num: 63,
center: Center.Head,
connected: [4]
},
{
num: 22,
center: Center.ESP,
connected: [12]
},
{
num: 36,
center: Center.ESP,
connected: [35]
},
{
num: 25,
center: Center.Identity,
connected: [51]
},
{
num: 17,
center: Center.Ajna,
connected: [62]
},
{
num: 21,
center: Center.Will,
connected: [45]
},
{
num: 51,
center: Center.Will,
connected: [26]
},
{
num: 42,
center: Center.Sacral,
connected: [53]
},
{
num: 3,
center: Center.Sacral,
connected: [60]
},
{
num: 27,
center: Center.Sacral,
connected: [50]
},
{
num: 24,
center: Center.Ajna,
connected: [61]
},
{
num: 2,
center: Center.Identity,
connected: [14]
},
{
num: 23,
center: Center.Throat,
connected: [43]
},
{
num: 8,
center: Center.Throat,
connected: [1]
},
{
num: 20,
center: Center.Throat,
connected: [10, 57, 34]
},
{
num: 16,
center: Center.Throat,
connected: [48]
},
{
num: 35,
center: Center.Throat,
connected: [36]
},
{
num: 45,
center: Center.Throat,
connected: [21]
},
{
num: 12,
center: Center.Throat,
connected: [22]
},
{
num: 15,
center: Center.Identity,
connected: [5]
},
{
num: 52,
center: Center.Root,
connected: [9]
},
{
num: 39,
center: Center.Root,
connected: [55]
},
{
num: 53,
center: Center.Root,
connected: [42]
},
{
num: 62,
center: Center.Throat,
connected: [17]
},
{
num: 56,
center: Center.Throat,
connected: [11]
},
{
num: 31,
center: Center.Throat,
connected: [7]
},
{
num: 33,
center: Center.Throat,
connected: [13]
},
{
num: 7,
center: Center.Identity,
connected: [31]
},
{
num: 4,
center: Center.Ajna,
connected: [63]
},
{
num: 29,
center: Center.Sacral,
connected: [46]
},
{
num: 59,
center: Center.Sacral,
connected: [6]
},
{
num: 40,
center: Center.Will,
connected: [37]
},
{
num: 64,
center: Center.Head,
connected: [47]
},
{
num: 47,
center: Center.Ajna,
connected: [64]
},
{
num: 6,
center: Center.ESP,
connected: [59]
},
{
num: 46,
center: Center.Identity,
connected: [29]
},
{
num: 18,
center: Center.Spleen,
connected: [58]
},
{
num: 48,
center: Center.Spleen,
connected: [16]
},
{
num: 57,
center: Center.Spleen,
connected: [34, 10, 20]
},
{
num: 32,
center: Center.Spleen,
connected: [54]
},
{
num: 50,
center: Center.Spleen,
connected: [27]
},
{
num: 28,
center: Center.Spleen,
connected: [36]
},
{
num: 44,
center: Center.Spleen,
connected: [26]
},
{
num: 1,
center: Center.Identity,
connected: [8]
},
{
num: 43,
center: Center.Ajna,
connected: [23]
},
{
num: 14,
center: Center.Sacral,
connected: [2]
},
{
num: 34,
center: Center.Sacral,
connected: [57, 10, 20]
},
{
num: 9,
center: Center.Sacral,
connected: [52]
},
{
num: 5,
center: Center.Sacral,
connected: [15]
},
{
num: 26,
center: Center.Will,
connected: [44]
},
{
num: 11,
center: Center.Ajna,
connected: [56]
},
{
num: 10,
center: Center.Identity,
connected: [20, 57, 34]
},
{
num: 58,
center: Center.Root,
connected: [18]
},
{
num: 38,
center: Center.Root,
connected: [28]
},
{
num: 54,
center: Center.Root,
connected: [32]
},
{
num: 61,
center: Center.Head,
connected: [24]
},
{
num: 60,
center: Center.Root,
connected: [3]
}
];