Commit 1187d864 authored by Robert Knight's avatar Robert Knight

Merge pull request #3247 from hypothesis/change-display-of-datetimes-on-annotations

Change display of datetimes on annotations
parents 2332e117 d78e2991
...@@ -9,19 +9,20 @@ var month = day * 30; ...@@ -9,19 +9,20 @@ var month = day * 30;
var year = day * 365; var year = day * 365;
var FIXTURES_TO_FUZZY_STRING = [ var FIXTURES_TO_FUZZY_STRING = [
[10, 'moments ago'], [10, 'Just now'],
[29, 'moments ago'], [29, 'Just now'],
[49, '49 seconds ago'], [49, '49 secs'],
[minute + 5, 'a minute ago'], [minute + 5, '1 min'],
[3 * minute + 5, '3 minutes ago'], [3 * minute + 5, '3 mins'],
[4 * hour, '4 hours ago'], [hour, '1 hr'],
[27 * hour, 'a day ago'], [4 * hour, '4 hrs'],
[3 * day + 30 * minute, '3 days ago'], [27 * hour, '1 Jan'],
[6 * month + 2 * day, '6 months ago'], [3 * day + 30 * minute, '1 Jan'],
[1 * year, 'one year ago'], [6 * month + 2 * day, '1 Jan'],
[1 * year + 2 * month, 'one year ago'], [1 * year, '1 Jan 1970'],
[2 * year, '2 years ago'], [1 * year + 2 * month, '1 Jan 1970'],
[8 * year, '8 years ago'] [2 * year, '1 Jan 1970'],
[8 * year, '1 Jan 1970']
]; ];
var FIXTURES_NEXT_FUZZY_UPDATE = [ var FIXTURES_NEXT_FUZZY_UPDATE = [
...@@ -31,10 +32,10 @@ var FIXTURES_NEXT_FUZZY_UPDATE = [ ...@@ -31,10 +32,10 @@ var FIXTURES_NEXT_FUZZY_UPDATE = [
[minute + 5, minute], [minute + 5, minute],
[3 * minute + 5, minute], [3 * minute + 5, minute],
[4 * hour, hour], [4 * hour, hour],
[27 * hour, day], [27 * hour, null],
[3 * day + 30 * minute, day], [3 * day + 30 * minute, null],
[6 * month + 2 * day, 24 * day], // longer times are not supported [6 * month + 2 * day, null],
[8 * year, 24 * day] // by setTimout [8 * year, null]
]; ];
describe('time', function () { describe('time', function () {
...@@ -50,18 +51,35 @@ describe('time', function () { ...@@ -50,18 +51,35 @@ describe('time', function () {
}); });
describe('.toFuzzyString', function () { describe('.toFuzzyString', function () {
function mockIntl() {
return {
DateTimeFormat: function () {
return {
format: function () {
if (new Date().getYear() === 70) {
return '1 Jan';
} else {
return '1 Jan 1970';
}
}
};
}
};
}
it('Handles empty dates', function () { it('Handles empty dates', function () {
var t = null; var t = null;
var expect = ''; var expect = '';
assert.equal(time.toFuzzyString(t), expect); assert.equal(time.toFuzzyString(t, mockIntl()), expect);
}); });
var testFixture = function (f) { var testFixture = function (f) {
return function () { return function () {
var t = new Date(); var t = new Date().toISOString();
var expect = f[1]; var expect = f[1];
sandbox.clock.tick(f[0] * 1000); sandbox.clock.tick(f[0] * 1000);
assert.equal(time.toFuzzyString(t), expect); assert.equal(time.toFuzzyString(t, mockIntl()), expect);
}; };
}; };
...@@ -70,6 +88,25 @@ describe('time', function () { ...@@ -70,6 +88,25 @@ describe('time', function () {
it('creates correct fuzzy string for fixture ' + i, it('creates correct fuzzy string for fixture ' + i,
testFixture(f)); testFixture(f));
} }
it('falls back to simple strings for >24hrs ago', function () {
// If window.Intl is not available then the date formatting for dates
// more than one day ago falls back to a simple date string.
var d = new Date().toISOString();
sandbox.clock.tick(day * 2 * 1000);
assert.equal(time.toFuzzyString(d, null), 'Thu Jan 01 1970');
});
it('falls back to simple strings for >1yr ago', function () {
// If window.Intl is not available then the date formatting for dates
// more than one year ago falls back to a simple date string.
var d = new Date().toISOString();
sandbox.clock.tick(year * 2 * 1000);
assert.equal(time.toFuzzyString(d, null), 'Thu Jan 01 1970');
});
}); });
describe('.decayingInterval', function () { describe('.decayingInterval', function () {
...@@ -105,6 +142,18 @@ describe('time', function () { ...@@ -105,6 +142,18 @@ describe('time', function () {
sandbox.clock.tick(minute * 1000); sandbox.clock.tick(minute * 1000);
assert.notCalled(callback); assert.notCalled(callback);
}); });
it('does not set a timeout for dates > 24hrs ago', function () {
var date = new Date();
var ONE_DAY = day * 1000;
sandbox.clock.tick(10 * ONE_DAY);
var callback = sandbox.stub();
time.decayingInterval(date, callback);
sandbox.clock.tick(ONE_DAY * 2);
assert.notCalled(callback);
});
}); });
describe('.nextFuzzyUpdate', function () { describe('.nextFuzzyUpdate', function () {
...@@ -116,7 +165,7 @@ describe('time', function () { ...@@ -116,7 +165,7 @@ describe('time', function () {
var testFixture = function (f) { var testFixture = function (f) {
return function () { return function () {
var t = new Date(); var t = new Date().toISOString();
var expect = f[1]; var expect = f[1];
sandbox.clock.tick(f[0] * 1000); sandbox.clock.tick(f[0] * 1000);
assert.equal(time.nextFuzzyUpdate(t), expect); assert.equal(time.nextFuzzyUpdate(t), expect);
......
...@@ -2,39 +2,148 @@ ...@@ -2,39 +2,148 @@
var minute = 60; var minute = 60;
var hour = minute * 60; var hour = minute * 60;
var day = hour * 24;
var month = day * 30; function lessThanThirtySecondsAgo(date, now) {
var year = day * 365; return ((now - date) < 30 * 1000);
}
function lessThanOneMinuteAgo(date, now) {
return ((now - date) < 60 * 1000);
}
function lessThanOneHourAgo(date, now) {
return ((now - date) < (60 * 60 * 1000));
}
function lessThanOneDayAgo(date, now) {
return ((now - date) < (24 * 60 * 60 * 1000));
}
function thisYear(date, now) {
return date.getFullYear() === now.getFullYear();
}
function delta(date, now) {
return Math.round((now - date) / 1000);
}
function nSec(date, now) {
return '{} secs'.replace('{}', Math.floor(delta(date, now)));
}
function nMin(date, now) {
var n = Math.floor(delta(date, now) / minute);
var template = '{} min';
if (n > 1) {
template = template + 's';
}
return template.replace('{}', n);
}
function nHr(date, now) {
var n = Math.floor(delta(date, now) / hour);
var template = '{} hr';
if (n > 1) {
template = template + 's';
}
return template.replace('{}', n);
}
// Cached DateTimeFormat instances,
// because instantiating a DateTimeFormat is expensive.
var formatters = {};
/**
* Efficiently return `date` formatted with `options`.
*
* This is a wrapper for Intl.DateTimeFormat.format() that caches
* DateTimeFormat instances because they're expensive to create.
* Calling Date.toLocaleDateString() lots of times is also expensive in some
* browsers as it appears to create a new formatter for each call.
*
* @returns {string}
*
*/
function format(date, options, Intl) {
// If the tests have passed in a mock Intl then use it, otherwise use the
// real one.
if (typeof Intl === 'undefined') {
Intl = window.Intl;
}
if (Intl && Intl.DateTimeFormat) {
var key = JSON.stringify(options);
var formatter = formatters[key];
if (!formatter) {
formatter = formatters[key] = new Intl.DateTimeFormat(undefined,
options);
}
return formatter.format(date);
} else {
// IE < 11, Safari <= 9.0.
return date.toDateString();
}
}
function dayAndMonth(date, now, Intl) {
return format(date, {month: 'short', day: 'numeric'}, Intl);
}
function dayAndMonthAndYear(date, now, Intl) {
return format(date, {day: 'numeric', month: 'short', year: 'numeric'}, Intl);
}
var BREAKPOINTS = [ var BREAKPOINTS = [
[30, 'moments ago', 1], {
[minute, '{} seconds ago', 1], test: lessThanThirtySecondsAgo,
[2 * minute, 'a minute ago', minute], format: function () {return 'Just now';},
[hour, '{} minutes ago', minute], nextUpdate: 1
[2 * hour, 'an hour ago', hour], },
[day, '{} hours ago', hour], {
[2 * day, 'a day ago', day], test: lessThanOneMinuteAgo,
[month, '{} days ago', day], format: nSec,
[year, '{} months ago', month], nextUpdate: 1
[2 * year, 'one year ago', year], },
[Infinity, '{} years ago', year] {
test: lessThanOneHourAgo,
format: nMin,
nextUpdate: minute
},
{
test: lessThanOneDayAgo,
format: nHr,
nextUpdate: hour
},
{
test: thisYear,
format: dayAndMonth,
nextUpdate: null
},
{
test: function () {return true;},
format: dayAndMonthAndYear,
nextUpdate: null
}
]; ];
function getBreakpoint(date) { function getBreakpoint(date, now) {
var delta = Math.round((new Date() - new Date(date)) / 1000);
var breakpoint; // Turn the given ISO 8601 string into a Date object.
date = new Date(date);
var breakpoint;
for (var i = 0; i < BREAKPOINTS.length; i++) { for (var i = 0; i < BREAKPOINTS.length; i++) {
if (BREAKPOINTS[i][0] > delta) { breakpoint = BREAKPOINTS[i];
breakpoint = BREAKPOINTS[i]; if (breakpoint.test(date, now)) {
break; return breakpoint;
} }
} }
return {
delta: delta,
breakpoint: breakpoint,
};
} }
function nextFuzzyUpdate(date) { function nextFuzzyUpdate(date) {
...@@ -42,13 +151,12 @@ function nextFuzzyUpdate(date) { ...@@ -42,13 +151,12 @@ function nextFuzzyUpdate(date) {
return null; return null;
} }
var breakpoint = getBreakpoint(date).breakpoint; var secs = getBreakpoint(date, new Date()).nextUpdate;
if (!breakpoint) {
if (secs === null) {
return null; return null;
} }
var secs = breakpoint[2];
// We don't want to refresh anything more often than 5 seconds // We don't want to refresh anything more often than 5 seconds
secs = Math.max(secs, 5); secs = Math.max(secs, 5);
...@@ -73,6 +181,9 @@ function decayingInterval(date, callback) { ...@@ -73,6 +181,9 @@ function decayingInterval(date, callback) {
var timer; var timer;
var update = function () { var update = function () {
var fuzzyUpdate = nextFuzzyUpdate(date); var fuzzyUpdate = nextFuzzyUpdate(date);
if (fuzzyUpdate === null) {
return;
}
var nextUpdate = (1000 * fuzzyUpdate) + 500; var nextUpdate = (1000 * fuzzyUpdate) + 500;
timer = setTimeout(function () { timer = setTimeout(function () {
callback(date); callback(date);
...@@ -92,19 +203,13 @@ function decayingInterval(date, callback) { ...@@ -92,19 +203,13 @@ function decayingInterval(date, callback) {
* @param {number} date - The absolute timestamp to format. * @param {number} date - The absolute timestamp to format.
* @return {string} A 'fuzzy' string describing the relative age of the date. * @return {string} A 'fuzzy' string describing the relative age of the date.
*/ */
function toFuzzyString(date) { function toFuzzyString(date, Intl) {
if (!date) { if (!date) {
return ''; return '';
} }
var breakpointInfo = getBreakpoint(date); var now = new Date();
var breakpoint = breakpointInfo.breakpoint;
var delta = breakpointInfo.delta; return getBreakpoint(date, now).format(new Date(date), now, Intl);
if (!breakpoint) {
return '';
}
var template = breakpoint[1];
var resolution = breakpoint[2];
return template.replace('{}', String(Math.floor(delta / resolution)));
} }
module.exports = { module.exports = {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment