Commit ff3d2cde authored by Nick Stenning's avatar Nick Stenning

Merge pull request #3030 from hypothesis/convert-time-service-to-js

Convert fuzzy timestamp formatting functions to JS
parents 4ba38053 05359351
......@@ -145,7 +145,6 @@ module.exports = angular.module('h', [
.service('session', require('./session'))
.service('streamFilter', require('./stream-filter'))
.service('tags', require('./tags'))
.service('time', require('./time'))
.service('threading', require('./threading'))
.service('unicode', require('./unicode'))
.service('viewFilter', require('./view-filter'))
......@@ -157,6 +156,7 @@ module.exports = angular.module('h', [
.value('Discovery', require('./discovery'))
.value('raven', require('./raven'))
.value('settings', settings)
.value('time', require('./time'))
.config(configureLocation)
.config(configureRoutes)
......
{module, inject} = angular.mock
minute = 60
hour = minute * 60
day = hour * 24
month = day * 30
year = day * 365
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']
]
FIXTURES_NEXT_FUZZY_UPDATE = [
[10, 5] # we have a minimum of 5 secs
[29, 5]
[49, 5]
[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
]
describe 'time', ->
time = null
sandbox = null
before ->
angular.module('h', []).service('time', require('../time'))
beforeEach module('h')
beforeEach inject (_time_) ->
time = _time_
sandbox = sinon.sandbox.create()
sandbox.useFakeTimers()
afterEach ->
sandbox.restore()
describe '.toFuzzyString', ->
it 'Handles empty dates', ->
t = null
expect = ''
assert.equal(time.toFuzzyString(t), expect)
testFixture = (f) ->
->
t = new Date()
expect = f[1]
sandbox.clock.tick(f[0]*1000)
assert.equal(time.toFuzzyString(t), expect)
for f, i in FIXTURES_TO_FUZZY_STRING
it "creates correct fuzzy string for fixture #{i}", testFixture(f)
describe '.decayingInterval', ->
it 'invokes the callback quickly for recent timestamps', ->
date = new Date()
callback = sandbox.stub()
time.decayingInterval(date, callback)
sandbox.clock.tick(6 * 1000)
assert.calledWith(callback, date)
sandbox.clock.tick(6 * 1000)
assert.calledTwice(callback)
it 'invokes the callback after a longer delay for older timestamps', ->
date = new Date();
ONE_MINUTE = minute * 1000
sandbox.clock.tick(10 * ONE_MINUTE)
callback = sandbox.stub()
time.decayingInterval(date, callback)
sandbox.clock.tick(ONE_MINUTE / 2)
assert.notCalled(callback)
sandbox.clock.tick(ONE_MINUTE)
assert.calledWith(callback, date)
sandbox.clock.tick(ONE_MINUTE)
assert.calledTwice(callback)
it 'returned function cancels the timer', ->
date = new Date()
callback = sandbox.stub()
cancel = time.decayingInterval(date, callback)
cancel()
sandbox.clock.tick(minute * 1000)
assert.notCalled(callback)
describe '.nextFuzzyUpdate', ->
it 'Handles empty dates', ->
t = null
expect = null
assert.equal(time.nextFuzzyUpdate(t), expect)
testFixture = (f) ->
->
t = new Date()
expect = f[1]
sandbox.clock.tick(f[0]*1000)
assert.equal(time.nextFuzzyUpdate(t), expect)
for f, i in FIXTURES_NEXT_FUZZY_UPDATE
it "gives correct next fuzzy update time for fixture #{i}", testFixture(f)
'use strict';
var time = require('../time');
var minute = 60;
var hour = minute * 60;
var day = hour * 24;
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']
];
var FIXTURES_NEXT_FUZZY_UPDATE = [
[10, 5], // we have a minimum of 5 secs
[29, 5],
[49, 5],
[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
];
describe('time', function () {
var sandbox;
beforeEach(function () {
sandbox = sinon.sandbox.create();
sandbox.useFakeTimers();
});
afterEach(function () {
sandbox.restore();
});
describe('.toFuzzyString', function () {
it('Handles empty dates', function () {
var t = null;
var expect = '';
assert.equal(time.toFuzzyString(t), expect);
});
var testFixture = function (f) {
return function () {
var t = new Date();
var expect = f[1];
sandbox.clock.tick(f[0] * 1000);
assert.equal(time.toFuzzyString(t), expect);
};
};
for (var i = 0, f; i < FIXTURES_TO_FUZZY_STRING.length; i++) {
f = FIXTURES_TO_FUZZY_STRING[i];
it('creates correct fuzzy string for fixture ' + i,
testFixture(f));
}
});
describe('.decayingInterval', function () {
it('uses a short delay for recent timestamps', function () {
var date = new Date();
var callback = sandbox.stub();
time.decayingInterval(date, callback);
sandbox.clock.tick(6 * 1000);
assert.calledWith(callback, date);
sandbox.clock.tick(6 * 1000);
assert.calledTwice(callback);
});
it('uses a longer delay for older timestamps', function () {
var date = new Date();
var ONE_MINUTE = minute * 1000;
sandbox.clock.tick(10 * ONE_MINUTE);
var callback = sandbox.stub();
time.decayingInterval(date, callback);
sandbox.clock.tick(ONE_MINUTE / 2);
assert.notCalled(callback);
sandbox.clock.tick(ONE_MINUTE);
assert.calledWith(callback, date);
sandbox.clock.tick(ONE_MINUTE);
assert.calledTwice(callback);
});
it('returned function cancels the timer', function () {
var date = new Date();
var callback = sandbox.stub();
var cancel = time.decayingInterval(date, callback);
cancel();
sandbox.clock.tick(minute * 1000);
assert.notCalled(callback);
});
});
describe('.nextFuzzyUpdate', function () {
it('Handles empty dates', function () {
var t = null;
var expect = null;
assert.equal(time.nextFuzzyUpdate(t), expect);
});
var testFixture = function (f) {
return function () {
var t = new Date();
var expect = f[1];
sandbox.clock.tick(f[0] * 1000);
assert.equal(time.nextFuzzyUpdate(t), expect);
};
};
for (var i = 0, f; i < FIXTURES_NEXT_FUZZY_UPDATE.length; i++) {
f = FIXTURES_NEXT_FUZZY_UPDATE[i];
it('gives correct next fuzzy update time for fixture ' + i,
testFixture(f));
}
});
});
minute = 60
hour = minute * 60
day = hour * 24
month = day * 30
year = day * 365
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 ]
]
arrayFind = (array, predicate) ->
if not array?
throw new TypeError('arrayFindIndex called on null or undefined')
if typeof predicate != 'function'
throw new TypeError('predicate must be a function')
for value, i in array
if predicate(value, i, array)
return value
return null
getBreakpoint = (date) ->
delta = Math.round((new Date() - new Date(date)) / 1000)
delta: delta
breakpoint: arrayFind(BREAKPOINTS, (x) -> x[0] > delta)
nextFuzzyUpdate = (date) ->
return null if not date
{_, breakpoint} = getBreakpoint(date)
return null unless breakpoint
secs = breakpoint[2]
# We don't want to refresh anything more often than 5 seconds
secs = Math.max secs, 5
# setTimeout limit is MAX_INT32=(2^31-1) (in ms),
# which is about 24.8 days. So we don't set up any timeouts
# longer than 24 days, that is, 2073600 seconds.
secs = Math.min secs, 2073600
module.exports = ->
# Starts an interval whose frequency decays depending on the relative
# age of 'date'. This can be used to refresh parts of a UI whose
# update frequency depends on the age of a timestamp.
#
# Returns a function that cancels the automatic refresh.
decayingInterval: (date, callback) ->
timer = undefined
update = ->
fuzzyUpdate = nextFuzzyUpdate(date)
nextUpdate = (1000 * fuzzyUpdate) + 500
timer = setTimeout(->
callback(date)
update()
, nextUpdate)
update()
return ->
clearTimeout(timer)
toFuzzyString: (date) ->
return '' unless date
{delta, breakpoint} = getBreakpoint(date)
return '' unless breakpoint
template = breakpoint[1]
resolution = breakpoint[2]
return template.replace('{}', String(Math.floor(delta / resolution)))
nextFuzzyUpdate: nextFuzzyUpdate
'use strict';
var minute = 60;
var hour = minute * 60;
var day = hour * 24;
var month = day * 30;
var year = day * 365;
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]
];
function getBreakpoint(date) {
var delta = Math.round((new Date() - new Date(date)) / 1000);
var breakpoint;
for (var i = 0; i < BREAKPOINTS.length; i++) {
if (BREAKPOINTS[i][0] > delta) {
breakpoint = BREAKPOINTS[i];
break;
}
}
return {
delta: delta,
breakpoint: breakpoint,
};
}
function nextFuzzyUpdate(date) {
if (!date) {
return null;
}
var breakpoint = getBreakpoint(date).breakpoint;
if (!breakpoint) {
return null;
}
var secs = breakpoint[2];
// We don't want to refresh anything more often than 5 seconds
secs = Math.max(secs, 5);
// setTimeout limit is MAX_INT32=(2^31-1) (in ms),
// which is about 24.8 days. So we don't set up any timeouts
// longer than 24 days, that is, 2073600 seconds.
secs = Math.min(secs, 2073600);
return secs;
}
/**
* Starts an interval whose frequency decays depending on the relative
* age of 'date'.
*
* This can be used to refresh parts of a UI whose
* update frequency depends on the age of a timestamp.
*
* @return {Function} A function that cancels the automatic refresh.
*/
function decayingInterval(date, callback) {
var timer;
var update = function () {
var fuzzyUpdate = nextFuzzyUpdate(date);
var nextUpdate = (1000 * fuzzyUpdate) + 500;
timer = setTimeout(function () {
callback(date);
update();
}, nextUpdate);
};
update();
return function () {
clearTimeout(timer);
};
}
/**
* Formats a date as a string relative to the current date.
*
* @param {number} date - The absolute timestamp to format.
* @return {string} A 'fuzzy' string describing the relative age of the date.
*/
function toFuzzyString(date) {
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)));
}
module.exports = {
decayingInterval: decayingInterval,
nextFuzzyUpdate: nextFuzzyUpdate,
toFuzzyString: toFuzzyString,
};
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