Skip to content

Commit

Permalink
Merge pull request #1 from egarkavy/fix-timezones
Browse files Browse the repository at this point in the history
fix(jkbrzt#391): timezones
  • Loading branch information
egarkavy committed Oct 31, 2021
2 parents 3dc6983 + 90359ed commit c4f85a3
Show file tree
Hide file tree
Showing 8 changed files with 91 additions and 39 deletions.
20 changes: 11 additions & 9 deletions README.md
Expand Up @@ -217,7 +217,7 @@ For more examples see

Optionally, it also supports use of the `TZID` parameter in the
[RFC](https://tools.ietf.org/html/rfc5545#section-3.2.19)
when the [Luxon](https://github.com/moment/luxon) library is provided. The
when the [Luxon](https://github.com/moment/luxon) library is provided. The
[specification](https://moment.github.io/luxon/docs/manual/zones.html#specifying-a-zone)
and [support matrix](https://moment.github.io/luxon/docs/manual/matrix.html) for Luxon apply.

Expand All @@ -230,9 +230,9 @@ new RRule({
tzid: 'Asia/Tokyo'
}).all()

// assuming the system timezone is set to America/Los_Angeles, you get:
[ '2018-01-31T17:30:00.000Z' ]
// which is the time in Los Angeles when it's 2018-02-01T10:30:00 in Tokyo.
// regardless of the system timezone, you get:
[ '2018-02-01T01:30:00.000Z' ]
// which is the time in UTC when it's 2018-02-01T10:30:00 in Tokyo.
```

Whether or not you use the `TZID` param, make sure to only use JS `Date` objects that are
Expand All @@ -243,19 +243,22 @@ represented in UTC to avoid unexpected timezone offsets being applied, for examp
new RRule({
freq: RRule.MONTHLY,
dtstart: new Date(2018, 1, 1, 10, 30),
until: new Date(2018, 2, 31)
until: new Date(2018, 2, 31),
tzid: 'Asia/Tokyo'
}).all()

[ '2018-02-01T18:30:00.000Z', '2018-03-01T18:30:00.000Z' ]
// assuming your local timezone is Asia/Tokyo
[ '2018-01-31T16:30:00.000Z', '2018-02-28T16:30:00.000Z' ]

// RIGHT: Will produce dates with recurrences at the correct time
new RRule({
freq: RRule.MONTHLY,
dtstart: new Date(Date.UTC(2018, 1, 1, 10, 30)),
until: new Date(Date.UTC(2018, 2, 31))
until: new Date(Date.UTC(2018, 2, 31)),
tzid: 'Asia/Tokyo'
}).all()

[ '2018-02-01T10:30:00.000Z', '2018-03-01T10:30:00.000Z' ]
[ '2018-02-01T01:30:00.000Z', '2018-03-01T01:30:00.000Z' ]
```

### API
Expand Down Expand Up @@ -824,4 +827,3 @@ more details.
#### Related projects

* https://rrules.com/ — RESTful API to get back occurrences of RRULEs that conform to RFC 5545.

6 changes: 5 additions & 1 deletion package.json
Expand Up @@ -28,7 +28,11 @@
"build": "yarn lint && tsc && webpack && tsc dist/esm/**/*.d.ts",
"lint": "yarn tslint --project . --fix --config tslint.json",
"test": "TS_NODE_PROJECT=tsconfig.test.json mocha **/*.test.ts",
"test-ci": "TS_NODE_PROJECT=tsconfig.test.json nyc mocha **/*.test.ts"
"test-ci": "TS_NODE_PROJECT=tsconfig.test.json nyc mocha **/*.test.ts",
"test:tz": "npm run test:tz1 && npm run test:tz2 && npm run test:tz3",
"test:tz1": "TZ=Asia/Tokyo npm t",
"test:tz2": "TZ=Europe/Madrid npm t",
"test:tz3": "TZ=America/Chicago npm t"
},
"nyc": {
"extension": [
Expand Down
4 changes: 2 additions & 2 deletions src/dateutil.ts
Expand Up @@ -67,7 +67,7 @@ export namespace dateutil {
/**
* @return {Number} the date's timezone offset in ms
*/
export const tzOffset = function (date: Date) {
export const tzOffset = function (date: Date): number {
return date.getTimezoneOffset() * 60 * 1000
}

Expand Down Expand Up @@ -109,7 +109,7 @@ export namespace dateutil {
/**
* @return {Number} python-like weekday
*/
export const getWeekday = function (date: Date) {
export const getWeekday = function (date: Date): number {
return PY_WEEKDAYS[date.getUTCDay()]
}

Expand Down
13 changes: 11 additions & 2 deletions src/datewithzone.ts
Expand Up @@ -33,8 +33,17 @@ export class DateWithZone {
}

try {
const datetime = DateTime
.fromJSDate(this.date)
const { date } = this
const local = new Date(
date.getUTCFullYear(),
date.getUTCMonth(),
date.getUTCDate(),
date.getUTCHours(),
date.getUTCMinutes(),
date.getUTCSeconds(),
date.valueOf() % 1000
)
const datetime = DateTime.fromJSDate(local)

const rezoned = datetime.setZone(this.tzid!, { keepLocalTime: true })

Expand Down
5 changes: 3 additions & 2 deletions test/datewithzone.test.ts
Expand Up @@ -37,8 +37,9 @@ describe('rezonedDate', () => {
const currentLocalDate = DateTime.local(2000, 2, 6, 1, 0, 0)
setMockDate(currentLocalDate.toJSDate())

const d = DateTime.fromISO('20101005T110000').toJSDate()
const d = DateTime.fromISO('20101005T110000Z').toJSDate()
const dt = new DateWithZone(d, targetZone)

expect(dt.rezonedDate()).to.deep.equal(
expectedDate(DateTime.fromISO('20101005T110000'), currentLocalDate, targetZone)
)
Expand All @@ -63,4 +64,4 @@ describe('rezonedDate', () => {
DateTime.fromJSDate = origfromJSDate
resetMockDate()
})
})
})
23 changes: 8 additions & 15 deletions test/rrule.test.ts
@@ -1,9 +1,8 @@
import { parse, datetime, testRecurring, expectedDate } from './lib/utils'
import { parse, datetime, testRecurring } from './lib/utils'
import { expect } from 'chai'
import { RRule, rrulestr, Frequency } from '../src/index'
import { DateTime } from 'luxon'
import { set as setMockDate, reset as resetMockDate } from 'mockdate'
import { optionsToString } from '../src/optionstostring';

describe('RRule', function () {
// Enable additional toString() / fromString() tests
Expand All @@ -27,7 +26,7 @@ describe('RRule', function () {
const s2 = rrulestr(s1).toString()
expect(s1).equals(s2, s1 + ' => ' + s2)
})

it('rrulestr itteration not infinite when interval 0', function () {
['FREQ=YEARLY;INTERVAL=0;BYSETPOS=1;BYDAY=MO',
'FREQ=MONTHLY;INTERVAL=0;BYSETPOS=1;BYDAY=MO',
Expand Down Expand Up @@ -3664,12 +3663,9 @@ describe('RRule', function () {
tzid: targetZone
})
const recurrence = rule.all()[0]
const expected = expectedDate(startDate, currentLocalDate, targetZone)
const expected = new Date('2013-08-06T18:00:00.000Z') // regardless in which timezone the local time is, the result in UTC must be the same!

expect(recurrence)
.to.deep.equal(
expected
)
expect(recurrence).to.deep.equal(expected)

resetMockDate()
})
Expand All @@ -3684,12 +3680,9 @@ describe('RRule', function () {
tzid: targetZone
})
const recurrence = rule.all()[0]
const expected = expectedDate(startDate, currentLocalDate, targetZone)
const expected = new Date('2013-08-06T18:00:00.000Z') // regardless in which timezone the local time is, the result in UTC must be the same!

expect(recurrence)
.to.deep.equal(
expected
)
expect(recurrence).to.deep.equal(expected)

resetMockDate()
})
Expand All @@ -3704,11 +3697,11 @@ describe('RRule', function () {
tzid: targetZone
})
const recurrence = rule.after(new Date(0))
const expected = expectedDate(startDate, currentLocalDate, targetZone)
const expected = new Date('2013-08-06T18:00:00.000Z') // regardless in which timezone the local time is, the result in UTC must be the same!

expect(recurrence)
.to.deep.equal(
expected
expected
)

resetMockDate()
Expand Down
16 changes: 8 additions & 8 deletions test/rruleset.test.ts
Expand Up @@ -520,17 +520,17 @@ describe('RRuleSet', function () {
set.rrule(new RRule({
freq: RRule.YEARLY,
count: 4,
dtstart: DateTime.fromISO('20000101T090000').toJSDate(),
dtstart: DateTime.fromISO('20000101T090000Z').toJSDate(), // always use date in UTC
tzid: targetZone
}))

set.exdate(
DateTime.fromISO('20010101T090000').toJSDate(),
DateTime.fromISO('20010101T090000Z').toJSDate(),
)

set.rdate(
DateTime.fromISO('20020301T090000').toJSDate(),
)
DateTime.fromISO('20020301T090000Z').toJSDate(),
)

expect(set.all()).to.deep.equal([
expectedDate(DateTime.fromISO('20000101T090000'), currentLocalDate, targetZone),
Expand Down Expand Up @@ -564,7 +564,7 @@ describe('RRuleSet', function () {
set.tzid(targetZone)

set.rdate(
DateTime.fromISO('20020301T090000').toJSDate(),
DateTime.fromISO('20020301T090000Z').toJSDate(),
)

expect(set.all()).to.deep.equal([
Expand Down Expand Up @@ -755,7 +755,7 @@ describe('RRuleSet', function () {

expect(set.rrules().map(e => e.toString())).eql([rrule.toString()]);
});

it('exrules()', () => {
let set = new RRuleSet();
let rrule = new RRule({
Expand All @@ -773,7 +773,7 @@ describe('RRuleSet', function () {
let set = new RRuleSet();
let dt = parse('19610201T090000');
set.rdate(dt);

expect(set.rdates()).eql([dt]);
});

Expand All @@ -785,4 +785,4 @@ describe('RRuleSet', function () {
expect(set.exdates()).eql([dt]);
});
});
});
});
43 changes: 43 additions & 0 deletions test/timezones.test.ts
@@ -0,0 +1,43 @@
import { RRule } from '../src/index'
import { expect } from "chai"

const ruleByTzid = (tzid: string) => new RRule({
freq: RRule.WEEKLY,
dtstart: new Date(Date.UTC(2020, 1, 1, 0, 0, 0)), // always use UTC dates!!!
tzid,
byweekday: [RRule.TU],
count: 2
})

const mapToIso = (arr: Array<Date>): Array<string> =>
arr.map(d => d.toISOString())

describe('timezones', () => {
it('Europe/Paris', () => {
const tzid = 'Europe/Paris'
const rule = ruleByTzid(tzid)
expect(rule.toString()).to.equal('DTSTART;TZID=Europe/Paris:20200201T000000\nRRULE:FREQ=WEEKLY;BYDAY=TU;COUNT=2')
expect(mapToIso(rule.all())).to.deep.equal([
'2020-02-03T23:00:00.000Z',
'2020-02-10T23:00:00.000Z'
])
})

it('America/New_York', () => {
const rule = ruleByTzid('America/New_York')
expect(rule.toString()).to.equal('DTSTART;TZID=America/New_York:20200201T000000\nRRULE:FREQ=WEEKLY;BYDAY=TU;COUNT=2')
expect(mapToIso(rule.all())).to.deep.equal([
'2020-02-04T05:00:00.000Z',
'2020-02-11T05:00:00.000Z'
])
})

it('UTC', () => {
const rule = ruleByTzid('UTC')
expect(rule.toString()).to.equal('DTSTART:20200201T000000Z\nRRULE:FREQ=WEEKLY;BYDAY=TU;COUNT=2')
expect(mapToIso(rule.all())).to.deep.equal([
'2020-02-04T00:00:00.000Z',
'2020-02-11T00:00:00.000Z'
])
})
})

0 comments on commit c4f85a3

Please sign in to comment.