Answers to exercises - CoffeeScript in Action (2014)

CoffeeScript in Action (2014)

Appendix B. Answers to exercises

About the exercises

These exercises are provided for you to practice using CoffeeScript and also to spend some time reflecting on CoffeeScript. These two activities are an essential component of learning a new programming language. Attempting the exercises is more important than looking at the solutions.

Exercise 2.3.3

torch = price: 21

umbrella = {}

combinedCost = (torch.price || 0) + (umbrella.price || 0)

# 21

Exercise 2.4.4

animal = "crocodile"

collective = switch animal

when "antelope" then "herd"

when "baboon" then "rumpus"

when "badger" then "cete"

when "cobra" then "quiver"

when "crocodile" then "bask"

# bask

Exercise 2.5.3

animal = "cobra"

collective = switch animal

when "antelope" then "herd"

when "baboon" then "rumpus"

when "badger" then "cete"

when "cobra" then "quiver"

when "crocodile" then "bask"

"The collective of #{animal} is #{collective}"

# The collective of cobra is quiver

Exercise 2.6.5

animals = 'baboons badgers antelopes cobras crocodiles'

result = for animal in animals.split " "

collective = switch animal

when "antelopes" then "herd"

when "baboons" then "rumpus"

when "badgers" then "cete"

when "cobras" then "quiver"

when "crocodiles" then "bask"

"A #{collective} of #{animal}"

Exercises 3.1.5

countWords = (text) ->

words = text.split /[\s,]/

significantWords = (word for word in words when word.length > 3)

significantWords.length

everyOtherWord = (text) ->

words = text.split /[\s,]/

takeOther = for word, index in words

if index % 2 then ""

else word

takeOther.join(" ").replace /\s\s/gi, " "

Exercises 3.3.4

http = require 'http'

fs = require 'fs'

sourceFile = 'attendees'

fileContents = 'File not read yet.'

readSourceFile = ->

fs.readFile sourceFile, 'utf-8', (error, data) ->

if error

console.log error

else

fileContents = data

fs.watchFile sourceFile, readSourceFile

countWords = (text) ->

text.split(/,/gi).length

readSourceFile sourceFile

server = http.createServer (request, response) ->

response.end "#{countWords(fileContents)}"

server.listen 8080, '127.0.0.1'

Exercises 3.4.4

accumulate = (initial, items, accumulator) ->

total = initial

for item in items

total = accumulator total, item

total

sumFractions = (fractions) ->

accumulator = (lhs, rhs) ->

if lhs is '0/0'

rhs

else if rhs is '0/0'

lhs

else

lhsSplit = lhs.split /\//gi

rhsSplit = rhs.split /\//gi

lhsNumer = 1*lhsSplit[0]

lhsDenom = 1*lhsSplit[1]

rhsNumer = 1*rhsSplit[0]

rhsDenom = 1*rhsSplit[1]

if lhsDenom isnt rhsDenom

commonDenom = lhsDenom*rhsDenom

else

commonDenom = lhsDenom

sumNumer = lhsNumer*(commonDenom/lhsDenom) + rhsNumer*(commonDenom/rhsDenom)

"#{sumNumer}/#{commonDenom}"

accumulate '0/0', fractions, accumulator

console.log sumFractions ['2/6', '1/4']

# '14/24'

And the keep function:

keep = (arr, cond) ->

item for item in arr when cond item

Exercises 4.2.3

Adding edit to listing 4.2:

phonebook =

numbers:

hannibal: '555-5551'

darth: '555-5552'

hal9000: 'disconnected'

freddy: '555-5554'

'T-800': '555-5555'

list: ->

"#{name}: #{number}" for name, number of @numbers

add: (name, number) ->

if not (name of @numbers)

@numbers[name] = number

else

"#{name} already exists"

edit: (name, number) ->

if name of @numbers

@numbers[name] = number

else

"#{name} not found"

get: (name) ->

if name of @numbers

"#{name}: #{@numbers[name]}"

else

"#{name} not found"

console.log "Phonebook. Commands are add, get, edit, list, and exit."

process.stdin.setEncoding 'utf8'

stdin = process.openStdin()

stdin.on 'data', (chunk) ->

args = chunk.split ' '

command = args[0].trim()

name = args[1].trim() if args[1]

number = args[2].trim() if args[2]

