CoffeeScript Application Development Cookbook
上QQ阅读APP看书,第一时间看更新

Working with dates and times

Working with dates is a very common task in our software. This section describes how we can perform date calculations in our CoffeeScript applications and provides some useful utility methods that you can use in your own applications.

Performing date calculations

Performing date calculations is not as intuitive as one would like in JavaScript. For example, subtracting two dates returns the number of milliseconds between the two dates. JavaScript does not provide methods to add hours, days, months, and so on for a date. In the next section, we will create methods to address each of these complications.

How to do it...

Let's define methods to calculate the difference between two dates and add a timespan to a date as follows:

MILLISECONDS_PER_SECOND = 1000
MILLISECONDS_PER_MINUTE = MILLISECONDS_PER_SECOND * 60
MILLISECONDS_PER_HOUR = MILLISECONDS_PER_MINUTE * 60
MILLISECONDS_PER_DAY  = MILLISECONDS_PER_HOUR * 24
MILLISECONDS_PER_WEEK = MILLISECONDS_PER_DAY * 7
MILLISECONDS_PER_YEAR = MILLISECONDS_PER_WEEK * 52

dateDifference = (startDate, endDate, units = 'days') ->
  elapsed = endDate - startDate
  switch units
    when 'days'
      return elapsed / MILLISECONDS_PER_DAY
    when 'weeks'
      return elapsed / MILLISECONDS_PER_WEEK
    when 'months'
      return elapsed / MILLISECONDS_PER_YEAR * 12
    when 'years'
      return elapsed / MILLISECONDS_PER_YEAR
    when 'hours'
      return elapsed / MILLISECONDS_PER_HOUR
    when 'minutes'
      return elapsed / MILLISECONDS_PER_MINUTE
    when 'seconds'
      return elapsed / MILLISECONDS_PER_SECOND

  return elapsed

dateAdd = (date, amount, units = 'days') ->
  workingDate = new Date(date.valueOf())
  switch units
    when 'days'
      workingDate.setDate date.getDate() + amount
    when 'weeks'
      workingDate.setDate date.getDate() + amount * 7
    when 'months'
      workingDate.setMonth date.getMonth() + amount
    when 'years'
      workingDate.setFullYear date.getFullYear() + amount
    when 'hours'
      workingDate.setHours date.getHours() + amount
    when 'minutes'
      workingDate.setMinutes date.getMinutes() + amount
    when 'seconds'
      workingDate.setSeconds date.getSeconds() + amount
  return workingDate

module.exports =
  dateAdd: dateAdd
  dateDifference: dateDifference

How it works...

First, define some useful constants to help convert milliseconds to days or years.

Next, we define a method to calculate the difference between two dates. The dateDifference() method takes a startDate and endDate parameter as well as an optional units parameter (which defaults to days) that represents the units to be returned.

The dateDifference() method essentially subtracts the two dates and converts the resultant milliseconds to the desired units.

We then define the dateAdd() method. This method accepts date, amount, and an optional units parameter representing the units of the amount being added (which defaults to days).

To add time to a date, you must use a little trick to set the proper date unit to its value and the amount to be added. For example, to add 5 days to the current date, you would use the following code:

currentDate = new Date()
currentDate.setDays currentDate.getDays() + 5
console.log currentDate

You can subtract amounts from the given date by using negative values. For example, 7 days ago would be as follows:

currentDate = new Date()
console.log dateAdd currentDate, -7

An example of using these date math functions is shown as follows:

dm = require './date_math'

zeroPad = (value, length = 2) ->
  return "00000000000000#{value}".split('')[-length..].join('')

formatDate = (date) ->
  year   = date.getFullYear()
  month  = date.getMonth() + 1
  day    = date.getDate()
  hour   = date.getHours()
  minute = date.getMinutes()

  return "#{year}-#{zeroPad month}-#{zeroPad day} #{zeroPad hour}:#{zeroPad minute}"

currentDate = new Date()
newCentury = new Date(2000, 0)

console.log 'Current date: ', formatDate currentDate

console.log 'Days since Jan. 1, 2000: ',
  dm.dateDifference newCentury, currentDate
console.log 'Years since Jan. 1, 2000: ',
  dm.dateDifference newCentury, currentDate, 'years'

console.log '3 days from now: ',
  formatDate dm.dateAdd currentDate, 3
console.log '3 days ago: ',
  formatDate dm.dateAdd currentDate, -3
console.log '3 months from now: ',
  formatDate dm.dateAdd currentDate, 3, 'months'
console.log '3 years from now: ',
  formatDate dm.dateAdd currentDate, 3, 'years'
console.log '3 hours from now: ',
  formatDate dm.dateAdd currentDate, 3, 'hours'

Its output will be:

