Current and Longest streak calculation fixed
Minor refactoring and unit tests added After CR: user-stats file moved to util folder, export keywork added to exported functions, new line added at the end of gulp file User-stats-test file moved to replicate user-stats path in test folder
This commit is contained in:
14
gulpfile.js
14
gulpfile.js
@ -39,8 +39,11 @@ var Rx = require('rx'),
|
||||
|
||||
// lint
|
||||
jsonlint = require('gulp-jsonlint'),
|
||||
eslint = require('gulp-eslint');
|
||||
|
||||
eslint = require('gulp-eslint'),
|
||||
|
||||
// unit-tests
|
||||
tape = require('gulp-tape'),
|
||||
tapSpec = require('tap-spec');
|
||||
|
||||
Rx.config.longStackSupport = true;
|
||||
|
||||
@ -533,3 +536,10 @@ gulp.task('default', [
|
||||
'watch',
|
||||
'sync'
|
||||
]);
|
||||
|
||||
gulp.task('test', function() {
|
||||
return gulp.src('test/**/*.js')
|
||||
.pipe(tape({
|
||||
reporter: tapSpec()
|
||||
}));
|
||||
});
|
||||
|
@ -135,12 +135,14 @@
|
||||
"chai": "^3.4.0",
|
||||
"envify": "^3.4.0",
|
||||
"gulp-sourcemaps": "^1.6.0",
|
||||
"gulp-tape": "0.0.7",
|
||||
"istanbul": "~0.4.0",
|
||||
"jsonlint": "^1.6.2",
|
||||
"loopback-component-explorer": "^2.1.1",
|
||||
"loopback-testing": "^1.1.0",
|
||||
"mocha": "^2.3.3",
|
||||
"tap-nyan": "0.0.2",
|
||||
"tap-spec": "^4.1.1",
|
||||
"tape": "^4.2.2"
|
||||
}
|
||||
}
|
||||
|
@ -10,9 +10,9 @@ import {
|
||||
} from '../utils/constantStrings.json';
|
||||
import { ifNoUser401, ifNoUserRedirectTo } from '../utils/middleware';
|
||||
import { observeQuery } from '../utils/rx';
|
||||
import { calcCurrentStreak, calcLongestStreak } from '../utils/user-stats';
|
||||
|
||||
const debug = debugFactory('freecc:boot:user');
|
||||
const daysBetween = 1.5;
|
||||
const sendNonUserToMap = ifNoUserRedirectTo('/map');
|
||||
|
||||
function replaceScriptTags(value) {
|
||||
@ -31,47 +31,6 @@ function encodeFcc(value = '') {
|
||||
return replaceScriptTags(replaceFormAction(value));
|
||||
}
|
||||
|
||||
function calcCurrentStreak(cals) {
|
||||
const revCals = cals.concat([Date.now()]).slice().reverse();
|
||||
let streakBroken = false;
|
||||
const lastDayInStreak = revCals
|
||||
.reduce((current, cal, index) => {
|
||||
const before = revCals[index === 0 ? 0 : index - 1];
|
||||
if (
|
||||
!streakBroken &&
|
||||
moment(before).diff(cal, 'days', true) < daysBetween
|
||||
) {
|
||||
return index;
|
||||
}
|
||||
streakBroken = true;
|
||||
return current;
|
||||
}, 0);
|
||||
|
||||
const lastTimestamp = revCals[lastDayInStreak];
|
||||
return Math.ceil(moment().diff(lastTimestamp, 'days', true));
|
||||
}
|
||||
|
||||
function calcLongestStreak(cals) {
|
||||
let tail = cals[0];
|
||||
const longest = cals.reduce((longest, head, index) => {
|
||||
const last = cals[index === 0 ? 0 : index - 1];
|
||||
// is streak broken
|
||||
if (moment(head).diff(last, 'days', true) > daysBetween) {
|
||||
tail = head;
|
||||
}
|
||||
if (dayDiff(longest) < dayDiff([head, tail])) {
|
||||
return [head, tail];
|
||||
}
|
||||
return longest;
|
||||
}, [cals[0], cals[0]]);
|
||||
|
||||
return Math.ceil(dayDiff(longest));
|
||||
}
|
||||
|
||||
function dayDiff([head, tail]) {
|
||||
return moment(head).diff(tail, 'days', true);
|
||||
}
|
||||
|
||||
module.exports = function(app) {
|
||||
var router = app.loopback.Router();
|
||||
var User = app.models.User;
|
||||
|
11
server/utils/date-utils.js
Normal file
11
server/utils/date-utils.js
Normal file
@ -0,0 +1,11 @@
|
||||
import moment from 'moment';
|
||||
|
||||
// day count between two epochs (inclusive)
|
||||
export function dayCount([head, tail]) {
|
||||
return Math.ceil(
|
||||
moment(moment(head).endOf('day')).diff(
|
||||
moment(tail).startOf('day'),
|
||||
'days',
|
||||
true)
|
||||
);
|
||||
}
|
46
server/utils/user-stats.js
Normal file
46
server/utils/user-stats.js
Normal file
@ -0,0 +1,46 @@
|
||||
import moment from 'moment';
|
||||
import { dayCount } from '../utils/date-utils';
|
||||
|
||||
const daysBetween = 1.5;
|
||||
|
||||
export function calcCurrentStreak(cals) {
|
||||
const revCals = cals.slice().reverse();
|
||||
|
||||
if (dayCount([moment(), revCals[0]]) > daysBetween) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let streakBroken = false;
|
||||
const lastDayInStreak = revCals
|
||||
.reduce((current, cal, index) => {
|
||||
const before = revCals[index === 0 ? 0 : index - 1];
|
||||
if (
|
||||
!streakBroken &&
|
||||
moment(before).diff(cal, 'days', true) < daysBetween
|
||||
) {
|
||||
return index;
|
||||
}
|
||||
streakBroken = true;
|
||||
return current;
|
||||
}, 0);
|
||||
|
||||
const lastTimestamp = revCals[lastDayInStreak];
|
||||
return dayCount([moment(), lastTimestamp]);
|
||||
}
|
||||
|
||||
export function calcLongestStreak(cals) {
|
||||
let tail = cals[0];
|
||||
const longest = cals.reduce((longest, head, index) => {
|
||||
const last = cals[index === 0 ? 0 : index - 1];
|
||||
// is streak broken
|
||||
if (moment(head).diff(last, 'days', true) > daysBetween) {
|
||||
tail = head;
|
||||
}
|
||||
if (dayCount(longest) < dayCount([head, tail])) {
|
||||
return [head, tail];
|
||||
}
|
||||
return longest;
|
||||
}, [cals[0], cals[0]]);
|
||||
|
||||
return dayCount(longest);
|
||||
}
|
@ -119,8 +119,8 @@ block content
|
||||
.row
|
||||
.hidden-xs.col-sm-12.text-center
|
||||
.row.text-primary
|
||||
h4.col-sm-6.text-right Longest Streak: #{longestStreak} #{longestStreak + longestStreak === 1 ? ' day' : ' days'}
|
||||
h4.col-sm-6.text-left Current Streak: #{currentStreak} #{currentStreak + currentStreak === 1 ? ' day' : ' days'}
|
||||
h4.col-sm-6.text-right Longest Streak: #{longestStreak} #{longestStreak === 1 ? ' day' : ' days'}
|
||||
h4.col-sm-6.text-left Current Streak: #{currentStreak} #{currentStreak === 1 ? ' day' : ' days'}
|
||||
|
||||
|
||||
if (user && user.username == username || !isLocked)
|
||||
|
34
test/server/utils/date-utils-test.js
Normal file
34
test/server/utils/date-utils-test.js
Normal file
@ -0,0 +1,34 @@
|
||||
import moment from 'moment';
|
||||
|
||||
import { dayCount } from '../../../server/utils/date-utils';
|
||||
|
||||
let test = require('tape');
|
||||
|
||||
test('Day count between two epochs (inclusive) calculation', function (t) {
|
||||
t.plan(5);
|
||||
|
||||
t.equal(dayCount([
|
||||
moment("8/3/2015 3:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("8/3/2015 2:00", "M/D/YYYY H:mm").valueOf()
|
||||
]), 1, "should return 1 day given epochs of the same day");
|
||||
|
||||
t.equal(dayCount([
|
||||
moment("8/3/2015 2:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("8/3/2015 2:00", "M/D/YYYY H:mm").valueOf()
|
||||
]), 1, "should return 1 day given same epochs");
|
||||
|
||||
t.equal(dayCount([
|
||||
moment("8/4/2015 2:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("8/3/2015 2:00", "M/D/YYYY H:mm").valueOf()
|
||||
]), 2, "should return 2 days when there is a 24 hours difference between given dates");
|
||||
|
||||
t.equal(dayCount([
|
||||
moment("8/4/2015 1:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("8/3/2015 23:00", "M/D/YYYY H:mm").valueOf()
|
||||
]), 2, "should return 2 days when the diff is less than 24h but days of the month are different");
|
||||
|
||||
t.equal(dayCount([
|
||||
moment("10/27/2015 1:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("5/12/1982 1:00", "M/D/YYYY H:mm").valueOf()
|
||||
]), 12222, "should return correct count when there is very big period");
|
||||
});
|
132
test/server/utils/user-stats-test.js
Normal file
132
test/server/utils/user-stats-test.js
Normal file
@ -0,0 +1,132 @@
|
||||
import moment from 'moment';
|
||||
|
||||
import { calcCurrentStreak, calcLongestStreak } from '../../../server/utils/user-stats';
|
||||
|
||||
let test = require('tape');
|
||||
|
||||
test('Current streak calculation', function (t) {
|
||||
t.plan(7);
|
||||
|
||||
t.equal(calcCurrentStreak([
|
||||
moment(moment().subtract(1, 'hours')).valueOf()
|
||||
]), 1, "should return 1 day when today one challenge was completed");
|
||||
|
||||
t.equal(calcCurrentStreak([
|
||||
moment(moment().subtract(2, 'hours')).valueOf(),
|
||||
moment(moment().subtract(1, 'hours')).valueOf()
|
||||
]), 1, "should return 1 day when today more than one challenge were completed");
|
||||
|
||||
t.equal(calcCurrentStreak([
|
||||
moment("9/11/2015 4:00", "M/D/YYYY H:mm").valueOf()
|
||||
]), 0, "should return 0 day when today 0 challenges were completed");
|
||||
|
||||
t.equal(calcCurrentStreak([
|
||||
moment(moment().subtract(1, 'days')).valueOf(),
|
||||
moment(moment().subtract(1, 'hours')).valueOf()
|
||||
]), 2, "should return 2 days when today and yesterday challenges were completed");
|
||||
|
||||
t.equal(calcCurrentStreak([
|
||||
moment("8/3/2015 2:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("9/11/2015 4:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("9/12/2015 1:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("9/12/2015 1:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("9/12/2015 2:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("9/12/2015 3:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("9/13/2015 4:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("9/14/2015 5:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment(moment().subtract(2, 'days')).valueOf(),
|
||||
moment(moment().subtract(1, 'days')).valueOf(),
|
||||
moment(moment().subtract(1, 'hours')).valueOf()
|
||||
]), 3, "should return 3 when today and for two days before user was activity");
|
||||
|
||||
t.equal(calcCurrentStreak([
|
||||
moment(moment().subtract(37, 'hours')).valueOf(),
|
||||
moment(moment().subtract(1, 'hours')).valueOf()
|
||||
]), 1, "should return 1 when between todays challenge completion and yesterdays there is a 1.5 day (36 hours) long break");
|
||||
|
||||
t.equal(calcCurrentStreak([
|
||||
moment(moment().subtract(35, 'hours')).valueOf(),
|
||||
moment(moment().subtract(1, 'hours')).valueOf()
|
||||
]), 2, "should return 2 days when between todays challenge completion and yesterdays there is less than 1.5 day (36 hours) long break");
|
||||
|
||||
});
|
||||
|
||||
test('Longest streak calculation', function (t) {
|
||||
t.plan(9);
|
||||
|
||||
t.equal(calcLongestStreak([
|
||||
moment("9/12/2015 4:00", "M/D/YYYY H:mm").valueOf()
|
||||
]), 1, "should return 1 when there is the only one one-day-long streak available");
|
||||
|
||||
t.equal(calcLongestStreak([
|
||||
moment("9/11/2015 4:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("9/12/2015 2:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("9/13/2015 3:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("9/14/2015 1:00", "M/D/YYYY H:mm").valueOf()
|
||||
]), 4, "should return 4 when there is the only one more-than-one-days-long streak available");
|
||||
|
||||
t.equal(calcLongestStreak([
|
||||
moment(moment().subtract(1, 'hours')).valueOf()
|
||||
]), 1, "should return 1 when there is only one one-day-long streak and it is today");
|
||||
|
||||
t.equal(calcLongestStreak([
|
||||
moment(moment().subtract(1, 'days')).valueOf(),
|
||||
moment(moment().subtract(1, 'hours')).valueOf()
|
||||
]), 2, "should return 2 when yesterday and today makes longest streak");
|
||||
|
||||
t.equal(calcLongestStreak([
|
||||
moment("8/3/2015 2:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("9/11/2015 4:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("9/12/2015 2:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("10/4/2015 1:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("10/5/2015 5:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("10/6/2015 4:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("10/7/2015 5:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("11/3/2015 2:00", "M/D/YYYY H:mm").valueOf()
|
||||
]), 4, "should return 4 when there is a month long break");
|
||||
|
||||
t.equal(calcLongestStreak([
|
||||
moment("8/3/2015 2:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("9/11/2015 4:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("9/12/2015 1:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment(moment("9/12/2015 1:00", "M/D/YYYY H:mm").add(37, 'hours')).valueOf(),
|
||||
moment("9/14/2015 22:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("9/15/2015 4:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("10/3/2015 2:00", "M/D/YYYY H:mm").valueOf()
|
||||
]), 3, "should return 3 when there is a more than 1.5 days long break of (36 hours)");
|
||||
|
||||
t.equal(calcLongestStreak([
|
||||
moment("8/3/2015 2:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("9/11/2015 4:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("9/12/2015 1:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("9/12/2015 1:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("9/12/2015 2:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("9/12/2015 3:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("9/13/2015 4:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("9/14/2015 5:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment(moment().subtract(2, 'days')).valueOf(),
|
||||
moment(moment().subtract(1, 'days')).valueOf(),
|
||||
moment().valueOf()
|
||||
]), 4, "should return 4 when the longest streak consist of several same day timestamps");
|
||||
|
||||
t.equal(calcLongestStreak([
|
||||
moment("8/3/2015 2:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("9/11/2015 4:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("9/12/2015 1:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("9/13/2015 4:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("9/14/2015 5:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("10/11/2015 4:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("10/12/2015 1:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("10/13/2015 4:00", "M/D/YYYY H:mm").valueOf(),
|
||||
moment("10/14/2015 5:00", "M/D/YYYY H:mm").valueOf()
|
||||
]), 4, "should return 4 when there are several longest streaks (same length)");
|
||||
|
||||
let cals = [];
|
||||
const n = 100;
|
||||
for (var i = 0; i < n; i++) {
|
||||
cals.push(moment(moment().subtract(i, 'days')).valueOf());
|
||||
}
|
||||
cals.sort();
|
||||
t.equal(calcLongestStreak(cals), n, "should return correct longest streak when there is a very long period");
|
||||
|
||||
});
|
Reference in New Issue
Block a user