Skip to content

Commit

Permalink
r1: Interval
Browse files Browse the repository at this point in the history
  • Loading branch information
missinglink committed Jul 18, 2024
1 parent 0c8f3e4 commit f25f75c
Show file tree
Hide file tree
Showing 2 changed files with 250 additions and 0 deletions.
125 changes: 125 additions & 0 deletions r1/Interval.ts
Original file line number Diff line number Diff line change
@@ -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)
}
}
125 changes: 125 additions & 0 deletions r1/Interval_test.ts
Original file line number Diff line number Diff line change
@@ -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]')
})

0 comments on commit f25f75c

Please sign in to comment.