Current date:  2013-10-21 18:54
Days since Jan. 1, 2000:  5042.746363993056
Years since Jan. 1, 2000:  13.806287101965928
3 days from now:  2013-10-24 18:54
3 days ago:  2013-10-18 18:54
3 months from now:  2014-01-21 18:54
3 years from now:  2016-10-21 18:54
3 hours from now:  2013-10-21 21:54

Measuring elapsed time

Using what we covered about working with dates and times, we can easily take it one step further and create a performance measurement tool that can clock the start and end times to execute a function and display execution statistics once complete.

How to do it...

In this section, we define helper methods to format and display results, and a timer() method that performs the timing function:

dm = require './date_math'

padRight = (value, zeroPadding) ->
  "00000000000000#{value}".split('')[-zeroPadding..].join('')

padLeft =  (value, zeroPadding) ->
  "#{value}00000000000000"[0...zeroPadding]

formatNumber = (value, decimalPlaces = 0, zeroPadding = 0) ->
  valueParts = (value + '').split '.'
  resultParts = []
  resultParts.push padRight valueParts[0], zeroPadding
  if decimalPlaces
    resultParts.push padLeft valueParts[1], decimalPlaces

  return resultParts.join '.'

formatTime = (value) ->
  hours   = 0
  minutes = 0
  seconds = value / 1000

  if seconds > 60
    minutes = Math.floor seconds / 60
    seconds -= minutes * 60

  if minutes > 60
    hours = Math.floor minutes / 60
    minutes -= hours * 60

  return "#{formatNumber hours, 0, 2}:" + \
    "#{formatNumber minutes, 0, 2}:" + \
    "#{formatNumber seconds, 4, 2}"

displayResults = (results) -> 
  totalTime   = 0
  minimumTime = Number.POSITIVE_INFINITY
  maximumTime = 0

  for result in results
    minimumTime = result if result < minimumTime
    maximumTime = result if result > maximumTime
    totalTime += result

  console.log "Statistics"
  console.log "Times run: #{results.length}"
  console.log "Total:     #{formatTime totalTime}"
  console.log "Minimum:   #{formatTime minimumTime}"
  console.log "Maximum:   #{formatTime maximumTime}"
  console.log "Average:   #{formatTime totalTime / results.length}"

timer = (func, numberOfTimesToExecute = 1) ->
  timerResults = []
  console.log 'Running...'

  for lap in [1..numberOfTimesToExecute]
    start = new Date()
    func()
    end = new Date()
    timerResults.push \
      dm.dateDifference(start, end, 'milliseconds')

  displayResults timerResults

module.exports =
  timer: timer

How it works...

This little performance utility module begins by requiring our date_utils library as we will be using the dateDifference() method to calculate how long a method takes to execute.

Then, we have some formatting helper methods. The formatNumber() method will format a number for display and includes optional parameters for the number of decimal places and zero padding. For example, formatNumber(5.4, 3, 2) will produce 05.400.

The formatTime() method will take a value in milliseconds and display it as hours, minutes, and seconds. For example, formatTime(5680) will be displayed as 00:00:05.680.

Tip

You may have noticed we need to define our helper methods before they are used. This is a requirement of JavaScript due to its dynamic nature.

After our formatting helper methods, we have a method that displays the performance measurement results, but let's look at the timer() method first.

The timer() method is really the heart of our module. It is responsible for executing the function being measured and gathering timing statistics with each run. The method takes a function (func) as a parameter and a numberOfTimesToExecute optional parameter representing the number of times to execute the function, which defaults to 1.

The timer() method then declares a timerResults array to store our execution times.

We then loop between 1 and numberOfTimesToExecute. With each iteration, we perform the following tasks:

  • Store the current date and time in a variable called start
  • Execute the function that is being measured
  • Store the current date and time after the execution of a variable called end
  • Push the results into our timerResults array

Once the function execution has been measured, the timer() method calls displayResults() to pass the timerResults array. The displayResults() method displays the number of executions, the total time, minimum time, maximum time, and the average time.

It is worth noting that the act of measuring performance negatively impacts the performance, however minimally. When working to improve performance in your code, it is better to compare results between tests with the understanding that each test executes with roughly the same overhead.

Let's try running our timer as follows:

tu = require './timer_utils'

test = () ->
  for i in [1..1000000]
    d = new Date()

tu.timer test, 5

Our little timer demo declares a test method that simply iterates from one to a million, and with each iteration, the current date/time is stored in the d variable.

Tip

Why a million? Turns out d = new Date() happens very quickly. If we do it a million times, it takes about 0.5 seconds.

We then pass our test() method to timer() and have it execute five times.

The output for the preceding code is:

Running...
Statistics
Times run: 5
Total:     00:00:02.2810
Minimum:   00:00:00.4220
Maximum:   00:00:00.4920
Average:   00:00:00.4562