From 158a4bac40fa9937f770d4786e212347b79929fc Mon Sep 17 00:00:00 2001 From: Joe Hildebrand Date: Sun, 8 Dec 2024 01:00:20 -0700 Subject: [PATCH] Add PointSet --- day5.ts | 14 +-- day8.ts | 35 +++---- deno.jsonc | 1 + lib/rect.ts | 220 +++++++++++++++++++++++++++++++++++++++++- lib/test/rect.test.ts | 113 +++++++++++++++++++++- 5 files changed, 351 insertions(+), 32 deletions(-) diff --git a/day5.ts b/day5.ts index 15473a1..16028b3 100644 --- a/day5.ts +++ b/day5.ts @@ -1,3 +1,4 @@ +import { Point, PointSet } from './lib/rect.ts'; import { type MainArgs, parseFile } from './lib/utils.ts'; type Parsed = [[number, number][], number[][]]; @@ -16,19 +17,18 @@ function part2(inp: number[][][]): number { export default async function main(args: MainArgs): Promise<[number, number]> { const inp = await parseFile(args); - const order = new Set(); + // These are number tuples, not points, but... shrug. + const order = new PointSet(); for (const [x, y] of inp[0]) { - order.add(`${x},${y}`); + order.add(new Point(x, y)); } const beforeAndAfter = inp[1].map((pages) => { const sorted = [...pages].sort((a, b) => { - if (order.has(`${a},${b}`)) { + if (order.has(new Point(a, b))) { return -1; } - if (order.has(`${b},${a}`)) { - return 1; - } - return 0; + // There are no pairs that aren't in one direction or the other. + return 1; }); return [pages, sorted]; }); diff --git a/day8.ts b/day8.ts index 47bfba4..932428f 100644 --- a/day8.ts +++ b/day8.ts @@ -1,4 +1,4 @@ -import { Point, Rect } from './lib/rect.ts'; +import { Point, PointSet, Rect } from './lib/rect.ts'; import { Sequence } from './lib/sequence.ts'; import { type MainArgs, parseFile } from './lib/utils.ts'; @@ -6,7 +6,7 @@ type Parsed = string[][]; class Field extends Rect { antennae = new Map(); - nodes: Point[] = []; + nodes = new PointSet(); constructor(inp: Parsed, self = false) { super(inp); @@ -22,22 +22,21 @@ class Field extends Rect { const p = new Point(x, y); m.push(p); if (self) { - this.nodes.push(p); + this.nodes.add(p); } }); } push(p: Point): boolean { if (this.check(p)) { - this.nodes.push(p); + this.nodes.add(p); return true; } return false; } - count(): number { - const locs = new Set(this.nodes.map((n) => n.toString())); - return locs.size; + get count(): number { + return this.nodes.size; } } @@ -46,14 +45,13 @@ function part1(inp: Parsed): number { for (const [_k, v] of r.antennae) { for (const [a, b] of new Sequence(v).combinations(2)) { - const dx = a.x - b.x; - const dy = a.y - b.y; + const [dx, dy] = a.delta(b); r.push(a.xlate(dx, dy)); r.push(b.xlate(-dx, -dy)); } } - return r.count(); + return r.count; } function part2(inp: Parsed): number { @@ -61,19 +59,18 @@ function part2(inp: Parsed): number { for (const [_k, v] of r.antennae) { for (const [a, b] of new Sequence(v).combinations(2)) { - const dx = a.x - b.x; - const dy = a.y - b.y; - let p = a.xlate(dx, dy); - while (r.push(p)) { + const [dx, dy] = a.delta(b); + let p = a; + do { p = p.xlate(dx, dy); - } - p = b.xlate(-dx, -dy); - while (r.push(p)) { + } while (r.push(p)); + p = b; + do { p = p.xlate(-dx, -dy); - } + } while (r.push(p)); } } - return r.count(); + return r.count; } export default async function main(args: MainArgs): Promise<[number, number]> { diff --git a/deno.jsonc b/deno.jsonc index b9ac204..d8af097 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -3,6 +3,7 @@ "tasks": { "check": "deno fmt --check && deno lint && deno check **/*.ts **/*.js", "test": "rm -rf coverage && deno test -A --coverage --parallel --shuffle && deno coverage coverage --html --exclude='test/**/*' --exclude=day.ts && deno coverage coverage --lcov --output=coverage/lcov.info", + "test:lib": "rm -rf coverage && deno test -A --coverage --parallel --shuffle lib/test/*.test.ts && deno coverage coverage --html --exclude='test/**/*' --exclude=day.ts && deno coverage coverage --lcov --output=coverage/lcov.info", "ci": "deno test -A --coverage && deno coverage coverage --lcov --output=coverage/lcov.info --exclude='test/**/*' --exclude=day.ts", "update": "deno run -A jsr:@molt/cli --dry-run", "docs": "deno doc --html --name=AdventOfCode2024 lib/*.ts", diff --git a/lib/rect.ts b/lib/rect.ts index 8da27df..c7537c3 100644 --- a/lib/rect.ts +++ b/lib/rect.ts @@ -78,7 +78,8 @@ export class Point implements PointLike { } static sort(a: Point, b: Point): number { - return (a.x - b.x) || (a.y - b.y); + const [dx, dy] = a.delta(b); + return dx || dy; } xlate(d: PointLike): Point; @@ -107,11 +108,16 @@ export class Point implements PointLike { } dist(p: PointLike): number { - return Math.sqrt(Math.abs(this.x - p.x) ** 2 + Math.abs(this.y - p.y) ** 2); + const [dx, dy] = this.delta(p); + return Math.sqrt((dx ** 2) + (dy ** 2)); } manhattan(p: PointLike): number { - return Math.abs(this.x - p.x) + Math.abs(this.y - p.y); + return this.delta(p).reduce((t, d) => t + Math.abs(d), 0); + } + + delta(p: PointLike): [dx: number, dy: number] { + return [this.x - p.x, this.y - p.y]; } equals(p: PointLike): boolean { @@ -584,10 +590,214 @@ export class InfiniteRect extends Rect { } slice(min: Point, max: Point): Rect { + const [dx, dy] = max.delta(min); return InfiniteRect.ofSize( - max.x - min.x + 1, - max.y - min.y + 1, + dx + 1, + dy + 1, (x: number, y: number): T => this.get(x, y), ); } } + +/** + * Massive overkill of a class so that I don't have to convert points to and + * from strings or numbers by hand as frequently just to store them in a set. + */ +export class PointSet { + #set: Set; + #bits: number; + + constructor(iterable?: Iterable | null, bits = 24) { + this.#set = new Set(); + this.#bits = bits; + if (iterable) { + for (const i of iterable) { + this.#set.add(i.toNumber(this.#bits)); + } + } + } + + /** + * Appends a new element with a specified value to the end of the Set. + */ + add(value: Point): this { + this.#set.add(value.toNumber(this.#bits)); + return this; + } + + /** + * Clear all entries from the set. + */ + clear(): void { + this.#set.clear(); + } + + /** + * Removes a specified value from the Set. + * + * @returns Returns true if an element in the Set existed and has been + * removed, or false if the element does not exist. + */ + delete(value: Point): boolean { + return this.#set.delete(value.toNumber(this.#bits)); + } + + /** + * Executes a provided function once per each value in the Set object, in + * insertion order. + */ + forEach( + callbackfn: (value: Point, key: Point, set: PointSet) => void, + thisArg?: unknown, + ): void { + thisArg ??= this; + this.#set.forEach((value, _key) => { + const p = Point.fromNumber(value); + callbackfn.call(thisArg, p, p, this); + }); + } + + /** + * @returns a boolean indicating whether an element with the specified value exists in the Set or not. + */ + has(value: Point): boolean { + return this.#set.has(value.toNumber(this.#bits)); + } + + /** + * @returns the number of (unique) elements in Set. + */ + get size(): number { + return this.#set.size; + } + + /** Iterates over values in the set. */ + *[Symbol.iterator](): SetIterator { + for (const n of this.#set) { + yield Point.fromNumber(n, this.#bits); + } + } + + /** + * Returns an iterable of [v,v] pairs for every value `v` in the set. + */ + *entries(): SetIterator<[Point, Point]> { + for (const n of this.#set) { + const p = Point.fromNumber(n, this.#bits); + yield [p, p]; + } + } + + /** + * Despite its name, returns an iterable of the values in the set. + */ + *keys(): SetIterator { + for (const n of this.#set) { + yield Point.fromNumber(n, this.#bits); + } + } + + /** + * Returns an iterable of values in the set. + */ + *values(): SetIterator { + for (const n of this.#set) { + yield Point.fromNumber(n, this.#bits); + } + } + + #checkBits(other: PointSet): void { + if (other.#bits !== this.#bits) { + throw new Error(`Incompatible bits: ${this.#bits} != ${other.#bits}`); + } + } + + /** + * @returns a new Set containing all the elements in this Set and also all + * the elements in the argument. + */ + union(other: PointSet): PointSet { + this.#checkBits(other); + const res = new PointSet(null, this.#bits); + res.#set = this.#set.union(other.#set); + return res; + } + + /** + * @returns a new Set containing all the elements which are both in this Set + * and in the argument. + */ + intersection(other: PointSet): PointSet { + this.#checkBits(other); + const res = new PointSet(null, this.#bits); + res.#set = this.#set.intersection(other.#set); + return res; + } + + /** + * @returns a new Set containing all the elements in this Set which are not + * also in the argument. + */ + difference(other: PointSet): PointSet { + this.#checkBits(other); + const res = new PointSet(null, this.#bits); + res.#set = this.#set.difference(other.#set); + return res; + } + + /** + * @returns a new Set containing all the elements which are in either this + * Set or in the argument, but not in both. + */ + symmetricDifference(other: PointSet): PointSet { + this.#checkBits(other); + const res = new PointSet(null, this.#bits); + res.#set = this.#set.symmetricDifference(other.#set); + return res; + } + + /** + * @returns a boolean indicating whether all the elements in this Set are + * also in the argument. + */ + isSubsetOf(other: PointSet): boolean { + this.#checkBits(other); + return this.#set.isSubsetOf(other.#set); + } + + /** + * @returns a boolean indicating whether all the elements in the argument + * are also in this Set. + */ + isSupersetOf(other: PointSet): boolean { + this.#checkBits(other); + return this.#set.isSupersetOf(other.#set); + } + + /** + * @returns a boolean indicating whether this Set has no elements in common + * with the argument. + */ + isDisjointFrom(other: PointSet): boolean { + this.#checkBits(other); + return this.#set.isDisjointFrom(other.#set); + } + + [Symbol.for('Deno.customInspect')](): string { + let ret = `PointSet(${this.size}) { `; + let first = true; + for (const p of this) { + if (first) { + first = false; + } else { + ret += ', '; + } + ret += `[${p.toString()}]`; + } + if (!first) { + ret += ' '; + } + ret += '}'; + return ret; + } +} diff --git a/lib/test/rect.test.ts b/lib/test/rect.test.ts index 91a1629..107b455 100644 --- a/lib/test/rect.test.ts +++ b/lib/test/rect.test.ts @@ -1,4 +1,4 @@ -import { Dir, InfiniteRect, Point, Rect } from '../rect.ts'; +import { Dir, InfiniteRect, Point, PointSet, Rect } from '../rect.ts'; import { assert, @@ -204,3 +204,114 @@ Deno.test('InfinitRect', async (t) => { assertEquals(Deno.inspect(s), 'ab\nde'); }); }); + +Deno.test('PointSet', async (t) => { + await t.step('create', () => { + let p = new PointSet(); + assertEquals(p.size, 0); + p = new PointSet(null); + assertEquals(p.size, 0); + p = new PointSet(undefined); + assertEquals(p.size, 0); + p = new PointSet([]); + assertEquals(p.size, 0); + p = new PointSet([new Point(1, 1)]); + assertEquals(p.size, 1); + }); + + await t.step('add/delete', () => { + const p = new Point(1, 1); + const s = new PointSet(); + assertEquals(s.size, 0); + s.add(p); + assertEquals(s.size, 1); + s.add(p); + assertEquals(s.size, 1); + s.delete(p); + assertEquals(s.size, 0); + s.add(p); + assertEquals(s.size, 1); + s.clear(); + assertEquals(s.size, 0); + }); + + await t.step('forEach', () => { + const s = new PointSet(); + for (let x = 0; x < 10; x++) { + s.add(new Point(x, 0)); + } + assertEquals(s.size, 10); + const boo = {}; + s.forEach(function (p): void { + assertEquals(p.constructor.name, 'Point'); + // @ts-expect-error Shadowed this + assertEquals(this, boo); + }, boo); + + assert(s.has(new Point(5, 0))); + + let count = 0; + for (const _p of s) { + count++; + } + assertEquals(count, 10); + + count = 0; + for (const [_p] of s.entries()) { + count++; + } + assertEquals(count, 10); + + count = 0; + for (const _p of s.keys()) { + count++; + } + assertEquals(count, 10); + + count = 0; + for (const _p of s.values()) { + count++; + } + assertEquals(count, 10); + }); + + await t.step('union', () => { + const a = new PointSet([ + new Point(0, 0), + new Point(0, 2), + new Point(0, 3), + new Point(0, 4), + ]); + const b = new PointSet([ + new Point(0, 1), + new Point(0, 3), + new Point(0, 5), + ]); + const c = new PointSet(null, 16); + + assertThrows(() => a.union(c)); + const u = a.union(b); + assertEquals(u.size, 6); + const i = a.intersection(b); + assertEquals(i.size, 1); + const d = a.difference(b); + assertEquals(d.size, 3); + const s = a.symmetricDifference(b); + assertEquals(s.size, 5); + assertFalse(a.isSubsetOf(b)); + assertFalse(a.isSupersetOf(b)); + assertFalse(a.isDisjointFrom(b)); + }); + + await t.step('customInspect', () => { + const a = new PointSet([ + new Point(0, 0), + new Point(0, 2), + new Point(0, 3), + new Point(0, 4), + ]); + // @ts-expect-error No type info available + const s = a[Symbol.for('Deno.customInspect')](); + assertEquals(s, 'PointSet(4) { [0,0], [0,2], [0,3], [0,4] }'); + }); +});