Skip to content

Commit

Permalink
Timezone support
Browse files Browse the repository at this point in the history
  • Loading branch information
Rafael Xavier de Souza authored and rxaviers committed Jul 3, 2017
1 parent 5e09cfc commit d558c11
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 28 deletions.
100 changes: 83 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,46 @@ Based on the [Unicode CLDR][] locale data. Powered by [globalizejs/globalize][].

## Why

- Leverages Unicode CLDR (via [Globalize](http://globalizejs.com)), the largest and most extensive standard repository of locale data available.
- It also means messages like `"today"`, `"yesterday"`, `"last month"` are available and properly localized in the various CLDR supported locales.
- What you get is correct, for example:
### Leverages Unicode CLDR

Leverages Unicode CLDR (via [Globalize](http://globalizejs.com)), the largest and most extensive standard repository of locale data available.

It also means messages like `"today"`, `"yesterday"`, `"last month"` are available and properly localized in the various CLDR supported locales.

### IANA time zone support

```
hr. | | | | | | | | | | | | | | | | | | | | | | | | | |
day | x . . N | . . |
PDT . . Mar 21 PDT . . Mar 23, 00:00 PDT
EDT . Mar 21 EDT . Mar 22, 00:00 EDT
UTC Mar 21 Mar 22, 00:00
```
The relative time between `x` and now `N` is:

| time zone | relative-time result |
| ------------------- | -------------------- |
| America/New_York | `"yesterday"` |
| America/Los_Angeles | `"21 hours ago"` |

### What you get is correct

#### day

```
Mar 21, 00:00 Mar 22, 00:00 Mar 23, 00:00 Mar 24, 00:00
hr. | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
day | b | a | N |
Mar 21 Mar 22 Mar 23 Mar 24
Mar 21 Mar 22 Mar 23 Mar 24
```

Let's assume now (`N`) is *Mar 23, 9 AM*.

| | relative-time | moment.js |
| --------------------- | ---------------------- | --------------- |
| *Mar 22, 11 AM* (`a`) | `"yesterday"` | `"a day ago"` |
| *Mar 21, 8 PM* (`b`) | `"2 days ago"` | `"a day ago"`|
| | relative-time | moment.js |
| --------------------- | -------------- | --------------- |
| *Mar 22, 11 AM* (`a`) | `"yesterday"` | `"a day ago"` |
| *Mar 21, 8 PM* (`b`) | `"2 days ago"` | `"a day ago"`|

Note `relative-time` checks for the actual day change instead of counting on approximate number of hours to turn the unit.

Expand All @@ -42,12 +62,12 @@ mo. | d c|b a N|

Let's assume now (`N`) is *Mar 31*.

| | relative-time | moment.js |
| -------------- | ---------------------- | ------------------ |
| *Mar 5* (`a`) | `"26 days ago"` | `"a month ago"`|
| *Mar 1* (`b`) | `"30 days ago"` | `"a month ago"`|
| *Feb 28* (`c`) | `"last month"` | `"a month ago"` |
| *Feb 9* (`d`) | `"last month"` | `"2 months ago"`|
| | relative-time | moment.js |
| -------------- | --------------- | ------------------ |
| *Mar 5* (`a`) | `"26 days ago"` | `"a month ago"`|
| *Mar 1* (`b`) | `"30 days ago"` | `"a month ago"`|
| *Feb 28* (`c`) | `"last month"` | `"a month ago"` |
| *Feb 9* (`d`) | `"last month"` | `"2 months ago"`|

Note `relative-time` checks for the actual month change instead of counting on approximate number of days to turn the unit.

Expand All @@ -69,13 +89,49 @@ console.log(relativeTime.format(new Date()));
// > now
```

### IANA time zone support

In addition to the above, install `iana-tz-data`.

```
npm install --save iana-tz-data
```

The example below assume now is `2016-04-10T12:00:00Z`, i.e.,

| | UTC | America/Los_Angeles | Europe/Berlin |
| ---- | -------------------- | ------------------------------- | ---------------------------------------- |
| date | 2016-04-10T00:00:00Z | 2016-04-09 17:00:00 GMT-7 (PDT) | 2016-04-10 14:00:00 GMT+2 (Central European Summer Time) |
| now | 2016-04-10T12:00:00Z | 2016-04-10 05:00:00 GMT-7 (PDT) | 2016-04-10 14:00:00 GMT+2 (Central European Summer Time) |

```js
var ianaTzData = require("iana-tz-data");
var date = new Date("2016-04-10T00:00:00Z");

// Target: 2016-04-09 17:00:00 GMT-7 (PDT)
// Now: 2016-04-10 05:00:00 GMT-7 (PDT)
relativeTime.format(date, {
timeZoneData: ianaTzData.zoneData.America.Los_Angeles
});
// > "yesterday"

// Target: 2016-04-10 14:00:00 GMT+2 (Central European Summer Time)
// Now: 2016-04-10 14:00:00 GMT+2 (Central European Summer Time)
relativeTime.format(date, {
timeZoneData: ianaTzData.zoneData.Europe.Berlin
});
// > "12 hours ago"
```

## API

### `format(date{, options})`

### date

### options.unit
A [JavaScript date object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date), i.e., `new Date()`.

### options.unit (optional)

Unit for formatting. If the unit is not provided, `"best-fit"` is used.

Expand All @@ -99,6 +155,16 @@ It automatically picks a unit based on the relative time scale. Basically, it lo
- If `absDiffMinutes > 0 && absDiffSeconds > threshold.second`, return `"minutes"`.
- Return `"second"`.

### options.timeZoneData (optional)

The *zdumped* IANA timezone data (found on the [iana-tz-data](https://github.com/rxaviers/iana-tz-data) package) for the desired timeZoneId.

If not provided, the user's environment time zone is used (default).

### Return

Returns the formatted relative time string given `date` and `options`.

## Appendix

### Relative time
Expand Down Expand Up @@ -163,7 +229,7 @@ g: 3 hours ago
Mar 21, 00:00 Mar 22, 00:00 Mar 23, 00:00 Mar 24, 00:00
hr. | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
day | e d | c b | a N |
Mar 21 Mar 22 Mar 23 Mar 24
Mar 21 Mar 22 Mar 23 Mar 24
N: The assumed now
a: today / 0 days ago
Expand Down Expand Up @@ -210,7 +276,7 @@ Note the months distances doesn't match weeks distance or days distance uniforml
#### year

```
Jan Jan Jan Jan Jan
Jan Jan Jan Jan Jan
mo. | | | | | | | | | | | | | | | | | | | | | | | | | | |
yr. |g f |e d |c b |a N |
2013 2014 2015 2016 2017
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "relative-time",
"version": "0.1.1",
"version": "0.2.0-rc.2",
"description": "Formats JavaScript dates to relative time strings (e.g., \"3 hours ago\")",
"main": "dist/relative-time.js",
"files": [
Expand Down Expand Up @@ -46,10 +46,12 @@
"cldr-data": ">=29.0.1",
"eslint": "^3.18.0",
"eslint-config-defaults": "^9.0.0",
"iana-tz-data": ">=2017.1.0",
"mocha": "^2.4.5",
"sinon": "^1.17.3"
},
"dependencies": {
"globalize": "^1.1.1"
"globalize": "^1.1.1",
"zoned-date-time": "^1.0.0"
}
}
11 changes: 9 additions & 2 deletions src/relative-time.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Globalize from "globalize";
import ZonedDateTime from "zoned-date-time";

const second = 1e3;
const minute = 6e4;
Expand All @@ -21,7 +22,7 @@ function defineGetter(obj, prop, get) {
}

function startOf(date, unit) {
date = new Date(date.getTime());
date = date instanceof ZonedDateTime ? date.clone() : new Date(date.getTime());
switch (unit) {
case "year": date.setMonth(0);
// falls through
Expand All @@ -43,9 +44,15 @@ export default class RelativeTime {
this.formatters = RelativeTime.initializeFormatters(...arguments);
}

format(date, {unit = "best-fit"} = {}) {
format(date, {timeZoneData = null, unit = "best-fit"} = {}) {
var formatters = this.formatters;
var now = new Date();

if (timeZoneData) {
date = new ZonedDateTime(date, timeZoneData);
now = new ZonedDateTime(now, timeZoneData);
}

var diff = {
_: {},
ms: date.getTime() - now.getTime(),
Expand Down
89 changes: 82 additions & 7 deletions test/relative-time.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import RelativeTime from "../src/relative-time";
import ianaTzData from "iana-tz-data";
import sinon from "sinon";

describe("relative-time", function() {
Expand All @@ -8,15 +9,15 @@ describe("relative-time", function() {
relativeTime = new RelativeTime();
});

beforeEach(function() {
clock = sinon.useFakeTimers(new Date("2016-04-10 12:00:00").getTime());
});
describe("bestFit", function() {
beforeEach(function() {
clock = sinon.useFakeTimers(new Date("2016-04-10 12:00:00").getTime());
});

afterEach(function() {
clock.restore();
});
afterEach(function() {
clock.restore();
});

describe("bestFit", function() {
it("should format seconds-distant dates", function() {
expect(relativeTime.format(new Date("2016-04-10 11:59:01"))).to.equal("59 seconds ago");
expect(relativeTime.format(new Date("2016-04-10 12:00:00"))).to.equal("now");
Expand Down Expand Up @@ -96,6 +97,14 @@ describe("relative-time", function() {
});

describe("explicit units", function() {
beforeEach(function() {
clock = sinon.useFakeTimers(new Date("2016-04-10 12:00:00").getTime());
});

afterEach(function() {
clock.restore();
});

it("shold format relative time using seconds", function() {
expect(relativeTime.format(new Date("2016-04-10 11:59:01"), {unit: "second"})).to.equal("59 seconds ago");
expect(relativeTime.format(new Date("2016-04-10 11:01:00"), {unit: "second"})).to.equal("3,540 seconds ago");
Expand Down Expand Up @@ -127,4 +136,70 @@ describe("relative-time", function() {
expect(relativeTime.format(new Date("2017-01-01 00:00"), {unit: "month"})).to.equal("in 9 months");
});
});

describe("time zone", function() {
beforeEach(function() {
clock = sinon.useFakeTimers(new Date("2016-04-10T12:00:00Z").getTime());
});

afterEach(function() {
clock.restore();
});

it("should support using specific time zone", function() {
// Target: 2016-04-09 17:00:00 GMT-7 (PDT)
// Now: 2016-04-10 05:00:00 GMT-7 (PDT)
expect(relativeTime.format(new Date("2016-04-10T00:00:00Z"), {
timeZoneData: ianaTzData.zoneData.America.Los_Angeles
})).to.equal("yesterday");

// Target: 2016-04-10 14:00:00 GMT+2 (Central European Summer Time)
// Now: 2016-04-10 14:00:00 GMT+2 (Central European Summer Time)
expect(relativeTime.format(new Date("2016-04-10T00:00:00Z"), {
timeZoneData: ianaTzData.zoneData.Europe.Berlin
})).to.equal("12 hours ago");

// Target: 2016-03-31 17:00:00 GMT-7 (PDT)
// Now: 2016-04-10 05:00:00 GMT-7 (PDT)
expect(relativeTime.format(new Date("2016-04-01T00:00:00Z"), {
timeZoneData: ianaTzData.zoneData.America.Los_Angeles
})).to.equal("last month");

// Target: 2016-04-01 02:00:00 GMT+2 (Central European Summer Time)
// Now: 2016-04-10 14:00:00 GMT+2 (Central European Summer Time)
expect(relativeTime.format(new Date("2016-04-01T00:00:00Z"), {
timeZoneData: ianaTzData.zoneData.Europe.Berlin
})).to.equal("9 days ago");

// Target: 2015-12-31 16:00:00 GMT-8 (PST)
// Now: 2016-04-10 05:00:00 GMT-7 (PDT)
expect(relativeTime.format(new Date("2016-01-01T00:00:00Z"), {
timeZoneData: ianaTzData.zoneData.America.Los_Angeles
})).to.equal("last year");

// Target: 2016-01-01 01:00:00 GMT+1 (Central European Standard Time)'
// Now: 2016-04-10 14:00:00 GMT+2 (Central European Summer Time)
expect(relativeTime.format(new Date("2016-01-01T00:00:00Z"), {
timeZoneData: ianaTzData.zoneData.Europe.Berlin
})).to.equal("3 months ago");
});

it("should support daylight savings edge cases", function() {
clock = sinon.useFakeTimers(new Date("2017-02-19T02:00:00.000Z").getTime());

// Target: 2017-02-18 23:00:00 GMT-2 (BRST)
// Now: 2017-02-18 23:00:00 GMT-3 (BRT)
// expect(relativeTime.format(new Date("2017-02-19T01:00:00.000Z"), {
// timeZoneData: ianaTzData.zoneData.America.Sao_Paulo
// })).to.equal("1 hour ago");
// TODO: This currently fails and returns "now".

// Target: 2017-03-12 01:00:00 GMT-8 (PST)
// Now: 2017-03-12 03:00:00 GMT-7 (PDT)
clock = sinon.useFakeTimers(new Date("2017-03-12T10:00:00.000Z").getTime());
expect(relativeTime.format(new Date("2017-03-12T09:00:00.000Z"), {
timeZoneData: ianaTzData.zoneData.America.Los_Angeles
})).to.equal("1 hour ago");
});
});
});

0 comments on commit d558c11

Please sign in to comment.