From 047c8b02ecfe0e6e1140d6311a2c57d4b258c2af Mon Sep 17 00:00:00 2001 From: Philip Chimento Date: Thu, 20 Mar 2025 18:15:39 -0700 Subject: [PATCH] Draft: Use 128-bit floats instead of BigInts I read a paper on double-double precision arithmetic (i.e., using two Numbers to implement 128-bit floats with the same range as Number but twice the precision.) This precision would allow safe integers covering the entire epoch nanoseconds range that we support, as well as being precise enough for all the floating point calculations that we do in methods like Duration.p.total(). The paper is: Hida, Li, Bailey (2008). Library for double-double and quad-double arithmetic. Technical report, Lawrence Berkeley National Laboratory. https://www.davidhbailey.com/dhbpapers/qd.pdf They published a C++ library at https://github.com/BL-highprecision/QD. It was a quick experiment to implement a subset of the operations in JS in lib/float128.ts. This allows us to get rid of both JSBI as well as the power-of-10-math-by-string-manipulation in lib/math.ts. --- lib/bigintmath.ts | 39 ----- lib/duration.ts | 11 +- lib/ecmascript.ts | 334 ++++++++++++++++++++------------------- lib/float128.ts | 336 +++++++++++++++++++++++++++++++++++++++ lib/instant.ts | 16 +- lib/intrinsicclass.ts | 5 +- lib/math.ts | 51 +----- lib/slots.ts | 4 +- lib/timeduration.ts | 141 +++++------------ lib/zoneddatetime.ts | 28 ++-- package-lock.json | 13 -- package.json | 3 - test/all.mjs | 6 +- test/ecmascript.mjs | 47 +++--- test/float128.mjs | 304 ++++++++++++++++++++++++++++++++++++ test/math.mjs | 110 ------------- test/timeduration.mjs | 355 ++++++++---------------------------------- 17 files changed, 958 insertions(+), 845 deletions(-) delete mode 100644 lib/bigintmath.ts create mode 100644 lib/float128.ts create mode 100644 test/float128.mjs delete mode 100644 test/math.mjs diff --git a/lib/bigintmath.ts b/lib/bigintmath.ts deleted file mode 100644 index 46047e3e..00000000 --- a/lib/bigintmath.ts +++ /dev/null @@ -1,39 +0,0 @@ -import JSBI from 'jsbi'; - -export const ZERO = JSBI.BigInt(0); -export const ONE = JSBI.BigInt(1); -export const TWO = JSBI.BigInt(2); -export const TEN = JSBI.BigInt(10); -const TWENTY_FOUR = JSBI.BigInt(24); -const SIXTY = JSBI.BigInt(60); -export const THOUSAND = JSBI.BigInt(1e3); -export const MILLION = JSBI.BigInt(1e6); -export const BILLION = JSBI.BigInt(1e9); -const HOUR_SECONDS = 3600; -export const HOUR_NANOS = JSBI.multiply(JSBI.BigInt(HOUR_SECONDS), BILLION); -export const MINUTE_NANOS_JSBI = JSBI.multiply(SIXTY, BILLION); -export const DAY_NANOS_JSBI = JSBI.multiply(HOUR_NANOS, TWENTY_FOUR); - -/** Handle a JSBI or native BigInt. For user input, use ES.ToBigInt instead */ -export function ensureJSBI(value: JSBI | bigint) { - return typeof value === 'bigint' ? JSBI.BigInt(value.toString(10)) : value; -} - -export function isEven(value: JSBI): boolean { - return JSBI.equal(JSBI.remainder(value, TWO), ZERO); -} - -export function abs(x: JSBI): JSBI { - if (JSBI.lessThan(x, ZERO)) return JSBI.unaryMinus(x); - return x; -} - -export function compare(x: JSBI, y: JSBI): -1 | 0 | 1 { - return JSBI.lessThan(x, y) ? -1 : JSBI.greaterThan(x, y) ? 1 : 0; -} - -export function divmod(x: JSBI, y: JSBI): { quotient: JSBI; remainder: JSBI } { - const quotient = JSBI.divide(x, y); - const remainder = JSBI.remainder(x, y); - return { quotient, remainder }; -} diff --git a/lib/duration.ts b/lib/duration.ts index 60f32814..7c140141 100644 --- a/lib/duration.ts +++ b/lib/duration.ts @@ -25,7 +25,6 @@ import { import { TimeDuration } from './timeduration'; import type { Temporal } from '..'; import type { DurationParams as Params, DurationReturn as Return } from './internaltypes'; -import JSBI from 'jsbi'; export class Duration implements Temporal.Duration { constructor( @@ -270,8 +269,10 @@ export class Duration implements Temporal.Duration { let internalDuration = ES.ToInternalDurationRecordWith24HourDays(this); if (smallestUnit === 'day') { // First convert time units up to days - const { quotient, remainder } = internalDuration.time.divmod(ES.DAY_NANOS); - let days = internalDuration.date.days + quotient + ES.TotalTimeDuration(remainder, 'day'); + const div = internalDuration.time.totalNs.fdiv(ES.DAY_NANOS); + const quotient = div.toInt(); + const fractionalDays = div.fadd(-quotient).toNumber(); + let days = internalDuration.date.days + quotient + fractionalDays; days = ES.RoundNumberToIncrement(days, roundingIncrement, roundingMode); const dateDuration = { years: 0, months: 0, weeks: 0, days }; internalDuration = ES.CombineDateAndTimeDuration(dateDuration, TimeDuration.ZERO); @@ -416,7 +417,7 @@ export class Duration implements Temporal.Duration { const after1 = ES.AddZonedDateTime(epochNs, timeZone, calendar, duration1); const after2 = ES.AddZonedDateTime(epochNs, timeZone, calendar, duration2); - return ES.ComparisonResult(JSBI.toNumber(JSBI.subtract(after1, after2))); + return ES.ComparisonResult(after1.sub(after2).toNumber()); } let d1 = duration1.date.days; @@ -430,7 +431,7 @@ export class Duration implements Temporal.Duration { } const timeDuration1 = duration1.time.add24HourDays(d1); const timeDuration2 = duration2.time.add24HourDays(d2); - return timeDuration1.cmp(timeDuration2); + return timeDuration1.totalNs.cmp(timeDuration2.totalNs); } [Symbol.toStringTag]!: 'Temporal.Duration'; } diff --git a/lib/ecmascript.ts b/lib/ecmascript.ts index bc56de7b..194944e9 100644 --- a/lib/ecmascript.ts +++ b/lib/ecmascript.ts @@ -1,10 +1,9 @@ import { DEBUG, ENABLE_ASSERTS } from './debug'; -import JSBI from 'jsbi'; import type { Temporal } from '..'; import { assert, assertNotReached } from './assert'; -import { abs, compare, DAY_NANOS_JSBI, divmod, ensureJSBI, isEven, MILLION, ONE, TWO, ZERO } from './bigintmath'; import type { CalendarImpl } from './calendar'; +import { F128 } from './float128'; import type { AnyTemporalLikeType, UnitSmallerThanOrEqualTo, @@ -33,7 +32,7 @@ import type { AnySlottedType } from './internaltypes'; import { GetIntrinsic } from './intrinsicclass'; -import { ApplyUnsignedRoundingMode, FMAPowerOf10, GetUnsignedRoundingMode, TruncatingDivModByPowerOf10 } from './math'; +import { ApplyUnsignedRoundingMode, GetUnsignedRoundingMode } from './math'; import { TimeDuration } from './timeduration'; import { CreateSlots, @@ -67,12 +66,12 @@ const MINUTE_NANOS = 60e9; // Instant range is 100 million days (inclusive) before or after epoch. const MS_MAX = DAY_MS * 1e8; const NS_MAX = epochMsToNs(MS_MAX); -const NS_MIN = JSBI.unaryMinus(NS_MAX); +const NS_MIN = NS_MAX.neg(); // PlainDateTime range is 24 hours wider (exclusive) than the Instant range on // both ends, to allow for valid Instant=>PlainDateTime conversion for all // built-in time zones (whose offsets must have a magnitude less than 24 hours). -const DATETIME_NS_MIN = JSBI.add(JSBI.subtract(NS_MIN, DAY_NANOS_JSBI), ONE); -const DATETIME_NS_MAX = JSBI.subtract(JSBI.add(NS_MAX, DAY_NANOS_JSBI), ONE); +const DATETIME_NS_MIN = NS_MIN.fadd(-DAY_NANOS + 1); +const DATETIME_NS_MAX = NS_MAX.fadd(DAY_NANOS - 1); // The pattern of leap years in the ISO 8601 calendar repeats every 400 years. // The constant below is the number of nanoseconds in 400 years. It is used to // avoid overflows when dealing with values at the edge legacy Date's range. @@ -1706,7 +1705,7 @@ export function InterpretISODateTimeOffset( const possibleEpochNs = GetPossibleEpochNanoseconds(timeZone, dt); for (let index = 0; index < possibleEpochNs.length; index++) { const candidate = possibleEpochNs[index]; - const candidateOffset = JSBI.toNumber(JSBI.subtract(utcEpochNs, candidate)); + const candidateOffset = utcEpochNs.sub(candidate).toNumber(); const roundedCandidateOffset = RoundNumberToIncrement(candidateOffset, 60e9, 'halfExpand'); if (candidateOffset === offsetNs || (matchMinute && roundedCandidateOffset === offsetNs)) { return candidate; @@ -1933,7 +1932,7 @@ export function CreateTemporalYearMonth(isoDate: ISODate, calendar: BuiltinCalen return result; } -export function CreateTemporalInstantSlots(result: Temporal.Instant, epochNanoseconds: JSBI) { +export function CreateTemporalInstantSlots(result: Temporal.Instant, epochNanoseconds: F128) { ValidateEpochNanoseconds(epochNanoseconds); CreateSlots(result); SetSlot(result, EPOCHNANOSECONDS, epochNanoseconds); @@ -1950,7 +1949,7 @@ export function CreateTemporalInstantSlots(result: Temporal.Instant, epochNanose } } -export function CreateTemporalInstant(epochNanoseconds: JSBI) { +export function CreateTemporalInstant(epochNanoseconds: F128) { const TemporalInstant = GetIntrinsic('%Temporal.Instant%'); const result: Temporal.Instant = Object.create(TemporalInstant.prototype); CreateTemporalInstantSlots(result, epochNanoseconds); @@ -1959,7 +1958,7 @@ export function CreateTemporalInstant(epochNanoseconds: JSBI) { export function CreateTemporalZonedDateTimeSlots( result: Temporal.ZonedDateTime, - epochNanoseconds: JSBI, + epochNanoseconds: F128, timeZone: string, calendar: BuiltinCalendarId ) { @@ -1982,7 +1981,7 @@ export function CreateTemporalZonedDateTimeSlots( } export function CreateTemporalZonedDateTime( - epochNanoseconds: JSBI, + epochNanoseconds: F128, timeZone: string, calendar: BuiltinCalendarId = 'iso8601' ) { @@ -2149,7 +2148,7 @@ export function TimeZoneEquals(one: string, two: string) { } } -export function GetOffsetNanosecondsFor(timeZone: string, epochNs: JSBI) { +export function GetOffsetNanosecondsFor(timeZone: string, epochNs: F128) { const offsetMinutes = ParseTimeZoneIdentifier(timeZone).offsetMinutes; if (offsetMinutes !== undefined) return offsetMinutes * 60e9; @@ -2168,7 +2167,7 @@ export function FormatUTCOffsetNanoseconds(offsetNs: number): string { return `${sign}${timeString}`; } -export function GetISODateTimeFor(timeZone: string, epochNs: JSBI) { +export function GetISODateTimeFor(timeZone: string, epochNs: F128) { const offsetNs = GetOffsetNanosecondsFor(timeZone, epochNs); let { isoDate: { year, month, day }, @@ -2188,7 +2187,7 @@ export function GetEpochNanosecondsFor( // TODO: See if this logic can be removed in favour of GetNamedTimeZoneEpochNanoseconds function DisambiguatePossibleEpochNanoseconds( - possibleEpochNs: JSBI[], + possibleEpochNs: F128[], timeZone: string, isoDateTime: ISODateTime, disambiguation: NonNullable @@ -2213,10 +2212,10 @@ function DisambiguatePossibleEpochNanoseconds( if (disambiguation === 'reject') throw new RangeError('multiple instants found'); const utcns = GetUTCEpochNanoseconds(isoDateTime); - const dayBefore = JSBI.subtract(utcns, DAY_NANOS_JSBI); + const dayBefore = utcns.fadd(-DAY_NANOS); ValidateEpochNanoseconds(dayBefore); const offsetBefore = GetOffsetNanosecondsFor(timeZone, dayBefore); - const dayAfter = JSBI.add(utcns, DAY_NANOS_JSBI); + const dayAfter = utcns.fadd(DAY_NANOS); ValidateEpochNanoseconds(dayAfter); const offsetAfter = GetOffsetNanosecondsFor(timeZone, dayAfter); const nanoseconds = offsetAfter - offsetBefore; @@ -2292,7 +2291,7 @@ export function GetStartOfDay(timeZone: string, isoDate: ISODate) { assert(!IsOffsetTimeZoneIdentifier(timeZone), 'should only be reached with named time zone'); const utcns = GetUTCEpochNanoseconds(isoDateTime); - const dayBefore = JSBI.subtract(utcns, DAY_NANOS_JSBI); + const dayBefore = utcns.fadd(-DAY_NANOS); ValidateEpochNanoseconds(dayBefore); return castExists(GetNamedTimeZoneNextTransition(timeZone, dayBefore)); } @@ -2403,7 +2402,7 @@ export function TemporalDurationToString( GetSlot(duration, NANOSECONDS) ); if ( - !secondsDuration.isZero() || + !secondsDuration.totalNs.isZero() || ['second', 'millisecond', 'microsecond', 'nanosecond'].includes(DefaultTemporalLargestUnit(duration)) || precision !== 'auto' ) { @@ -2651,7 +2650,7 @@ function GetNamedTimeZoneOffsetNanosecondsImpl(id: string, epochMilliseconds: nu return (utc - epochMilliseconds) * 1e6; } -function GetNamedTimeZoneOffsetNanoseconds(id: string, epochNanoseconds: JSBI) { +function GetNamedTimeZoneOffsetNanoseconds(id: string, epochNanoseconds: F128) { // Optimization: We get the offset nanoseconds only with millisecond // resolution, assuming that time zone offset changes don't happen in the // middle of a millisecond @@ -2698,12 +2697,12 @@ function GetUTCEpochMilliseconds({ function GetUTCEpochNanoseconds(isoDateTime: ISODateTime) { const ms = GetUTCEpochMilliseconds(isoDateTime); const subMs = isoDateTime.time.microsecond * 1e3 + isoDateTime.time.nanosecond; - return JSBI.add(epochMsToNs(ms), JSBI.BigInt(subMs)); + return epochMsToNs(ms).fadd(subMs); } -function GetISOPartsFromEpoch(epochNanoseconds: JSBI) { +function GetISOPartsFromEpoch(epochNanoseconds: F128) { let epochMilliseconds = epochNsToMs(epochNanoseconds, 'trunc'); - let nanos = JSBI.toNumber(JSBI.remainder(epochNanoseconds, MILLION)); + let nanos = epochNanoseconds.sub(new F128(epochMilliseconds).fmul(1e6)).toNumber(); if (nanos < 0) { nanos += 1e6; epochMilliseconds -= 1; @@ -2728,7 +2727,7 @@ function GetISOPartsFromEpoch(epochNanoseconds: JSBI) { } // ts-prune-ignore-next TODO: remove this after tests are converted to TS -export function GetNamedTimeZoneDateTimeParts(id: string, epochNanoseconds: JSBI) { +export function GetNamedTimeZoneDateTimeParts(id: string, epochNanoseconds: F128) { const { epochMilliseconds, time: { millisecond, microsecond, nanosecond } @@ -2737,7 +2736,7 @@ export function GetNamedTimeZoneDateTimeParts(id: string, epochNanoseconds: JSBI return BalanceISODateTime(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond); } -export function GetNamedTimeZoneNextTransition(id: string, epochNanoseconds: JSBI): JSBI | null { +export function GetNamedTimeZoneNextTransition(id: string, epochNanoseconds: F128) { if (id === 'UTC') return null; // UTC fast path // Optimization: we floor the instant to the previous millisecond boundary @@ -2778,7 +2777,7 @@ export function GetNamedTimeZoneNextTransition(id: string, epochNanoseconds: JSB return epochMsToNs(result); } -export function GetNamedTimeZonePreviousTransition(id: string, epochNanoseconds: JSBI): JSBI | null { +export function GetNamedTimeZonePreviousTransition(id: string, epochNanoseconds: F128): F128 | null { if (id === 'UTC') return null; // UTC fast path // Optimization: we raise the instant to the next millisecond boundary so @@ -2793,7 +2792,7 @@ export function GetNamedTimeZonePreviousTransition(id: string, epochNanoseconds: const lookahead = now + DAY_MS * 366 * 3; if (epochMilliseconds > lookahead) { const prevBeforeLookahead = GetNamedTimeZonePreviousTransition(id, epochMsToNs(lookahead)); - if (prevBeforeLookahead === null || JSBI.lessThan(prevBeforeLookahead, epochMsToNs(now))) { + if (prevBeforeLookahead === null || prevBeforeLookahead.lt(epochMsToNs(now))) { return prevBeforeLookahead; } } @@ -2889,10 +2888,10 @@ function GetNamedTimeZoneEpochNanoseconds(id: string, isoDateTime: ISODateTime) // Get the offset of one day before and after the requested calendar date and // clock time, avoiding overflows if near the edge of the Instant range. let ns = GetUTCEpochNanoseconds(isoDateTime); - let nsEarlier = JSBI.subtract(ns, DAY_NANOS_JSBI); - if (JSBI.lessThan(nsEarlier, NS_MIN)) nsEarlier = ns; - let nsLater = JSBI.add(ns, DAY_NANOS_JSBI); - if (JSBI.greaterThan(nsLater, NS_MAX)) nsLater = ns; + let nsEarlier = ns.fadd(-DAY_NANOS); + if (nsEarlier.lt(NS_MIN)) nsEarlier = ns; + let nsLater = ns.fadd(DAY_NANOS); + if (nsLater.gt(NS_MAX)) nsLater = ns; const earlierOffsetNs = GetNamedTimeZoneOffsetNanoseconds(id, nsEarlier); const laterOffsetNs = GetNamedTimeZoneOffsetNanoseconds(id, nsLater); @@ -2903,13 +2902,13 @@ function GetNamedTimeZoneEpochNanoseconds(id: string, isoDateTime: ISODateTime) // offsets to see which one(s) will yield a matching exact time. const found = earlierOffsetNs === laterOffsetNs ? [earlierOffsetNs] : [earlierOffsetNs, laterOffsetNs]; const candidates = found.map((offsetNanoseconds) => { - const epochNanoseconds = JSBI.subtract(ns, JSBI.BigInt(offsetNanoseconds)); + const epochNanoseconds = ns.fadd(-offsetNanoseconds); const parts = GetNamedTimeZoneDateTimeParts(id, epochNanoseconds); if (CompareISODateTime(isoDateTime, parts) !== 0) return undefined; ValidateEpochNanoseconds(epochNanoseconds); return epochNanoseconds; }); - return candidates.filter((x) => x !== undefined) as JSBI[]; + return candidates.filter((x) => x !== undefined) as F128[]; } export function LeapYear(year: number) { @@ -2960,7 +2959,7 @@ function DateDurationSign(dateDuration: DateDuration) { function InternalDurationSign(duration: InternalDuration) { const dateSign = DateDurationSign(duration.date); if (dateSign !== 0) return dateSign; - return duration.time.sign(); + return duration.time.totalNs.sign(); } export function BalanceISOYearMonth(yearParam: number, monthParam: number) { @@ -3049,17 +3048,16 @@ function BalanceTime( let millisecond = millisecondParam; let microsecond = microsecondParam; let nanosecond = nanosecondParam; - let div; - ({ div, mod: nanosecond } = TruncatingDivModByPowerOf10(nanosecond, 3)); - microsecond += div; + microsecond += Math.trunc(nanosecond / 1000); + nanosecond %= 1000; if (nanosecond < 0) { microsecond -= 1; nanosecond += 1000; } - ({ div, mod: microsecond } = TruncatingDivModByPowerOf10(microsecond, 3)); - millisecond += div; + millisecond += Math.trunc(microsecond / 1000); + microsecond %= 1000; if (microsecond < 0) { millisecond -= 1; microsecond += 1000; @@ -3193,7 +3191,7 @@ export function RejectDateTime( export function RejectDateTimeRange(isoDateTime: ISODateTime) { const ns = GetUTCEpochNanoseconds(isoDateTime); - if (JSBI.lessThan(ns, DATETIME_NS_MIN) || JSBI.greaterThan(ns, DATETIME_NS_MAX)) { + if (ns.lt(DATETIME_NS_MIN) || ns.gt(DATETIME_NS_MAX)) { // Because PlainDateTime's range is wider than Instant's range, the line // below will always throw. Calling `ValidateEpochNanoseconds` avoids // repeating the same error message twice. @@ -3205,7 +3203,7 @@ export function RejectDateTimeRange(isoDateTime: ISODateTime) { function AssertISODateTimeWithinLimits(isoDateTime: ISODateTime) { const ns = GetUTCEpochNanoseconds(isoDateTime); assert( - JSBI.greaterThanOrEqual(ns, DATETIME_NS_MIN) && JSBI.lessThanOrEqual(ns, DATETIME_NS_MAX), + ns.geq(DATETIME_NS_MIN) && ns.leq(DATETIME_NS_MAX), `${ISODateTimeToString(isoDateTime, 'iso8601', 'auto')} is outside the representable range` ); } @@ -3213,8 +3211,8 @@ function AssertISODateTimeWithinLimits(isoDateTime: ISODateTime) { // In the spec, IsValidEpochNanoseconds returns a boolean and call sites are // responsible for throwing. In the polyfill, ValidateEpochNanoseconds takes its // place so that we can DRY the throwing code. -function ValidateEpochNanoseconds(epochNanoseconds: JSBI) { - if (JSBI.lessThan(epochNanoseconds, NS_MIN) || JSBI.greaterThan(epochNanoseconds, NS_MAX)) { +function ValidateEpochNanoseconds(epochNanoseconds: F128) { + if (epochNanoseconds.lt(NS_MIN) || epochNanoseconds.gt(NS_MAX)) { throw new RangeError('date/time value is outside of supported range'); } } @@ -3254,11 +3252,16 @@ export function RejectDuration( if (Math.abs(y) >= 2 ** 32 || Math.abs(mon) >= 2 ** 32 || Math.abs(w) >= 2 ** 32) { throw new RangeError('years, months, and weeks must be < 2³²'); } - const msResult = TruncatingDivModByPowerOf10(ms, 3); - const µsResult = TruncatingDivModByPowerOf10(µs, 6); - const nsResult = TruncatingDivModByPowerOf10(ns, 9); - const remainderSec = TruncatingDivModByPowerOf10(msResult.mod * 1e6 + µsResult.mod * 1e3 + nsResult.mod, 9).div; - const totalSec = d * 86400 + h * 3600 + min * 60 + s + msResult.div + µsResult.div + nsResult.div + remainderSec; + const remainderSec = Math.trunc(((ms % 1000) * 1e6 + (µs % 1e6) * 1000 + (ns % 1e9)) / 1e9); + const totalSec = + d * 86400 + + h * 3600 + + min * 60 + + s + + new F128(ms).fdiv(1000).toInt() + + new F128(µs).fdiv(1e6).toInt() + + new F128(ns).fdiv(1e9).toInt() + + remainderSec; if (!Number.isSafeInteger(totalSec)) { throw new RangeError('total of duration time units cannot exceed 9007199254740991.999999999 s'); } @@ -3319,7 +3322,7 @@ function ToDateDurationRecordWithoutTime(duration: Temporal.Duration) { } export function TemporalDurationFromInternal(internalDuration: InternalDuration, largestUnit: Temporal.DateTimeUnit) { - const sign = internalDuration.time.sign(); + const sign = internalDuration.time.totalNs.sign(); let nanoseconds = internalDuration.time.abs().subsec; let microseconds = 0; let milliseconds = 0; @@ -3379,17 +3382,23 @@ export function TemporalDurationFromInternal(internalDuration: InternalDuration, case 'millisecond': microseconds = Math.trunc(nanoseconds / 1000); nanoseconds %= 1000; - milliseconds = FMAPowerOf10(seconds, 3, Math.trunc(microseconds / 1000)); + milliseconds = new F128(seconds) + .fmul(1000) + .fadd(Math.trunc(microseconds / 1000)) + .toNumber(); microseconds %= 1000; seconds = 0; break; case 'microsecond': - microseconds = FMAPowerOf10(seconds, 6, Math.trunc(nanoseconds / 1000)); + microseconds = new F128(seconds) + .fmul(1e6) + .fadd(Math.trunc(nanoseconds / 1000)) + .toNumber(); nanoseconds %= 1000; seconds = 0; break; case 'nanosecond': - nanoseconds = FMAPowerOf10(seconds, 9, nanoseconds); + nanoseconds = new F128(seconds).fmul(1e9).fadd(nanoseconds).toNumber(); seconds = 0; break; default: @@ -3413,7 +3422,7 @@ export function TemporalDurationFromInternal(internalDuration: InternalDuration, export function CombineDateAndTimeDuration(dateDuration: DateDuration, timeDuration: TimeDuration) { const dateSign = DateDurationSign(dateDuration); - const timeSign = timeDuration.sign(); + const timeSign = timeDuration.totalNs.sign(); assert( dateSign === 0 || timeSign === 0 || dateSign === timeSign, 'should not be able to create mixed sign duration fields here' @@ -3454,8 +3463,8 @@ function DifferenceTime(time1: TimeRecord, time2: TimeRecord) { } function DifferenceInstant( - ns1: JSBI, - ns2: JSBI, + ns1: F128, + ns2: F128, increment: number, smallestUnit: Temporal.TimeUnit, roundingMode: Temporal.RoundingMode @@ -3475,7 +3484,7 @@ function DifferenceISODateTime( AssertISODateTimeWithinLimits(isoDateTime2); let timeDuration = DifferenceTime(isoDateTime1.time, isoDateTime2.time); - const timeSign = timeDuration.sign(); + const timeSign = timeDuration.totalNs.sign(); const dateSign = CompareISODate(isoDateTime1.isoDate, isoDateTime2.isoDate); // back-off a day from date2 so that the signs of the date and time diff match @@ -3496,15 +3505,15 @@ function DifferenceISODateTime( } function DifferenceZonedDateTime( - ns1: JSBI, - ns2: JSBI, + ns1: F128, + ns2: F128, timeZone: string, calendar: BuiltinCalendarId, largestUnit: Temporal.DateTimeUnit ) { - const nsDiff = JSBI.subtract(ns2, ns1); - if (JSBI.equal(nsDiff, ZERO)) return { date: ZeroDateDuration(), time: TimeDuration.ZERO }; - const sign = JSBI.lessThan(nsDiff, ZERO) ? -1 : 1; + const nsDiff = ns2.sub(ns1); + if (nsDiff.isZero()) return { date: ZeroDateDuration(), time: TimeDuration.ZERO }; + const sign = nsDiff.sign() as -1 | 1; // Convert start/end instants to datetimes const isoDtStart = GetISODateTimeFor(timeZone, ns1); @@ -3531,7 +3540,7 @@ function DifferenceZonedDateTime( // If the diff of the ISO wall-clock times is opposite to the overall diff's sign, // we are guaranteed to need at least one day correction. let timeDuration = DifferenceTime(isoDtStart.time, isoDtEnd.time); - if (timeDuration.sign() === -sign) { + if (timeDuration.totalNs.sign() === -sign) { dayCorrection++; } @@ -3553,7 +3562,7 @@ function DifferenceZonedDateTime( // Did intermediateNs NOT surpass ns2? // If so, exit the loop with success (without incrementing dayCorrection past maxDayCorrection) - if (timeDuration.sign() !== -sign) { + if (timeDuration.totalNs.sign() !== -sign) { break; } } @@ -3575,7 +3584,7 @@ function DifferenceZonedDateTime( function NudgeToCalendarUnit( sign: -1 | 1, durationParam: InternalDuration, - destEpochNs: JSBI, + destEpochNs: F128, isoDateTime: ISODateTime, timeZone: string | null, calendar: BuiltinCalendarId, @@ -3652,38 +3661,27 @@ function NudgeToCalendarUnit( // Round the smallestUnit within the epoch-nanosecond span if (sign === 1) { - assert( - JSBI.lessThanOrEqual(startEpochNs, destEpochNs) && JSBI.lessThanOrEqual(destEpochNs, endEpochNs), - `${unit} was 0 days long` - ); + assert(startEpochNs.leq(destEpochNs) && destEpochNs.leq(endEpochNs), `${unit} was 0 days long`); } if (sign === -1) { - assert( - JSBI.lessThanOrEqual(endEpochNs, destEpochNs) && JSBI.lessThanOrEqual(destEpochNs, startEpochNs), - `${unit} was 0 days long` - ); + assert(endEpochNs.leq(destEpochNs) && destEpochNs.leq(startEpochNs), `${unit} was 0 days long`); } - assert(!JSBI.equal(endEpochNs, startEpochNs), 'startEpochNs must ≠ endEpochNs'); - const numerator = TimeDuration.fromEpochNsDiff(destEpochNs, startEpochNs); - const denominator = TimeDuration.fromEpochNsDiff(endEpochNs, startEpochNs); + assert(!endEpochNs.eq(startEpochNs), 'startEpochNs must ≠ endEpochNs'); + const progress = destEpochNs.sub(startEpochNs).div(endEpochNs.sub(startEpochNs)); const unsignedRoundingMode = GetUnsignedRoundingMode(roundingMode, sign < 0 ? 'negative' : 'positive'); - const cmp = numerator.add(numerator).abs().subtract(denominator.abs()).sign(); + const cmp = progress.cmp(new F128(0.5)); const even = (Math.abs(r1) / increment) % 2 === 0; // prettier-ignore - const roundedUnit = numerator.isZero() + const roundedUnit = progress.isZero() ? Math.abs(r1) - : !numerator.cmp(denominator) // equal? + : progress.eq(F128[1]) ? Math.abs(r2) : ApplyUnsignedRoundingMode(Math.abs(r1), Math.abs(r2), cmp, even, unsignedRoundingMode); - // Trick to minimize rounding error, due to the lack of fma() in JS - const fakeNumerator = new TimeDuration( - JSBI.add( - JSBI.multiply(denominator.totalNs, JSBI.BigInt(r1)), - JSBI.multiply(numerator.totalNs, JSBI.BigInt(increment * sign)) - ) - ); - const total = fakeNumerator.fdiv(denominator.totalNs); + const total = progress + .fmul(increment * sign) + .fadd(r1) + .toNumber(); assert(Math.abs(r1) <= Math.abs(total) && Math.abs(total) <= Math.abs(r2), 'r1 ≤ total ≤ r2'); // Determine whether expanded or contracted @@ -3727,16 +3725,16 @@ function NudgeToZonedTime( // The signed amount of time from the start of the whole-day interval to the end const daySpan = TimeDuration.fromEpochNsDiff(endEpochNs, startEpochNs); - if (daySpan.sign() !== sign) throw new RangeError('time zone returned inconsistent Instants'); + if (daySpan.totalNs.sign() !== sign) throw new RangeError('time zone returned inconsistent Instants'); // Compute time parts of the duration to nanoseconds and round // Result could be negative - const unitIncrement = JSBI.BigInt(NS_PER_TIME_UNIT[unit] * increment); + const unitIncrement = NS_PER_TIME_UNIT[unit] * increment; let roundedTimeDuration = duration.time.round(unitIncrement, roundingMode); // Does the rounded time exceed the time-in-day? const beyondDaySpan = roundedTimeDuration.subtract(daySpan); - const didRoundBeyondDay = beyondDaySpan.sign() !== -sign; + const didRoundBeyondDay = beyondDaySpan.totalNs.sign() !== -sign; let dayDelta, nudgedEpochNs; if (didRoundBeyondDay) { @@ -3744,12 +3742,12 @@ function NudgeToZonedTime( // the rounding dayDelta = sign; roundedTimeDuration = beyondDaySpan.round(unitIncrement, roundingMode); - nudgedEpochNs = roundedTimeDuration.addToEpochNs(endEpochNs); + nudgedEpochNs = roundedTimeDuration.totalNs.add(endEpochNs); } else { // Otherwise, if time not rounded beyond day, use the day-start as the local // origin dayDelta = 0; - nudgedEpochNs = roundedTimeDuration.addToEpochNs(startEpochNs); + nudgedEpochNs = roundedTimeDuration.totalNs.add(startEpochNs); } const dateDuration = AdjustDateDurationRecord(duration.date, duration.date.days + dayDelta); @@ -3764,7 +3762,7 @@ function NudgeToZonedTime( // Converts all fields to nanoseconds and does integer rounding. function NudgeToDayOrTime( durationParam: InternalDuration, - destEpochNs: JSBI, + destEpochNs: F128, largestUnit: Temporal.DateTimeUnit, increment: number, smallestUnit: Temporal.TimeUnit | 'day', @@ -3775,15 +3773,15 @@ function NudgeToDayOrTime( const timeDuration = duration.time.add24HourDays(duration.date.days); // Convert to nanoseconds and round - const roundedTime = timeDuration.round(JSBI.BigInt(increment * NS_PER_TIME_UNIT[smallestUnit]), roundingMode); + const roundedTime = timeDuration.round(increment * NS_PER_TIME_UNIT[smallestUnit], roundingMode); const diffTime = roundedTime.subtract(timeDuration); // Determine if whole days expanded - const { quotient: wholeDays } = timeDuration.divmod(DAY_NANOS); - const { quotient: roundedWholeDays } = roundedTime.divmod(DAY_NANOS); - const didExpandDays = Math.sign(roundedWholeDays - wholeDays) === timeDuration.sign(); + const wholeDays = timeDuration.totalNs.fdiv(DAY_NANOS).toInt(); + const roundedWholeDays = roundedTime.totalNs.fdiv(DAY_NANOS).toInt(); + const didExpandDays = Math.sign(roundedWholeDays - wholeDays) === timeDuration.totalNs.sign(); - const nudgedEpochNs = diffTime.addToEpochNs(destEpochNs); + const nudgedEpochNs = diffTime.totalNs.add(destEpochNs); let days = 0; let remainder = roundedTime; @@ -3805,7 +3803,7 @@ function NudgeToDayOrTime( function BubbleRelativeDuration( sign: -1 | 1, durationParam: InternalDuration, - nudgedEpochNs: JSBI, + nudgedEpochNs: F128, isoDateTime: ISODateTime, timeZone: string | null, calendar: BuiltinCalendarId, @@ -3866,7 +3864,7 @@ function BubbleRelativeDuration( endEpochNs = GetUTCEpochNanoseconds(endDateTime); } - const didExpandToEnd = compare(nudgedEpochNs, endEpochNs) !== -sign; + const didExpandToEnd = nudgedEpochNs.cmp(endEpochNs) !== -sign; // Is nudgedEpochNs at the end-of-unit? This means it should bubble-up to // the next highest unit (and possibly further...) @@ -3883,7 +3881,7 @@ function BubbleRelativeDuration( function RoundRelativeDuration( durationParam: InternalDuration, - destEpochNs: JSBI, + destEpochNs: F128, isoDateTime: ISODateTime, timeZone: string | null, calendar: BuiltinCalendarId, @@ -3964,7 +3962,7 @@ function RoundRelativeDuration( function TotalRelativeDuration( duration: InternalDuration, - destEpochNs: JSBI, + destEpochNs: F128, isoDateTime: ISODateTime, timeZone: string | null, calendar: BuiltinCalendarId, @@ -4032,15 +4030,15 @@ export function DifferencePlainDateTimeWithTotal( RejectDateTimeRange(isoDateTime2); const duration = DifferenceISODateTime(isoDateTime1, isoDateTime2, calendar, unit); - if (unit === 'nanosecond') return JSBI.toNumber(duration.time.totalNs); + if (unit === 'nanosecond') return duration.time.totalNs.toNumber(); const destEpochNs = GetUTCEpochNanoseconds(isoDateTime2); return TotalRelativeDuration(duration, destEpochNs, isoDateTime1, null, calendar, unit); } export function DifferenceZonedDateTimeWithRounding( - ns1: JSBI, - ns2: JSBI, + ns1: F128, + ns2: F128, timeZone: string, calendar: BuiltinCalendarId, largestUnit: Temporal.DateTimeUnit, @@ -4072,8 +4070,8 @@ export function DifferenceZonedDateTimeWithRounding( } export function DifferenceZonedDateTimeWithTotal( - ns1: JSBI, - ns2: JSBI, + ns1: F128, + ns2: F128, timeZone: string, calendar: BuiltinCalendarId, unit: Temporal.DateTimeUnit @@ -4368,7 +4366,7 @@ export function DifferenceTemporalZonedDateTime( ); } - if (JSBI.equal(ns1, ns2)) return new Duration(); + if (ns1.eq(ns2)) return new Duration(); const duration = DifferenceZonedDateTimeWithRounding( ns1, @@ -4399,14 +4397,14 @@ export function AddTime( return BalanceTime(hour, minute, second, millisecond, microsecond, nanosecond); } -function AddInstant(epochNanoseconds: JSBI, timeDuration: TimeDuration) { - const result = timeDuration.addToEpochNs(epochNanoseconds); +function AddInstant(epochNanoseconds: F128, timeDuration: TimeDuration) { + const result = timeDuration.totalNs.add(epochNanoseconds); ValidateEpochNanoseconds(result); return result; } export function AddZonedDateTime( - epochNs: JSBI, + epochNs: F128, timeZone: string, calendar: BuiltinCalendarId, duration: InternalDuration, @@ -4606,42 +4604,36 @@ export function RoundNumberToIncrement(quantity: number, increment: number, mode } // ts-prune-ignore-next TODO: remove this after tests are converted to TS -export function RoundNumberToIncrementAsIfPositive( - quantityParam: JSBI | bigint, - incrementParam: JSBI | bigint, - mode: Temporal.RoundingMode -) { - const quantity = ensureJSBI(quantityParam); - const increment = ensureJSBI(incrementParam); - const quotient = JSBI.divide(quantity, increment); - const remainder = JSBI.remainder(quantity, increment); +export function RoundNumberToIncrementAsIfPositive(quantity: F128, increment: number, mode: Temporal.RoundingMode) { + const quotient = quantity.fdiv(increment).trunc(); + const remainder = quantity.sub(quotient.fmul(increment)); const unsignedRoundingMode = GetUnsignedRoundingMode(mode, 'positive'); - let r1: JSBI, r2: JSBI; - if (JSBI.lessThan(quantity, ZERO)) { - r1 = JSBI.subtract(quotient, ONE); + const quantityLess0 = quantity.sign() === -1; + let r1: F128, r2: F128; + if (quantityLess0) { + r1 = quotient.fadd(-1); r2 = quotient; } else { r1 = quotient; - r2 = JSBI.add(quotient, ONE); + r2 = quotient.fadd(1); } // Similar to the comparison in RoundNumberToIncrement, but multiplied by an // extra sign to make sure we treat it as positive - const cmp = (compare(abs(JSBI.multiply(remainder, TWO)), increment) * (JSBI.lessThan(quantity, ZERO) ? -1 : 1) + - 0) as -1 | 0 | 1; - const rounded = JSBI.equal(remainder, ZERO) + const cmp = (remainder.fmul(2).abs().cmp(new F128(increment)) * (quantityLess0 ? -1 : 1) + 0) as -1 | 0 | 1; + const rounded = remainder.isZero() ? quotient - : ApplyUnsignedRoundingMode(r1, r2, cmp, isEven(r1), unsignedRoundingMode); - return JSBI.multiply(rounded, increment); + : ApplyUnsignedRoundingMode(r1, r2, cmp, r1.isEvenInt(), unsignedRoundingMode); + return rounded.fmul(increment); } export function RoundTemporalInstant( - epochNs: JSBI, + epochNs: F128, increment: number, unit: TimeUnitOrDay, roundingMode: Temporal.RoundingMode ) { const incrementNs = NS_PER_TIME_UNIT[unit] * increment; - return RoundNumberToIncrementAsIfPositive(epochNs, JSBI.BigInt(incrementNs), roundingMode); + return RoundNumberToIncrementAsIfPositive(epochNs, incrementNs, roundingMode); } export function RoundISODateTime( @@ -4714,12 +4706,12 @@ export function RoundTimeDuration( ) { // unit must be a time unit const divisor = NS_PER_TIME_UNIT[unit]; - return timeDuration.round(JSBI.BigInt(divisor * increment), roundingMode); + return timeDuration.round(divisor * increment, roundingMode); } export function TotalTimeDuration(timeDuration: TimeDuration, unit: TimeUnitOrDay) { const divisor = NS_PER_TIME_UNIT[unit]; - return timeDuration.fdiv(JSBI.BigInt(divisor)); + return timeDuration.totalNs.fdiv(divisor).toNumber(); } export function CompareISODate(isoDate1: ISODate, isoDate2: ISODate) { @@ -4754,67 +4746,73 @@ export function CompareISODateTime(isoDateTime1: ISODateTime, isoDateTime2: ISOD type ExternalBigInt = bigint; export function ToBigIntExternal(arg: unknown): ExternalBigInt { - const jsbiBI = ToBigInt(arg); - if (typeof (globalThis as any).BigInt !== 'undefined') return (globalThis as any).BigInt(jsbiBI.toString(10)); - return jsbiBI as unknown as ExternalBigInt; + const f128 = BigIntLikeToFloat128(arg); + if (typeof (globalThis as any).BigInt !== 'undefined') return (globalThis as any).BigInt(f128.toBIString()); + const fake = { + f128, + toString() { + return this.f128.toBIString(); + } + }; + return fake as unknown as ExternalBigInt; } // rounding modes supported: floor, ceil, trunc -export function epochNsToMs(epochNanosecondsParam: JSBI | bigint, mode: 'floor' | 'ceil' | 'trunc') { - const epochNanoseconds = ensureJSBI(epochNanosecondsParam); - const { quotient, remainder } = divmod(epochNanoseconds, MILLION); - let epochMilliseconds = JSBI.toNumber(quotient); - if (mode === 'floor' && JSBI.toNumber(remainder) < 0) epochMilliseconds -= 1; - if (mode === 'ceil' && JSBI.toNumber(remainder) > 0) epochMilliseconds += 1; - return epochMilliseconds; +export function epochNsToMs(epochNanoseconds: F128, mode: 'floor' | 'ceil' | 'trunc') { + const epochMsFractional = epochNanoseconds.fdiv(1e6); + if (mode === 'floor' || mode === 'ceil') return epochMsFractional.round(mode).toNumber(); + return epochMsFractional.toInt(); } export function epochMsToNs(epochMilliseconds: number) { if (!Number.isInteger(epochMilliseconds)) throw new RangeError('epoch milliseconds must be an integer'); - return JSBI.multiply(JSBI.BigInt(epochMilliseconds), MILLION); + return new F128(epochMilliseconds).fmul(1e6); } -export function ToBigInt(arg: unknown): JSBI { +export function BigIntLikeToFloat128(arg: unknown): F128 { let prim = arg; if (typeof arg === 'object') { + if (arg instanceof F128) return arg; + if (arg !== null && arg.constructor.name === 'JSBI') { + // If it claims to behave like a JSBI object, treat it as such. + return F128.fromString(arg.toString()); + } + const toPrimFn = (arg as { [Symbol.toPrimitive]: unknown })[Symbol.toPrimitive]; if (toPrimFn && typeof toPrimFn === 'function') { prim = toPrimFn.call(arg, 'number'); } } - // The AO ToBigInt throws on numbers because it does not allow implicit - // conversion between number and bigint (unlike the bigint constructor). - if (typeof prim === 'number') { - throw new TypeError('cannot convert number to bigint'); - } - if (typeof prim === 'bigint') { - // JSBI doesn't know anything about the bigint type, and intentionally - // assumes it doesn't exist. Passing one to the BigInt function will throw - // an error. - return JSBI.BigInt(prim.toString(10)); + switch (typeof prim) { + case 'bigint': + return F128.fromString(prim.toString(10)); + case 'boolean': + return new F128(prim ? 1 : 0); + case 'string': { + // If real BigInt is available, use BigInt's string parser + if (typeof globalThis.BigInt !== 'undefined') return F128.fromString(BigInt(prim).toString(10)); + // If not, fake it. This ignores base-2,8,16 strings + const s = prim.trim(); + if (!/^[+-]?[0-9]+$/.test(s)) throw new SyntaxError(`invalid BigInt string ${s}`); + return F128.fromString(s); + } } - // JSBI will properly coerce types into a BigInt the same as the native BigInt - // constructor will, with the exception of native bigint which is handled - // above. - // As of 2023-04-07, the only runtime type that neither of those can handle is - // 'symbol', and both native bigint and the JSBI.BigInt function will throw an - // error if they are given a Symbol. - return JSBI.BigInt(prim as string | boolean | object); + + throw new TypeError(`cannot convert ${typeof prim} to bigint`); } // Note: This method returns values with bogus nanoseconds based on the previous iteration's // milliseconds. That way there is a guarantee that the full nanoseconds are always going to be // increasing at least and that the microsecond and nanosecond fields are likely to be non-zero. -export const SystemUTCEpochNanoSeconds: () => JSBI = (() => { - let ns = JSBI.BigInt(Date.now() % 1e6); +export const SystemUTCEpochNanoSeconds: () => F128 = (() => { + let ns = Date.now() % 1e6; return () => { - const now = Date.now(); - const ms = JSBI.BigInt(now); - const result = JSBI.add(epochMsToNs(now), ns); - ns = JSBI.remainder(ms, MILLION); - if (JSBI.greaterThan(result, NS_MAX)) return NS_MAX; - if (JSBI.lessThan(result, NS_MIN)) return NS_MIN; + const ms = Date.now(); + const result = epochMsToNs(ms).fadd(ns); + ns = ms % 1e6; + if (result.gt(NS_MAX)) return NS_MAX; + if (result.lt(NS_MIN)) return NS_MIN; return result; }; })(); diff --git a/lib/float128.ts b/lib/float128.ts new file mode 100644 index 00000000..34493e33 --- /dev/null +++ b/lib/float128.ts @@ -0,0 +1,336 @@ +// Adapted from a paper and the accompanying code: +// Hida, Li, Bailey (2008). Library for double-double and quad-double arithmetic +// https://github.com/BL-highprecision/QD + +const NDIGITS = 31; + +export class F128 { + readonly hi: number; + readonly lo: number; + constructor(hi = 0, lo = 0) { + this.hi = hi; + this.lo = lo; + } + + static [0] = new F128(0); + static [1] = new F128(1); + static [10] = new F128(10); + + static fromString(s: string) { + let sign = 0; + let point = -1; + let nd = 0; + let e = 0; + let done = false; + let r = F128[0]; + + // Skip any leading spaces + let p = [...s.trimStart()]; + + let ch; + while (!done && (ch = p.shift())) { + switch (ch) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + r = r.fmul(10).fadd(parseInt(ch, 10)); + nd++; + break; + + case '.': + if (point >= 0) throw new Error('multiple decimal points'); + point = nd; + break; + + case '-': + case '+': + if (sign !== 0 || nd > 0) throw new Error('multiple signs'); + sign = ch == '-' ? -1 : 1; + break; + + case 'E': + case 'e': + e = parseInt(p.join(''), 10); + done = true; + if (Number.isNaN(e)) throw new Error('invalid exponent'); + break; + + case '_': // numeric separator + break; + + default: + throw new Error('unrecognized character'); + } + } + + if (point >= 0) { + e -= nd - point; + } + + if (e > 0) { + r = r.mul(F128.fromString('1' + '0'.repeat(e))); + } else if (e < 0) { + r = r.div(F128.fromString('1' + '0'.repeat(e))); + } + + return sign == -1 ? r.neg() : r; + } + + abs() { + return this.hi < 0 ? this.neg() : this; + } + + add(other: F128) { + let s = twoSum(this.hi, other.hi); + const t = twoSum(this.lo, other.lo); + s = quickTwoSum(s.hi, s.lo + t.hi); + return quickTwoSum(s.hi, s.lo + t.lo); + } + + fadd(other: number) { + const s = twoSum(this.hi, other); + return quickTwoSum(s.hi, s.lo + this.lo); + } + + cmp(other: F128) { + return (this.sub(other).sign() + 0) as -1 | 0 | 1; + } + + div(other: F128) { + let q1 = this.hi / other.hi; // approximate quotient + + let r = this.sub(other.fmul(q1)); + + let q2 = r.hi / other.hi; + r = r.sub(other.fmul(q2)); + + const q3 = r.hi / other.hi; + + return quickTwoSum(q1, q2).fadd(q3); + } + + fdiv(other: number) { + const q1 = this.hi / other; // approximate quotient + + // Compute this - q1 * d + const p = twoProd(q1, other); + const s = twoDiff(this.hi, p.hi); + + // get next approximation + const q2 = (s.hi + s.lo + this.lo - p.lo) / other; + + // renormalize + return quickTwoSum(q1, q2); + } + + eq(other: F128) { + return this.hi === other.hi && this.lo === other.lo; + } + + geq(other: F128) { + return this.hi > other.hi || (this.hi === other.hi && this.lo >= other.lo); + } + + gt(other: F128) { + return this.hi > other.hi || (this.hi === other.hi && this.lo > other.lo); + } + + isEvenInt() { + return this.fdiv(2).trunc().fmul(2).eq(this); + } + + isZero() { + return this.hi === 0; + } + + leq(other: F128) { + return this.hi < other.hi || (this.hi === other.hi && this.lo <= other.lo); + } + + lt(other: F128) { + return this.hi < other.hi || (this.hi === other.hi && this.lo < other.lo); + } + + mul(other: F128) { + const p = twoProd(this.hi, other.hi); + return quickTwoSum(p.hi, p.lo + this.hi * other.lo + this.lo * other.hi); + } + + fmul(other: number) { + const p = twoProd(this.hi, other); + return quickTwoSum(p.hi, p.lo + this.lo * other); + } + + neg() { + return new F128(-this.hi, -this.lo); + } + + round(mode: 'ceil' | 'floor') { + let hi = Math[mode](this.hi); + if (hi !== this.hi) return new F128(hi); + // High word is integer already. Round the low word. + const lo = Math[mode](this.lo); + return quickTwoSum(hi, lo); + } + + sign() { + return Math.sign(this.hi) as -1 | -0 | 0 | 1; + } + + sub(other: F128) { + const s = twoDiff(this.hi, other.hi); + return quickTwoSum(s.hi, s.lo + this.lo - other.lo); + } + + toInt() { + return this.trunc().toNumber() + 0; + } + + toNumber() { + return this.hi; + } + + toBIString() { + if (Number.isNaN(this.hi) || Number.isNaN(this.lo)) throw new Error('NaN'); + if (!Number.isFinite(this.hi)) throw new Error('infinity'); + if (this.isZero()) return '0'; + + const D = NDIGITS + 2; // number of digits to compute + + // First determine the (approximate) exponent + let e = Math.floor(Math.log10(Math.abs(this.hi))); + if (e < 0 || e > NDIGITS + 1) throw new Error('not an integer'); + + let r = this.abs().div(F128.fromString('1' + '0'.repeat(e))); + + // Fix exponent if we are off by one + if (r.geq(F128[10])) { + r = r.fdiv(10); + e++; + } else if (r.lt(F128[1])) { + r = r.fmul(10); + e--; + } + + if (r.geq(F128[10]) || r.lt(F128[1])) throw new Error("can't compute exponent"); + + // Extract the digits + let digits: number[] = []; + for (let i = 0; i < D; i++) { + const d = Math.trunc(r.hi) + 0; + r = r.fadd(-d); + r = r.fmul(10); + + digits.push(d); + } + + // Fix out of range digits + for (let i = D - 1; i > 0; i--) { + if (digits[i] < 0) { + digits[i - 1]--; + digits[i] += 10; + } else if (digits[i] > 9) { + digits[i - 1]++; + digits[i] -= 10; + } + } + + if (digits[0] <= 0) throw new Error('non-positive leading digit'); + + // Round, handle carry + if (digits[D - 1] >= 5) { + digits[D - 2]++; + + let i = D - 2; + while (i > 0 && digits[i] > 9) { + digits[i] -= 10; + digits[--i]++; + } + } + + // If first digit is 10, shift everything + if (digits[0] > 9) { + e++; + digits.splice(0, 1, 1, 0); + } + + const t = digits + .slice(0, NDIGITS + 1) + .map((d) => d.toString(10)) + .join(''); + + let s = ''; + const sign = this.sign(); + if (sign === -1 || Object.is(sign, -0)) { + s += '-'; + } + + if (e < NDIGITS + 1) return s + t.slice(0, e + 1); + + throw new Error('not an integer'); + } + + trunc() { + if (Object.is(this.hi, -0)) return new F128(-0); + return this.round(this.hi >= 0 ? 'floor' : 'ceil'); + } +} + +/** Computes precise a+b of two float64s. Assumes |a| >= |b|. */ +function quickTwoSum(a: number, b: number) { + const s = a + b; + const err = b - (s - a); + return new F128(s, err); +} + +/** Computes precise a+b of two float64s. */ +function twoSum(a: number, b: number) { + const s = a + b; + const bb = s - a; + const err = a - (s - bb) + (b - bb); + return new F128(s, err); +} + +/** Computes precise a-b of two float64s. */ +function twoDiff(a: number, b: number) { + const s = a - b; + const bb = s - a; + const err = a - (s - bb) - (b + bb); + return new F128(s, err); +} + +const _QD_SPLITTER = 134217729; // = 2^27 + 1 +const _QD_SPLIT_THRESH = 6.69692879491417e299; // = 2^996 +/** Computes high word and low word of a */ +function split(a: number) { + let hi, lo; + if (a > _QD_SPLIT_THRESH || a < -_QD_SPLIT_THRESH) { + const scaled = a * 3.7252902984619140625e-9; // 2^-28 + const temp = _QD_SPLITTER * scaled; + hi = temp - (temp - scaled); + lo = scaled - hi; + hi *= 268435456; // 2^28 + lo *= 268435456; // 2^28 + } else { + const temp = _QD_SPLITTER * a; + hi = temp - (temp - a); + lo = a - hi; + } + return new F128(hi, lo); +} + +/** Computes precise a*b of two float64s. */ +function twoProd(a: number, b: number) { + const p = a * b; + const aa = split(a); + const bb = split(b); + const err = aa.hi * bb.hi - p + aa.hi * bb.lo + aa.lo * bb.hi + aa.lo * bb.lo; + return new F128(p, err); +} diff --git a/lib/instant.ts b/lib/instant.ts index 18ec6b31..93af8403 100644 --- a/lib/instant.ts +++ b/lib/instant.ts @@ -5,17 +5,15 @@ import type { Temporal } from '..'; import { DateTimeFormat } from './intl'; import type { InstantParams as Params, InstantReturn as Return } from './internaltypes'; -import JSBI from 'jsbi'; - export class Instant implements Temporal.Instant { - constructor(epochNanoseconds: bigint | JSBI) { + constructor(epochNanoseconds: unknown) { // Note: if the argument is not passed, ToBigInt(undefined) will throw. This check exists only // to improve the error message. if (arguments.length < 1) { throw new TypeError('missing argument: epochNanoseconds is required'); } - const ns = ES.ToBigInt(epochNanoseconds); + const ns = ES.BigIntLikeToFloat128(epochNanoseconds); ES.CreateTemporalInstantSlots(this, ns); } @@ -26,7 +24,7 @@ export class Instant implements Temporal.Instant { } get epochNanoseconds(): Return['epochNanoseconds'] { ES.CheckReceiver(this, ES.IsTemporalInstant); - return ES.ToBigIntExternal(JSBI.BigInt(GetSlot(this, EPOCHNANOSECONDS))); + return ES.ToBigIntExternal(GetSlot(this, EPOCHNANOSECONDS)); } add(temporalDurationLike: Params['add'][0]): Return['add'] { @@ -73,7 +71,7 @@ export class Instant implements Temporal.Instant { const other = ES.ToTemporalInstant(otherParam); const one = GetSlot(this, EPOCHNANOSECONDS); const two = GetSlot(other, EPOCHNANOSECONDS); - return JSBI.equal(JSBI.BigInt(one), JSBI.BigInt(two)); + return one.eq(two); } toString(options: Params['toString'][0] = undefined): string { ES.CheckReceiver(this, ES.IsTemporalInstant); @@ -117,7 +115,7 @@ export class Instant implements Temporal.Instant { static fromEpochNanoseconds( epochNanosecondsParam: Params['fromEpochNanoseconds'][0] ): Return['fromEpochNanoseconds'] { - const epochNanoseconds = ES.ToBigInt(epochNanosecondsParam); + const epochNanoseconds = ES.BigIntLikeToFloat128(epochNanosecondsParam); return ES.CreateTemporalInstant(epochNanoseconds); } static from(item: Params['from'][0]): Return['from'] { @@ -128,8 +126,8 @@ export class Instant implements Temporal.Instant { const two = ES.ToTemporalInstant(twoParam); const oneNs = GetSlot(one, EPOCHNANOSECONDS); const twoNs = GetSlot(two, EPOCHNANOSECONDS); - if (JSBI.lessThan(oneNs, twoNs)) return -1; - if (JSBI.greaterThan(oneNs, twoNs)) return 1; + if (oneNs.lt(twoNs)) return -1; + if (oneNs.gt(twoNs)) return 1; return 0; } [Symbol.toStringTag]!: 'Temporal.Instant'; diff --git a/lib/intrinsicclass.ts b/lib/intrinsicclass.ts index 173fcebb..7c8fb8c1 100644 --- a/lib/intrinsicclass.ts +++ b/lib/intrinsicclass.ts @@ -1,4 +1,3 @@ -import type JSBI from 'jsbi'; import type { Temporal } from '..'; import type { CalendarImpl } from './calendar'; import type { BuiltinCalendarId } from './internaltypes'; @@ -13,14 +12,14 @@ type TemporalIntrinsics = { ['Intl.DateTimeFormat']: typeof globalThis.Intl.DateTimeFormat; ['Temporal.Duration']: typeof Temporal.Duration; ['Temporal.Instant']: OmitConstructor & - (new (epochNanoseconds: JSBI) => Temporal.Instant) & { prototype: typeof Temporal.Instant.prototype }; + (new (epochNanoseconds: unknown) => Temporal.Instant) & { prototype: typeof Temporal.Instant.prototype }; ['Temporal.PlainDate']: typeof Temporal.PlainDate; ['Temporal.PlainDateTime']: typeof Temporal.PlainDateTime; ['Temporal.PlainMonthDay']: typeof Temporal.PlainMonthDay; ['Temporal.PlainTime']: typeof Temporal.PlainTime; ['Temporal.PlainYearMonth']: typeof Temporal.PlainYearMonth; ['Temporal.ZonedDateTime']: OmitConstructor & - (new (epochNanoseconds: JSBI, timeZone: string, calendar?: string) => Temporal.ZonedDateTime) & { + (new (epochNanoseconds: unknown, timeZone: string, calendar?: string) => Temporal.ZonedDateTime) & { prototype: typeof Temporal.ZonedDateTime.prototype; from: typeof Temporal.ZonedDateTime.from; compare: typeof Temporal.ZonedDateTime.compare; diff --git a/lib/math.ts b/lib/math.ts index f85d00f5..4a7f3a21 100644 --- a/lib/math.ts +++ b/lib/math.ts @@ -1,52 +1,5 @@ -import type JSBI from 'jsbi'; import type { Temporal } from '..'; - -// Computes trunc(x / 10**p) and x % 10**p, returning { div, mod }, with -// precision loss only once in the quotient, by string manipulation. If the -// quotient and remainder are safe integers, then they are exact. x must be an -// integer. p must be a non-negative integer. Both div and mod have the sign of -// x. -export function TruncatingDivModByPowerOf10(xParam: number, p: number) { - let x = xParam; - if (x === 0) return { div: x, mod: x }; // preserves signed zero - - const sign = Math.sign(x); - x = Math.abs(x); - - const xDigits = Math.trunc(1 + Math.log10(x)); - if (p >= xDigits) return { div: sign * 0, mod: sign * x }; - if (p === 0) return { div: sign * x, mod: sign * 0 }; - - // would perform nearest rounding if x was not an integer: - const xStr = x.toPrecision(xDigits); - const div = sign * Number.parseInt(xStr.slice(0, xDigits - p), 10); - const mod = sign * Number.parseInt(xStr.slice(xDigits - p), 10); - - return { div, mod }; -} - -// Computes x * 10**p + z with precision loss only at the end, by string -// manipulation. If the result is a safe integer, then it is exact. x must be -// an integer. p must be a non-negative integer. z must have the same sign as -// x and be less than 10**p. -export function FMAPowerOf10(xParam: number, p: number, zParam: number) { - let x = xParam; - let z = zParam; - if (x === 0) return z; - - const sign = Math.sign(x) || Math.sign(z); - x = Math.abs(x); - z = Math.abs(z); - - const xStr = x.toPrecision(Math.trunc(1 + Math.log10(x))); - - if (z === 0) return sign * Number.parseInt(xStr + '0'.repeat(p), 10); - - const zStr = z.toPrecision(Math.trunc(1 + Math.log10(z))); - - const resStr = xStr + zStr.padStart(p, '0'); - return sign * Number.parseInt(resStr, 10); -} +import type { F128 } from './float128'; type UnsignedRoundingMode = 'half-even' | 'half-infinity' | 'half-zero' | 'infinity' | 'zero'; @@ -79,7 +32,7 @@ export function GetUnsignedRoundingMode( // Omits first step from spec algorithm so that it can be used both for // RoundNumberToIncrement and RoundTimeDurationToIncrement -export function ApplyUnsignedRoundingMode( +export function ApplyUnsignedRoundingMode( r1: T, r2: T, cmp: -1 | 0 | 1, diff --git a/lib/slots.ts b/lib/slots.ts index 27a2b586..8ed4becb 100644 --- a/lib/slots.ts +++ b/lib/slots.ts @@ -1,5 +1,5 @@ -import type JSBI from 'jsbi'; import type { Temporal } from '..'; +import type { F128 } from './float128'; import type { BuiltinCalendarId, AnySlottedType, @@ -63,7 +63,7 @@ interface SlotInfoRecord { interface Slots extends SlotInfoRecord { // Instant - [EPOCHNANOSECONDS]: SlotInfo; // number? JSBI? + [EPOCHNANOSECONDS]: SlotInfo; // number? JSBI? // DateTime, Date, Time, YearMonth, MonthDay [ISO_DATE]: SlotInfo; diff --git a/lib/timeduration.ts b/lib/timeduration.ts index 523ce1e1..5a2d910d 100644 --- a/lib/timeduration.ts +++ b/lib/timeduration.ts @@ -1,149 +1,80 @@ -import JSBI from 'jsbi'; - import { assert } from './assert'; -import { - abs, - BILLION, - compare, - DAY_NANOS_JSBI, - divmod, - ensureJSBI, - HOUR_NANOS, - isEven, - MILLION, - MINUTE_NANOS_JSBI, - ONE, - TEN, - THOUSAND, - TWO, - ZERO -} from './bigintmath'; +import { F128 } from './float128'; import { ApplyUnsignedRoundingMode, GetUnsignedRoundingMode } from './math'; import type { Temporal } from '..'; export class TimeDuration { - static MAX = JSBI.BigInt('9007199254740991999999999'); - static ZERO = new TimeDuration(ZERO); + static MAX = F128.fromString('9007199254740991999999999'); + static ZERO = new TimeDuration(F128[0]); - totalNs: JSBI; + totalNs: F128; sec: number; subsec: number; - constructor(totalNs: bigint | JSBI) { - assert(typeof totalNs !== 'number', 'big integer required'); - this.totalNs = ensureJSBI(totalNs); - assert(JSBI.lessThanOrEqual(abs(this.totalNs), TimeDuration.MAX), 'integer too big'); + constructor(totalNs: F128) { + this.totalNs = totalNs.fadd(0); // normalize -0 + assert(this.totalNs.abs().leq(TimeDuration.MAX), 'integer too big'); - this.sec = JSBI.toNumber(JSBI.divide(this.totalNs, BILLION)); - this.subsec = JSBI.toNumber(JSBI.remainder(this.totalNs, BILLION)); + const sec = this.totalNs.fdiv(1e9).trunc(); + this.sec = sec.toNumber() + 0; + this.subsec = this.totalNs.sub(sec.fmul(1e9)).toNumber() + 0; assert(Number.isSafeInteger(this.sec), 'seconds too big'); - assert(Math.abs(this.subsec) <= 999_999_999, 'subseconds too big'); + assert(Math.abs(this.subsec) <= 999_999_999, 'subseconds too big ' + this.subsec); } - static validateNew(totalNs: JSBI, operation: string) { - if (JSBI.greaterThan(abs(totalNs), TimeDuration.MAX)) { + static validateNew(totalNs: F128, operation: string) { + if (totalNs.abs().gt(TimeDuration.MAX)) { throw new RangeError(`${operation} of duration time units cannot exceed ${TimeDuration.MAX} s`); } return new TimeDuration(totalNs); } - static fromEpochNsDiff(epochNs1: JSBI | bigint, epochNs2: JSBI | bigint) { - const diff = JSBI.subtract(ensureJSBI(epochNs1), ensureJSBI(epochNs2)); + static fromEpochNsDiff(epochNs1: F128, epochNs2: F128) { + const diff = epochNs1.sub(epochNs2); // No extra validate step. Should instead fail assertion if too big return new TimeDuration(diff); } static fromComponents(h: number, min: number, s: number, ms: number, µs: number, ns: number) { - const totalNs = JSBI.add( - JSBI.add( - JSBI.add( - JSBI.add( - JSBI.add(JSBI.BigInt(ns), JSBI.multiply(JSBI.BigInt(µs), THOUSAND)), - JSBI.multiply(JSBI.BigInt(ms), MILLION) - ), - JSBI.multiply(JSBI.BigInt(s), BILLION) - ), - JSBI.multiply(JSBI.BigInt(min), MINUTE_NANOS_JSBI) - ), - JSBI.multiply(JSBI.BigInt(h), HOUR_NANOS) - ); + const totalNs = new F128(ns) + .add(new F128(µs).fmul(1e3)) + .add(new F128(ms).fmul(1e6)) + .add(new F128(s).fmul(1e9)) + .add(new F128(min).fmul(60e9)) + .add(new F128(h).fmul(3600e9)); return TimeDuration.validateNew(totalNs, 'total'); } abs() { - return new TimeDuration(abs(this.totalNs)); + return new TimeDuration(this.totalNs.abs()); } add(other: TimeDuration) { - return TimeDuration.validateNew(JSBI.add(this.totalNs, other.totalNs), 'sum'); + return TimeDuration.validateNew(this.totalNs.add(other.totalNs), 'sum'); } add24HourDays(days: number) { assert(Number.isInteger(days), 'days must be an integer'); - return TimeDuration.validateNew(JSBI.add(this.totalNs, JSBI.multiply(JSBI.BigInt(days), DAY_NANOS_JSBI)), 'sum'); - } - - addToEpochNs(epochNs: JSBI | bigint) { - return JSBI.add(ensureJSBI(epochNs), this.totalNs); - } - - cmp(other: TimeDuration) { - return compare(this.totalNs, other.totalNs); - } - - divmod(n: number) { - assert(n !== 0, 'division by zero'); - const { quotient, remainder } = divmod(this.totalNs, JSBI.BigInt(n)); - const q = JSBI.toNumber(quotient); - const r = new TimeDuration(remainder); - return { quotient: q, remainder: r }; + return TimeDuration.validateNew(this.totalNs.add(new F128(days).fmul(86400e9)), 'sum'); } - fdiv(nParam: JSBI | bigint) { - const n = ensureJSBI(nParam); - assert(!JSBI.equal(n, ZERO), 'division by zero'); - const nBigInt = JSBI.BigInt(n); - let { quotient, remainder } = divmod(this.totalNs, nBigInt); - - // Perform long division to calculate the fractional part of the quotient - // remainder / n with more accuracy than 64-bit floating point division - const precision = 50; - const decimalDigits: number[] = []; - let digit; - const sign = (JSBI.lessThan(this.totalNs, ZERO) ? -1 : 1) * Math.sign(JSBI.toNumber(n)); - while (!JSBI.equal(remainder, ZERO) && decimalDigits.length < precision) { - remainder = JSBI.multiply(remainder, TEN); - ({ quotient: digit, remainder } = divmod(remainder, nBigInt)); - decimalDigits.push(Math.abs(JSBI.toNumber(digit))); - } - return sign * Number(abs(quotient).toString() + '.' + decimalDigits.join('')); - } - - isZero() { - return JSBI.equal(this.totalNs, ZERO); - } - - round(incrementParam: JSBI | bigint, mode: Temporal.RoundingMode) { - const increment = ensureJSBI(incrementParam); - if (JSBI.equal(increment, ONE)) return this; - const { quotient, remainder } = divmod(this.totalNs, increment); - const sign = JSBI.lessThan(this.totalNs, ZERO) ? 'negative' : 'positive'; - const r1 = JSBI.multiply(abs(quotient), increment); - const r2 = JSBI.add(r1, increment); - const cmp = compare(abs(JSBI.multiply(remainder, TWO)), increment); + round(increment: number, mode: Temporal.RoundingMode) { + if (increment === 1) return this; + const quotient = this.totalNs.fdiv(increment).trunc(); + const remainder = this.totalNs.sub(quotient.fmul(increment)); + const sign = this.totalNs.sign() === -1 ? 'negative' : 'positive'; + const r1 = quotient.abs().fmul(increment); + const r2 = r1.fadd(increment); + const cmp = remainder.fmul(2).abs().cmp(new F128(increment)); const unsignedRoundingMode = GetUnsignedRoundingMode(mode, sign); - const rounded = JSBI.equal(abs(this.totalNs), r1) + const rounded = this.totalNs.abs().eq(r1) ? r1 - : ApplyUnsignedRoundingMode(r1, r2, cmp, isEven(quotient), unsignedRoundingMode); - const result = sign === 'positive' ? rounded : JSBI.unaryMinus(rounded); + : ApplyUnsignedRoundingMode(r1, r2, cmp, quotient.isEvenInt(), unsignedRoundingMode); + const result = sign === 'positive' ? rounded : rounded.neg(); return TimeDuration.validateNew(result, 'rounding'); } - sign() { - return this.cmp(new TimeDuration(ZERO)); - } - subtract(other: TimeDuration) { - return TimeDuration.validateNew(JSBI.subtract(this.totalNs, other.totalNs), 'difference'); + return TimeDuration.validateNew(this.totalNs.sub(other.totalNs), 'difference'); } } diff --git a/lib/zoneddatetime.ts b/lib/zoneddatetime.ts index 7e13b1c4..81bf0e0d 100644 --- a/lib/zoneddatetime.ts +++ b/lib/zoneddatetime.ts @@ -12,18 +12,16 @@ import type { ZonedDateTimeReturn as Return } from './internaltypes'; -import JSBI from 'jsbi'; - const customResolvedOptions = DateTimeFormat.prototype.resolvedOptions as Intl.DateTimeFormat['resolvedOptions']; export class ZonedDateTime implements Temporal.ZonedDateTime { - constructor(epochNanosecondsParam: bigint | JSBI, timeZoneParam: string, calendarParam = 'iso8601') { + constructor(epochNanosecondsParam: unknown, timeZoneParam: string, calendarParam = 'iso8601') { // Note: if the argument is not passed, ToBigInt(undefined) will throw. This check exists only // to improve the error message. if (arguments.length < 1) { throw new TypeError('missing argument: epochNanoseconds is required'); } - const epochNanoseconds = ES.ToBigInt(epochNanosecondsParam); + const epochNanoseconds = ES.BigIntLikeToFloat128(epochNanosecondsParam); let timeZone = ES.RequireString(timeZoneParam); const { tzName, offsetMinutes } = ES.ParseTimeZoneIdentifier(timeZone); if (offsetMinutes === undefined) { @@ -273,21 +271,15 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { const dateEnd = ES.BalanceISODate(dateStart.year, dateStart.month, dateStart.day + 1); const startNs = ES.GetStartOfDay(timeZone, dateStart); - assert( - JSBI.greaterThanOrEqual(thisNs, startNs), - 'cannot produce an instant during a day that occurs before start-of-day instant' - ); + assert(thisNs.geq(startNs), 'cannot produce an instant during a day that occurs before start-of-day instant'); const endNs = ES.GetStartOfDay(timeZone, dateEnd); - assert( - JSBI.lessThan(thisNs, endNs), - 'cannot produce an instant during a day that occurs on or after end-of-day instant' - ); + assert(thisNs.lt(endNs), 'cannot produce an instant during a day that occurs on or after end-of-day instant'); - const dayLengthNs = JSBI.subtract(endNs, startNs); + const dayLengthNs = endNs.sub(startNs); const dayProgressNs = TimeDuration.fromEpochNsDiff(thisNs, startNs); - const roundedDayNs = dayProgressNs.round(dayLengthNs, roundingMode); - epochNanoseconds = roundedDayNs.addToEpochNs(startNs); + const roundedDayNs = dayProgressNs.round(dayLengthNs.toNumber(), roundingMode); + epochNanoseconds = roundedDayNs.totalNs.add(startNs); } else { // smallestUnit < day // Round based on ISO-calendar time units @@ -318,7 +310,7 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { const other = ES.ToTemporalZonedDateTime(otherParam); const one = GetSlot(this, EPOCHNANOSECONDS); const two = GetSlot(other, EPOCHNANOSECONDS); - if (!JSBI.equal(JSBI.BigInt(one), JSBI.BigInt(two))) return false; + if (!one.eq(two)) return false; if (!ES.TimeZoneEquals(GetSlot(this, TIME_ZONE), GetSlot(other, TIME_ZONE))) return false; return ES.CalendarEquals(GetSlot(this, CALENDAR), GetSlot(other, CALENDAR)); } @@ -464,9 +456,7 @@ export class ZonedDateTime implements Temporal.ZonedDateTime { const two = ES.ToTemporalZonedDateTime(twoParam); const ns1 = GetSlot(one, EPOCHNANOSECONDS); const ns2 = GetSlot(two, EPOCHNANOSECONDS); - if (JSBI.lessThan(JSBI.BigInt(ns1), JSBI.BigInt(ns2))) return -1; - if (JSBI.greaterThan(JSBI.BigInt(ns1), JSBI.BigInt(ns2))) return 1; - return 0; + return ns1.cmp(ns2); } [Symbol.toStringTag]!: 'Temporal.ZonedDateTime'; } diff --git a/package-lock.json b/package-lock.json index 6c1e72e5..af7572a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,6 @@ "name": "@js-temporal/polyfill", "version": "0.4.4", "license": "ISC", - "dependencies": { - "jsbi": "^4.3.0" - }, "devDependencies": { "@babel/core": "^7.22.5", "@babel/preset-env": "^7.22.5", @@ -3743,11 +3740,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsbi": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.0.tgz", - "integrity": "sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g==" - }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -7567,11 +7559,6 @@ "argparse": "^2.0.1" } }, - "jsbi": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.0.tgz", - "integrity": "sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g==" - }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", diff --git a/package.json b/package.json index 67c4b0fe..cf87c8a8 100644 --- a/package.json +++ b/package.json @@ -84,9 +84,6 @@ "overrides": { "@rollup/pluginutils": "^5.0.2" }, - "dependencies": { - "jsbi": "^4.3.0" - }, "devDependencies": { "@babel/core": "^7.22.5", "@babel/preset-env": "^7.22.5", diff --git a/test/all.mjs b/test/all.mjs index ea0c706e..dc7cbe9e 100644 --- a/test/all.mjs +++ b/test/all.mjs @@ -14,11 +14,9 @@ import './datemath.mjs'; // tests of internals, not suitable for test262 import './ecmascript.mjs'; -// Power-of-10 math -import './math.mjs'; - -// Internal 96-bit integer implementation, not suitable for test262 +// Internal 128-bit float implementation, not suitable for test262 import './timeduration.mjs'; +import './float128.mjs'; Promise.resolve() .then(() => { diff --git a/test/ecmascript.mjs b/test/ecmascript.mjs index 3b8f90c9..7d08647f 100644 --- a/test/ecmascript.mjs +++ b/test/ecmascript.mjs @@ -8,6 +8,7 @@ import { strict as assert } from 'assert'; const { deepEqual, throws, equal } = assert; import * as ES from '../lib/ecmascript'; +import { F128 } from '../lib/float128'; import { readFileSync } from 'fs'; @@ -384,7 +385,7 @@ describe('ECMAScript', () => { // Normally, this would have been done upstream by another part of the // Temporal APIs, but since we are directly calling into the ES function // we must convert in the test instead. - const nanosAsBigIntInternal = ES.ToBigInt(nanos); + const nanosAsBigIntInternal = ES.BigIntLikeToFloat128(nanos); it(`${nanos} @ ${zone}`, () => deepEqual(ES.GetNamedTimeZoneDateTimeParts(zone, nanosAsBigIntInternal), { isoDate: { year, month, day }, @@ -478,26 +479,26 @@ describe('ECMAScript', () => { }); describe('RoundNumberToIncrementAsIfPositive', () => { - const increment = 100n; + const increment = 100; const testValues = [-150, -100, -80, -50, -30, 0, 30, 50, 80, 100, 150]; const expectations = { - ceil: [-100, -100, -0, -0, -0, 0, 100, 100, 100, 100, 200], - expand: [-100, -100, -0, -0, -0, 0, 100, 100, 100, 100, 200], + ceil: [-100, -100, 0, 0, 0, 0, 100, 100, 100, 100, 200], + expand: [-100, -100, 0, 0, 0, 0, 100, 100, 100, 100, 200], floor: [-200, -100, -100, -100, -100, 0, 0, 0, 0, 100, 100], trunc: [-200, -100, -100, -100, -100, 0, 0, 0, 0, 100, 100], - halfCeil: [-100, -100, -100, -0, -0, 0, 0, 100, 100, 100, 200], - halfExpand: [-100, -100, -100, -0, -0, 0, 0, 100, 100, 100, 200], - halfFloor: [-200, -100, -100, -100, -0, 0, 0, 0, 100, 100, 100], - halfTrunc: [-200, -100, -100, -100, -0, 0, 0, 0, 100, 100, 100], - halfEven: [-200, -100, -100, -0, -0, 0, 0, 0, 100, 100, 200] + halfCeil: [-100, -100, -100, 0, 0, 0, 0, 100, 100, 100, 200], + halfExpand: [-100, -100, -100, 0, 0, 0, 0, 100, 100, 100, 200], + halfFloor: [-200, -100, -100, -100, 0, 0, 0, 0, 100, 100, 100], + halfTrunc: [-200, -100, -100, -100, 0, 0, 0, 0, 100, 100, 100], + halfEven: [-200, -100, -100, 0, 0, 0, 0, 0, 100, 100, 200] }; for (const roundingMode of Object.keys(expectations)) { describe(roundingMode, () => { testValues.forEach((value, ix) => { const expected = expectations[roundingMode][ix]; it(`rounds ${value} to ${expected}`, () => { - const result = ES.RoundNumberToIncrementAsIfPositive(BigInt(value), increment, roundingMode); - equal(Number(String(result)), Number(BigInt(expected))); + const result = ES.RoundNumberToIncrementAsIfPositive(new F128(value), increment, roundingMode); + equal(result.toNumber(), expected); }); }); }); @@ -654,11 +655,11 @@ describe('ECMAScript', () => { describe('epochNsToMs', () => { it('returns 0 for 0n', () => { - equal(ES.epochNsToMs(0n, 'floor'), 0); - equal(ES.epochNsToMs(0n, 'ceil'), 0); + equal(ES.epochNsToMs(F128[0], 'floor'), 0); + equal(ES.epochNsToMs(F128[0], 'ceil'), 0); }); - const oneBillionSeconds = 10n ** 18n; + const oneBillionSeconds = new F128(1e18); it('for a positive value already on ms boundary, divides by 1e6', () => { equal(ES.epochNsToMs(oneBillionSeconds, 'floor'), 1e12); @@ -666,30 +667,30 @@ describe('ECMAScript', () => { }); it('positive value just ahead of ms boundary', () => { - const plusOne = oneBillionSeconds + 1n; + const plusOne = oneBillionSeconds.fadd(1); equal(ES.epochNsToMs(plusOne, 'floor'), 1e12); equal(ES.epochNsToMs(plusOne, 'ceil'), 1e12 + 1); }); it('positive value just behind ms boundary', () => { - const minusOne = oneBillionSeconds - 1n; + const minusOne = oneBillionSeconds.fadd(-1); equal(ES.epochNsToMs(minusOne, 'floor'), 1e12 - 1); equal(ES.epochNsToMs(minusOne, 'ceil'), 1e12); }); it('positive value just behind next ms boundary', () => { - const plus999999 = oneBillionSeconds + 999999n; + const plus999999 = oneBillionSeconds.fadd(999999); equal(ES.epochNsToMs(plus999999, 'floor'), 1e12); equal(ES.epochNsToMs(plus999999, 'ceil'), 1e12 + 1); }); it('positive value just behind ms boundary', () => { - const minus999999 = oneBillionSeconds - 999999n; + const minus999999 = oneBillionSeconds.fadd(-999999); equal(ES.epochNsToMs(minus999999, 'floor'), 1e12 - 1); equal(ES.epochNsToMs(minus999999, 'ceil'), 1e12); }); - const minusOneBillionSeconds = -(10n ** 18n); + const minusOneBillionSeconds = new F128(-1e18); it('for a negative value already on ms boundary, divides by 1e6', () => { equal(ES.epochNsToMs(minusOneBillionSeconds, 'floor'), -1e12); @@ -697,25 +698,25 @@ describe('ECMAScript', () => { }); it('negative value just ahead of ms boundary', () => { - const plusOne = minusOneBillionSeconds + 1n; + const plusOne = minusOneBillionSeconds.fadd(1); equal(ES.epochNsToMs(plusOne, 'floor'), -1e12); equal(ES.epochNsToMs(plusOne, 'ceil'), -1e12 + 1); }); it('negative value just behind ms boundary', () => { - const minusOne = minusOneBillionSeconds - 1n; + const minusOne = minusOneBillionSeconds.fadd(-1); equal(ES.epochNsToMs(minusOne, 'floor'), -1e12 - 1); equal(ES.epochNsToMs(minusOne, 'ceil'), -1e12); }); it('negative value just behind next ms boundary', () => { - const plus999999 = minusOneBillionSeconds + 999999n; + const plus999999 = minusOneBillionSeconds.fadd(999999); equal(ES.epochNsToMs(plus999999, 'floor'), -1e12); equal(ES.epochNsToMs(plus999999, 'ceil'), -1e12 + 1); }); it('negative value just behind ms boundary', () => { - const minus999999 = minusOneBillionSeconds - 999999n; + const minus999999 = minusOneBillionSeconds.fadd(-999999); equal(ES.epochNsToMs(minus999999, 'floor'), -1e12 - 1); equal(ES.epochNsToMs(minus999999, 'ceil'), -1e12); }); diff --git a/test/float128.mjs b/test/float128.mjs new file mode 100644 index 00000000..655a6fa3 --- /dev/null +++ b/test/float128.mjs @@ -0,0 +1,304 @@ +import Demitasse from '@pipobscure/demitasse'; +const { describe, it, report } = Demitasse; + +import Pretty from '@pipobscure/demitasse-pretty'; +const { reporter } = Pretty; + +import { strict as assert } from 'assert'; +const { equal, ok } = assert; + +import { F128 } from '../lib/float128'; + +describe('Float128', function () { + describe('fromString', function () { + it('works', function () { + const f1 = F128.fromString('321_987654321'); + assert(f1.eq(new F128(321987654321))); + + const f2 = F128.fromString('123_123456789'); + assert(f2.eq(new F128(123_123456789))); + }); + }); + + describe('isZero', function () { + it('works', function () { + assert(F128[0].isZero()); + const nonzero = new F128(0.5); + assert(!nonzero.isZero()); + }); + + it('handles -0', function () { + const negZero = new F128(-0); + assert(negZero.isZero()); + }); + }); + + describe('toBIString', function () { + it('works', function () { + const f1 = F128.fromString('1000000000000000000'); + equal(f1.toBIString(), '1000000000000000000'); + }); + }); + + describe('trunc', function () { + it('works', function () { + const f1 = new F128(3.5); + assert(f1.trunc().eq(new F128(3))); + const f2 = new F128(-3.5); + assert(f2.trunc().eq(new F128(-3))); + }); + + it('preserves -0', function () { + const negZero = new F128(-0); + assert(Object.is(negZero.trunc().toNumber(), -0)); + }); + }); + + describe('integration tests', () => { + // Adapted from the test suite of the C library that F128 came from: + // https://github.com/BL-highprecision/QD/blob/main/tests/qd_test.cpp + + // Some constants and functions not in the library because they're not + // needed by Temporal: + const F128_EPSILON = new F128(4.93038065763132e-32); // 2^-104 + const F128_PI = new F128(3.141592653589793116, 1.224646799147353207e-16); + function square(a) { + const p = new F128(a.hi).fmul(a.hi); + return new F128(p.hi).fadd(p.lo + 2 * a.hi * a.lo + a.lo * a.lo); + } + function sqrt(a) { + // a must be positive + const x = 1 / Math.sqrt(a.hi); + const ax = new F128(a.hi * x); + return ax.fadd(a.sub(square(ax)).hi * (x * 0.5)); + } + function pow(x, n) { + if (!Number.isInteger(n)) throw new Error('integer exponentiation required'); + if (n == 0) { + if (x.isZero()) throw new Error('0 ** 0'); + return F128[1]; + } + + let r = new F128(x.hi, x.lo); + let s = F128[1]; + let N = Math.abs(n); + + if (N > 1) { + // Use binary exponentiation + while (N > 0) { + if (N % 2 === 1) s = s.mul(r); + N = Math.trunc(N / 2); + if (N > 0) r = square(r); + } + } else { + s = r; + } + + // Compute the reciprocal if n is negative + if (n < 0) return F128[1].div(s); + + return s; + } + function nroot(a, n) { + // n must be positive, and if n is even a must be nonnegative + if (n === 1) return a; + if (n === 2) return sqrt(a); + if (a.isZero()) return F128[0]; + + /* Note a^{-1/n} = exp(-log(a)/n) */ + const r = a.abs(); + let x = new F128(Math.exp(-Math.log(r.hi) / n)); + + /* Perform Newton's iteration. */ + x = x.add(x.mul(new F128(1).sub(r.mul(pow(x, n)))).fdiv(n)); + if (a.hi < 0) x = x.neg(); + return F128[1].div(x); + } + + it('polynomial evaluation', () => { + function polyeval(c, n, x) { + /* Just use Horner's method of polynomial evaluation. */ + let r = c[n]; + + for (let i = n - 1; i >= 0; i--) { + r = r.mul(x).add(c[i]); + } + + return r; + } + function polyroot(c, n, x0, maxIter = 32, thresh = F128_EPSILON) { + let x = x0; + const d = []; + let conv = false; + let maxc = Math.abs(c[0].toNumber()); + + /* Compute the coefficients of the derivatives. */ + for (let i = 1; i <= n; i++) { + const v = Math.abs(c[i].toNumber()); + if (v > maxc) maxc = v; + d[i - 1] = c[i].fmul(i); + } + thresh = thresh.fmul(maxc); + + /* Newton iteration. */ + for (let i = 0; i < maxIter; i++) { + const f = polyeval(c, n, x); + + if (f.abs().lt(thresh)) { + conv = true; + break; + } + x = x.sub(f.div(polyeval(d, n - 1, x))); + } + + ok(conv, 'should converge'); + + return x; + } + + const n = 8; + const c = []; + + for (let i = 0; i < n; i++) { + c[i] = new F128(i + 1); + } + + const x = polyroot(c, n - 1, F128[0]); + const y = polyeval(c, n - 1, x); + + assert(y.toNumber() < 4 * F128_EPSILON.toNumber()); + }); + + it("Machin's formula for pi", () => { + function arctan(t) { + let d = 1; + const r = square(t); + let result = F128[0]; + + let sign = 1; + while (t.gt(F128_EPSILON)) { + if (sign < 0) { + result = result.sub(t.fdiv(d)); + } else { + result = result.add(t.fdiv(d)); + } + + d += 2; + t = t.mul(r); + sign = -sign; + } + return result; + } + + const s1 = arctan(new F128(1).fdiv(5)); + const s2 = arctan(new F128(1).fdiv(239)); + const p = s1.fmul(4).sub(s2).fmul(4); + const err = Math.abs(p.sub(F128_PI).toNumber()); + + equal(p.toNumber(), Math.PI); + ok(err < F128_EPSILON.fmul(8).toNumber()); + }); + + it('Salamin-Brent quadratically convergent formula for pi', () => { + const maxIter = 20; + + let a = new F128(1); + let b = sqrt(new F128(0.5)); + equal(b.toNumber(), Math.sqrt(0.5)); + let s = new F128(0.5); + let m = 1; + + let p = square(a).fmul(2).div(s); + + let err; + for (let i = 1; i <= maxIter; i++) { + m *= 2; + + const aNew = a.add(b).fmul(0.5); + const bNew = a.mul(b); + + s = s.sub(square(aNew).sub(bNew).fmul(m)); + + a = aNew; + b = sqrt(bNew); + const pOld = p; + + p = square(a).fmul(2).div(s); + + // Test for convergence by looking at |p - p_old|. + err = p.sub(pOld).abs(); + if (err.cmp(new F128(1e-60)) < 0) break; + } + + equal(p.toNumber(), Math.PI); + ok(err.lt(F128_EPSILON.fmul(1024))); + }); + + it('Borwein quartic formula for pi', () => { + const maxIter = 20; + + let a = new F128(6).sub(sqrt(new F128(2)).fmul(4)); + let y = sqrt(new F128(2)).fadd(-1); + let m = 2; + + let p = new F128(1).div(a); + + let err; + for (let i = 1; i <= maxIter; i++) { + m *= 4; + const r = nroot(new F128(1).sub(square(square(y))), 4); + y = new F128(1).sub(r).div(r.fadd(1)); + a = a.mul(square(square(y.fadd(1)))).sub(y.fmul(m).mul(square(y).add(y).fadd(1))); + + const pOld = p; + p = new F128(1).div(a); + if (p.sub(pOld).abs().lt(F128_EPSILON.fmul(16))) { + break; + } + } + + equal(p.toNumber(), Math.PI); + err = Math.abs(p.sub(F128_PI).toNumber()); + ok(err < F128_EPSILON.fmul(128).toNumber()); + }); + + it('Taylor series formula for e', () => { + const F128_E = new F128(2.718281828459045091, 1.445646891729250158e-16); + let s = new F128(2); + let t = new F128(1); + let n = 1; + + while (t.gt(F128_EPSILON)) { + t = t.fdiv(++n); + s = s.add(t); + } + + const delta = Math.abs(s.sub(F128_E).toNumber()); + + equal(s.toNumber(), Math.E); + ok(delta < F128_EPSILON.fmul(64).toNumber()); + }); + + it('Taylor series formula for log 2', () => { + const F128_LOG2 = new F128(6.931471805599452862e-1, 2.319046813846299558e-17); + let s = new F128(0.5); + let t = new F128(0.5); + let n = 1; + + while (t.abs().gt(F128_EPSILON)) { + t = t.fmul(0.5); + s = s.add(t.fdiv(++n)); + } + + const delta = Math.abs(s.sub(F128_LOG2).toNumber()); + + equal(s.toNumber(), Math.log(2)); + ok(delta < F128_EPSILON.fmul(4).toNumber()); + }); + }); +}); + +import { normalize } from 'path'; +if (normalize(import.meta.url.slice(8)) === normalize(process.argv[1])) { + report(reporter).then((failed) => process.exit(failed ? 1 : 0)); +} diff --git a/test/math.mjs b/test/math.mjs deleted file mode 100644 index a2d77ce2..00000000 --- a/test/math.mjs +++ /dev/null @@ -1,110 +0,0 @@ -import Demitasse from '@pipobscure/demitasse'; -const { describe, it, report } = Demitasse; - -import Pretty from '@pipobscure/demitasse-pretty'; -const { reporter } = Pretty; - -import { strict as assert } from 'assert'; -const { deepEqual, equal } = assert; - -import { TruncatingDivModByPowerOf10 as div, FMAPowerOf10 as fma } from '../lib/math'; - -describe('Math', () => { - describe('TruncatingDivModByPowerOf10', () => { - it('12345/10**0 = 12345, 0', () => deepEqual(div(12345, 0), { div: 12345, mod: 0 })); - it('12345/10**1 = 1234, 5', () => deepEqual(div(12345, 1), { div: 1234, mod: 5 })); - it('12345/10**2 = 123, 45', () => deepEqual(div(12345, 2), { div: 123, mod: 45 })); - it('12345/10**3 = 12, 345', () => deepEqual(div(12345, 3), { div: 12, mod: 345 })); - it('12345/10**4 = 1, 2345', () => deepEqual(div(12345, 4), { div: 1, mod: 2345 })); - it('12345/10**5 = 0, 12345', () => deepEqual(div(12345, 5), { div: 0, mod: 12345 })); - it('12345/10**6 = 0, 12345', () => deepEqual(div(12345, 6), { div: 0, mod: 12345 })); - - it('-12345/10**0 = -12345, -0', () => deepEqual(div(-12345, 0), { div: -12345, mod: -0 })); - it('-12345/10**1 = -1234, -5', () => deepEqual(div(-12345, 1), { div: -1234, mod: -5 })); - it('-12345/10**2 = -123, -45', () => deepEqual(div(-12345, 2), { div: -123, mod: -45 })); - it('-12345/10**3 = -12, -345', () => deepEqual(div(-12345, 3), { div: -12, mod: -345 })); - it('-12345/10**4 = -1, -2345', () => deepEqual(div(-12345, 4), { div: -1, mod: -2345 })); - it('-12345/10**5 = -0, -12345', () => deepEqual(div(-12345, 5), { div: -0, mod: -12345 })); - it('-12345/10**6 = -0, -12345', () => deepEqual(div(-12345, 6), { div: -0, mod: -12345 })); - - it('0/10**27 = 0, 0', () => deepEqual(div(0, 27), { div: 0, mod: 0 })); - it('-0/10**27 = -0, -0', () => deepEqual(div(-0, 27), { div: -0, mod: -0 })); - - it('1001/10**3 = 1, 1', () => deepEqual(div(1001, 3), { div: 1, mod: 1 })); - it('-1001/10**3 = -1, -1', () => deepEqual(div(-1001, 3), { div: -1, mod: -1 })); - - it('4019125567429664768/10**3 = 4019125567429664, 768', () => - deepEqual(div(4019125567429664768, 3), { div: 4019125567429664, mod: 768 })); - it('-4019125567429664768/10**3 = -4019125567429664, -768', () => - deepEqual(div(-4019125567429664768, 3), { div: -4019125567429664, mod: -768 })); - it('3294477463410151260160/10**6 = 3294477463410151, 260160', () => - deepEqual(div(3294477463410151260160, 6), { div: 3294477463410151, mod: 260160 })); - it('-3294477463410151260160/10**6 = -3294477463410151, -260160', () => - deepEqual(div(-3294477463410151260160, 6), { div: -3294477463410151, mod: -260160 })); - it('7770017954545649059889152/10**9 = 7770017954545649, 59889152', () => - deepEqual(div(7770017954545649059889152, 9), { div: 7770017954545649, mod: 59889152 })); - it('-7770017954545649059889152/-10**9 = -7770017954545649, -59889152', () => - deepEqual(div(-7770017954545649059889152, 9), { div: -7770017954545649, mod: -59889152 })); - - // Largest/smallest representable float that will result in a safe quotient, - // for each of the divisors 10**3, 10**6, 10**9 - it('9007199254740990976/10**3 = MAX_SAFE_INTEGER-1, 976', () => - deepEqual(div(9007199254740990976, 3), { div: Number.MAX_SAFE_INTEGER - 1, mod: 976 })); - it('-9007199254740990976/10**3 = -MAX_SAFE_INTEGER+1, -976', () => - deepEqual(div(-9007199254740990976, 3), { div: -Number.MAX_SAFE_INTEGER + 1, mod: -976 })); - it('9007199254740990951424/10**6 = MAX_SAFE_INTEGER-1, 951424', () => - deepEqual(div(9007199254740990951424, 6), { div: Number.MAX_SAFE_INTEGER - 1, mod: 951424 })); - it('-9007199254740990951424/10**6 = -MAX_SAFE_INTEGER+1, -951424', () => - deepEqual(div(-9007199254740990951424, 6), { div: -Number.MAX_SAFE_INTEGER + 1, mod: -951424 })); - it('9007199254740990926258176/10**9 = MAX_SAFE_INTEGER-1, 926258176', () => - deepEqual(div(9007199254740990926258176, 9), { div: Number.MAX_SAFE_INTEGER - 1, mod: 926258176 })); - it('-9007199254740990926258176/10**9 = -MAX_SAFE_INTEGER+1, -926258176', () => - deepEqual(div(-9007199254740990926258176, 9), { div: -Number.MAX_SAFE_INTEGER + 1, mod: -926258176 })); - }); - - describe('FMAPowerOf10', () => { - it('0*10**0+0 = 0', () => equal(fma(0, 0, 0), 0)); - it('-0*10**0-0 = -0', () => equal(fma(-0, 0, -0), -0)); - it('1*10**0+0 = 1', () => equal(fma(1, 0, 0), 1)); - it('-1*10**0+0 = -1', () => equal(fma(-1, 0, 0), -1)); - it('0*10**50+1234 = 1234', () => equal(fma(0, 50, 1234), 1234)); - it('-0*10**50-1234 = -1234', () => equal(fma(-0, 50, -1234), -1234)); - it('1234*10**12+0', () => equal(fma(1234, 12, 0), 1234000000000000)); - it('-1234*10**12-0', () => equal(fma(-1234, 12, -0), -1234000000000000)); - - it('2*10**2+45 = 245', () => equal(fma(2, 2, 45), 245)); - it('2*10**3+45 = 2045', () => equal(fma(2, 3, 45), 2045)); - it('2*10**4+45 = 20045', () => equal(fma(2, 4, 45), 20045)); - it('2*10**5+45 = 200045', () => equal(fma(2, 5, 45), 200045)); - it('2*10**6+45 = 2000045', () => equal(fma(2, 6, 45), 2000045)); - - it('-2*10**2-45 = -245', () => equal(fma(-2, 2, -45), -245)); - it('-2*10**3-45 = -2045', () => equal(fma(-2, 3, -45), -2045)); - it('-2*10**4-45 = -20045', () => equal(fma(-2, 4, -45), -20045)); - it('-2*10**5-45 = -200045', () => equal(fma(-2, 5, -45), -200045)); - it('-2*10**6-45 = -2000045', () => equal(fma(-2, 6, -45), -2000045)); - - it('8692288669465520*10**9+321414345 = 8692288669465520321414345, rounded to 8692288669465520839327744', () => - equal(fma(8692288669465520, 9, 321414345), 8692288669465520839327744)); - it('-8692288669465520*10**9-321414345 = -8692288669465520321414345, rounded to -8692288669465520839327744', () => - equal(fma(-8692288669465520, 9, -321414345), -8692288669465520839327744)); - - it('MAX_SAFE_INTEGER*10**3+999 rounded to 9007199254740992000', () => - equal(fma(Number.MAX_SAFE_INTEGER, 3, 999), 9007199254740992000)); - it('-MAX_SAFE_INTEGER*10**3-999 rounded to -9007199254740992000', () => - equal(fma(-Number.MAX_SAFE_INTEGER, 3, -999), -9007199254740992000)); - it('MAX_SAFE_INTEGER*10**6+999999 rounded to 9007199254740992000000', () => - equal(fma(Number.MAX_SAFE_INTEGER, 6, 999999), 9007199254740992000000)); - it('-MAX_SAFE_INTEGER*10**6-999999 rounded to -9007199254740992000000', () => - equal(fma(-Number.MAX_SAFE_INTEGER, 6, -999999), -9007199254740992000000)); - it('MAX_SAFE_INTEGER*10**3+999 rounded to 9007199254740992000', () => - equal(fma(Number.MAX_SAFE_INTEGER, 9, 999999999), 9007199254740992000000000)); - it('-MAX_SAFE_INTEGER*10**3-999 rounded to -9007199254740992000', () => - equal(fma(-Number.MAX_SAFE_INTEGER, 9, -999999999), -9007199254740992000000000)); - }); -}); - -import { normalize } from 'path'; -if (normalize(import.meta.url.slice(8)) === normalize(process.argv[1])) { - report(reporter).then((failed) => process.exit(failed ? 1 : 0)); -} diff --git a/test/timeduration.mjs b/test/timeduration.mjs index 88149c3a..680e5bc6 100644 --- a/test/timeduration.mjs +++ b/test/timeduration.mjs @@ -4,56 +4,40 @@ const { describe, it, report } = Demitasse; import Pretty from '@pipobscure/demitasse-pretty'; const { reporter } = Pretty; -import { strict as assert, AssertionError } from 'assert'; +import { strict as assert } from 'assert'; const { equal, throws } = assert; -import JSBI from 'jsbi'; -import { ensureJSBI } from '../lib/bigintmath'; +import { F128 } from '../lib/float128'; import { TimeDuration } from '../lib/timeduration'; +function b(bi) { + return F128.fromString(bi.toString(10)); +} + function check(timeDuration, sec, subsec) { equal(timeDuration.sec, sec); equal(timeDuration.subsec, subsec); } -function checkBigInt(value, bigint) { - if (value && typeof value === 'object') { - assert(JSBI.equal(value, ensureJSBI(bigint))); // bigInteger wrapper - } else { - equal(value, bigint); // real bigint - } -} - -function checkFloat(value, float) { - if (!Number.isFinite(value) || Math.abs(value - float) > Number.EPSILON) { - throw new AssertionError({ - message: `Expected ${value} to be within ɛ of ${float}`, - expected: float, - actual: value, - operator: 'checkFloat' - }); - } -} - describe('Normalized time duration', () => { describe('construction', () => { it('basic', () => { - check(new TimeDuration(123456789_987654321n), 123456789, 987654321); - check(new TimeDuration(-987654321_123456789n), -987654321, -123456789); + check(new TimeDuration(b(123456789_987654321n)), 123456789, 987654321); + check(new TimeDuration(b(-987654321_123456789n)), -987654321, -123456789); }); it('either sign with zero in the other component', () => { - check(new TimeDuration(123n), 0, 123); - check(new TimeDuration(-123n), 0, -123); - check(new TimeDuration(123_000_000_000n), 123, 0); - check(new TimeDuration(-123_000_000_000n), -123, 0); + check(new TimeDuration(b(123n)), 0, 123); + check(new TimeDuration(b(-123n)), 0, -123); + check(new TimeDuration(b(123_000_000_000n)), 123, 0); + check(new TimeDuration(b(-123_000_000_000n)), -123, 0); }); }); describe('construction impossible', () => { it('out of range', () => { - throws(() => new TimeDuration(2n ** 53n * 1_000_000_000n)); - throws(() => new TimeDuration(-(2n ** 53n * 1_000_000_000n))); + throws(() => new TimeDuration(b(2n ** 53n * 1_000_000_000n))); + throws(() => new TimeDuration(b(-(2n ** 53n * 1_000_000_000n)))); }); it('not an integer', () => { @@ -63,24 +47,24 @@ describe('Normalized time duration', () => { describe('fromEpochNsDiff()', () => { it('basic', () => { - check(TimeDuration.fromEpochNsDiff(1695930183_043174412n, 1695930174_412168313n), 8, 631006099); - check(TimeDuration.fromEpochNsDiff(1695930174_412168313n, 1695930183_043174412n), -8, -631006099); + check(TimeDuration.fromEpochNsDiff(b(1695930183_043174412n), b(1695930174_412168313n)), 8, 631006099); + check(TimeDuration.fromEpochNsDiff(b(1695930174_412168313n), b(1695930183_043174412n)), -8, -631006099); }); it('pre-epoch', () => { - check(TimeDuration.fromEpochNsDiff(-80000_987_654_321n, -86400_123_456_789n), 6399, 135802468); - check(TimeDuration.fromEpochNsDiff(-86400_123_456_789n, -80000_987_654_321n), -6399, -135802468); + check(TimeDuration.fromEpochNsDiff(b(-80000_987_654_321n), b(-86400_123_456_789n)), 6399, 135802468); + check(TimeDuration.fromEpochNsDiff(b(-86400_123_456_789n), b(-80000_987_654_321n)), -6399, -135802468); }); it('cross-epoch', () => { - check(TimeDuration.fromEpochNsDiff(1_000_001_000n, -2_000_002_000n), 3, 3000); - check(TimeDuration.fromEpochNsDiff(-2_000_002_000n, 1_000_001_000n), -3, -3000); + check(TimeDuration.fromEpochNsDiff(b(1_000_001_000n), b(-2_000_002_000n)), 3, 3000); + check(TimeDuration.fromEpochNsDiff(b(-2_000_002_000n), b(1_000_001_000n)), -3, -3000); }); it('maximum epoch difference', () => { const max = 86400_0000_0000_000_000_000n; - check(TimeDuration.fromEpochNsDiff(max, -max), 172800_0000_0000, 0); - check(TimeDuration.fromEpochNsDiff(-max, max), -172800_0000_0000, 0); + check(TimeDuration.fromEpochNsDiff(b(max), b(-max)), 172800_0000_0000, 0); + check(TimeDuration.fromEpochNsDiff(b(-max), b(max)), -172800_0000_0000, 0); }); }); @@ -123,334 +107,127 @@ describe('Normalized time duration', () => { describe('abs()', () => { it('positive', () => { - const d = new TimeDuration(123_456_654_321n); + const d = new TimeDuration(b(123_456_654_321n)); check(d.abs(), 123, 456_654_321); }); it('negative', () => { - const d = new TimeDuration(-123_456_654_321n); + const d = new TimeDuration(b(-123_456_654_321n)); check(d.abs(), 123, 456_654_321); }); it('zero', () => { - const d = new TimeDuration(0n); + const d = new TimeDuration(b(0n)); check(d.abs(), 0, 0); }); }); describe('add()', () => { it('basic', () => { - const d1 = new TimeDuration(123_456_654_321_123_456n); - const d2 = new TimeDuration(654_321_123_456_654_321n); + const d1 = new TimeDuration(b(123_456_654_321_123_456n)); + const d2 = new TimeDuration(b(654_321_123_456_654_321n)); check(d1.add(d2), 777_777_777, 777_777_777); }); it('negative', () => { - const d1 = new TimeDuration(-123_456_654_321_123_456n); - const d2 = new TimeDuration(-654_321_123_456_654_321n); + const d1 = new TimeDuration(b(-123_456_654_321_123_456n)); + const d2 = new TimeDuration(b(-654_321_123_456_654_321n)); check(d1.add(d2), -777_777_777, -777_777_777); }); it('signs differ', () => { - const d1 = new TimeDuration(333_333_333_333_333_333n); - const d2 = new TimeDuration(-222_222_222_222_222_222n); + const d1 = new TimeDuration(b(333_333_333_333_333_333n)); + const d2 = new TimeDuration(b(-222_222_222_222_222_222n)); check(d1.add(d2), 111_111_111, 111_111_111); - const d3 = new TimeDuration(-333_333_333_333_333_333n); - const d4 = new TimeDuration(222_222_222_222_222_222n); + const d3 = new TimeDuration(b(-333_333_333_333_333_333n)); + const d4 = new TimeDuration(b(222_222_222_222_222_222n)); check(d3.add(d4), -111_111_111, -111_111_111); }); it('cross zero', () => { - const d1 = new TimeDuration(222_222_222_222_222_222n); - const d2 = new TimeDuration(-333_333_333_333_333_333n); + const d1 = new TimeDuration(b(222_222_222_222_222_222n)); + const d2 = new TimeDuration(b(-333_333_333_333_333_333n)); check(d1.add(d2), -111_111_111, -111_111_111); }); it('overflow from subseconds to seconds', () => { - const d1 = new TimeDuration(999_999_999n); - const d2 = new TimeDuration(2n); + const d1 = new TimeDuration(b(999_999_999n)); + const d2 = new TimeDuration(b(2n)); check(d1.add(d2), 1, 1); }); it('fails on overflow', () => { - const d1 = new TimeDuration(2n ** 52n * 1_000_000_000n); + const d1 = new TimeDuration(b(2n ** 52n * 1_000_000_000n)); throws(() => d1.add(d1), RangeError); }); }); describe('add24HourDays()', () => { it('basic', () => { - const d = new TimeDuration(111_111_111_111_111_111n); + const d = new TimeDuration(b(111_111_111_111_111_111n)); check(d.add24HourDays(10), 111_975_111, 111_111_111); }); it('negative', () => { - const d = new TimeDuration(-111_111_111_111_111_111n); + const d = new TimeDuration(b(-111_111_111_111_111_111n)); check(d.add24HourDays(-10), -111_975_111, -111_111_111); }); it('signs differ', () => { - const d1 = new TimeDuration(864000_000_000_000n); + const d1 = new TimeDuration(b(864000_000_000_000n)); check(d1.add24HourDays(-5), 432000, 0); - const d2 = new TimeDuration(-864000_000_000_000n); + const d2 = new TimeDuration(b(-864000_000_000_000n)); check(d2.add24HourDays(5), -432000, 0); }); it('cross zero', () => { - const d1 = new TimeDuration(86400_000_000_000n); + const d1 = new TimeDuration(b(86400_000_000_000n)); check(d1.add24HourDays(-2), -86400, 0); - const d2 = new TimeDuration(-86400_000_000_000n); + const d2 = new TimeDuration(b(-86400_000_000_000n)); check(d2.add24HourDays(3), 172800, 0); }); it('overflow from subseconds to seconds', () => { - const d1 = new TimeDuration(-86400_333_333_333n); + const d1 = new TimeDuration(b(-86400_333_333_333n)); check(d1.add24HourDays(2), 86399, 666_666_667); - const d2 = new TimeDuration(86400_333_333_333n); + const d2 = new TimeDuration(b(86400_333_333_333n)); check(d2.add24HourDays(-2), -86399, -666_666_667); }); it('does not accept non-integers', () => { - const d = new TimeDuration(0n); + const d = new TimeDuration(b(0n)); throws(() => d.add24HourDays(1.5), Error); }); it('fails on overflow', () => { - const d = new TimeDuration(0n); + const d = new TimeDuration(b(0n)); throws(() => d.add24HourDays(104249991375), RangeError); throws(() => d.add24HourDays(-104249991375), RangeError); }); }); - describe('addToEpochNs()', () => { - it('basic', () => { - const d = new TimeDuration(123_456_654_321_123_456n); - checkBigInt(d.addToEpochNs(654_321_123_456_654_321n), 777_777_777_777_777_777n); - }); - - it('negative', () => { - const d = new TimeDuration(-123_456_654_321_123_456n); - checkBigInt(d.addToEpochNs(-654_321_123_456_654_321n), -777_777_777_777_777_777n); - }); - - it('signs differ', () => { - const d1 = new TimeDuration(333_333_333_333_333_333n); - checkBigInt(d1.addToEpochNs(-222_222_222_222_222_222n), 111_111_111_111_111_111n); - - const d2 = new TimeDuration(-333_333_333_333_333_333n); - checkBigInt(d2.addToEpochNs(222_222_222_222_222_222n), -111_111_111_111_111_111n); - }); - - it('cross zero', () => { - const d = new TimeDuration(222_222_222_222_222_222n); - checkBigInt(d.addToEpochNs(-333_333_333_333_333_333n), -111_111_111_111_111_111n); - }); - - it('does not fail on overflow, epochNs overflow is checked elsewhere', () => { - const d = new TimeDuration(86400_0000_0000_000_000_000n); - checkBigInt(d.addToEpochNs(86400_0000_0000_000_000_000n), 172800_0000_0000_000_000_000n); - }); - }); - - describe('cmp()', () => { - it('equal', () => { - const d1 = new TimeDuration(123_000_000_456n); - const d2 = new TimeDuration(123_000_000_456n); - equal(d1.cmp(d2), 0); - equal(d2.cmp(d1), 0); - }); - - it('unequal', () => { - const smaller = new TimeDuration(123_000_000_456n); - const larger = new TimeDuration(654_000_000_321n); - equal(smaller.cmp(larger), -1); - equal(larger.cmp(smaller), 1); - }); - - it('cross sign', () => { - const neg = new TimeDuration(-654_000_000_321n); - const pos = new TimeDuration(123_000_000_456n); - equal(neg.cmp(pos), -1); - equal(pos.cmp(neg), 1); - }); - }); - - describe('divmod()', () => { - it('divide by 1', () => { - const d = new TimeDuration(1_234_567_890_987n); - const { quotient, remainder } = d.divmod(1); - equal(quotient, 1234567890987); - check(remainder, 0, 0); - }); - - it('divide by self', () => { - const d = new TimeDuration(1_234_567_890n); - const { quotient, remainder } = d.divmod(1_234_567_890); - equal(quotient, 1); - check(remainder, 0, 0); - }); - - it('no remainder', () => { - const d = new TimeDuration(1_234_000_000n); - const { quotient, remainder } = d.divmod(1e6); - equal(quotient, 1234); - check(remainder, 0, 0); - }); - - it('divide by -1', () => { - const d = new TimeDuration(1_234_567_890_987n); - const { quotient, remainder } = d.divmod(-1); - equal(quotient, -1_234_567_890_987); - check(remainder, 0, 0); - }); - - it('zero seconds remainder has sign of dividend', () => { - const d1 = new TimeDuration(1_234_567_890n); - let { quotient, remainder } = d1.divmod(-1e6); - equal(quotient, -1234); - check(remainder, 0, 567890); - const d2 = new TimeDuration(-1_234_567_890n); - ({ quotient, remainder } = d2.divmod(1e6)); - equal(quotient, -1234); - check(remainder, 0, -567890); - }); - - it('nonzero seconds remainder has sign of dividend', () => { - const d1 = new TimeDuration(10_234_567_890n); - let { quotient, remainder } = d1.divmod(-9e9); - equal(quotient, -1); - check(remainder, 1, 234567890); - const d2 = new TimeDuration(-10_234_567_890n); - ({ quotient, remainder } = d2.divmod(9e9)); - equal(quotient, -1); - check(remainder, -1, -234567890); - }); - - it('negative with zero seconds remainder', () => { - const d = new TimeDuration(-1_234_567_890n); - const { quotient, remainder } = d.divmod(-1e6); - equal(quotient, 1234); - check(remainder, 0, -567890); - }); - - it('negative with nonzero seconds remainder', () => { - const d = new TimeDuration(-10_234_567_890n); - const { quotient, remainder } = d.divmod(-9e9); - equal(quotient, 1); - check(remainder, -1, -234567890); - }); - - it('quotient larger than seconds', () => { - const d = TimeDuration.fromComponents(25 + 5 * 24, 0, 86401, 333, 666, 999); - const { quotient, remainder } = d.divmod(86400e9); - equal(quotient, 7); - check(remainder, 3601, 333666999); - }); - - it('quotient smaller than seconds', () => { - const d = new TimeDuration(90061_333666999n); - const result1 = d.divmod(1000); - equal(result1.quotient, 90061333666); - check(result1.remainder, 0, 999); - - const result2 = d.divmod(10); - equal(result2.quotient, 9006133366699); - check(result2.remainder, 0, 9); - - const result3 = d.divmod(3); - equal(result3.quotient, 30020444555666); - check(result3.remainder, 0, 1); - }); - - it('divide by 0', () => { - const d = new TimeDuration(90061_333666999n); - throws(() => d.divmod(0), Error); - }); - }); - - describe('fdiv()', () => { - it('divide by 1', () => { - const d = new TimeDuration(1_234_567_890_987n); - equal(d.fdiv(1n), 1_234_567_890_987); - }); - - it('no remainder', () => { - const d = new TimeDuration(1_234_000_000n); - equal(d.fdiv(1_000_000n), 1234); - }); - - it('divide by -1', () => { - const d = new TimeDuration(1_234_567_890_987n); - equal(d.fdiv(-1n), -1_234_567_890_987); - }); - - it('opposite sign', () => { - const d1 = new TimeDuration(1_234_567_890n); - checkFloat(d1.fdiv(-1_000_000n), -1234.56789); - const d2 = new TimeDuration(-1_234_567_890n); - checkFloat(d2.fdiv(1_000_000n), -1234.56789); - const d3 = new TimeDuration(-432n); - checkFloat(d3.fdiv(864n), -0.5); - }); - - it('negative', () => { - const d = new TimeDuration(-1_234_567_890n); - checkFloat(d.fdiv(-1_000_000n), 1234.56789); - }); - - it('quotient larger than seconds', () => { - const d = TimeDuration.fromComponents(25 + 5 * 24, 0, 86401, 333, 666, 999); - checkFloat(d.fdiv(86400_000_000_000n), 7.041682102627303); - }); - - it('quotient smaller than seconds', () => { - const d = new TimeDuration(90061_333666999n); - checkFloat(d.fdiv(1000n), 90061333666.999); - checkFloat(d.fdiv(10n), 9006133366699.9); - // eslint-disable-next-line no-loss-of-precision - checkFloat(d.fdiv(3n), 30020444555666.333); - }); - - it('divide by 0', () => { - const d = new TimeDuration(90061_333666999n); - throws(() => d.fdiv(0n), Error); - }); - - it('large number', () => { - const d = new TimeDuration(2939649_187497660n); - checkFloat(d.fdiv(3600_000_000_000n), 816.56921874935); - }); - }); - - it('isZero()', () => { - assert(new TimeDuration(0n).isZero()); - assert(!new TimeDuration(1_000_000_000n).isZero()); - assert(!new TimeDuration(-1n).isZero()); - assert(!new TimeDuration(1_000_000_001n).isZero()); - }); - describe('round()', () => { it('basic', () => { - const d = new TimeDuration(1_234_567_890n); - check(d.round(1000n, 'halfExpand'), 1, 234568000); + const d = new TimeDuration(b(1_234_567_890n)); + check(d.round(1000, 'halfExpand'), 1, 234568000); }); it('increment 1', () => { - const d = new TimeDuration(1_234_567_890n); - check(d.round(1n, 'ceil'), 1, 234567890); + const d = new TimeDuration(b(1_234_567_890n)); + check(d.round(1, 'ceil'), 1, 234567890); }); it('rounds up from subseconds to seconds', () => { - const d = new TimeDuration(1_999_999_999n); - check(d.round(BigInt(1e9), 'halfExpand'), 2, 0); + const d = new TimeDuration(b(1_999_999_999n)); + check(d.round(1e9, 'halfExpand'), 2, 0); }); describe('Rounding modes', () => { - const increment = 100n; + const increment = 100; const testValues = [-150, -100, -80, -50, -30, 0, 30, 50, 80, 100, 150]; const expectations = { ceil: [-100, -100, 0, 0, 0, 0, 100, 100, 100, 100, 200], @@ -469,14 +246,14 @@ describe('Normalized time duration', () => { const expected = expectations[roundingMode][ix]; it(`rounds ${value} ns to ${expected} ns`, () => { - const d = new TimeDuration(BigInt(value)); + const d = new TimeDuration(new F128(value)); const result = d.round(increment, roundingMode); check(result, 0, expected); }); it(`rounds ${value} s to ${expected} s`, () => { - const d = new TimeDuration(BigInt(value * 1e9)); - const result = d.round(increment * BigInt(1e9), roundingMode); + const d = new TimeDuration(new F128(value * 1e9)); + const result = d.round(increment * 1e9, roundingMode); check(result, expected, 0); }); }); @@ -485,25 +262,17 @@ describe('Normalized time duration', () => { }); }); - it('sign()', () => { - equal(new TimeDuration(0n).sign(), 0); - equal(new TimeDuration(-1n).sign(), -1); - equal(new TimeDuration(-1_000_000_000n).sign(), -1); - equal(new TimeDuration(1n).sign(), 1); - equal(new TimeDuration(1_000_000_000n).sign(), 1); - }); - describe('subtract', () => { it('basic', () => { - const d1 = new TimeDuration(321_987654321n); - const d2 = new TimeDuration(123_123456789n); + const d1 = new TimeDuration(b(321_987654321n)); + const d2 = new TimeDuration(b(123_123456789n)); check(d1.subtract(d2), 198, 864197532); check(d2.subtract(d1), -198, -864197532); }); it('signs differ in result', () => { - const d1 = new TimeDuration(3661_001001001n); - const d2 = new TimeDuration(86400_000_000_000n); + const d1 = new TimeDuration(b(3661_001001001n)); + const d2 = new TimeDuration(b(86400_000_000_000n)); check(d1.subtract(d2), -82738, -998998999); check(d2.subtract(d1), 82738, 998998999); });