Skip to content

Commit

Permalink
Merge pull request #261 from jakubroztocil/timezones
Browse files Browse the repository at this point in the history
Implement Timezone support (#38)
  • Loading branch information
davidgoli authored Aug 16, 2018
2 parents 977dfb3 + 3218f92 commit 781b030
Show file tree
Hide file tree
Showing 45 changed files with 12,290 additions and 207 deletions.
141 changes: 101 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ Alternatively, download manually:

* [rrule.min.js](https://raw.github.com/jakubroztocil/rrule/master/dist/es5/rrule.min.js) (bundled, minified)
* [rrule.js](https://raw.github.com/jakubroztocil/rrule/master/dist/es5/rrule.js) (bundled, not minified)
* [rrule.js](https://raw.github.com/jakubroztocil/rrule/master/dist/es6/rrule.js) (es6 source with optional TypeScript types)
* [rrule-tz.min.js](https://raw.github.com/jakubroztocil/rrule/master/dist/es5/rrule-tz.min.js) (with timezone support, bundled, minified)
* [rrule-tz.js](https://raw.github.com/jakubroztocil/rrule/master/dist/es5/rrule-tz.js) (with timezone support, bundled, not minified)
* [rrule.js](https://raw.github.com/jakubroztocil/rrule/master/dist/esm/rrule.js) (ES module source with optional TypeScript types)

```html
<script src="rrule/dist/es5/rrule.min.js"></script>
Expand All @@ -59,22 +61,26 @@ const rule = new RRule({
freq: RRule.WEEKLY,
interval: 5,
byweekday: [RRule.MO, RRule.FR],
dtstart: new Date(2012, 1, 1, 10, 30),
until: new Date(2012, 12, 31)
dtstart: new Date(Date.UTC(2012, 1, 1, 10, 30)),
until: new Date(Date.UTC(2012, 12, 31))
})

// Get all occurrence dates (Date instances):
rule.all()
['Fri Feb 03 2012 10:30:00 GMT+0100 (CET)',
'Mon Mar 05 2012 10:30:00 GMT+0100 (CET)',
'Fri Mar 09 2012 10:30:00 GMT+0100 (CET)',
'Mon Apr 09 2012 10:30:00 GMT+0200 (CEST)',
[ '2012-02-03T10:30:00.000Z',
'2012-03-05T10:30:00.000Z',
'2012-03-09T10:30:00.000Z',
'2012-04-09T10:30:00.000Z',
'2012-04-13T10:30:00.000Z',
'2012-05-14T10:30:00.000Z',
'2012-05-18T10:30:00.000Z',

/**/]

// Get a slice:
rule.between(new Date(2012, 7, 1), new Date(2012, 8, 1))
['Mon Aug 27 2012 10:30:00 GMT+0200 (CEST)',
'Fri Aug 31 2012 10:30:00 GMT+0200 (CEST)']
rule.between(new Date(Date.UTC(2012, 7, 1)), new Date(Date.UTC(2012, 8, 1)))
['2012-08-27T10:30:00.000Z',
'2012-08-31T10:30:00.000Z']

// Get an iCalendar RRULE string representation:
// The output can be used with RRule.fromString().
Expand All @@ -95,36 +101,36 @@ const rruleSet = new RRuleSet()
rruleSet.rrule(new RRule({
freq: RRule.MONTHLY,
count: 5,
dtstart: new Date(2012, 1, 1, 10, 30)
dtstart: new Date(Date.UTC(2012, 1, 1, 10, 30))
}))

// Add a date to rruleSet
rruleSet.rdate(new Date(2012, 6, 1, 10, 30))
rruleSet.rdate(new Date(Date.UTC(2012, 6, 1, 10, 30)))

// Add another date to rruleSet
rruleSet.rdate(new Date(2012, 6, 2, 10, 30))
rruleSet.rdate(new Date(Date.UTC(2012, 6, 2, 10, 30)))

// Add a exclusion rrule to rruleSet
rruleSet.exrule(new r.RRule({
freq: RRule.MONTHLY,
count: 2,
dtstart: new Date(2012, 2, 1, 10, 30)
dtstart: new Date(Date.UTC(2012, 2, 1, 10, 30))
}))

// Add a exclusion date to rruleSet
rruleSet.exdate(new Date(2012, 5, 1, 10, 30))
rruleSet.exdate(new Date(Date.UTC(2012, 5, 1, 10, 30)))

// Get all occurrence dates (Date instances):
rruleSet.all()
['Wed Feb 01 2012 10:30:00 GMT+0800 (CST)',
'Tue May 01 2012 10:30:00 GMT+0800 (CST)',
'Sun Jul 01 2012 10:30:00 GMT+0800 (CST)',
'Mon Jul 02 2012 10:30:00 GMT+0800 (CST)']
[ '2012-02-01T10:30:00.000Z',
'2012-05-01T10:30:00.000Z',
'2012-07-01T10:30:00.000Z',
'2012-07-02T10:30:00.000Z' ]

// Get a slice:
rruleSet.between(new Date(2012, 2, 1), new Date(2012, 6, 2))
['Tue May 01 2012 10:30:00 GMT+0800 (CST)',
'Sun Jul 01 2012 10:30:00 GMT+0800 (CST)']
rruleSet.between(new Date(Date.UTC(2012, 2, 1)), new Date(Date.UTC(2012, 6, 2)))
[ '2012-05-01T10:30:00.000Z', '2012-07-01T10:30:00.000Z' ]


// To string
rruleSet.valueOf()
Expand Down Expand Up @@ -154,6 +160,55 @@ rrulestr('RRULE:FREQ=MONTHLY;COUNT=5;DTSTART=20120201T023000Z\nRDATE:20120701T02
For more examples see
[python-dateutil](http://labix.org/python-dateutil/) documentation.

* * * * *

### Timezone Support

By default, `RRule` only correctly supports
["floating" times or UTC timezones](https://tools.ietf.org/html/rfc2445#section-4.2.19).
Optionally, it also supports use of the `TZID` parameter in the
[RFC](https://tools.ietf.org/html/rfc2445#section-4.2.19)
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.

Example with `TZID`:

```js
new RRule({
dtstart: new Date(Date.UTC(2018, 1, 1, 10, 30)),
count: 1,
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.
```

Whether or not you use the `TZID` param, make sure to only use JS `Date` objects that are
represented in UTC to avoid unexpected timezone offsets being applied, for example:

```js
// WRONG: Will produce dates with TZ offsets added
new RRule({
freq: RRule.MONTHLY,
dtstart: new Date(2018, 1, 1, 10, 30),
until: new Date(2018, 2, 31)
}).all()

[ '2018-02-01T18: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))
}).all()

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

### API

#### `RRule` Constructor
Expand Down Expand Up @@ -229,6 +284,13 @@ iCalendar RFC. Only `freq` is required.
argument, this will be the last occurrence.
</td>
</tr>
<tr>
<td><code>tzid</code></td>
<td>If given, this must be a string <a href="https://moment.github.io/luxon/docs/manual/zones.html#specifying-a-zone">supported</a>
by Luxon, and the <a href="https://moment.github.io/luxon/">Luxon</a> library must be provided. See
discussion under <a href="#timezone-support">Timezone support</a>.
</td>
</tr>
<tr>
<td><code>bysetpos</code></td>
<td>If given, it must be either an integer, or an array of
Expand Down Expand Up @@ -365,15 +427,14 @@ the result and the iteration is interrupted (possibly prematurely).

```javascript
rule.all()
['Fri Feb 03 2012 10:30:00 GMT+0100 (CET)',
'Mon Mar 05 2012 10:30:00 GMT+0100 (CET)',
'Fri Mar 09 2012 10:30:00 GMT+0100 (CET)',
'Mon Apr 09 2012 10:30:00 GMT+0200 (CEST)',
/**/]
[ '2012-02-01T10:30:00.000Z',
'2012-05-01T10:30:00.000Z',
'2012-07-01T10:30:00.000Z',
'2012-07-02T10:30:00.000Z' ]

rule.all(function (date, i){return i < 2})
['Fri Feb 03 2012 10:30:00 GMT+0100 (CET)',
'Mon Mar 05 2012 10:30:00 GMT+0100 (CET)',]
[ '2012-02-01T10:30:00.000Z',
'2012-05-01T10:30:00.000Z' ]
```

##### `RRule.prototype.between(after, before, inc=false [, iterator])`
Expand All @@ -387,9 +448,9 @@ Optional `iterator` has the same function as it has with
`RRule.prototype.all()`.

```javascript
rule.between(new Date(2012, 7, 1), new Date(2012, 8, 1))
['Mon Aug 27 2012 10:30:00 GMT+0200 (CEST)',
'Fri Aug 31 2012 10:30:00 GMT+0200 (CEST)']
rule.between(new Date(Date.UTC(2012, 7, 1)), new Date(Date.UTC(2012, 8, 1)))
['2012-08-27T10:30:00.000Z',
'2012-08-31T10:30:00.000Z']
```

##### `RRule.prototype.before(dt, inc=false)`
Expand Down Expand Up @@ -463,7 +524,7 @@ Only parse RFC string and return `options`.

```javascript
var options = RRule.parseString('FREQ=DAILY;INTERVAL=6')
options.dtstart = new Date(2000, 1, 1)
options.dtstart = new Date(Date.UTC(2000, 1, 1))
var rule = new RRule(options)
```

Expand Down Expand Up @@ -599,16 +660,16 @@ If set to True, the parser will operate in RFC-compatible mode. Right now it
means that unfold will be turned on, and if a DTSTART is found, it will be
considered the first recurrence instance, as documented in the RFC.

`tzid`
If given, it must be a string that will be used when no `TZID` property is found
in the parsed string. If it is not given, and the property is not found, `'UTC'`
will be used by default.


* * * * *

### Differences From iCalendar RFC

* *Timezones:* `RRule` does not implement the `TZID` keyword in the RFC. It
only correctly supports "floating" times or UTC timezones. While it will work
the same regardless of the host system's timezone, for best results, only pass
in JS `Date` objects that are represented in UTC to avoid unexpected timezone
offsets being applied.

* `RRule` has no `byday` keyword. The equivalent keyword has been replaced by
the `byweekday` keyword, to remove the ambiguity present in the original
keyword.
Expand All @@ -621,7 +682,7 @@ original behavior by using a `RRuleSet` and adding the `dtstart` as an `rdate`.

```javascript
var rruleSet = new RRuleSet()
var start = new Date(2012, 1, 1, 10, 30)
var start = new Date(Date.UTC(2012, 1, 1, 10, 30))

// Add a rrule to rruleSet
rruleSet.rrule(new RRule({
Expand Down
2 changes: 2 additions & 0 deletions demo/demo.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ $ ->
value = value.map(getDay)
else
value = getDay(value)
else if key is 'tzid'
value = value
else if /^by/.test(key)
if not (value instanceof Array)
value = value.split(/[,\s]+/)
Expand Down
108 changes: 107 additions & 1 deletion demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<head>
<title>rrule.js demo</title>
<link rel="stylesheet" href="./dist/demo.css" />
<script type="text/javascript" src="./dist/es5/rrule.js"></script>
<script type="text/javascript" src="./dist/es5/rrule-tz.js"></script>
<script type="text/javascript" src="./dist/demo.js"></script>
</head>

Expand Down Expand Up @@ -125,6 +125,112 @@ <h3>Options</h3>
<input name="dtstart" type="datetime-local" />
</td>
</tr>
<tr>
<th>
<div class="option-name">
<code>tzid</code>

<div class="help">The timezone for the rule. If present, all recurrences will be interpreted as being in the local time
of the given timezone. If not present, <code>UTC</code> will be used instead.
</div>
</div>
</th>
<td>
<select name="tzid">
<option value="">none (UTC)</option>
<option value="Pacific/Midway">(GMT-11:00) Midway Island, Samoa</option>
<option value="America/Adak">(GMT-10:00) Hawaii-Aleutian</option>
<option value="Etc/GMT+10">(GMT-10:00) Hawaii</option>
<option value="Pacific/Marquesas">(GMT-09:30) Marquesas Islands</option>
<option value="Pacific/Gambier">(GMT-09:00) Gambier Islands</option>
<option value="America/Anchorage">(GMT-09:00) Alaska</option>
<option value="America/Ensenada">(GMT-08:00) Tijuana, Baja California</option>
<option value="Etc/GMT+8">(GMT-08:00) Pitcairn Islands</option>
<option value="America/Los_Angeles">(GMT-08:00) Pacific Time (US & Canada)</option>
<option value="America/Denver">(GMT-07:00) Mountain Time (US & Canada)</option>
<option value="America/Chihuahua">(GMT-07:00) Chihuahua, La Paz, Mazatlan</option>
<option value="America/Dawson_Creek">(GMT-07:00) Arizona</option>
<option value="America/Belize">(GMT-06:00) Saskatchewan, Central America</option>
<option value="America/Cancun">(GMT-06:00) Guadalajara, Mexico City, Monterrey</option>
<option value="Chile/EasterIsland">(GMT-06:00) Easter Island</option>
<option value="America/Chicago">(GMT-06:00) Central Time (US & Canada)</option>
<option value="America/New_York">(GMT-05:00) Eastern Time (US & Canada)</option>
<option value="America/Havana">(GMT-05:00) Cuba</option>
<option value="America/Bogota">(GMT-05:00) Bogota, Lima, Quito, Rio Branco</option>
<option value="America/Caracas">(GMT-04:30) Caracas</option>
<option value="America/Santiago">(GMT-04:00) Santiago</option>
<option value="America/La_Paz">(GMT-04:00) La Paz</option>
<option value="Atlantic/Stanley">(GMT-04:00) Faukland Islands</option>
<option value="America/Campo_Grande">(GMT-04:00) Brazil</option>
<option value="America/Goose_Bay">(GMT-04:00) Atlantic Time (Goose Bay)</option>
<option value="America/Glace_Bay">(GMT-04:00) Atlantic Time (Canada)</option>
<option value="America/St_Johns">(GMT-03:30) Newfoundland</option>
<option value="America/Araguaina">(GMT-03:00) UTC-3</option>
<option value="America/Montevideo">(GMT-03:00) Montevideo</option>
<option value="America/Miquelon">(GMT-03:00) Miquelon, St. Pierre</option>
<option value="America/Godthab">(GMT-03:00) Greenland</option>
<option value="America/Argentina/Buenos_Aires">(GMT-03:00) Buenos Aires</option>
<option value="America/Sao_Paulo">(GMT-03:00) Brasilia</option>
<option value="America/Noronha">(GMT-02:00) Mid-Atlantic</option>
<option value="Atlantic/Cape_Verde">(GMT-01:00) Cape Verde Is.</option>
<option value="Atlantic/Azores">(GMT-01:00) Azores</option>
<option value="Europe/Belfast">(GMT) Greenwich Mean Time : Belfast</option>
<option value="Europe/Dublin">(GMT) Greenwich Mean Time : Dublin</option>
<option value="Europe/Lisbon">(GMT) Greenwich Mean Time : Lisbon</option>
<option value="Europe/London">(GMT) Greenwich Mean Time : London</option>
<option value="Africa/Abidjan">(GMT) Monrovia, Reykjavik</option>
<option value="Europe/Amsterdam">(GMT+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna</option>
<option value="Europe/Belgrade">(GMT+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague</option>
<option value="Europe/Brussels">(GMT+01:00) Brussels, Copenhagen, Madrid, Paris</option>
<option value="Africa/Algiers">(GMT+01:00) West Central Africa</option>
<option value="Africa/Windhoek">(GMT+01:00) Windhoek</option>
<option value="Asia/Beirut">(GMT+02:00) Beirut</option>
<option value="Africa/Cairo">(GMT+02:00) Cairo</option>
<option value="Asia/Gaza">(GMT+02:00) Gaza</option>
<option value="Africa/Blantyre">(GMT+02:00) Harare, Pretoria</option>
<option value="Asia/Jerusalem">(GMT+02:00) Jerusalem</option>
<option value="Europe/Minsk">(GMT+02:00) Minsk</option>
<option value="Asia/Damascus">(GMT+02:00) Syria</option>
<option value="Europe/Moscow">(GMT+03:00) Moscow, St. Petersburg, Volgograd</option>
<option value="Africa/Addis_Ababa">(GMT+03:00) Nairobi</option>
<option value="Asia/Tehran">(GMT+03:30) Tehran</option>
<option value="Asia/Dubai">(GMT+04:00) Abu Dhabi, Muscat</option>
<option value="Asia/Yerevan">(GMT+04:00) Yerevan</option>
<option value="Asia/Kabul">(GMT+04:30) Kabul</option>
<option value="Asia/Yekaterinburg">(GMT+05:00) Ekaterinburg</option>
<option value="Asia/Tashkent">(GMT+05:00) Tashkent</option>
<option value="Asia/Kolkata">(GMT+05:30) Chennai, Kolkata, Mumbai, New Delhi</option>
<option value="Asia/Katmandu">(GMT+05:45) Kathmandu</option>
<option value="Asia/Dhaka">(GMT+06:00) Astana, Dhaka</option>
<option value="Asia/Novosibirsk">(GMT+06:00) Novosibirsk</option>
<option value="Asia/Rangoon">(GMT+06:30) Yangon (Rangoon)</option>
<option value="Asia/Bangkok">(GMT+07:00) Bangkok, Hanoi, Jakarta</option>
<option value="Asia/Krasnoyarsk">(GMT+07:00) Krasnoyarsk</option>
<option value="Asia/Hong_Kong">(GMT+08:00) Beijing, Chongqing, Hong Kong, Urumqi</option>
<option value="Asia/Irkutsk">(GMT+08:00) Irkutsk, Ulaan Bataar</option>
<option value="Australia/Perth">(GMT+08:00) Perth</option>
<option value="Australia/Eucla">(GMT+08:45) Eucla</option>
<option value="Asia/Tokyo">(GMT+09:00) Osaka, Sapporo, Tokyo</option>
<option value="Asia/Seoul">(GMT+09:00) Seoul</option>
<option value="Asia/Yakutsk">(GMT+09:00) Yakutsk</option>
<option value="Australia/Adelaide">(GMT+09:30) Adelaide</option>
<option value="Australia/Darwin">(GMT+09:30) Darwin</option>
<option value="Australia/Brisbane">(GMT+10:00) Brisbane</option>
<option value="Australia/Hobart">(GMT+10:00) Hobart</option>
<option value="Asia/Vladivostok">(GMT+10:00) Vladivostok</option>
<option value="Australia/Lord_Howe">(GMT+10:30) Lord Howe Island</option>
<option value="Etc/GMT-11">(GMT+11:00) Solomon Is., New Caledonia</option>
<option value="Asia/Magadan">(GMT+11:00) Magadan</option>
<option value="Pacific/Norfolk">(GMT+11:30) Norfolk Island</option>
<option value="Asia/Anadyr">(GMT+12:00) Anadyr, Kamchatka</option>
<option value="Pacific/Auckland">(GMT+12:00) Auckland, Wellington</option>
<option value="Etc/GMT-12">(GMT+12:00) Fiji, Kamchatka, Marshall Is.</option>
<option value="Pacific/Chatham">(GMT+12:45) Chatham Islands</option>
<option value="Pacific/Tongatapu">(GMT+13:00) Nuku'alofa</option>
<option value="Pacific/Kiritimati">(GMT+14:00) Kiritimati</option>
</select>
</td>
</tr>
<tr>
<th>
<div class="option-name">
Expand Down
Loading

0 comments on commit 781b030

Please sign in to comment.