Maths
You can find all the code for this chapter here
For all the power of modern computers to perform huge sums at lightning speed, the average developer rarely uses any mathematics to do their job. But not today! Today we'll use mathematics to solve a real problem. And not boring mathematics - we're going to use trigonometry and vectors and all sorts of stuff that you always said you'd never have to use after highschool.
The Problem
You want to make an SVG of a clock. Not a digital clock - no, that would be easy - an analogue clock, with hands. You're not looking for anything fancy, just a nice function that takes a Time
from the time
package and spits out an SVG of a clock with all the hands - hour, minute and second - pointing in the right direction. How hard can that be?
First we're going to need an SVG of a clock for us to play with. SVGs are a fantastic image format to manipulate programmatically because they're written as a series of shapes, described in XML. So this clock:
is described like this:
It's a circle with three lines, each of the lines starting in the middle of the circle (x=150, y=150), and ending some distance away.
So what we're going to do is reconstruct the above somehow, but change the lines so they point in the appropriate directions for a given time.
An Acceptance Test
Before we get too stuck in, lets think about an acceptance test.
Wait, you don't know what an acceptance test is yet. Look, let me try to explain.
Let me ask you: what does winning look like? How do we know we've finished work? TDD provides a good way of knowing when you've finished: when the test passes. Sometimes it's nice - actually, almost all of the time it's nice - to write a test that tells you when you've finished writing the whole usable feature. Not just a test that tells you that a particular function is working in the way you expect, but a test that tells you that the whole thing you're trying to achieve - the 'feature' - is complete.
These tests are sometimes called 'acceptance tests', sometimes called 'feature tests'. The idea is that you write a really high level test to describe what you're trying to achieve - a user clicks a button on a website, and they see a complete list of the Pokémon they've caught, for instance. When we've written that test, we can then write more tests - unit tests - that build towards a working system that will pass the acceptance test. So for our example these tests might be about rendering a webpage with a button, testing route handlers on a web server, performing database look ups, etc. All of these things will be TDD'd, and all of them will go towards making the original acceptance test pass.
Something like this classic picture by Nat Pryce and Steve Freeman
Anyway, let's try and write that acceptance test - the one that will let us know when we're done.
We've got an example clock, so let's think about what the important parameters are going to be.
The centre of the clock (the attributes x1
and y1
for this line) is the same for each hand of the clock. The numbers that need to change for each hand of the clock - the parameters to whatever builds the SVG - are the x2
and y2
attributes. We'll need an X and a Y for each of the hands of the clock.
I could think about more parameters - the radius of the clockface circle, the size of the SVG, the colours of the hands, their shape, etc... but it's better to start off by solving a simple, concrete problem with a simple, concrete solution, and then to start adding parameters to make it generalised.
So we'll say that
every clock has a centre of (150, 150)
the hour hand is 50 long
the minute hand is 80 long
the second hand is 90 long.
A thing to note about SVGs: the origin - point (0,0) - is at the top left hand corner, not the bottom left as we might expect. It'll be important to remember this when we're working out where what numbers to plug in to our lines.
Finally, I'm not deciding how to construct the SVG - we could use a template from the text/template
package, or we could just send bytes into a bytes.Buffer
or a writer. But we know we'll need those numbers, so let's focus on testing something that creates them.
Write the test first
So my first test looks like this:
Remember how SVGs plot their coordinates from the top left hand corner? To place the second hand at midnight we expect that it hasn't moved from the centre of the clockface on the X axis - still 150 - and the Y axis is the length of the hand 'up' from the centre; 150 minus 90.
Try to run the test
This drives out the expected failures around the missing functions and types:
So a Point
where the tip of the second hand should go, and a function to get it.
Write the minimal amount of code for the test to run and check the failing test output
Let's implement those types to get the code to compile
and now we get:
Write enough code to make it pass
When we get the expected failure, we can fill in the return value of SecondHand
:
Behold, a passing test.
Refactor
No need to refactor yet - there's barely enough code!
Repeat for new requirements
We probably need to do some work here that doesn't just involve returning a clock that shows midnight for every time...
Write the test first
Same idea, but now the second hand is pointing downwards so we add the length to the Y axis.
This will compile... but how do we make it pass?
Thinking time
How are we going to solve this problem?
Every minute the second hand goes through the same 60 states, pointing in 60 different directions. When it's 0 seconds it points to the top of the clockface, when it's 30 seconds it points to the bottom of the clockface. Easy enough.
So if I wanted to think about in what direction the second hand was pointing at, say, 37 seconds, I'd want the angle between 12 o'clock and 37/60ths around the circle. In degrees this is (360 / 60 ) * 37 = 222
, but it's easier just to remember that it's 37/60
of a complete rotation.
But the angle is only half the story; we need to know the X and Y coordinate that the tip of the second hand is pointing at. How can we work that out?
Math
Imagine a circle with a radius of 1 drawn around the origin - the coordinate 0, 0
.
This is called the 'unit circle' because... well, the radius is 1 unit!
The circumference of the circle is made of points on the grid - more coordinates. The x and y components of each of these coordinates form a triangle, the hypotenuse of which is always 1 (i.e. the radius of the circle).
Now, trigonometry will let us work out the lengths of X and Y for each triangle if we know the angle they make with the origin. The X coordinate will be cos(a), and the Y coordinate will be sin(a), where a is the angle made between the line and the (positive) x axis.
(If you don't believe this, go and look at Wikipedia...)
One final twist - because we want to measure the angle from 12 o'clock rather than from the X axis (3 o'clock), we need to swap the axis around; now x = sin(a) and y = cos(a).
So now we know how to get the angle of the second hand (1/60th of a circle for each second) and the X and Y coordinates. We'll need functions for both sin
and cos
.
math
math
Happily the Go math
package has both, with one small snag we'll need to get our heads around; if we look at the description of math.Cos
:
Cos returns the cosine of the radian argument x.
It wants the angle to be in radians. So what's a radian? Instead of defining the full turn of a circle to be made up of 360 degrees, we define a full turn as being 2π radians. There are good reasons to do this that we won't go in to.
Now that we've done some reading, some learning and some thinking, we can write our next test.
Write the test first
All this maths is hard and confusing. I'm not confident I understand what's going on - so let's write a test! We don't need to solve the whole problem in one go - let's start off with working out the correct angle, in radians, for the second hand at a particular time.
I'm going to comment out the acceptance test that I was working on while I'm working on these tests - I don't want to get distracted by that test while I'm getting this one to pass.
A recap on packages
At the moment, our acceptance tests are in the clockface_test
package. Our tests can be outside of the clockface
package - as long as their name ends with _test.go
they can be run.
I'm going to write these radians tests within the clockface
package; they may never get exported, and they may get deleted (or moved) once I have a better grip on what's going on. I'll rename my acceptance test file to clockface_acceptance_test.go
, so that I can create a new file called clockface_test
to test seconds in radians.
Here we're testing that 30 seconds past the minute should put the second hand at halfway around the clock. And it's our first use of the math
package! If a full turn of a circle is 2π radians, we know that halfway round should just be π radians. math.Pi
provides us with a value for π.
Try to run the test
Write the minimal amount of code for the test to run and check the failing test output
Write enough code to make it pass
Refactor
Nothing needs refactoring yet
Repeat for new requirements
Now we can extend the test to cover a few more scenarios. I'm going to skip forward a bit and show some already refactored test code - it should be clear enough how I got where I want to.
I added a couple of helper functions to make writing this table based test a little less tedious. testName
converts a time into a digital watch format (HH:MM:SS), and simpleTime
constructs a time.Time
using only the parts we actually care about (again, hours, minutes and seconds). Here they are:
These two functions should help make these tests (and future tests) a little easier to write and maintain.
This gives us some nice test output:
Time to implement all of that maths stuff we were talking about above:
One second is (2π / 60) radians... cancel out the 2 and we get π/30 radians. Multiply that by the number of seconds (as a float64
) and we should now have all the tests passing...
Wait, what?
Floats are horrible
Floating point arithmetic is notoriously inaccurate. Computers can only really handle integers, and rational numbers to some extent. Decimal numbers start to become inaccurate, especially when we factor them up and down as we are in the secondsInRadians
function. By dividing math.Pi
by 30 and then by multiplying it by 30 we've ended up with a number that's no longer the same as math.Pi
.
There are two ways around this:
Live with it
Refactor our function by refactoring our equation
Now (1) may not seem all that appealing, but it's often the only way to make floating point equality work. Being inaccurate by some infinitesimal fraction is frankly not going to matter for the purposes of drawing a clockface, so we could write a function that defines a 'close enough' equality for our angles. But there's a simple way we can get the accuracy back: we rearrange the equation so that we're no longer dividing down and then multiplying up. We can do it all by just dividing.
So instead of
we can write
which is equivalent.
In Go:
And we get a pass.
It should all look something like this.
A note on dividing by zero
Computers often don't like dividing by zero because infinity is a bit strange.
In Go if you try to explicitly divide by zero you will get a compilation error.
Obviously the compiler can't always predict that you'll divide by zero, such as our t.Second()
Try this
It will print +Inf
(infinity). Dividing by +Inf seems to result in zero and we can see this with the following:
Repeat for new requirements
So we've got the first part covered here - we know what angle the second hand will be pointing at in radians. Now we need to work out the coordinates.
Again, let's keep this as simple as possible and only work with the unit circle; the circle with a radius of 1. This means that our hands will all have a length of one but, on the bright side, it means that the maths will be easy for us to swallow.
Write the test first
Try to run the test
Write the minimal amount of code for the test to run and check the failing test output
Write enough code to make it pass
Repeat for new requirements
Try to run the test
Write enough code to make it pass
Remember our unit circle picture?
Also recall that we want to measure the angle from 12 o'clock which is the Y axis instead of from the X axis which we would like measuring the angle between the second hand and 3 o'clock.
We now want the equation that produces X and Y. Let's write it into seconds:
Now we get
Wait, what (again)? Looks like we've been cursed by the floats once more - both of those unexpected numbers are infinitesimal - way down at the 16th decimal place. So again we can either choose to try to increase precision, or to just say that they're roughly equal and get on with our lives.
One option to increase the accuracy of these angles would be to use the rational type Rat
from the math/big
package. But given the objective is to draw an SVG and not land on the moon, I think we can live with a bit of fuzziness.
We've defined two functions to define approximate equality between two Points
- they'll work if the X and Y elements are within 0.0000001 of each other. That's still pretty accurate.
And now we get:
Refactor
I'm still pretty happy with this.
Here's what it looks like now
Repeat for new requirements
Well, saying new isn't entirely accurate - really what we can do now is get that acceptance test passing! Let's remind ourselves of what it looks like:
Try to run the test
Write enough code to make it pass
We need to do three things to convert our unit vector into a point on the SVG:
Scale it to the length of the hand
Flip it over the X axis to account for the SVG having an origin in the top left hand corner
Translate it to the right position (so that it's coming from an origin of (150,150))
Fun times!
Scale, flip, and translate in exactly that order. Hooray maths!
Refactor
There's a few magic numbers here that should get pulled out as constants, so let's do that
Draw the clock
Well... the second hand anyway...
Let's do this thing - because there's nothing worse than not delivering some value when it's just sitting there waiting to get out into the world to dazzle people. Let's draw a second hand!
We're going to stick a new directory under our main clockface
package directory, called (confusingly), clockface
. In there we'll put the main
package that will create the binary that will build an SVG:
Inside main.go
, you'll start with this code but change the import for the clockface package to point at your own version:
Oh boy am I not trying to win any prizes for beautiful code with this mess - but it does the job. It's writing an SVG out to os.Stdout
- one string at a time.
If we build this
and run it, sending the output into a file
We should see something like
And this is how the code looks.
Refactor
This stinks. Well, it doesn't quite stink stink, but I'm not happy about it.
That whole
SecondHand
function is super tied to being an SVG... without mentioning SVGs or actually producing an SVG...... while at the same time I'm not testing any of my SVG code.
Yeah, I guess I screwed up. This feels wrong. Let's try to recover with a more SVG-centric test.
What are our options? Well, we could try testing that the characters spewing out of the SVGWriter
contain things that look like the sort of SVG tag we're expecting for a particular time. For instance:
But is this really an improvement?
Not only will it still pass if I don't produce a valid SVG (as it's only testing that a string appears in the output), but it will also fail if I make the smallest, unimportant change to that string - if I add an extra space between the attributes, for instance.
The biggest smell is that I'm testing a data structure - XML - by looking at its representation as a series of characters - as a string. This is never, ever a good idea as it produces problems just like the ones I outline above: a test that's both too fragile and not sensitive enough. A test that's testing the wrong thing!
So the only solution is to test the output as XML. And to do that we'll need to parse it.
Parsing XML
encoding/xml
is the Go package that can handle all things to do with simple XML parsing.
The function xml.Unmarshal
takes a []byte
of XML data, and a pointer to a struct for it to get unmarshalled in to.
So we'll need a struct to unmarshall our XML into. We could spend some time working out what the correct names for all of the nodes and attributes, and how to write the correct structure but, happily, someone has written zek
a program that will automate all of that hard work for us. Even better, there's an online version at https://xml-to-go.github.io/. Just paste the SVG from the top of the file into one box and - bam - out pops:
We could make adjustments to this if we needed to (like changing the name of the struct to SVG
) but it's definitely good enough to start us off. Paste the struct into the clockface_acceptance_test
file and let's write a test with it:
We write the output of clockface.SVGWriter
to a bytes.Buffer
and then Unmarshal
it into an Svg
. We then look at each Line
in the Svg
to see if any of them have the expected X2
and Y2
values. If we get a match we return early (passing the test); if not we fail with a (hopefully) informative message.
Looks like we'd better create SVGWriter.go
...
The most beautiful SVG writer? No. But hopefully it'll do the job...
Oooops! The %f
format directive is printing our coordinates to the default level of precision - six decimal places. We should be explicit as to what level of precision we're expecting for the coordinates. Let's say three decimal places.
And after we update our expectations in the test
We get:
We can now shorten our main
function:
This is what things should look like now.
And we can write a test for another time following the same pattern, but not before...
Refactor
Three things stick out:
We're not really testing for all of the information we need to ensure is present - what about the
x1
values, for instance?Also, those attributes for
x1
etc. aren't reallystrings
are they? They're numbers!Do I really care about the
style
of the hand? Or, for that matter, the emptyText
node that's been generated byzak
?
We can do better. Let's make a few adjustments to the Svg
struct, and the tests, to sharpen everything up.
Here I've
Made the important parts of the struct named types -- the
Line
and theCircle
Turned the numeric attributes into
float64
s instead ofstring
s.Deleted unused attributes like
Style
andText
Renamed
Svg
toSVG
because it's the right thing to do.
This will let us assert more precisely on the line we're looking for:
Finally we can take a leaf out of the unit tests' tables, and we can write a helper function containsLine(line Line, lines []Line) bool
to really make these tests shine:
Here's what it looks like
Now that's what I call an acceptance test!
Write the test first
So that's the second hand done. Now let's get started on the minute hand.
Try to run the test
We'd better start building some other clock hands, Much in the same way as we produced the tests for the second hand, we can iterate to produce the following set of tests. Again we'll comment out our acceptance test while we get this working:
Try to run the test
Write the minimal amount of code for the test to run and check the failing test output
Repeat for new requirements
Well, OK - now let's make ourselves do some real work. We could model the minute hand as only moving every full minute - so that it 'jumps' from 30 to 31 minutes past without moving in between. But that would look a bit rubbish. What we want it to do is move a tiny little bit every second.
How much is that tiny little bit? Well...
Sixty seconds in a minute
thirty minutes in a half turn of the circle (
math.Pi
radians)so
30 * 60
seconds in a half turn.So if the time is 7 seconds past the hour ...
... we're expecting to see the minute hand at
7 * (math.Pi / (30 * 60))
radians past the 12.
Try to run the test
Write enough code to make it pass
In the immortal words of Jennifer Aniston: Here comes the science bit
Rather than working out how far to push the minute hand around the clockface for every second from scratch, here we can just leverage the secondsInRadians
function. For every second the minute hand will move 1/60th of the angle the second hand moves.
Then we just add on the movement for the minutes - similar to the movement of the second hand.
And...
Nice and easy. This is what things look like now
Repeat for new requirements
Should I add more cases to the minutesInRadians
test? At the moment there are only two. How many cases do I need before I move on to the testing the minuteHandPoint
function?
One of my favourite TDD quotes, often attributed to Kent Beck, is
Write tests until fear is transformed into boredom.
And, frankly, I'm bored of testing that function. I'm confident I know how it works. So it's on to the next one.
Write the test first
Try to run the test
Write the minimal amount of code for the test to run and check the failing test output
Write enough code to make it pass
Repeat for new requirements
And now for some actual work
Write enough code to make it pass
A quick copy and paste of the secondHandPoint
function with some minor changes ought to do it...
Refactor
We've definitely got a bit of repetition in the minuteHandPoint
and secondHandPoint
- I know because we just copied and pasted one to make the other. Let's DRY it out with a function.
and we can rewrite minuteHandPoint
and secondHandPoint
as one liners:
Now we can uncomment the acceptance test and get to work drawing the minute hand.
Write enough code to make it pass
The minuteHand
function is a copy-and-paste of secondHand
with some minor adjustments, such as declaring a minuteHandLength
:
And a call to it in our SVGWriter
function:
Now we should see that TestSVGWriterMinuteHand
passes:
But the proof of the pudding is in the eating - if we now compile and run our clockface
program, we should see something like
Refactor
Let's remove the duplication from the secondHand
and minuteHand
functions, putting all of that scale, flip and translate logic all in one place.
This is where we're up to now.
There... now it's just the hour hand to do!
Write the test first
Try to run the test
Again, let's comment this one out until we've got the some coverage with the lower level tests:
Write the test first
Try to run the test
Write the minimal amount of code for the test to run and check the failing test output
Repeat for new requirements
Try to run the test
Write enough code to make it pass
Repeat for new requirements
Try to run the test
Write enough code to make it pass
Remember, this is not a 24-hour clock; we have to use the remainder operator to get the remainder of the current hour divided by 12.
Write the test first
Now let's try to move the hour hand around the clockface based on the minutes and the seconds that have passed.
Try to run the test
Write enough code to make it pass
Again, a bit of thinking is now required. We need to move the hour hand along a little bit for both the minutes and the seconds. Luckily we have an angle already to hand for the minutes and the seconds - the one returned by minutesInRadians
. We can reuse it!
So the only question is by what factor to reduce the size of that angle. One full turn is one hour for the minute hand, but for the hour hand it's twelve hours. So we just divide the angle returned by minutesInRadians
by twelve:
and behold:
Floating point arithmetic strikes again.
Let's update our test to use roughlyEqualFloat64
for the comparison of the angles.