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;
var year = day * 365;
var FIXTURES_TO_FUZZY_STRING = [
[10, 'moments ago'],
[29, 'moments ago'],
[49, '49 seconds ago'],
[minute + 5, 'a minute ago'],
[3 * minute + 5, '3 minutes ago'],
[4 * hour, '4 hours ago'],
[27 * hour, 'a day ago'],
[3 * day + 30 * minute, '3 days ago'],
[6 * month + 2 * day, '6 months ago'],
[1 * year, 'one year ago'],
[1 * year + 2 * month, 'one year ago'],
[2 * year, '2 years ago'],
[8 * year, '8 years ago']
[10, 'Just now'],
[29, 'Just now'],
[49, '49 secs'],
[minute + 5, '1 min'],
[3 * minute + 5, '3 mins'],
[hour, '1 hr'],
[4 * hour, '4 hrs'],
[27 * hour, '1 Jan'],
[3 * day + 30 * minute, '1 Jan'],
[6 * month + 2 * day, '1 Jan'],
[1 * year, '1 Jan 1970'],
[1 * year + 2 * month, '1 Jan 1970'],
[2 * year, '1 Jan 1970'],
[8 * year, '1 Jan 1970']
];
var FIXTURES_NEXT_FUZZY_UPDATE = [
......@@ -31,10 +32,10 @@ var FIXTURES_NEXT_FUZZY_UPDATE = [
[minute + 5, minute],
[3 * minute + 5, minute],
[4 * hour, hour],
[27 * hour, day],
[3 * day + 30 * minute, day],
[6 * month + 2 * day, 24 * day], // longer times are not supported
[8 * year, 24 * day] // by setTimout
[27 * hour, null],
[3 * day + 30 * minute, null],
[6 * month + 2 * day, null],
[8 * year, null]
];
describe('time', function () {
......@@ -50,18 +51,35 @@ describe('time', 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 () {
var t = null;
var expect = '';
assert.equal(time.toFuzzyString(t), expect);
assert.equal(time.toFuzzyString(t, mockIntl()), expect);
});
var testFixture = function (f) {
return function () {
var t = new Date();
var t = new Date().toISOString();
var expect = f[1];
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 () {
it('creates correct fuzzy string for fixture ' + i,
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 () {
......@@ -105,6 +142,18 @@ describe('time', function () {
sandbox.clock.tick(minute * 1000);
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 () {
......@@ -116,7 +165,7 @@ describe('time', function () {
var testFixture = function (f) {
return function () {
var t = new Date();
var t = new Date().toISOString();
var expect = f[1];
sandbox.clock.tick(f[0] * 1000);
assert.equal(time.nextFuzzyUpdate(t), expect);
......
......@@ -2,39 +2,148 @@
var minute = 60;
var hour = minute * 60;
var day = hour * 24;
var month = day * 30;
var year = day * 365;
function lessThanThirtySecondsAgo(date, now) {
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 = [
[30, 'moments ago', 1],
[minute, '{} seconds ago', 1],
[2 * minute, 'a minute ago', minute],
[hour, '{} minutes ago', minute],
[2 * hour, 'an hour ago', hour],
[day, '{} hours ago', hour],
[2 * day, 'a day ago', day],
[month, '{} days ago', day],
[year, '{} months ago', month],
[2 * year, 'one year ago', year],
[Infinity, '{} years ago', year]
{
test: lessThanThirtySecondsAgo,
format: function () {return 'Just now';},
nextUpdate: 1
},
{
test: lessThanOneMinuteAgo,
format: nSec,
nextUpdate: 1
},
{
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) {
var delta = Math.round((new Date() - new Date(date)) / 1000);
var breakpoint;
function getBreakpoint(date, now) {
// Turn the given ISO 8601 string into a Date object.
date = new Date(date);
var breakpoint;
for (var i = 0; i < BREAKPOINTS.length; i++) {
if (BREAKPOINTS[i][0] > delta) {
breakpoint = BREAKPOINTS[i];
break;
if (breakpoint.test(date, now)) {
return breakpoint;
}
}
return {
delta: delta,
breakpoint: breakpoint,
};
}
function nextFuzzyUpdate(date) {
......@@ -42,13 +151,12 @@ function nextFuzzyUpdate(date) {
return null;
}
var breakpoint = getBreakpoint(date).breakpoint;
if (!breakpoint) {
var secs = getBreakpoint(date, new Date()).nextUpdate;
if (secs === null) {
return null;
}
var secs = breakpoint[2];
// We don't want to refresh anything more often than 5 seconds
secs = Math.max(secs, 5);
......@@ -73,6 +181,9 @@ function decayingInterval(date, callback) {
var timer;
var update = function () {
var fuzzyUpdate = nextFuzzyUpdate(date);
if (fuzzyUpdate === null) {
return;
}
var nextUpdate = (1000 * fuzzyUpdate) + 500;
timer = setTimeout(function () {
callback(date);
......@@ -92,19 +203,13 @@ function decayingInterval(date, callback) {
* @param {number} date - The absolute timestamp to format.
* @return {string} A 'fuzzy' string describing the relative age of the date.
*/
function toFuzzyString(date) {
function toFuzzyString(date, Intl) {
if (!date) {
return '';
}
var breakpointInfo = getBreakpoint(date);
var breakpoint = breakpointInfo.breakpoint;
var delta = breakpointInfo.delta;
if (!breakpoint) {
return '';
}
var template = breakpoint[1];
var resolution = breakpoint[2];
return template.replace('{}', String(Math.floor(delta / resolution)));
var now = new Date();
return getBreakpoint(date, now).format(new Date(date), now, Intl);
}
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