From 6aa9483a0bfa4dc4cf9e88a6960809e64788b476 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 20 Aug 2013 21:33:37 -0400 Subject: [PATCH] cron: add timezone calculation into the midst. Note: this requires user.preferences.timezoneOffset exist on the user, otherwise it will default to the script-runner timezone (which works fine if user or browser) --- script/algos.coffee | 6 ++-- script/helpers.coffee | 45 ++++++++++++++++++-------- tests/algos.mocha.coffee | 68 +++++++++++++++++++++------------------- 3 files changed, 71 insertions(+), 48 deletions(-) diff --git a/script/algos.coffee b/script/algos.coffee index afb8c70988..f5442b3141 100644 --- a/script/algos.coffee +++ b/script/algos.coffee @@ -120,7 +120,7 @@ randomDrop = (user, delta, priority, streak = 0, options={}) -> count: 0 paths['items.lastDrop'] = true - reachedDropLimit = (helpers.daysBetween(user.items.lastDrop.date, +new Date) is 0) and (user.items.lastDrop.count >= 2) + reachedDropLimit = (helpers.daysSince(user.items.lastDrop.date, user.preferences) is 0) and (user.items.lastDrop.count >= 2) return if reachedDropLimit # % chance of getting a pet or meat @@ -359,7 +359,7 @@ obj.cron = (user, options={}) -> user.lastCron = now; paths['lastCron'] = true return - daysMissed = helpers.daysBetween(user.lastCron, now, user.preferences?.dayStart) + daysMissed = helpers.daysSince user.lastCron, _.defaults({now}, user.preferences) return unless daysMissed > 0 user.lastCron = now; paths['lastCron'] = true @@ -380,7 +380,7 @@ obj.cron = (user, options={}) -> scheduleMisses = 0 _.times daysMissed, (n) -> thatDay = moment(now).subtract('days', n + 1) - scheduleMisses++ if helpers.shouldDo(thatDay, repeat, {dayStart:obj.preferences?.dayStart}) + scheduleMisses++ if helpers.shouldDo(thatDay, repeat, user.preferences) obj.score(user, task, 'down', {times:scheduleMisses, cron:true, paths:paths}) if scheduleMisses > 0 switch type diff --git a/script/helpers.coffee b/script/helpers.coffee index 82d145e8c0..58bf9e6a7b 100644 --- a/script/helpers.coffee +++ b/script/helpers.coffee @@ -2,31 +2,48 @@ moment = require 'moment' _ = require 'lodash' items = require('./items.coffee') -sod = (timestamp, dayStart=0) -> - #sanity-check reset-time (is it 24h time?) - dayStart = 0 unless (dayStart = +dayStart) and (0 <= dayStart <= 24) - moment(timestamp).startOf('day').add('h', dayStart) +### + Each time we're performing date math (cron, task-due-days, etc), we need to take user preferences into consideration. + Specifically {dayStart} (custom day start) and {timezoneOffset}. This function sanitizes / defaults those values. + {now} is also passed in for various purposes, one example being the test scripts scripts testing different "now" times +### +sanitizeOptions = (o) -> + dayStart = if (o.dayStart and 0 <= +o.dayStart <= 24) then +o.dayStart else 0 + timezoneOffset = if o.timezoneOffset then +(o.timezoneOffset) else +moment().zone() + now = if o.now then moment(o.now).zone(timezoneOffset) else moment(+new Date).zone(timezoneOffset) + # return a new object, we don't want to add "now" to user object + {dayStart, timezoneOffset, now} + +startOfWeek = (options={}) -> + o = sanitizeOptions(options) + moment(o.now).startOf('week') + +startOfDay = (options={}) -> + o = sanitizeOptions(options) + moment(o.now).startOf('day').add('h', options.dayStart) dayMapping = {0:'su',1:'m',2:'t',3:'w',4:'th',5:'f',6:'s'} ### - Absolute diff between two dates + Absolute diff from "yesterday" till now ### -daysBetween = (yesterday, now, dayStart) -> Math.abs sod(yesterday, dayStart).diff(now, 'days') +daysSince = (yesterday, options = {}) -> + o = sanitizeOptions options + Math.abs startOfDay(_.defaults {now:yesterday}, o).diff(o.now, 'days') ### Should the user do this taks on this date, given the task's repeat options and user.preferences.dayStart? ### shouldDo = (day, repeat, options={}) -> return false unless repeat - [dayStart,now] = [options.dayStart||0, options.now||+new Date] - selected = repeat[dayMapping[sod(day, dayStart).day()]] - return selected unless moment(day).isSame(now,'d') - if dayStart <= moment(now).hour() # we're past the dayStart mark, is it due today? + o = sanitizeOptions options + selected = repeat[dayMapping[startOfDay(_.defaults {now:day}, o).day()]] + return selected unless moment(day).zone(o.timezoneOffset).isSame(o.now,'d') + if options.dayStart <= o.now.hour() # we're past the dayStart mark, is it due today? return selected else # we're not past dayStart mark, check if it was due "yesterday" - yesterday = moment(now).subtract(1,'d').day() - return repeat[dayMapping[yesterday]] + yesterday = moment(o.now).subtract(1,'d').day() # have to wrap o.now so as not to modify original + return repeat[dayMapping[yesterday]] # FIXME is this correct?? Do I need to do any timezone calcaulation here? uuid = -> "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace /[xy]/g, (c) -> @@ -124,7 +141,9 @@ module.exports = return undefined if ~path.indexOf('undefined') _.reduce path.split('.'), ((curr, next) -> curr[next]), obj - daysBetween: daysBetween + daysSince: daysSince + startOfWeek: startOfWeek + startOfDay: startOfDay shouldDo: shouldDo diff --git a/tests/algos.mocha.coffee b/tests/algos.mocha.coffee index cf3d872a87..98f5d97864 100644 --- a/tests/algos.mocha.coffee +++ b/tests/algos.mocha.coffee @@ -3,9 +3,9 @@ expect = require 'expect.js' moment = require 'moment' # Custom modules -algos = require '../script/algos' -helpers = require '../script/helpers' -items = require '../script/items' +algos = require '../script/algos.coffee' +helpers = require '../script/helpers.coffee' +items = require '../script/items.coffee' ### Helper Functions #### @@ -16,6 +16,7 @@ beforeAfter = (options={}) -> user = helpers.newUser() [before, after] = [_.cloneDeep(user), _.cloneDeep(user)] before.preferences.dayStart = after.preferences.dayStart = options.dayStart if options.dayStart + before.preferences.timezoneOffset = after.preferences.timezoneOffset = (options.timezoneOffset or moment().zone()) if options.limitOne before["#{options.limitOne}s"] = [before["#{options.limitOne}s"][0]] after["#{options.limitOne}s"] = [after["#{options.limitOne}s"][0]] @@ -61,15 +62,12 @@ expectDayResetNoDamage = (b,a) -> expect(after).to.eql before - - ###### Specs ###### describe 'User', -> it 'sets correct user defaults', -> user = helpers.newUser() expect(user.stats).to.eql { gp: 0, exp: 0, lvl: 1, hp: 50 } - expect(user.party).to.eql { invitation: null } expect(user.items).to.eql { weapon: 0, armor: 0, head: 0, shield: 0 } expect(user.preferences).to.eql { gender: 'm', skin: 'white', hair: 'blond', armorSet: 'v1', dayStart:0, showHelm: true } expect(user.balance).to.eql 0 @@ -152,21 +150,28 @@ describe 'Cron', -> describe 'dailies', -> describe 'new day', -> - #TODO check helpers.isDue() + + ### + This section runs through a "cron matrix" of all permutations (that I can easily account for). It sets + task due days, user custom day start, timezoneOffset, etc - then runs cron, jumps to tomorrow and runs cron, + and so on - testing each possible outcome along the way + ### runCron = (options) -> - now = moment().startOf('week').add('hours',options.currentHour || 0) - {before,after} = beforeAfter({now, daysAgo:1, dayStart:options.dayStart||0, limitOne:'daily'}) - before.dailys[0].repeat = after.dailys[0].repeat = options.repeat if options.repeat - before.dailys[0].streak = after.dailys[0].streak = 10 - before.dailys[0].completed = after.dailys[0].completed = true if options.checked - expect(helpers.shouldDo(now, options.repeat, {dayStart:options.dayStart,now})).to.be.ok() if options.shouldDo - algos.cron(after,{now}) - switch options.expect - when 'losePoints' then expectLostPoints(before,after,'daily') - when 'noChange' then expectNoChange(before,after) - when 'noDamage' then expectDayResetNoDamage(before,after) - {before,after} + _.each [480, 240, 0, -120], (timezoneOffset) -> # test different timezones + now = helpers.startOfWeek({timezoneOffset}).add('hours', options.currentHour||0) + {before,after} = beforeAfter({now, timezoneOffset, daysAgo:1, dayStart:options.dayStart||0, limitOne:'daily'}) + before.dailys[0].repeat = after.dailys[0].repeat = options.repeat if options.repeat + before.dailys[0].streak = after.dailys[0].streak = 10 + before.dailys[0].completed = after.dailys[0].completed = true if options.checked + if options.shouldDo + expect(helpers.shouldDo(now, options.repeat, {timezoneOffset, dayStart:options.dayStart, now})).to.be.ok() + algos.cron(after,{now}) + switch options.expect + when 'losePoints' then expectLostPoints(before,after,'daily') + when 'noChange' then expectNoChange(before,after) + when 'noDamage' then expectDayResetNoDamage(before,after) + {before,after} cronMatrix = steps: @@ -178,7 +183,8 @@ describe 'Cron', -> '(simple)': {expect:'losePoints'} 'due today': - defaults: {repeat:{su:true,m:1,t:1,w:1,th:1,f:1,s:true}} + # NOTE: a strange thing here, moment().startOf('week') is Sunday, but moment.zone(myTimeZone).startOf('week') is Monday. + defaults: {repeat:{su:1,m:true,t:1,w:1,th:1,f:1,s:1}} steps: 'pre-dayStart': defaults: {currentHour:3, dayStart:4, shouldDo:true} @@ -192,7 +198,7 @@ describe 'Cron', -> 'unchecked': {checked:false, expect: 'losePoints'} 'NOT due today': - defaults: {repeat:{su:false,m:1,t:1,w:1,th:1,f:1,s:1}} + defaults: {repeat:{su:1,m:false,t:1,w:1,th:1,f:1,s:1}} steps: 'pre-dayStart': defaults: {currentHour:3, dayStart:4, shouldDo:true} @@ -206,7 +212,7 @@ describe 'Cron', -> 'unchecked': {checked:false, expect: 'losePoints'} 'not due yesterday': - defaults: {repeat:{su:1,m:1,t:1,w:1,th:1,f:1,s:false}} + defaults: {repeat:{su:false,m:1,t:1,w:1,th:1,f:1,s:1}} steps: '(simple)': {expect:'noDamage'} 'post-dayStart': {currentHour:5,dayStart:4, expect:'noDamage'} @@ -226,13 +232,11 @@ describe 'Cron', -> it '3 day missed, only 1 due' it '3 day missed, only 1 due, not day start yet' - it 'calculates day differences with dayStart properly', -> - dayStart = 4 - yesterday = moment().subtract('d', 1).add('h', dayStart) - now = moment().startOf('day').add('h', dayStart-1) #today - console.log {yesterday: yesterday.format('MM/DD HH:00'), now: now.format('MM/DD HH:00')} - console.log {diff: Math.abs(moment(yesterday).diff(moment(now), 'days'))} - expect(helpers.daysBetween(yesterday, now, dayStart)).to.eql 0 - now = moment().startOf('day').add('h', dayStart) - console.log {now: now.format('MM/DD HH:00')} - expect(helpers.daysBetween(yesterday, now, dayStart)).to.eql 1 + it 'calculates day differences with dayStart properly', -> + dayStart = 4 + yesterday = helpers.startOfDay {now: moment().subtract('d', 1), dayStart} + now = helpers.startOfDay {dayStart: dayStart-1} + expect(helpers.daysSince(yesterday, {now, dayStart})).to.eql 0 + now = moment().startOf('day').add('h', dayStart).add('m', 1) + console.log {yesterday,now} + expect(helpers.daysSince(yesterday, {now, dayStart})).to.eql 1