Building Trello Cycles

in CoffeeScript , JavaScript

Recently I built a little utility project for the fine people at UserTesting.com to help them get some insights into project lifecycle and “cycle time,” the time it takes any given project to move through pre-determined phases of the lifecycle. Hence, “Trello Cycles."

Seeing as how Trello‘s API is damn robust and they have a sweet piece of JS to help with authentication and some basic API requests I decided to do the whole thing client side and give Yeoman a try at the same time.

Yeoman

The webapp generator for Yeoman is pretty sweet: it scaffolds out some basic JS, CSS, and HTML; sets up Grunt tasks for local testing with live reload and building to a destination folder; and has support for CoffeeScript, HTML5 Boilerplate, Twitter Bootstrap, and RequireJS.

Grunt the Fuck?

Maybe I’m just not hip to the new JS jazz, but Gruntfiles make no fucking sense to me. Check out the auto-generated Gruntfile.js for Trello Cycles and the Rakefile for my Sinatra Boilerplate project. Both will build assets and run a development server (I realize there isn’t perfect feature parity so an apples-to-apples comparison is impossible, what I’m digging at here isn’t a functionality comparison) but I find the former damn near inscrutable and the latter generally more legible. It’s a minor thing. Sort of.

Statistics in JavaScript

Turns out doing basic statistical work in JavaScript isn’t super straight forward. Calculating something simple like the sum and mean (average) of an array of numbers isn’t as easy as just calling [1, 2, 3].sum() or [1, 2, 3].mean(). Instead, you’d have to do something absolutely goofy looking like pass the array into a dedicated function like arraySum([1, 2, 3]) and just looking at that makes my blood boil. Because I’ve been using Ruby so much I’m used to calling methods directly on objects and monkey patching core classes to do what I want so I decided I’d better get to it. (Sidenote: I think there’s probably a discussion of encapsulation and API consistency in here along the lines of Underscore vs. core extension, but I’m not ready for that one yet).

This script adds some basic mathematical methods to the Array prototype chain including:

  • max
  • min
  • median
  • sum which depends on reduce
  • mean which depends on sum
  • variance which gives you the variance of the set
  • stddev which gives you the standard deviation, or more simply the square root of the variance

With all of that, you can call [1, 2, 3].sum() and [1, 2, 3].mean() to your heart’s content.

 1# max
 2if "function" isnt typeof Array::max Array::max = -> "use strict"
 3
 4    # throw on null
 5    throw new TypeError("Array.prototype.max called on null or undefined")  if null is this or "undefined" is typeof this
 6
 7    Math.max.apply null, this
 8
 9# min
10if "function" isnt typeof Array::min
11  Array::min = ->
12    "use strict"
13
14    # throw on null
15    throw new TypeError("Array.prototype.min called on null or undefined")  if null is this or "undefined" is typeof this
16
17    Math.min.apply null, this
18
19# median
20if "function" isnt typeof Array::median
21  Array::median = ->
22    "use strict"
23
24    # throw on null
25    throw new TypeError("Array.prototype.median called on null or undefined")  if null is this or "undefined" is typeof this
26
27    # locals
28    sorted = @sort (a,b) => return a - b
29    length = @length >>> 0
30    half   = Math.floor length/2
31
32    if length % 2
33      return this[half]
34    else
35      return [this[half - 1], this[half]].mean()
36
37# sum
38# depends on Array.prototype.reduce to work properly.
39# shim for that available here https://gist.github.com/evanleck/6418510
40if "function" isnt typeof Array::sum
41  Array::sum = ->
42    "use strict"
43
44    # throw on null
45    throw new TypeError("Array.prototype.sum called on null or undefined")  if null is this or "undefined" is typeof this
46
47    @reduce (a, b) ->
48      a + b
49
50# mean
51if "function" isnt typeof Array::mean
52  Array::mean = ->
53    "use strict"
54
55    # throw on null
56    throw new TypeError("Array.prototype.mean called on null or undefined")  if null is this or "undefined" is typeof this
57
58    length = @length >>> 0
59
60    @sum()/length
61
62# variance
63if "function" isnt typeof Array::variance
64  Array::variance = ->
65    "use strict"
66
67    # throw on null
68    throw new TypeError("Array.prototype.variance called on null or undefined")  if null is this or "undefined" is typeof this
69
70    index  = 0
71    length = @length >>> 0
72    mean   = @mean()
73    powed  = []
74
75    # loop to generate powed
76    while length > index
77      continue unless @hasOwnProperty(index)
78      powed.push Math.pow(this[index] - mean, 2)
79      ++index
80
81    # return mean of powed
82    powed.mean()
83
84# standard deviation
85if "function" isnt typeof Array::stddev
86  Array::stddev = ->
87    "use strict"
88
89    # throw on null
90    throw new TypeError("Array.prototype.stddev called on null or undefined")  if null is this or "undefined" is typeof this
91
92    # square root of variance
93    Math.sqrt @variance()