Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disallow infinite Rat instances #62

Merged
merged 2 commits into from
Jul 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading