From 6c7d2685fda9c66b42a1ebd963c92f72f1a9f510 Mon Sep 17 00:00:00 2001 From: JelenaBarinova Date: Thu, 10 Dec 2015 14:52:09 -0800 Subject: [PATCH] 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 --- gulpfile.js | 14 ++- package.json | 2 + server/boot/user.js | 43 +-------- server/utils/date-utils.js | 11 +++ server/utils/user-stats.js | 46 ++++++++++ server/views/account/show.jade | 4 +- test/server/utils/date-utils-test.js | 34 +++++++ test/server/utils/user-stats-test.js | 132 +++++++++++++++++++++++++++ 8 files changed, 240 insertions(+), 46 deletions(-) create mode 100644 server/utils/date-utils.js create mode 100644 server/utils/user-stats.js create mode 100644 test/server/utils/date-utils-test.js create mode 100644 test/server/utils/user-stats-test.js diff --git a/gulpfile.js b/gulpfile.js index b1e28b9c0f..45fadfd7bb 100644 --- a/gulpfile.js +++ b/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() + })); +}); diff --git a/package.json b/package.json index e74d9868a2..63f1fd73ca 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/server/boot/user.js b/server/boot/user.js index 4d7b44b013..82684857c9 100644 --- a/server/boot/user.js +++ b/server/boot/user.js @@ -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; diff --git a/server/utils/date-utils.js b/server/utils/date-utils.js new file mode 100644 index 0000000000..a1b5156ac5 --- /dev/null +++ b/server/utils/date-utils.js @@ -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) + ); +} diff --git a/server/utils/user-stats.js b/server/utils/user-stats.js new file mode 100644 index 0000000000..324c4761f5 --- /dev/null +++ b/server/utils/user-stats.js @@ -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); +} diff --git a/server/views/account/show.jade b/server/views/account/show.jade index f8454f4b4e..ca5b5286d0 100644 --- a/server/views/account/show.jade +++ b/server/views/account/show.jade @@ -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) diff --git a/test/server/utils/date-utils-test.js b/test/server/utils/date-utils-test.js new file mode 100644 index 0000000000..41a37edb89 --- /dev/null +++ b/test/server/utils/date-utils-test.js @@ -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"); +}); diff --git a/test/server/utils/user-stats-test.js b/test/server/utils/user-stats-test.js new file mode 100644 index 0000000000..f14a312138 --- /dev/null +++ b/test/server/utils/user-stats-test.js @@ -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"); + +});