Maths
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.
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:
an svg of a clock
is described like this:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 300 300"
version="2.0">
<!-- bezel -->
<circle cx="150" cy="150" r="100" style="fill:#fff;stroke:#000;stroke-width:5px;"/>
<!-- hour hand -->
<line x1="150" y1="150" x2="114.150000" y2="132.260000"
style="fill:none;stroke:#000;stroke-width:7px;"/>
<!-- minute hand -->
<line x1="150" y1="150" x2="101.290000" y2="99.730000"
style="fill:none;stroke:#000;stroke-width:7px;"/>
<!-- second hand -->
<line x1="150" y1="150" x2="77.190000" y2="202.900000"
style="fill:none;stroke:#f00;stroke-width:3px;"/>
</svg>
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.
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

Outside-in feedback loops in TDD
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.
<line x1="150" y1="150" x2="114.150000" y2="132.260000"
style="fill:none;stroke:#000;stroke-width:7px;"/>
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.So my first test looks like this:
package clockface_test
import (
"projectpath/clockface"
"testing"
"time"
)
func TestSecondHandAtMidnight(t *testing.T) {
tm := time.Date(1337, time.January, 1, 0, 0, 0, 0, time.UTC)
want := clockface.Point{X: 150, Y: 150 - 90}
got := clockface.SecondHand(tm)
if got != want {
t.Errorf("Got %v, wanted %v", got, want)
}
}
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.
This drives out the expected failures around the missing functions and types:
--- FAIL: TestSecondHandAtMidnight (0.00s)
./clockface_test.go:13:10: undefined: clockface.Point
./clockface_test.go:14:9: undefined: clockface.SecondHand
So a
Point
where the tip of the second hand should go, and a function to get it.Let's implement those types to get the code to compile
package clockface
import "time"
// A Point represents a two-dimensional Cartesian coordinate
type Point struct {
X float64
Y float64
}
// SecondHand is the unit vector of the second hand of an analogue clock at time `t`
// represented as a Point.
func SecondHand(t time.Time) Point {
return Point{}
}
and now we get:
--- FAIL: TestSecondHandAtMidnight (0.00s)
clockface_test.go:17: Got {0 0}, wanted {150 60}
FAIL
exit status 1
FAIL learn-go-with-tests/math/clockface 0.006s
When we get the expected failure, we can fill in the return value of
SecondHand
:// SecondHand is the unit vector of the second hand of an analogue clock at time `t`
// represented as a Point.
func SecondHand(t time.Time) Point {
return Point{150, 60}
}
Behold, a passing test.
PASS
ok clockface 0.006s
No need to refactor yet - there's barely enough code!
We probably need to do some work here that doesn't just involve returning a clock that shows midnight for every time...
func TestSecondHandAt30Seconds(t *testing.T) {
tm := time.Date(1337, time.January, 1, 0, 0, 30, 0, time.UTC)
want := clockface.Point{X: 150, Y: 150 + 90}
got := clockface.SecondHand(tm)
if got != want {
t.Errorf("Got %v, wanted %v", got, want)
}
}
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?
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?
Imagine a circle with a radius of 1 drawn around the origin - the coordinate
0, 0
.
picture of the unit circle
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).

picture of the unit circle with a point defined on the circumference
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.

picture of the unit circle with the x and y elements of a ray defined as cos(a) and sin(a) respectively, where a is the angle made by the ray with the x axis
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).

unit circle ray defined from by angle from y axis
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
.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.
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.
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.package clockface
import (
"math"
"testing"
"time"
)
func TestSecondsInRadians(t *testing.T) {
thirtySeconds := time.Date(312, time.October, 28, 0, 0, 30, 0, time.UTC)
want := math.Pi
got := secondsInRadians(thirtySeconds)
if want != got {
t.Fatalf("Wanted %v radians, but got %v", want, got)
}
}
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 π../clockface_test.go:12:9: undefined: secondsInRadians
func secondsInRadians(t time.Time) float64 {
return 0
}
clockface_test.go:15: Wanted 3.141592653589793 radians, but got 0
func secondsInRadians(t time.Time) float64 {
return math.Pi
}
PASS
ok clockface 0.011s
Nothing needs refactoring yet
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.
func TestSecondsInRadians(t *testing.T) {
cases := []struct {
time time.Time
angle float64
}{
{simpleTime(0, 0, 30), math.Pi},
{simpleTime(0, 0, 0), 0},
{simpleTime(0, 0, 45), (math.Pi / 2) * 3},
{simpleTime(0, 0, 7), (math.Pi / 30) * 7},
}
for _, c := range cases {
t.Run(testName(c.time), func(t *testing.T) {
got := secondsInRadians(c.time)
if got != c.angle {
t.Fatalf("Wanted %v radians, but got %v", c.angle, got)
}
})
}
}
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:func simpleTime(hours, minutes, seconds int) time.Time {
return time.Date(312, time.October, 28, hours, minutes, seconds, 0, time.UTC)
}
func testName(t time.Time) string {
return t.Format("15:04:05")
}
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:
clockface_test.go:24: Wanted 0 radians, but got 3.141592653589793
clockface_test.go:24: Wanted 4.71238898038469 radians, but got 3.141592653589793
Time to implement all of that maths stuff we were talking about above:
func secondsInRadians(t time.Time) float64 {
return float64(t.Second()) * (math.Pi / 30)
}
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...clockface_test.go:24: Wanted 3.141592653589793 radians, but got 3.1415926535897936
Wait, what?
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:
- 1.Live with it
- 2.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
numberOfSeconds * π / 30
we can write
π / (30 / numberOfSeconds)
which is equivalent.
In Go:
func secondsInRadians(t time.Time) float64 {
return (math.Pi / (30 / (float64(t.Second()))))
}
And we get a pass.
PASS
ok clockface 0.005s
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.
package main