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); });