From f25f75cc53dd2488b92b6ecc70db2afc53063fd3 Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Thu, 18 Jul 2024 18:47:02 +0200 Subject: [PATCH] r1: Interval --- r1/Interval.ts | 125 ++++++++++++++++++++++++++++++++++++++++++++ r1/Interval_test.ts | 125 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 r1/Interval.ts create mode 100644 r1/Interval_test.ts diff --git a/r1/Interval.ts b/r1/Interval.ts new file mode 100644 index 0000000..03f74de --- /dev/null +++ b/r1/Interval.ts @@ -0,0 +1,125 @@ +export class Interval { + lo: number + hi: number + + constructor(lo: number = 0.0, hi: number = 0.0) { + this.lo = lo + this.hi = hi + } + + // isEmpty reports whether the interval is empty + isEmpty(): boolean { + return this.lo > this.hi + } + + // equal returns true iff the interval contains the same points as oi + equal(oi: Interval): boolean { + return (this.lo == oi.lo && this.hi == oi.hi) || (this.isEmpty() && oi.isEmpty()) + } + + // center returns the midpoint of the interval + // It is undefined for empty intervals + center(): number { + return 0.5 * (this.lo + this.hi) + } + + // length returns the length of the interval + // The length of an empty interval is negative + length(): number { + return this.hi - this.lo + } + + // contains returns true iff the interval contains p + contains(p: number): boolean { + return this.lo <= p && p <= this.hi + } + + // containsInterval returns true iff the interval contains oi + containsInterval(oi: Interval): boolean { + if (oi.isEmpty()) return true + return this.lo <= oi.lo && oi.hi <= this.hi + } + + // interiorContains returns true iff the interval strictly contains p + interiorContains(p: number): boolean { + return this.lo < p && p < this.hi + } + + // interiorContainsInterval returns true iff the interval strictly contains oi + interiorContainsInterval(oi: Interval): boolean { + if (oi.isEmpty()) return true + return this.lo < oi.lo && oi.hi < this.hi + } + + // intersects returns true iff the interval contains any points in common with oi. + intersects(oi: Interval): boolean { + if (this.lo <= oi.lo) return oi.lo <= this.hi && oi.lo <= oi.hi // oi.lo ∈ i and oi is not empty + return this.lo <= oi.hi && this.lo <= this.hi // i.lo ∈ oi and i is not empty + } + + // interiorIntersects returns true iff the interior of the interval contains any points in common with oi, including the latter's boundary. + interiorIntersects(oi: Interval): boolean { + return oi.lo < this.hi && this.lo < oi.hi && this.lo < this.hi && oi.lo <= oi.hi + } + + // intersection returns the interval containing all points common to i and j. + intersection(j: Interval): Interval { + // Empty intervals do not need to be special-cased. + return new Interval(Math.max(this.lo, j.lo), Math.min(this.hi, j.hi)) + } + + // addPoint returns the interval expanded so that it contains the given point. + addPoint(p: number): Interval { + if (this.isEmpty()) return new Interval(p, p) + if (p < this.lo) return new Interval(p, this.hi) + if (p > this.hi) return new Interval(this.lo, p) + return this + } + + // clampPoint returns the closest point in the interval to the given point "p". + // The interval must be non-empty. + clampPoint(p: number): number { + return Math.max(this.lo, Math.min(this.hi, p)) + } + + // expanded returns an interval that has been expanded on each side by margin. + // If margin is negative, then the function shrinks the interval on + // each side by margin instead. The resulting interval may be empty. Any + // expansion of an empty interval remains empty. + expanded(margin: number): Interval { + if (this.isEmpty()) return this + return new Interval(this.lo - margin, this.hi + margin) + } + + // union returns the smallest interval that contains this interval and the given interval. + union(other: Interval): Interval { + if (this.isEmpty()) return other + if (other.isEmpty()) return this + return new Interval(Math.min(this.lo, other.lo), Math.max(this.hi, other.hi)) + } + + // trunc truncates {lo, hi} floats to n digits of precision + trunc(n: number = 15): Interval { + const p = Number(`1e${n}`) + const trunc = (dim: number) => Math.round(dim * p) / p + return new Interval(trunc(this.lo), trunc(this.hi)) + } + + // toString generates a human readable string + toString(): string { + const t = this.trunc(7) + return `[${t.lo.toFixed(7)}, ${t.hi.toFixed(7)}]` + } + + // Constructors + + // empty returns an empty interval. + static empty(): Interval { + return new Interval(1, 0) + } + + // IntervalFromPoint returns an interval representing a single point. + static fromPoint(p: number): Interval { + return new Interval(p, p) + } +} diff --git a/r1/Interval_test.ts b/r1/Interval_test.ts new file mode 100644 index 0000000..1204126 --- /dev/null +++ b/r1/Interval_test.ts @@ -0,0 +1,125 @@ +import test from 'node:test' +import { ok, equal } from 'node:assert/strict' +import { Interval } from './Interval' + +// Some standard intervals for use throughout the tests. +const UNIT = new Interval(0, 1) +const NEG_UNIT = new Interval(-1, 0) +const HALF = new Interval(0.5, 0.5) +const EMPTY = Interval.empty() + +test('isEmpty', t => { + ok(!UNIT.isEmpty(), 'should not be empty') + ok(!NEG_UNIT.isEmpty(), 'should not be empty') + ok(!HALF.isEmpty(), 'should not be empty') + ok(EMPTY.isEmpty(), 'should not empty') +}) + +test('center', t => { + equal(UNIT.center(), 0.5) + equal(NEG_UNIT.center(), -0.5) + equal(HALF.center(), 0.5) +}) + +test('length', t => { + equal(UNIT.length(), 1) + equal(NEG_UNIT.length(), 1) + equal(HALF.length(), 0) +}) + +test('contains', t => { + ok(UNIT.contains(0.5)) + ok(UNIT.interiorContains(0.5)) + + ok(UNIT.contains(0)) + ok(!UNIT.interiorContains(0)) + + ok(UNIT.contains(1)) + ok(!UNIT.interiorContains(1)) +}) + +test('operations', t => { + ok(EMPTY.containsInterval(EMPTY)) + ok(EMPTY.interiorContainsInterval(EMPTY)) + ok(!EMPTY.intersects(EMPTY)) + ok(!EMPTY.interiorIntersects(EMPTY)) + + ok(!EMPTY.containsInterval(UNIT)) + ok(!EMPTY.interiorContainsInterval(UNIT)) + ok(!EMPTY.intersects(UNIT)) + ok(!EMPTY.interiorIntersects(UNIT)) + + ok(UNIT.containsInterval(HALF)) + ok(UNIT.interiorContainsInterval(HALF)) + ok(UNIT.intersects(HALF)) + ok(UNIT.interiorIntersects(HALF)) + + ok(UNIT.containsInterval(UNIT)) + ok(!UNIT.interiorContainsInterval(UNIT)) + ok(UNIT.intersects(UNIT)) + ok(UNIT.interiorIntersects(UNIT)) + + ok(UNIT.containsInterval(EMPTY)) + ok(UNIT.interiorContainsInterval(EMPTY)) + ok(!UNIT.intersects(EMPTY)) + ok(!UNIT.interiorIntersects(EMPTY)) + + ok(!UNIT.containsInterval(NEG_UNIT)) + ok(!UNIT.interiorContainsInterval(NEG_UNIT)) + ok(UNIT.intersects(NEG_UNIT)) + ok(!UNIT.interiorIntersects(NEG_UNIT)) + + const i = new Interval(0, 0.5) + ok(UNIT.containsInterval(i)) + ok(!UNIT.interiorContainsInterval(i)) + ok(UNIT.intersects(i)) + ok(UNIT.interiorIntersects(i)) + + ok(!HALF.containsInterval(i)) + ok(!HALF.interiorContainsInterval(i)) + ok(HALF.intersects(i)) + ok(!HALF.interiorIntersects(i)) +}) + +test('intersection', t => { + ok(UNIT.intersection(HALF).equal(HALF)) + ok(UNIT.intersection(NEG_UNIT).equal(new Interval(0, 0))) + ok(NEG_UNIT.intersection(HALF).equal(EMPTY)) + ok(UNIT.intersection(EMPTY).equal(EMPTY)) + ok(EMPTY.intersection(UNIT).equal(EMPTY)) +}) + +test('union', t => { + ok(new Interval(99, 100).union(EMPTY).equal(new Interval(99, 100))) + ok(EMPTY.union(new Interval(99, 100)).equal(new Interval(99, 100))) + ok(new Interval(5, 3).union(new Interval(0, -2)).equal(EMPTY)) + ok(new Interval(0, -2).union(new Interval(5, 3)).equal(EMPTY)) + ok(UNIT.union(UNIT).equal(UNIT)) + ok(UNIT.union(NEG_UNIT).equal(new Interval(-1, 1))) + ok(NEG_UNIT.union(UNIT).equal(new Interval(-1, 1))) + ok(HALF.union(UNIT).equal(UNIT)) +}) + +test('addPoint', t => { + ok(EMPTY.addPoint(5).equal(new Interval(5, 5))) + ok(new Interval(5, 5).addPoint(-1).equal(new Interval(-1, 5))) + ok(new Interval(-1, 5).addPoint(0).equal(new Interval(-1, 5))) + ok(new Interval(-1, 5).addPoint(6).equal(new Interval(-1, 6))) +}) + +test('clampPoint', t => { + equal(new Interval(0.1, 0.4).clampPoint(0.3), 0.3) + equal(new Interval(0.1, 0.4).clampPoint(-7.0), 0.1) + equal(new Interval(0.1, 0.4).clampPoint(0.6), 0.4) +}) + +test('expanded', t => { + ok(EMPTY.expanded(0.45).equal(EMPTY)) + ok(UNIT.expanded(0.5).equal(new Interval(-0.5, 1.5))) + ok(UNIT.expanded(-0.5).equal(new Interval(0.5, 0.5))) + ok(UNIT.expanded(-0.51).equal(EMPTY)) +}) + +test('toString', t => { + equal(new Interval(2, 4.5).toString(), '[2.0000000, 4.5000000]') +})