Skip to content

Commit

Permalink
Merge pull request #17 from knicola/feature/add-timezone-support
Browse files Browse the repository at this point in the history
add timezone support
  • Loading branch information
shadowgate15 authored Oct 26, 2021
2 parents e8d16f3 + 7f31ae1 commit 5f5ac40
Show file tree
Hide file tree
Showing 5 changed files with 306 additions and 14 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"remark-preset-github": "latest",
"semver": "^7.3.2",
"should": ">=13.2.3",
"sinon": "^11.1.2",
"tinyify": "^3.0.0",
"xo": "^0.33.0"
},
Expand Down
74 changes: 60 additions & 14 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1185,16 +1185,45 @@ later.schedule = function (sched) {
};
};

later.setTimeout = function (fn, sched) {
later.setTimeout = function (fn, sched, timezone) {
const s = later.schedule(sched);
let t;
if (fn) {
scheduleTimeout();
}

function scheduleTimeout() {
const now = Date.now();
const next = s.next(2, now);
const date = new Date();
const now = date.getTime();

const next = (() => {
if (!timezone || ['local', 'system'].includes(timezone)) {
return s.next(2, now);
}

const localOffsetMillis = date.getTimezoneOffset() * 6e4;
const offsetMillis = getOffset(date, timezone);

// Specified timezone has the same offset as local timezone.
// ie. America/New_York = America/Nassau = GMT-4
if (offsetMillis === localOffsetMillis) {
return s.next(2, now);
}

// Offsets differ, adjust current time to match what
// it should've been for the specified timezone.
const adjustedNow = new Date(now + localOffsetMillis - offsetMillis);

return (s.next(2, adjustedNow) || /* istanbul ignore next */ []).map(
(sched) => {
// adjust scheduled times to match their intended timezone
// ie. scheduled = 2021-08-22T11:30:00.000-04:00 => America/New_York
// intended = 2021-08-22T11:30:00.000-05:00 => America/Mexico_City
return new Date(sched.getTime() + offsetMillis - localOffsetMillis);
}
);
})();

if (!next[0]) {
t = undefined;
return;
Expand All @@ -1205,12 +1234,11 @@ later.setTimeout = function (fn, sched) {
diff = next[1] ? next[1].getTime() - now : 1e3;
}

if (diff < 2147483647) {
t = setTimeout(fn, diff);
} else {
t = setTimeout(scheduleTimeout, 2147483647);
}
}
t =
diff < 2147483647
? setTimeout(fn, diff)
: setTimeout(scheduleTimeout, 2147483647);
} // scheduleTimeout()

return {
isDone() {
Expand All @@ -1220,19 +1248,20 @@ later.setTimeout = function (fn, sched) {
clearTimeout(t);
}
};
};
}; // setTimeout()

later.setInterval = function (fn, sched) {
later.setInterval = function (fn, sched, timezone) {
if (!fn) {
return;
}

let t = later.setTimeout(scheduleTimeout, sched);
let t = later.setTimeout(scheduleTimeout, sched, timezone);
let done = t.isDone();
function scheduleTimeout() {
/* istanbul ignore else */
if (!done) {
fn();
t = later.setTimeout(scheduleTimeout, sched);
t = later.setTimeout(scheduleTimeout, sched, timezone);
}
}

Expand All @@ -1245,7 +1274,7 @@ later.setInterval = function (fn, sched) {
t.clear();
}
};
};
}; // setInterval()

