Skip to content

Commit

Permalink
Disallow infinite Rat instances (#62)
Browse files Browse the repository at this point in the history
* Rat and Second are now vaguely intelligible

* Adjust Second constructor
  • Loading branch information
qntm authored Jul 20, 2024
1 parent 99913b0 commit 86e00d0
Show file tree
Hide file tree
Showing 11 changed files with 339 additions and 515 deletions.
12 changes: 6 additions & 6 deletions src/munge.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const MODELS = {
}

const NOV = 10
const secondsPerDay = new Second(86_400n, 1n)
const secondsPerDay = new Second(new Rat(86_400n, 1n))
const mjdEpoch = {
unix: Second.fromMillis(Date.UTC(1858, NOV, 17))
}
Expand Down Expand Up @@ -63,20 +63,20 @@ export const munge = (data, model) => {
// Convert from a floating point number to a precise ratio
// Offsets are given in TAI seconds to seven decimal places, e.g. `1.422_818_0`.
// So we have to do some rounding
offsetAtRoot.atomic = new Second(
offsetAtRoot.atomic = new Second(new Rat(
BigInt(Math.round(offsetAtRoot.atomicFloat * 10_000_000)),
BigInt(10_000_000)
)
))

root.unix = mjdEpoch.unix.plusS(secondsPerDay.timesR(new Rat(BigInt(root.mjds))))

// Convert from a floating point number to a precise ratio
// Drift rates are given in TAI seconds to seven decimal places, e.g. `0.001_123_2`
// So we have to do some rounding
driftRate.atomicPerUnixDay = new Second(
driftRate.atomicPerUnixDay = new Second(new Rat(
BigInt(Math.round(driftRate.atomicPerUnixDayFloat * 10_000_000)),
BigInt(10_000_000)
)
))
driftRate.atomicPerUnix = driftRate.atomicPerUnixDay.divideS(secondsPerDay)

const slope = {}
Expand Down Expand Up @@ -104,7 +104,7 @@ export const munge = (data, model) => {
: Second.END_OF_TIME
}

if (datum.end.atomic.leS(datum.start.atomic)) {
if (datum.end.atomic === Second.END_OF_TIME ? datum.start.atomic === Second.END_OF_TIME : datum.end.atomic.leS(datum.start.atomic)) {
throw Error('Disordered data')
}
})
Expand Down
15 changes: 5 additions & 10 deletions src/rat.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,24 @@ import { div, gcd } from './div.js'
export class Rat {
constructor (nu, de = 1n) {
if (typeof nu !== 'bigint') {
throw Error('Numerator must be a BigInt')
throw Error('numerator must be a BigInt')
}
if (typeof de !== 'bigint') {
throw Error('Denominator must be a BigInt')
throw Error('denominator must be a BigInt')
}
if (de === 0n && nu <= 0n) {
throw Error('Numerator must be positive if denominator is zero')
if (de === 0n) {
throw Error('denominator cannot be zero')
}

const g = gcd(nu, de) // non-zero

const g2 = (de < 0) === (g < 0) ? g : -g

this.nu = nu / g2 // sign of `this.nu` is the sign of the represented rational
this.de = de / g2 // non-negative
this.de = de / g2 // positive
}

plus (other) {
if (this.de === 0n && other.de === 0n) {
return new Rat(this.nu + other.nu, 0n)
}
return new Rat(this.nu * other.de + this.de * other.nu, this.de * other.de)
}

Expand Down Expand Up @@ -56,5 +53,3 @@ export class Rat {
return div(this.nu, this.de)
}
}

Rat.INFINITY = new Rat(1n, 0n)
23 changes: 8 additions & 15 deletions src/second.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,24 @@
import { Rat } from './rat.js'

export class Second {
constructor (nu, de) {
this.rat = new Rat(nu, de)
constructor (rat) {
this.rat = rat
}

plusS (other) {
const sum = this.rat.plus(other.rat)
return new Second(sum.nu, sum.de)
return new Second(this.rat.plus(other.rat))
}

minusS (other) {
const difference = this.rat.minus(other.rat)
return new Second(difference.nu, difference.de)
return new Second(this.rat.minus(other.rat))
}

timesR (other) {
const product = this.rat.times(other)
return new Second(product.nu, product.de)
return new Second(this.rat.times(other))
}

divideR (other) {
const quotient = this.rat.divide(other)
return new Second(quotient.nu, quotient.de)
return new Second(this.rat.divide(other))
}

divideS (other) {
Expand Down Expand Up @@ -51,10 +47,7 @@ Second.fromMillis = millis => {
throw Error(`Not an integer: ${millis}`)
}

return new Second(BigInt(millis), 1_000n)
return new Second(new Rat(BigInt(millis), 1_000n))
}

// Support for this special value is limited. In all cases it either returns
// a correct, meaningful result, or throws an exception - it does NOT return
// bad results.
Second.END_OF_TIME = new Second(1n, 0n)
Second.END_OF_TIME = Symbol('end of time')
22 changes: 13 additions & 9 deletions src/segment.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@ import { Second } from './second.js'
export class Segment {
constructor (start, end = { atomic: Second.END_OF_TIME }, slope = { unixPerAtomic: new Rat(1n) }) {
if (!(start.atomic instanceof Second)) {
throw Error('TAI start must be a rational number of seconds')
throw Error('TAI start must be a `Second`')
}
if (!(start.unix instanceof Second)) {
throw Error('Unix start must be a rational number of seconds')
throw Error('Unix start must be a `Second`')
}
if (!(end.atomic instanceof Second)) {
throw Error('TAI end must be a rational number of seconds')
if (!(end.atomic instanceof Second || end.atomic === Second.END_OF_TIME)) {
throw Error('TAI end must be a `Second` or `Second.END_OF_TIME`')
}
if (!(slope.unixPerAtomic instanceof Rat)) {
throw Error('Slope must be a pure ratio')
throw Error('slope must be a `Rat`')
}
if (end.atomic.leS(start.atomic)) {
throw Error('Segment length must be positive')
if (end.atomic === Second.END_OF_TIME ? start.atomic === Second.END_OF_TIME : end.atomic.leS(start.atomic)) {
throw Error('segment length must be positive')
}

this.slope = {
Expand Down Expand Up @@ -59,6 +59,10 @@ export class Segment {
}

atomicToUnix (atomic) {
if (atomic === Second.END_OF_TIME) {
return Second.END_OF_TIME
}

return atomic
.minusS(this.start.atomic)
.timesR(this.slope.unixPerAtomic)
Expand All @@ -71,12 +75,12 @@ export class Segment {
// Unix by the segment.

atomicOnSegment (atomic) {
return this.start.atomic.leS(atomic) && this.end.atomic.gtS(atomic)
return this.start.atomic.leS(atomic) && (this.end.atomic === Second.END_OF_TIME ? atomic !== Second.END_OF_TIME : this.end.atomic.gtS(atomic))
}

unixOnSegment (unix) {
return this.slope.unixPerAtomic.eq(new Rat(0n))
? this.start.unix.eqS(unix)
: this.start.unix.leS(unix) && this.end.unix.gtS(unix)
: this.start.unix.leS(unix) && (this.end.unix === Second.END_OF_TIME ? unix !== Second.END_OF_TIME : this.end.unix.gtS(unix))
}
}
29 changes: 15 additions & 14 deletions test/converter.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { describe, it } from 'mocha'
import { Converter } from '../src/converter.js'
import { MODELS } from '../src/munge.js'
import { Range } from '../src/range.js'
import { Rat } from '../src/rat.js'
import { Second } from '../src/second.js'

const JAN = 0
Expand Down Expand Up @@ -267,7 +268,7 @@ describe('Converter', () => {
])
assert.deepStrictEqual(converter.unixToAtomic(Second.fromMillis(Date.UTC(1979, DEC, 31, 12, 0, 0, 1))),
[
new Range(Second.fromMillis(Date.UTC(1979, DEC, 31, 12, 0, 0, 1)).plusS(new Second(1n, 86_400_000n)))
new Range(Second.fromMillis(Date.UTC(1979, DEC, 31, 12, 0, 0, 1)).plusS(new Second(new Rat(1n, 86_400_000n))))
])

// SMEAR MIDPOINT
Expand All @@ -279,7 +280,7 @@ describe('Converter', () => {
// SMEAR ENDS, ATOMIC IS A FULL SECOND AHEAD (actually Unix is a full second behind)
assert.deepStrictEqual(converter.unixToAtomic(Second.fromMillis(Date.UTC(1980, JAN, 1, 11, 59, 59, 999))),
[
new Range(Second.fromMillis(Date.UTC(1980, JAN, 1, 11, 59, 59, 999)).plusS(new Second(86_399_999n, 86_400_000n)))
new Range(Second.fromMillis(Date.UTC(1980, JAN, 1, 11, 59, 59, 999)).plusS(new Second(new Rat(86_399_999n, 86_400_000n))))
])
assert.deepStrictEqual(converter.unixToAtomic(Second.fromMillis(Date.UTC(1980, JAN, 1, 12, 0, 0, 0))),
[
Expand All @@ -301,15 +302,15 @@ describe('Converter', () => {
assert.deepStrictEqual(converter.atomicToUnix(Second.fromMillis(Date.UTC(1979, DEC, 31, 12, 0, 0, 0))),
Second.fromMillis(Date.UTC(1979, DEC, 31, 12, 0, 0, 0)))
assert.deepStrictEqual(converter.atomicToUnix(Second.fromMillis(Date.UTC(1979, DEC, 31, 12, 0, 0, 1))),
Second.fromMillis(Date.UTC(1979, DEC, 31, 12, 0, 0, 1)).minusS(new Second(1n, 86_401_000n)))
Second.fromMillis(Date.UTC(1979, DEC, 31, 12, 0, 0, 1)).minusS(new Second(new Rat(1n, 86_401_000n))))

// SMEAR MIDPOINT
assert.deepStrictEqual(converter.atomicToUnix(Second.fromMillis(Date.UTC(1980, JAN, 1, 0, 0, 0, 500))),
Second.fromMillis(Date.UTC(1980, JAN, 1, 0, 0, 0, 0)))

// SMEAR ENDS, UNIX HAS DROPPED A FULL SECOND BEHIND
assert.deepStrictEqual(converter.atomicToUnix(Second.fromMillis(Date.UTC(1980, JAN, 1, 12, 0, 0, 999))),
Second.fromMillis(Date.UTC(1980, JAN, 1, 12, 0, 0, 999)).minusS(new Second(86_400_999n, 86_401_000n)))
Second.fromMillis(Date.UTC(1980, JAN, 1, 12, 0, 0, 999)).minusS(new Second(new Rat(86_400_999n, 86_401_000n))))
assert.deepStrictEqual(converter.atomicToUnix(Second.fromMillis(Date.UTC(1980, JAN, 1, 12, 0, 1, 0))),
Second.fromMillis(Date.UTC(1980, JAN, 1, 12, 0, 0, 0)))
assert.deepStrictEqual(converter.atomicToUnix(Second.fromMillis(Date.UTC(1980, JAN, 1, 12, 0, 1, 1))),
Expand Down Expand Up @@ -482,7 +483,7 @@ describe('Converter', () => {
])
assert.deepStrictEqual(converter.unixToAtomic(Second.fromMillis(Date.UTC(1979, DEC, 31, 12, 0, 0, 1))),
[
new Range(Second.fromMillis(Date.UTC(1979, DEC, 31, 12, 0, 0, 1)).minusS(new Second(1n, 86_400_000n)))
new Range(Second.fromMillis(Date.UTC(1979, DEC, 31, 12, 0, 0, 1)).minusS(new Second(new Rat(1n, 86_400_000n))))
])

// SMEAR MIDPOINT
Expand All @@ -494,7 +495,7 @@ describe('Converter', () => {
// SMEAR ENDS, ATOMIC IS A FULL SECOND BEHIND (actually Unix is a full second ahead)
assert.deepStrictEqual(converter.unixToAtomic(Second.fromMillis(Date.UTC(1980, JAN, 1, 11, 59, 59, 999))),
[
new Range(Second.fromMillis(Date.UTC(1980, JAN, 1, 11, 59, 59, 999)).minusS(new Second(86_399_999n, 86_400_000n)))
new Range(Second.fromMillis(Date.UTC(1980, JAN, 1, 11, 59, 59, 999)).minusS(new Second(new Rat(86_399_999n, 86_400_000n))))
])
assert.deepStrictEqual(converter.unixToAtomic(Second.fromMillis(Date.UTC(1980, JAN, 1, 12, 0, 0, 0))),
[
Expand All @@ -516,15 +517,15 @@ describe('Converter', () => {
assert.deepStrictEqual(converter.atomicToUnix(Second.fromMillis(Date.UTC(1979, DEC, 31, 12, 0, 0, 0))),
Second.fromMillis(Date.UTC(1979, DEC, 31, 12, 0, 0, 0)))
assert.deepStrictEqual(converter.atomicToUnix(Second.fromMillis(Date.UTC(1979, DEC, 31, 12, 0, 0, 1))),
Second.fromMillis(Date.UTC(1979, DEC, 31, 12, 0, 0, 1)).plusS(new Second(1n, 86_399_000n)))
Second.fromMillis(Date.UTC(1979, DEC, 31, 12, 0, 0, 1)).plusS(new Second(new Rat(1n, 86_399_000n))))

// SMEAR MIDPOINT
assert.deepStrictEqual(converter.atomicToUnix(Second.fromMillis(Date.UTC(1979, DEC, 31, 23, 59, 59, 500))),
Second.fromMillis(Date.UTC(1980, JAN, 1, 0, 0, 0, 0)))

// SMEAR ENDS, UNIX HAS RUN A FULL SECOND FASTER THAN ATOMIC
assert.deepStrictEqual(converter.atomicToUnix(Second.fromMillis(Date.UTC(1980, JAN, 1, 11, 59, 58, 999))),
Second.fromMillis(Date.UTC(1980, JAN, 1, 11, 59, 58, 999)).plusS(new Second(86_398_999n, 86_399_000n)))
Second.fromMillis(Date.UTC(1980, JAN, 1, 11, 59, 58, 999)).plusS(new Second(new Rat(86_398_999n, 86_399_000n))))
assert.deepStrictEqual(converter.atomicToUnix(Second.fromMillis(Date.UTC(1980, JAN, 1, 11, 59, 59, 0))),
Second.fromMillis(Date.UTC(1980, JAN, 1, 12, 0, 0, 0)))
assert.deepStrictEqual(converter.atomicToUnix(Second.fromMillis(Date.UTC(1980, JAN, 1, 11, 59, 59, 1))),
Expand All @@ -543,9 +544,9 @@ describe('Converter', () => {

assert.deepStrictEqual(converter.unixToAtomic(Second.fromMillis(1)),
[
new Range(new Second(900n, 1_000_000n))
new Range(new Second(new Rat(900n, 1_000_000n)))
])
assert.deepStrictEqual(converter.atomicToUnix(new Second(900n, 1_000_000n)),
assert.deepStrictEqual(converter.atomicToUnix(new Second(new Rat(900n, 1_000_000n))),
Second.fromMillis(1))
})

Expand All @@ -558,9 +559,9 @@ describe('Converter', () => {

assert.deepStrictEqual(converter.unixToAtomic(Second.fromMillis(-1)),
[
new Range(new Second(-900n, 1_000_000n))
new Range(new Second(new Rat(-900n, 1_000_000n)))
])
assert.deepStrictEqual(converter.atomicToUnix(new Second(-900n, 1_000_000n)),
assert.deepStrictEqual(converter.atomicToUnix(new Second(new Rat(-900n, 1_000_000n))),
Second.fromMillis(-1))
})
})
Expand Down Expand Up @@ -678,14 +679,14 @@ describe('Converter', () => {
describe('BREAK', () => {
it('says no', () => {
assert.throws(() => new Converter(data, MODELS.BREAK),
/Segment length must be positive/)
/segment length must be positive/)
})
})

describe('STALL', () => {
it('says no', () => {
assert.throws(() => new Converter(data, MODELS.STALL),
/Segment length must be positive/)
/segment length must be positive/)
})
})
})
Expand Down
Loading

0 comments on commit 86e00d0

Please sign in to comment.