/** * @module node-opcua-date-time */ import { assert } from "node-opcua-assert"; import { hrtime } from "node-opcua-utils"; export const offsetFactor1601 = (function offset_factor_1601() { const utc1600 = new Date(Date.UTC(1601, 0, 1, 0, 0, 0)); const t1600 = utc1600.getTime(); const utc1600PlusOneDay = new Date(Date.UTC(1601, 0, 2, 0, 0, 0)); const t1600OneDay = utc1600PlusOneDay.getTime(); const factor1 = (24 * 60 * 60 * 1000 * 10000) / (t1600OneDay - t1600); const utc1970 = new Date(Date.UTC(1970, 0, 1, 0, 0, 0)); const t1970 = utc1970.getTime(); const offsetToGregorianCalendarZero = -t1600 + t1970; assert(factor1 === 10000); assert(offsetToGregorianCalendarZero === 11644473600000); return [offsetToGregorianCalendarZero, factor1]; })(); const offset = offsetFactor1601[0]; const factor = offsetFactor1601[1]; const TWO_POW_32 = 0x100000000; const TWO_POW_63 = 2 ** 63; /** 2^32 = factor * TWO_POW_32_DIV_FACTOR + TWO_POW_32_MOD_FACTOR */ const TWO_POW_32_DIV_FACTOR = Math.floor(TWO_POW_32 / factor); const TWO_POW_32_MOD_FACTOR = TWO_POW_32 % factor; /** the largest excess of 100 ns ticks for which the encoding below stays exact */ const maxExcess = 2 ** 50; /** * a number as a 64-bit signed integer, the way DateTime encoding has always treated one: NaN * is 0, a value beyond the Int64 range is clamped to it, a fraction is truncated */ function toInt64(value: number): bigint { if (Number.isNaN(value)) { return BigInt(0); } if (value <= -TWO_POW_63) { return -(BigInt(2) ** BigInt(63)); } if (value + 1 >= TWO_POW_63) { return BigInt(2) ** BigInt(63) - BigInt(1); } return BigInt(Math.trunc(value)); } /** the high and low 32-bit words, both signed, of a value taken modulo 2^64 */ function int64ToHighLow(value: bigint): number[] { const wrapped = BigInt.asIntN(64, value); return [Number(BigInt.asIntN(32, wrapped >> BigInt(32))), Number(BigInt.asIntN(32, wrapped))]; } // Extracted from OpcUA Spec v1.02 : part 6: // // 5.2.2.5 DateTime // A DateTime value shall be encoded as a 64-bit signed integer (see Clause 5.2.2.2) which represents // the number of 100 nanosecond intervals since January 1, 1601 (UTC) . // Not all DevelopmentPlatforms will be able to represent the full range of dates and times that can be // represented with this DataEncoding. For example, the UNIX time_t structure only has a 1 second // resolution and cannot represent dates prior to 1970. For this reason, a number of rules shall be // applied when dealing with date/time values that exceed the dynamic range of a DevelopmentPlatform. // // These rules are: // a) A date/time value is encoded as 0 if either // 1) The value is equal to or earlier than 1601-01-01 12:00AM. // 2) The value is the earliest date that can be represented with the DevelopmentPlatform‟s encoding. // // b) A date/time is encoded as the maximum value for an Int64 if either // 1) The value is equal to or greater than 9999-01-01 11:59:59PM, // 2) The value is the latest date that can be represented with the DevelopmentPlatform‟s encoding. // // c) A date/time is decoded as the earliest time that can be represented on the platform if either // 1) The encoded value is 0, // 2) The encoded value represents a time earlier than the earliest time that can be // represented with the DevelopmentPlatform‟s encoding. // // d) A date/time is decoded as the latest time that can be represented on the platform if either // 1) The encoded value is the maximum value for an Int64, // 2) The encoded value represents a time later than the latest time that can be represented with the // DevelopmentPlatform‟s encoding. // // These rules imply that the earliest and latest times that can be represented on a given platform are // invalid date/time values and should be treated that way by Applications. // A decoder shall truncate the value if a decoder encounters a DateTime value with a resolution that is // greater than the resolution supported on the DevelopmentPlatform. // /** * * @param date {Date} * @param picoseconds {Number} : represent the portion of the date that cannot be managed by the javascript Date object * * @returns {[high,low]} */ export function bn_dateToHundredNanoSecondFrom1601(date: Date, picoseconds?: number) { // note : The value returned by the getTime method is the number // of milliseconds since 1 January 1970 00:00:00 UTC. // const t = date.getTime(); // number of milliseconds since since 1 January 1970 00:00:00 UTC. const excess100nanosecond = picoseconds !== undefined ? Math.floor(picoseconds / 100000) : 0; // value_64 = (t + offset ) * factor + excess; // computed on plain numbers: every intermediate value below stays under 2^53, so the result // is exact, and wraps modulo 2^64. The few inputs that would not stay exact (an invalid Date, // absurd picoseconds) go through 64-bit integers instead. const ms = t + offset; if (!Number.isSafeInteger(ms) || !Number.isSafeInteger(excess100nanosecond) || Math.abs(excess100nanosecond) > maxExcess) { return int64ToHighLow((toInt64(t) + BigInt(offset)) * BigInt(factor) + toInt64(excess100nanosecond)); } const msHigh = Math.floor(ms / TWO_POW_32); const lowSum = (ms - msHigh * TWO_POW_32) * factor + excess100nanosecond; const carry = Math.floor(lowSum / TWO_POW_32); const high = (msHigh * factor + carry) | 0; const low = (lowSum - carry * TWO_POW_32) | 0; return [high, low]; } export function bn_dateToHundredNanoSecondFrom1601Excess(_date: Date, picoseconds?: number): number { // 100 nano seconds = 100 x 1000 picoseconds return (picoseconds || 0) % 100000; } export function bn_hundredNanoSecondFrom1601ToDate( high: number, low: number, picoseconds = 0, _value: Date | null = null ): [Date, number] { assert(low !== undefined); // value_64 / factor - offset = t // value_64 is read as unsigned: high * 2^32 + low. With 2^32 = factor * 429496 + 7296, // value_64 / factor = high * 429496 + (high * 7296 + low) / factor, where high * 7296 + low // stays under 2^45: the quotient and the remainder are exact on plain numbers. const h = high >>> 0; const rest = h * TWO_POW_32_MOD_FACTOR + (low >>> 0); const restQuotient = Math.floor(rest / factor); const value1 = h * TWO_POW_32_DIV_FACTOR + restQuotient - offset; // const date = _value || new Date(value1); // if (_value) _value.setTime(value1); const date = new Date(value1); // enrich the date const excess100nanoInPico = (rest - restQuotient * factor) * 100000; // picosecond will contains un-decoded 100 nanoseconds => 10 x 100 nanoseconds = 1 microsecond const picoseconds2 = excess100nanoInPico + (picoseconds || 0); return [date, picoseconds2]; } let lastNowDate: Date; let lastPicoseconds = 0; const smallTickPicosecond: number = 1000 * 100; // 100 nano second in picoseconds export interface PreciseClock { timestamp: Date; picoseconds: number; } export interface PreciseClockEx extends PreciseClock { tick: number[]; } /** * * @return PreciseClock */ export function getCurrentClockWithJavascriptDate(): PreciseClock { const now = new Date(); if (lastNowDate && now.getTime() === lastNowDate.getTime()) { lastPicoseconds += smallTickPicosecond; // add 100-nano-second which is the resolution of OPCUA DateTime } else { lastPicoseconds = 0; lastNowDate = now; } return { timestamp: lastNowDate, picoseconds: lastPicoseconds }; } let origin = hrtime(); let refTime = Date.now(); export const periodicClockAdjustment = { adjustmentCount: 0, interval: 3000 /* every 30 seconds */, timerInstallationCount: 0 }; // update refTime now and then to make sure that we don't miss // any system time adjustment here such as a NTP clock event // see #651 let timerId: NodeJS.Timeout | null; const g_setInterval = typeof global === "object" ? global.setInterval : setInterval; const g_clearInterval = typeof global === "object" ? global.clearInterval : clearInterval; export function installPeriodicClockAdjustment() { periodicClockAdjustment.timerInstallationCount++; if (timerId) { return; } timerId = g_setInterval(() => { origin = hrtime(); refTime = Date.now(); periodicClockAdjustment.adjustmentCount++; }, periodicClockAdjustment.interval); } export function uninstallPeriodicClockAdjustment() { periodicClockAdjustment.timerInstallationCount--; if (periodicClockAdjustment.timerInstallationCount <= 0 && timerId) { g_clearInterval(timerId); timerId = null; } } const gClock: PreciseClockEx = { tick: [0, 0], timestamp: new Date(), picoseconds: 0 }; // make sure we get a pointer to the actual process.hrtime, // just in case it get overridden by some library (such as sinon) const original_hrtime = hrtime; const setTimeout_check = setTimeout; /*kWithProcessHRTime*/ export function getCurrentClock(): PreciseClock { if (setTimeout_check !== setTimeout) { // is fake sinon clock being used ? // in this case hrtime is not working return getCurrentClockWithJavascriptDate(); } gClock.tick = original_hrtime(origin); // [seconds, nanoseconds] const milliseconds = gClock.tick[0] * 1000 + Math.floor(gClock.tick[1] / 1000000) + refTime; const picoseconds = (gClock.tick[1] % 1000000) * 1000; gClock.timestamp = new Date(milliseconds); gClock.picoseconds = picoseconds; return gClock; } export function coerceClock(timestamp: undefined | null | Date, picoseconds = 0): PreciseClock { if (timestamp) { return { timestamp: timestamp as Date, picoseconds }; } else { return getCurrentClock(); } } export function isMinDate(date?: Date | null): boolean { if (typeof date === "number") { throw new Error("isMinDate: invalid argument"); } return !date || date.getTime() === minDate.getTime(); } export function getMinOPCUADate() { return new Date(Date.UTC(1601, 0, 1, 0, 0, 0)); } // @deprecated use getMinOPCUADate() instead export const minDate = new Date(Date.UTC(1601, 0, 1, 0, 0, 0)); // @deprecated use getMinOPCUADate() instead export const minOPCUADate = minDate;