switch command

when 'add'

res = phonebook.add(name, number) if name and number

console.log res

when 'get'

console.log phonebook.get(name) if name

when 'edit'

console.log phonebook.edit(name, number) if name and number

when 'list'

console.log phonebook.list()

when 'exit'

process.exit 1

Setting properties on an object:

css = (element, styles) ->

element.style ?= {}

for key, value of styles

element.style[key] = value

class Element

div = new Element

css div, width: 10

div.style.width

# 10

Exercise 4.6.3

The original music device is

cassette =

title: "Awesome songs. To the max!"

duration: "10:34"

released: "1988"

track1: "Safety Dance - Men Without Hats"

track2: "Funkytown - Lipps, Inc"

track3: "Electric Avenue - Eddy Grant"

track4: "We built this city - Starship"

The music device was created from it:

musicDevice = Object.create cassette

Creating another one from the first is the same:

secondMusicDevice = Object.create musicDevice

Changes to either the original cassette or the music device will be visible on the second music device:

cassette.track5 = "Hello - Lionel Richie"

secondMusicDevice.track5

# "Hello - Lionel Richie"

musicDevice.track6 = "Mickey - Toni Basil"

secondMusicDevice.track6

# "Mickey - Toni Basil"

Multiple prototype references like this are called the prototype chain. You’ll learn more about it in chapter 5.

Exercise 4.7.2

views =

excluded: []

pages: {}

clear: ->

@pages = {}

increment: (key) ->

unless key in @excluded

@pages[key] ?= 0

@pages[key] = @pages[key] + 1

ignore: (page) ->

@excluded = @excluded.concat page

total: ->

sum = 0

for own page, count of @pages

sum = sum + count

sum

Exercises 4.8.3

class GranTurismo

constructor: (options) ->

@options = options

summary: ->

("#{key}: #{value}" for key, value of @options).join "\n"

options =

wheels: 'phat'

dice: 'fluffy'

scruffysGranTurismo = new GranTurismo options

scruffysGranTurismo.summary()

# wheels: phat

# dice: fluffy

The constructor could use the shorthand for arguments:

class GranTurismo

constructor: (@options) ->

summary: ->

("#{key}: #{value}" for key, value of @options).join "\n"

This is equivalent to the first version.

Exercise 5.3.3

Here are Product and Camera classes based on listing 5.4 with an alphabetical class method added to the Camera class:

class Product

# any implementation of Product

class Camera extends Product

cameras = []

@alphabetical = ->

cameras.sort (a, b) -> a.name > b.name

constructor: ->

all.push @

super

The Camera class can keep an array of all instances for alphabetical just as the Product class kept an array of products for find. The Camera constructor uses super to ensure the Product constructor is also invoked so that Product.find doesn’t break.

Exercise 5.8.1

Applying some basic class techniques to the server-side application helps to increase clarity:

fs = require 'fs'

http = require 'http'

url = require 'url'

coffee = require 'coffee-script'

class ShopServer

constructor: (@host, @port, @shopData, @shopNews) ->

@css = ''

fs.readFile './client.css', 'utf-8', (err, data) =>

if err then throw err

@css = data

readClientScript: (callback) ->

script = "./client.coffee"

fs.readFile script, 'utf-8', (err, data) ->

if err then throw err

callback data

headers: (res, status, type) ->

res.writeHead status, 'Content-Type': "text/#{type}"

renderView: ->

"""

<!doctype html>

<head>

<title>Agtron's Emporium</title>

<link rel='stylesheet' href='/css/client.css' />

</head>

<body>

<div class='page'>

<h1>----Agtron’s Emporium----</h1>

<script src='/js/client.js'></script>

</div>

</body>

</html>

"""

handleClientJs: (path, req, res) ->

@headers res, 200, 'javascript'

writeClientScript = (script) ->

res.end coffee.compile(script)

@readClientScript writeClientScript

handleClientCss: (path, req, res) ->

@headers res, 200, 'css'

res.end @css

handleImage: (path, req, res) ->

fs.readFile ".#{path}", (err, data) =>

if err

@headers res, 404, 'image/png'

res.end()

else

@headers res, 200, 'image/png'

res.end data, 'binary'

handleJson: (path, req, res) ->

switch path

when '/json/list'

@headers res, 200, 'json'

