'use strict'; var assert = require('assert'); var tzdata = require('./lib/tzdata.js'); exports.tzdata = tzdata; exports.timeZones = Object.keys(tzdata); var _Date = null; exports._Date = Date; var mockDateOptions = {}; var timezone; var offsets; var weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec']; var HOUR = 60 * 60 * 1000; var date_iso_8601_regex = /^\d\d\d\d(-\d\d(-\d\d(T\d\d:\d\d(:\d\d)?(\.\d\d\d)?(\d\d\d)?(Z|[+-]\d\d:?\d\d))?)?)?$/; var date_with_offset = /^\d\d\d\d-\d\d-\d\d( \d\d:\d\d:\d\d(\.\d\d\d)? )?(Z|(-|\+|)\d\d:\d\d)$/; var date_rfc_2822_regex = /^\d\d-\w\w\w-\d\d\d\d \d\d:\d\d:\d\d (\+|-)\d\d\d\d$/; var local_date_regex = /^(\d\d\d\d)-(\d\d)-(\d\d)[T ](\d\d):(\d\d)(?::(\d\d)(?:\.(\d\d\d))?)?$/; var local_GMT_regex = /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d\d \w\w\w \d\d\d\d \d\d:\d\d:\d\d GMT$/; function MockDate(param) { if (arguments.length === 0) { this.d = new _Date(); } else if (arguments.length === 1) { if (param instanceof MockDate) { this.d = new _Date(param.d); } else if (typeof param === 'string') { var localDateMatch; if (param.match(date_iso_8601_regex) || param.match(date_with_offset) || param.match(date_rfc_2822_regex) || param.match(local_GMT_regex) || param === '' ) { this.d = new _Date(param); // eslint-disable-next-line no-cond-assign } else if (localDateMatch = param.match(local_date_regex)) { // FYI, if() condition assigns and then checks nullish; not logical comparison var segments = localDateMatch .slice(1) .filter(function (g) { return g !== undefined; }) .map(function (n) { return Number.parseInt(n, 10); }); segments[1]--; // Correct month to monthIndex this.d = new _Date(); this.fromLocal.apply(this, segments); } else if (mockDateOptions.fallbackFn) { this.d = mockDateOptions.fallbackFn(param); } else { assert.ok(false, 'Unhandled date format passed to MockDate constructor: ' + param); } } else if (typeof param === 'number' || param === null || param === undefined) { this.d = new _Date(param); } else if (mockDateOptions.fallbackFn) { this.d = mockDateOptions.fallbackFn(param); } else { assert.ok(false, 'Unhandled type passed to MockDate constructor: ' + typeof param); } } else { this.d = new _Date(); this.fromLocal.apply(this, arguments); } } function getAllOffsets(timezoneName) { return tzdata[timezoneName].transitions.reduce(function (acc, o, i) { if (i % 2 === 1 && !acc.includes(o)) { acc.push(o); } return acc; }, []); } // eslint-disable-next-line consistent-return MockDate.prototype.calcTZO = function (ts) { var data = tzdata[timezone]; assert.ok(data, 'Unsupported timezone: ' + timezone); ts = (ts || this.d.getTime()) / 1000; if (Number.isNaN(ts)) { return NaN; } for (var ii = 2; ii < data.transitions.length; ii += 2) { if (data.transitions[ii] > ts) { return -data.transitions[ii - 1]; } } // note: should never reach here! assert.ok(false, ts); }; function passthrough(fn) { MockDate.prototype[fn] = function () { var real_date; if (this instanceof MockDate) { real_date = this.d; } else if (this instanceof _Date) { // console.log calls our prototype to format regular Date objects! // This should only be hit while debugging MockDate itself though, as // there should be no _Date objects in user code when using MockDate. real_date = this; } else { assert(false, 'Unexpected object type'); } return real_date[fn].apply(real_date, arguments); }; } function localgetter(fn) { MockDate.prototype[fn] = function () { if (Number.isNaN(this.d.getTime())) { return NaN; } var d = new _Date(this.d.getTime() - this.calcTZO() * HOUR); return d['getUTC' + fn.slice(3)](); }; } var dateProps = [ 'FullYear', 'Month', 'Date', ]; var timeProps = [ 'Hours', 'Minutes', 'Seconds', 'Milliseconds', ]; function localsetter(fn) { function getArgsToUse(date, settableProps, propToSet, calledWithArgs) { var i = settableProps.indexOf(propToSet); var args = settableProps.map(function (p) { return date['get' + p](); }); if (i >= 0) { for (var j = 0; j < calledWithArgs.length; j++) { args[j + i] = calledWithArgs[j]; } } return args; } MockDate.prototype[fn] = function () { var propToSet = fn.slice(3); var dateArgs = getArgsToUse(this, dateProps, propToSet, arguments); var timeArgs = getArgsToUse(this, timeProps, propToSet, arguments); this.fromLocal.apply(this, dateArgs.concat(timeArgs)); return this.getTime(); }; } // Convert a local timestamp to a Unix time // // Matches Node.js behavior for handling invalid or ambiguous times. // // Arguments are the individual date and time component values for the // local time: year, monthIndex (January = 0), day, hour, minute, // second, millisecond; just as in the corresponding `Date` constructor. // // A local timestamp usually describes exactly one instant, but it can // match zero or more instants if its representation falls around a UTC // offset change. // // For example, the timestamp 2015-11-01T01:30:00 corresponds to two // Unix times if interpreted in a U.S. time zone, because U.S. clocks // were set backward from 02:00 Daylight Time to 01:00 Standard Time. // // Conversely, 2015-03-08T02:30:00 does not refer to any valid U.S. time // at all -- Standard Time ends at 02:00 and Daylight Time begins at // 03:00. // // When attempting to set the local time to a time falling within an // offset transition (usually daylight saving time), we follow the // EcmaScript specification. // // Ecma International. _EcmaScript 2025 Language Specification_. 16th // edition. ed. Kevin Gibbons. (2025). https://tc39.es/ecma262/#sec-intro. // Section 21.4.1.26 ("UTC (t)"), p. 483. // // Human-readable explanation from MDN: // > When attempting to set the local time to a time falling within an // > offset transition (usually daylight saving time), the exact time is // > derived using the same behavior as Temporal's disambiguation: // > "compatible" option. That is, if the local time corresponds to two // > instants, the earlier one is chosen; if the local time does not exist // > (there is a gap), we go forward by the gap duration. // // JavaScript Reference, Mozilla Developer Network, "Date", accessed 16 // January 2026, // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_components_and_time_zones. // // A similar phenomenon occurs around leap seconds, but we do not // account for those. // MockDate.prototype.fromLocal = function () { var localComponents = arguments; var mockDate = this; this.d.setTime( offsets.reduce(function (acc, o) { var bestTs = acc[0]; var correctOffset = acc[1]; var ts = new _Date( _Date.UTC.apply(null, localComponents) ).setUTCFullYear(localComponents[0]) - (o * HOUR); if (-mockDate.calcTZO(ts) === o) { return [correctOffset === false || ts < bestTs ? ts : bestTs, true]; } return [correctOffset === false && ts > bestTs ? ts : bestTs, false]; }, [null, false])[0] ); }; [ 'getUTCDate', 'getUTCDay', 'getUTCFullYear', 'getUTCHours', 'getUTCMilliseconds', 'getUTCMinutes', 'getUTCMonth', 'getUTCSeconds', 'getTime', 'setTime', 'setUTCDate', 'setUTCFullYear', 'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds', 'toGMTString', 'toISOString', 'toJSON', 'toUTCString', 'valueOf', ].forEach(passthrough); dateProps .concat(timeProps) .concat('Day') .map(function (s) { return 'get' + s; }) .forEach(localgetter); dateProps .concat(timeProps) .map(function (s) { return 'set' + s; }) .forEach(localsetter); MockDate.prototype.getYear = function () { return this.getFullYear() - 1900; }; MockDate.prototype.setYear = function (yr) { if (yr < 1900) { return this.setFullYear(1900 + yr); } return this.setFullYear(yr); }; MockDate.parse = function (dateString) { return new MockDate(dateString).getTime(); }; MockDate.prototype.getTimezoneOffset = function () { if (Number.isNaN(this.d.getTime())) { return NaN; } return this.calcTZO() * 60; }; MockDate.prototype.toString = function () { if (this instanceof _Date) { // someone, like util.inspect, calling Date.prototype.toString.call(foo) return _Date.prototype.toString.call(this); } if (Number.isNaN(this.d.getTime())) { return new _Date('').toString(); } var str = [this.d.toISOString() + ' UTC (MockDate: GMT']; var tzo = -this.calcTZO(); if (tzo < 0) { str.push('-'); tzo *= -1; } else { str.push('+'); } str.push(Math.floor(tzo).toString().padStart(2, '0')); tzo -= Math.floor(tzo); if (tzo) { str.push(tzo * 60); } else { str.push('00'); } str.push(')'); return str.join(''); }; MockDate.now = function () { return _Date.now(); }; MockDate.UTC = function () { return _Date.UTC.apply(_Date, arguments); }; MockDate.prototype.toDateString = function () { if (Number.isNaN(this.d.getTime())) { return new _Date('').toDateString(); } return weekDays[this.getDay()] + ' ' + months[this.getMonth()] + ' ' + this.getDate().toString().padStart(2, '0') + ' ' + this.getFullYear(); }; MockDate.prototype.toLocaleString = function (locales, opts) { opts = Object.assign({ timeZone: timezone }, opts); var time = this.d.getTime(); if (Number.isNaN(time)) { return new _Date('').toDateString(); } return new _Date(time).toLocaleString(locales, opts); }; MockDate.prototype.toLocaleDateString = function (locales, opts) { opts = Object.assign({ timeZone: timezone }, opts); var time = this.d.getTime(); if (Number.isNaN(time)) { return new _Date('').toDateString(); } return new _Date(time).toLocaleDateString(locales, opts); }; MockDate.prototype.toLocaleTimeString = function (locales, opts) { opts = Object.assign({ timeZone: timezone }, opts); var time = this.d.getTime(); if (Number.isNaN(time)) { return new _Date('').toDateString(); } return new _Date(time).toLocaleTimeString(locales, opts); }; // TODO: // 'toTimeString', function options(opts) { mockDateOptions = opts || {}; } exports.options = options; var orig_object_toString; function mockDateObjectToString() { if (this instanceof MockDate) { // Look just like a regular Date to anything doing very low-level Object.prototype.toString calls // See: https://github.com/Jimbly/timezone-mock/issues/48 return '[object Date]'; } return orig_object_toString.call(this); } function register(new_timezone, glob) { if (!glob) { if (typeof window !== 'undefined') { glob = window; } else { glob = global; } } timezone = new_timezone || 'US/Pacific'; offsets = getAllOffsets(timezone); if (glob.Date !== MockDate) { _Date = glob.Date; exports._Date = glob.Date; } glob.Date = MockDate; if (!orig_object_toString) { orig_object_toString = Object.prototype.toString; Object.prototype.toString = mockDateObjectToString; } } exports.register = register; function unregister(glob) { if (!glob) { if (typeof window !== 'undefined') { glob = window; } else { glob = global; } } if (glob.Date === MockDate) { assert(_Date); glob.Date = _Date; } } exports.unregister = unregister;