later.date = {};
later.date.timezone = function (useLocalTime) {
Expand Down Expand Up @@ -2117,4 +2146,21 @@ later.parse.text = function (string) {
return parseScheduleExpr(string.toLowerCase());
};

function getOffset(date, zone) {
const d = date
.toLocaleString('en-US', {
hour12: false,
timeZone: zone,
timeZoneName: 'short'
}) //=> ie. "8/22/2021, 24:30:00 EDT"
.match(/(\d+)\/(\d+)\/(\d+),? (\d+):(\d+):(\d+)/)
.map((n) => (n.length === 1 ? '0' + n : n));

const zdate = new Date(
`${d[3]}-${d[1]}-${d[2]}T${d[4].replace('24', '00')}:${d[5]}:${d[6]}Z`
);

return date.getTime() - zdate.getTime();
} // getOffset()

module.exports = later;
20 changes: 20 additions & 0 deletions test/core/setinterval-test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const later = require('../..');
const should = require('should');
const sinon = require('sinon');

describe('Set interval', function () {
it('should execute a callback after the specified amount of time', function (done) {
Expand Down Expand Up @@ -33,4 +34,23 @@ describe('Set interval', function () {

setTimeout(done, 3000);
});

it('should call .setTimeout() with a timezone param', (done) => {
const spy = sinon.spy(later, 'setTimeout');
const s = later.parse
.recur()
.on(new Date(Date.now() + 1e3))
.fullDate();
later.setInterval(
() => {
/* noop */
},
s,
'America/New_York'
);
should.equal(later.setTimeout.calledOnce, true);
should.equal(later.setTimeout.getCall(0).args[2], 'America/New_York');
spy.restore();
done();
});
});
140 changes: 140 additions & 0 deletions test/core/settimeout-test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
const later = require('../..');
const should = require('should');
const sinon = require('sinon');

const noop = () => {
/* noop */
};

describe('Set timeout', function () {
it('should execute a callback after the specified amount of time', function (done) {
Expand Down Expand Up @@ -61,4 +66,139 @@ describe('Set timeout', function () {

later.setTimeout(test, s);
});

describe('timezone support', () => {
it('should accept IANA timezone strings', (done) => {
should.doesNotThrow(() => {
const s = later.parse
.recur()
.on(new Date(Date.now() + 1e3))
.fullDate();
later.setTimeout(noop, s, 'America/New_York');
});
done();
});

it('should accept "local" as a valid timezone string', (done) => {
should.doesNotThrow(() => {
const s = later.parse
.recur()
.on(new Date(Date.now() + 1e3))
.fullDate();
later.setTimeout(noop, s, 'local');
});
done();
});

it('should accept "system" as a valid timezone string', (done) => {
should.doesNotThrow(() => {
const s = later.parse
.recur()
.on(new Date(Date.now() + 1e3))
.fullDate();
later.setTimeout(noop, s, 'system');
});
done();
});

it('should throw a RangeError when given an invalid or unsupported timezone string', (done) => {
should.throws(() => {
const s = later.parse
.recur()
.on(new Date(Date.now() + 1e3))
.fullDate();
later.setTimeout(noop, s, 'bogus_zone');
}, 'RangeError');
done();
});

it('should adjust scheduled time if the local timezone is ahead of the one specified', (done) => {
const datetimeNow = new Date('2021-08-22T10:30:00.000-04:00'); // zone = America/New_York
const timezone = 'America/Mexico_City'; // time now = 2021-08-22T09:30:00.000-05:00
const msHalfHour = 18e5;

// Run half hour later.
// Intended datetime: 2021-08-22T10:00:00.000-05:00
// But instead, we don't specify timezone here
const intendedDatetime = '2021-08-22T10:00:00.000';
// And so, `new Date()` will use it's local timezone:
// Assumed datetime: 2021-08-22T10:00:00.000-04:00
const assumedTimezone = '-04:00';
const s = later.parse
.recur()
.on(new Date(intendedDatetime + assumedTimezone))
.fullDate();

const clock = sinon.useFakeTimers({
now: datetimeNow.getTime()
});
clock.Date.prototype.getTimezoneOffset = () => 240;

clock.setTimeout = (fn, ms) => {
should.equal(ms, msHalfHour);
};

later.setTimeout(noop, s, timezone);

clock.uninstall();
done();
});

it('should adjust scheduled time if the local timezone is behind of the one specified', (done) => {
const datetimeNow = new Date('2021-08-22T10:30:00.000-04:00'); // zone = America/New_York
const timezone = 'Europe/Athens'; // time now = 2021-08-22T17:30:00.000+03:00
const msHalfHour = 18e5;

// Run half hour later.
// Intended datetime: 2021-08-22T18:00:00.000+03:00
// But instead, we don't specify timezone here
const intendedDatetime = '2021-08-22T18:00:00.000';
// And so, `new Date()` will use it's local timezone:
// Assumed datetime: 2021-08-22T18:00:00.000-04:00
const assumedTimezone = '-04:00';
const s = later.parse
.recur()
.on(new Date(intendedDatetime + assumedTimezone))
.fullDate();

const clock = sinon.useFakeTimers({
now: datetimeNow.getTime()
});
clock.Date.prototype.getTimezoneOffset = () => 240;

clock.setTimeout = (fn, ms) => {
should.equal(ms, msHalfHour);
};

later.setTimeout(noop, s, timezone);

clock.uninstall();
done();
});

it('should not adjust time if specified and local timezones are the same', (done) => {
const datetimeNow = new Date('2021-08-22T10:30:00.000-04:00'); // zone = America/New_York
const timezone = 'America/New_York';
const msOneHour = 36e5;

// Intended run time is one hour later: 2021-08-22 at 11:30, New York time
const intendedRunTime = '2021-08-22T11:30:00.000-04:00';

const s = later.parse.recur().on(new Date(intendedRunTime)).fullDate();

const clock = sinon.useFakeTimers({
now: datetimeNow.getTime()
});
clock.Date.prototype.getTimezoneOffset = () => 240;

clock.setTimeout = (fn, ms) => {
should.equal(ms, msOneHour);
};

later.setTimeout(noop, s, timezone);

clock.uninstall();
done();
});
});
});
Loading

0 comments on commit 5f5ac40

Please sign in to comment.