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)

This commit is contained in:
Tyler Renelle 2013-08-20 21:33:37 -04:00
parent b23057593b
commit 6aa9483a0b
3 changed files with 71 additions and 48 deletions

View file

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

View file

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

View file

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