PRT (Planet Renox Time): ms since 1998-03-14T03:00:00Z

Zero-dependency ESM package with types. now, fromDate, toDate,
fromUnixMs, toUnixMs; units ms|s|m|h|d|w, floored toward -Infinity,
exact across the full JS Date range. 0BSD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 19:46:17 +00:00
co-authored by Claude Opus 5.5
commit 45daf60bb5
7 changed files with 260 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
node_modules/
*.tgz
+12
View File
@@ -0,0 +1,12 @@
Copyright (C) 2026 by planetrenox
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+43
View File
@@ -0,0 +1,43 @@
# ortus
**PRT (Planet Renox Time)**: milliseconds since `1998-03-14T03:00:00.000Z`.
```sh
npm i ortus
```
```js
import { PRT } from 'ortus'
PRT.now() // PRT ms, the official unit
PRT.now('d') // days
PRT.fromDate(new Date('2010-06-01'), 'd') // 4461
PRT.toDate(1e9, 's') // 2029-11-20T04:46:40.000Z
PRT.fromUnixMs(Date.now()) // same as PRT.now()
PRT.toUnixMs(0) // 889844400000
```
## API
| | |
|---|---|
| `PRT.now(unit = 'ms')` | Current PRT |
| `PRT.fromDate(date, unit = 'ms')` | `Date` → PRT |
| `PRT.toDate(n, unit = 'ms')` | PRT → `Date` |
| `PRT.fromUnixMs(ms, unit = 'ms')` | Unix ms → PRT |
| `PRT.toUnixMs(n, unit = 'ms')` | PRT → Unix ms |
Units: `ms` (official), `s`, `m`, `h`, `d`, `w`.
## Rules
- **Always an integer, rounded down toward −∞.** `1.5s → 1`, `-1.5s → -2`, so every unit is the same length on both sides of the epoch.
- **Before the epoch is negative.**
- **Strict input.** `fromDate` takes only valid `Date`s. `fromUnixMs`, `toDate`, and `toUnixMs` take only integers. Anything else throws.
- **No overflow.** Every value is exact across the full JS `Date` range (±275,000 years). Anything outside it throws.
- **Leap seconds are ignored**, the same as JS `Date`.
- **No dependencies.**
## License
0BSD. Use it however you want, no attribution needed.
Vendored
+17
View File
@@ -0,0 +1,17 @@
/** PRT units: milliseconds (official), seconds, minutes, hours, days, weeks */
export type Unit = 'ms' | 's' | 'm' | 'h' | 'd' | 'w'
export declare const PRT: Readonly<{
/** Current PRT, rounded down. Defaults to ms. */
now(unit?: Unit): number
/** PRT at the given Date, rounded down. Defaults to ms. */
fromDate(date: Date, unit?: Unit): number
/** Date at the given integer PRT value. Defaults to ms. */
toDate(value: number, unit?: Unit): Date
/** PRT at the given integer Unix ms, rounded down. Defaults to ms. */
fromUnixMs(unixMs: number, unit?: Unit): number
/** Unix ms at the given integer PRT value. Defaults to ms. */
toUnixMs(value: number, unit?: Unit): number
}>
export default PRT
+37
View File
@@ -0,0 +1,37 @@
// PRT — Planet Renox Time: ms since 1998-03-14T03:00:00.000Z
const E = 889844400000
const MAX = 8.64e15 // JS Date range, ±275k years
const U = { __proto__: null, ms: 1, s: 1e3, m: 6e4, h: 36e5, d: 864e5, w: 6048e5 }
const T = Date.prototype.getTime
const unit = u => {
if (typeof u != 'string' || !U[u]) throw new RangeError(`PRT: unknown unit ${String(u)}, use ms|s|m|h|d|w`)
return U[u]
}
// Math.floor rounds toward -Infinity, exact since every in-range ms diff is an integer below 2^53
const fromUnixMs = (t, u = 'ms') => {
if (!Number.isInteger(t)) throw new TypeError('PRT.fromUnixMs: expected an integer')
if (Math.abs(t) > MAX) throw new RangeError('PRT.fromUnixMs: out of Date range')
return Math.floor((t - E) / unit(u))
}
const fromDate = (date, u = 'ms') => {
let t
try { t = T.call(date) } catch {} // brand check: real Dates only, works across realms
if (!Number.isFinite(t)) throw new TypeError('PRT.fromDate: expected a valid Date')
return fromUnixMs(t, u)
}
const toUnixMs = (n, u = 'ms') => {
if (!Number.isSafeInteger(n)) throw new TypeError('PRT: expected an integer PRT value')
const t = n * unit(u) + E
if (!(Math.abs(t) <= MAX)) throw new RangeError('PRT: out of Date range')
return t
}
const now = (u = 'ms') => fromUnixMs(Date.now(), u)
const toDate = (n, u = 'ms') => new Date(toUnixMs(n, u))
export const PRT = Object.freeze({ now, fromDate, toDate, fromUnixMs, toUnixMs })
export default PRT
+116
View File
@@ -0,0 +1,116 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { runInNewContext } from 'node:vm'
import PRT, { PRT as named } from './index.js'
const E = 889844400000
const EPOCH = new Date('1998-03-14T03:00:00.000Z')
const at = ms => new Date(E + ms)
const U = { ms: 1, s: 1e3, m: 6e4, h: 36e5, d: 864e5, w: 6048e5 }
const big = (a, b) => { const q = a / b; return a % b < 0n ? q - 1n : q }
const exact = (t, u) => Number(big(BigInt(t) - BigInt(E), BigInt(U[u])))
test('exports', () => {
assert.equal(PRT, named)
assert.ok(Object.isFrozen(PRT))
assert.deepEqual(Object.keys(PRT), ['now', 'fromDate', 'toDate', 'fromUnixMs', 'toUnixMs'])
})
test('epoch is 0 in every unit', () => {
assert.equal(EPOCH.getTime(), E)
for (const u in U) {
assert.equal(PRT.fromDate(EPOCH, u), 0)
assert.equal(PRT.fromUnixMs(E, u), 0)
}
})
test('defaults to ms', () => {
assert.equal(PRT.fromDate(at(1234)), 1234)
assert.equal(PRT.fromUnixMs(E + 1234), 1234)
assert.equal(PRT.toDate(1234).getTime(), E + 1234)
assert.equal(PRT.toUnixMs(1234), E + 1234)
})
test('units', () => {
assert.deepEqual(Object.keys(U).map(u => PRT.fromDate(at(6048e5), u)), [6048e5, 604800, 10080, 168, 7, 1])
})
test('rounds down toward -Infinity', () => {
assert.equal(PRT.fromDate(at(1999), 's'), 1)
assert.equal(PRT.fromDate(at(-1), 's'), -1)
assert.equal(PRT.fromDate(at(-1500), 's'), -2)
assert.equal(PRT.fromDate(at(-1000), 's'), -1)
assert.equal(PRT.fromUnixMs(E - 1, 'w'), -1)
assert.equal(PRT.fromUnixMs(0, 'd'), -10300) // -10299.125
})
test('exact at the edges of Date range', () => {
for (const t of [8.64e15, -8.64e15, 8.64e15 - 1, -8.64e15 + 1])
for (const u in U) {
assert.equal(PRT.fromDate(new Date(t), u), exact(t, u), `${t} ${u}`)
assert.equal(PRT.fromUnixMs(t, u), exact(t, u), `${t} ${u}`)
}
})
test('exact for 60k random times across the full range', () => {
for (let i = 0; i < 1e4; i++) {
const t = Math.round((Math.random() * 2 - 1) * 8.64e15)
for (const u in U) assert.equal(PRT.fromUnixMs(t, u), exact(t, u), `${t} ${u}`)
}
})
test('exact one ms before every unit boundary at huge values', () => {
for (const u in U) {
const b = U[u], k = Math.floor((8.64e15 - E) / b)
assert.equal(PRT.fromUnixMs(E + k * b - 1, u), k - 1, u)
assert.equal(PRT.fromUnixMs(E + k * b, u), k, u)
}
})
test('now', () => {
const a = Date.now(), n = PRT.now(), b = Date.now()
assert.ok(n >= a - E && n <= b - E)
assert.ok(Number.isSafeInteger(PRT.now('w')))
})
test('round trips', () => {
for (const ms of [0, 1, -1, 123456789, -987654321, 8.64e15 - E, -8.64e15 - E]) {
assert.equal(PRT.fromDate(PRT.toDate(ms)), ms)
assert.equal(PRT.fromUnixMs(PRT.toUnixMs(ms)), ms)
}
for (const u in U) for (const n of [0, 1, -1, 4461])
assert.equal(PRT.toUnixMs(n, u), PRT.toDate(n, u).getTime())
assert.equal(PRT.toUnixMs(1, 'w'), E + 6048e5)
assert.equal(PRT.toUnixMs(-1, 'd'), E - 864e5)
})
test('accepts Dates from other realms', () => {
assert.equal(PRT.fromDate(runInNewContext('new Date("1998-03-14T03:00:01Z")'), 's'), 1)
})
test('fromDate rejects non-Dates, including numbers', () => {
for (const x of [Date.now(), 0, '1998-03-14', null, undefined, {}, { getTime: () => 0 }, new Date(NaN), Object.create(Date.prototype)])
assert.throws(() => PRT.fromDate(x), TypeError)
})
test('fromUnixMs rejects non-integers and out of range', () => {
for (const x of [1.5, NaN, Infinity, -Infinity, '0', null, undefined, 0n, new Date(0), {}])
assert.throws(() => PRT.fromUnixMs(x), TypeError)
for (const x of [8.64e15 + 1, -8.64e15 - 1, 2 ** 53, 1e300])
assert.throws(() => PRT.fromUnixMs(x), RangeError)
})
test('rejects unknown units', () => {
for (const u of ['y', 'mo', 'us', 'MS', 'toString', '__proto__', null, 1, {}, Symbol('s')])
for (const f of [() => PRT.now(u), () => PRT.fromDate(EPOCH, u), () => PRT.fromUnixMs(E, u), () => PRT.toDate(0, u), () => PRT.toUnixMs(0, u)])
assert.throws(f, RangeError)
})
test('toDate and toUnixMs reject bad values', () => {
for (const f of [PRT.toDate, PRT.toUnixMs]) {
for (const n of [1.5, NaN, Infinity, -Infinity, '1', null, 0n, 2 ** 53])
assert.throws(() => f(n), TypeError)
for (const [n, u] of [[1e15, 'w'], [9e15, 'ms'], [8.64e15 - E + 1, 'ms'], [-8.64e15 - E - 1, 'ms'], [Number.MAX_SAFE_INTEGER, 'w']])
assert.throws(() => f(n, u), RangeError)
}
})
+33
View File
@@ -0,0 +1,33 @@
{
"name": "ortus",
"version": "0.1.1",
"description": "PRT (Planet Renox Time): milliseconds since 1998-03-14T03:00:00Z",
"type": "module",
"exports": {
".": {
"types": "./index.d.ts",
"default": "./index.js"
}
},
"types": "./index.d.ts",
"files": [
"index.js",
"index.d.ts"
],
"sideEffects": false,
"scripts": {
"test": "node --test"
},
"keywords": [
"prt",
"planet renox time",
"time",
"timestamp",
"epoch"
],
"license": "0BSD",
"repository": {
"type": "git",
"url": "git+https://github.com/planetrenox/ortus.git"
}
}