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:
JelenaBarinova
2015-12-10 14:52:09 -08:00
parent cf65c698bb
commit 6c7d2685fd
8 changed files with 240 additions and 46 deletions

View File

@ -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()
}));
});

View File

@ -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"
}
}

View File

@ -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;

View 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)
);
}

View 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);
}

View File

@ -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)

View 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");
});

View 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");
});