mirror of
https://github.com/planetrenox/ortus.git
synced 2026-10-29 04:34:53 +00:00
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:
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
*.tgz
|
||||
@@ -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.
|
||||
@@ -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
@@ -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
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
})
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user