res.end JSON.stringify(@shopData)

when '/json/list/camera'

@headers res, 200, 'json'

camera = @shopData.camera

res.end JSON.stringify(camera)

when '/json/news'

@headers res, 200, 'json'

res.end JSON.stringify(@shopNews)

else

@headers res, 404, 'json'

res.end JSON.stringify(status: 404)

handlePost: (path, req, res) ->

category = /^\/json\/purchase\/([^/]*)\/([^/]*)$/.exec(path)?[1]

item = /^\/json\/purchase\/([^/]*)\/([^/]*)$/.exec(path)?[2]

if category? and item? and data[category][item].stock > 0

data[category][item].stock -= 1

@headers res, 200, 'json'

res.write JSON.stringify

status: 'success',

update: data[category][item]

else

res.write JSON.stringify

status: 'failure'

res.end()

handleGet: (path, req, res) ->

if path is '/'

@headers res, 200, 'html'

res.end @renderView()

else if path.match /\/json/

@handleJson path, req, res

else if path is '/js/client.js'

@handleClientJs path, req, res

else if path is '/css/client.css'

@handleClientCss path, req, res

else if path.match /^\/images\/(.*)\.png$/gi

@handleImage path, req, res

else

@headers res, 404, 'html'

res.end '404'

start: ->

@httpServer = http.createServer (req, res) =>

path = url.parse(req.url).pathname

if req.method == "POST"

@handlePost path, req, res

else

@handleGet path, req, res

@httpServer.listen @port, @host, =>

console.log "Running at #{@host}:#{@port}"

stop: ->

@httpServer?.close()

data = require('./data').all

news = require('./news').all

shopServer = new ShopServer '127.0.0.1', 9999, data, news

shopServer.start()

There are some further opportunities in the preceding program for encapsulation, but for a program of little over 100 lines you’d likely find any more to be overkill.

Exercises 7.2.5

Your first thought might be to do something like this:

swapPairs = (array) ->

for index in array by 2

[first, second] = array[index-1..index]

[second, first]

This is close, but you’ll end up with an array of arrays:

swapPairs([3,4,3,4,3,4])

# [ [ 4, 3 ], [ 4, 3 ], [ 4, 3 ] ]

swapPairs([1,2,3,4,5,6])

# [ [ 2, 1 ], [ 4, 3 ], [ 6, 5 ] ]

Solve this by using the array concat method with rest:

swapPairs = (array) ->

reversedPairs = for index in array by 2

[first, second] = array[index-1..index]

[second, first]

[].concat reversedPairs...

swapPairs([3,4,3,4,3,4])

# [ 4, 3, 4, 3, 4, 3 ]

swapPairs([1,2,3,4,5,6])

# [ 2, 1, 4, 3, 6, 5 ]

For the second exercise, use a combination of rest, array, and object destructuring:

phoneDirectory =

A: [

name: 'Abe'

phone: '555 1110'

,

name: 'Andy'

phone: '555 1111'

,

name: 'Alice'

phone: '555 1112'

]

B: [

name: 'Bam'

phone: '555 1113'

]

lastNumberForLetter = (letter, directory) ->

[..., lastForLetter] = directory[letter]

{phone} = lastForLetter

phone

lastNumberForLetter 'A', phoneDirectory

# 555 1112

Exercise 10.4.4

Suppose the Tracking class and http object are as follows:

class Tracking

constructor: (prefs, http) ->

@http = http

start: ->

@http.listen()

http =

listen: ->

A potential double function follows:

double = (original) ->

mock = {}

for key, value of original

if value.call?

do ->

stub = ->

stub.called = true

mock[key] = stub

mock

This double function returns a mock version of the original object. It has all the same method names, but the methods themselves are just empty functions that remember if they’ve been called or not.

In other circumstances your test might call for a spy instead. When you spy on an object, any method calls still occur on the original object but are seen by the spy. A double function that returns a spy follows:

double = (original) ->

spy = Object.create original

for key, value of original

if value.call?

do ->

originalMethod = value

spyMethod = (args...) ->

spyMethod.called = true

originalMethod args...

spy[key] = spyMethod

spy

Depending on the test framework you’re using, there may be both mocking and spying libraries provided for you. The pros and cons of using mocks, spies, and other testing techniques aren’t covered here.

That’s it for the exercises. Happy travels.