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:

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. 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_testimport ("testing""time""github.com/gypsydave5/learn-go-with-tests/math/v1/clockface")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)# github.com/gypsydave5/learn-go-with-tests/math/v1/clockface_test [github.com/gypsydave5/learn-go-with-tests/math/v1/clockface.test]./clockface_test.go:13:10: undefined: clockface.Point./clockface_test.go:14:9: undefined: clockface.SecondHandFAIL github.com/gypsydave5/learn-go-with-tests/math/v1/clockface [build failed]

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 clockfaceimport "time"// A Point represents a two dimensional Cartesian coordinatetype Point struct {X float64Y 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}FAILexit status 1FAIL github.com/gypsydave5/learn-go-with-tests/math/v1/clockface 0.006s

When we get the expected failure, we can fill in the return value of `HandsAt`

:

// 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.

PASSok github.com/gypsydave5/learn-go-with-tests/math/v1/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`

.

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 - 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`

.

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 write these 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'm also 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.

package clockfaceimport ("math""testing""time")func TestSecondsInRadians(t *testing.T) {thirtySeconds := time.Date(312, time.October, 28, 0, 0, 30, 0, time.UTC)want := math.Pigot := 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 π.

# github.com/gypsydave5/learn-go-with-tests/math/v2/clockface [github.com/gypsydave5/learn-go-with-tests/math/v2/clockface.test]./clockface_test.go:12:9: undefined: secondsInRadiansFAIL github.com/gypsydave5/learn-go-with-tests/math/v2/clockface [build failed]

func secondsInRadians(t time.Time) float64 {return 0}

--- FAIL: TestSecondsInRadians (0.00s)clockface_test.go:15: Wanted 3.141592653589793 radians, but got 0FAILexit status 1FAIL github.com/gypsydave5/learn-go-with-tests/math/v2/clockface 0.007s

func secondsInRadians(t time.Time) float64 {return math.Pi}

PASSok github.com/gypsydave5/learn-go-with-tests/math/v2/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.Timeangle 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).

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:

--- FAIL: TestSecondsInRadians (0.00s)--- FAIL: TestSecondsInRadians/00:00:00 (0.00s)clockface_test.go:24: Wanted 0 radians, but got 3.141592653589793--- FAIL: TestSecondsInRadians/00:00:45 (0.00s)clockface_test.go:24: Wanted 4.71238898038469 radians, but got 3.141592653589793--- FAIL: TestSecondsInRadians/00:00:07 (0.00s)clockface_test.go:24: Wanted 0.7330382858376184 radians, but got 3.141592653589793FAILexit status 1FAIL github.com/gypsydave5/learn-go-with-tests/math/v3/clockface 0.007s

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...

--- FAIL: TestSecondsInRadians (0.00s)--- FAIL: TestSecondsInRadians/00:00:30 (0.00s)clockface_test.go:24: Wanted 3.141592653589793 radians, but got 3.1415926535897936FAILexit status 1FAIL github.com/gypsydave5/learn-go-with-tests/math/v3/clockface 0.006s

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:

Live with the 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

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.

PASSok github.com/gypsydave5/learn-go-with-tests/math/v2/clockface 0.005s

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.

func TestSecondHandVector(t *testing.T) {cases := []struct {time time.Timepoint Point}{{simpleTime(0, 0, 30), Point{0, -1}},}for _, c := range cases {t.Run(testName(c.time), func(t *testing.T) {got := secondHandPoint(c.time)if got != c.point {t.Fatalf("Wanted %v Point, but got %v", c.point, got)}})}}

# github.com/gypsydave5/learn-go-with-tests/math/v4/clockface [github.com/gypsydave5/learn-go-with-tests/math/v4/clockface.test]./clockface_test.go:40:11: undefined: secondHandPointFAIL github.com/gypsydave5/learn-go-with-tests/math/v4/clockface [build failed]

func secondHandPoint(t time.Time) Point {return Point{}}

--- FAIL: TestSecondHandPoint (0.00s)--- FAIL: TestSecondHandPoint/00:00:30 (0.00s)clockface_test.go:42: Wanted {0 -1} Point, but got {0 0}FAILexit status 1FAIL github.com/gypsydave5/learn-go-with-tests/math/v4/clockface 0.010s

func secondHandPoint(t time.Time) Point {return Point{0, -1}}

PASSok github.com/gypsydave5/learn-go-with-tests/math/v4/clockface 0.007s

func TestSecondHandPoint(t *testing.T) {cases := []struct {time time.Timepoint Point}{{simpleTime(0, 0, 30), Point{0, -1}},{simpleTime(0, 0, 45), Point{-1, 0}},}for _, c := range cases {t.Run(testName(c.time), func(t *testing.T) {got := secondHandPoint(c.time)if got != c.point {t.Fatalf("Wanted %v Point, but got %v", c.point, got)}})}}

--- FAIL: TestSecondHandPoint (0.00s)--- FAIL: TestSecondHandPoint/00:00:45 (0.00s)clockface_test.go:43: Wanted {-1 0} Point, but got {0 -1}FAILexit status 1FAIL github.com/gypsydave5/learn-go-with-tests/math/v4/clockface 0.006s

Remember our unit circle picture?

We now want the equation that produces X and Y. Let's write it into seconds:

func secondHandPoint(t time.Time) Point {angle := secondsInRadians(t)x := math.Sin(angle)y := math.Cos(angle)return Point{x, y}}

Now we get

--- FAIL: TestSecondHandPoint (0.00s)--- FAIL: TestSecondHandPoint/00:00:30 (0.00s)clockface_test.go:43: Wanted {0 -1} Point, but got {1.2246467991473515e-16 -1}--- FAIL: TestSecondHandPoint/00:00:45 (0.00s)clockface_test.go:43: Wanted {-1 0} Point, but got {-1 -1.8369701987210272e-16}FAILexit status 1FAIL github.com/gypsydave5/learn-go-with-tests/math/v4/clockface 0.007s

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 the moon landings I think we can live with a bit of fuzziness.

func TestSecondHandPoint(t *testing.T) {cases := []struct {time time.Timepoint Point}{{simpleTime(0, 0, 30), Point{0, -1}},{simpleTime(0, 0, 45), Point{-1, 0}},}for _, c := range cases {t.Run(testName(c.time), func(t *testing.T) {got := secondHandPoint(c.time)if !roughlyEqualPoint(got, c.point) {t.Fatalf("Wanted %v Point, but got %v", c.point, got)}})}}func roughlyEqualFloat64(a, b float64) bool {const equalityThreshold = 1e-7return math.Abs(a-b) < equalityThreshold}func roughlyEqualPoint(a, b Point) bool {return roughlyEqualFloat64(a.X, b.X) &&roughlyEqualFloat64(a.Y, b.Y)}

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

PASSok github.com/gypsydave5/learn-go-with-tests/math/v4/clockface 0.007s

I'm still pretty happy with this.

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:

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)}}

--- FAIL: TestSecondHandAt30Seconds (0.00s)clockface_acceptance_test.go:28: Got {150 60}, wanted {150 240}FAILexit status 1FAIL github.com/gypsydave5/learn-go-with-tests/math/v5/clockface 0.007s

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 because 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!

// 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 {p := secondHandPoint(t)p = Point{p.X * 90, p.Y * 90} // scalep = Point{p.X, -p.Y} // flipp = Point{p.X + 150, p.Y + 150} // translatereturn p}

Scale, flip, and translated in exactly that order. Hooray maths!

PASSok github.com/gypsydave5/learn-go-with-tests/math/v5/clockface 0.007s

There's a few magic numbers here that should get pulled out as constants, so let's do that

const secondHandLength = 90const clockCentreX = 150const clockCentreY = 150// 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 {p := secondHandPoint(t)p = Point{p.X * secondHandLength, p.Y * secondHandLength}p = Point{p.X, -p.Y}p = Point{p.X + clockCentreX, p.Y + clockCentreY} //translatereturn p}

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:

├── clockface│ └── main.go├── clockface.go├── clockface_acceptance_test.go└── clockface_test.go

and inside `main.go`

package mainimport ("fmt""io""os""time""github.com/gypsydave5/learn-go-with-tests/math/v6/clockface")func main() {t := time.Now()sh := clockface.SecondHand(t)io.WriteString(os.Stdout, svgStart)io.WriteString(os.Stdout, bezel)io.WriteString(os.Stdout, secondHandTag(sh))io.WriteString(os.Stdout, svgEnd)}func secondHandTag(p clockface.Point) string {return fmt.Sprintf(`<line x1="150" y1="150" x2="%f" y2="%f" style="fill:none;stroke:#f00;stroke-width:3px;"/>`, p.X, p.Y)}const svgStart = `<?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">`const bezel = `<circle cx="150" cy="150" r="100" style="fill:#fff;stroke:#000;stroke-width:5px;"/>`const svgEnd = `</svg>`

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

go build

and run it, sending the output into a file

./clockface > clock.svg

We should see something like

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... withoutmentioning 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 and 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:

func TestSVGWriterAtMidnight(t *testing.T) {tm := time.Date(1337, time.January, 1, 0, 0, 0, 0, time.UTC)var b strings.Builderclockface.SVGWriter(&b, tm)got := b.String()want := `<line x1="150" y1="150" x2="150" y2="60"`if !strings.Contains(got, want) {t.Errorf("Expected to find the second hand %v, in the SVG output %v", want, got)}}

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 really 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.

`encoding/xml`

is the Go package that can handle all things to do with simple XML parsing.

The function `xml.Unmarshall`

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://www.onlinetool.io/xmltogo/. Just paste the SVG from the top of the file into one box and - bam

out pops:

type Svg struct {XMLName xml.Name `xml:"svg"`Text string `xml:",chardata"`Xmlns string `xml:"xmlns,attr"`Width string `xml:"width,attr"`Height string `xml:"height,attr"`ViewBox string `xml:"viewBox,attr"`Version string `xml:"version,attr"`Circle struct {Text string `xml:",chardata"`Cx string `xml:"cx,attr"`Cy string `xml:"cy,attr"`R string `xml:"r,attr"`Style string `xml:"style,attr"`} `xml:"circle"`Line []struct {Text string `xml:",chardata"`X1 string `xml:"x1,attr"`Y1 string `xml:"y1,attr"`X2 string `xml:"x2,attr"`Y2 string `xml:"y2,attr"`Style string `xml:"style,attr"`} `xml:"line"`}

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.

func TestSVGWriterAtMidnight(t *testing.T) {tm := time.Date(1337, time.January, 1, 0, 0, 0, 0, time.UTC)b := bytes.Buffer{}clockface.SVGWriter(&b, tm)svg := Svg{}xml.Unmarshal(b.Bytes(), &svg)x2 := "150"y2 := "60"for _, line := range svg.Line {if line.X2 == x2 && line.Y2 == y2 {return}}t.Errorf("Expected to find the second hand with x2 of %+v and y2 of %+v, in the SVG output %v", x2, y2, b.String())}

We write the output of `clockface.SVGWriter`

to a `bytes.Buffer`

and then `Unmarshall`

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.

# github.com/gypsydave5/learn-go-with-tests/math/v7b/clockface_test [github.com/gypsydave5/learn-go-with-tests/math/v7b/clockface.test]./clockface_acceptance_test.go:41:2: undefined: clockface.SVGWriterFAIL github.com/gypsydave5/learn-go-with-tests/math/v7b/clockface [build failed]

Looks like we'd better write that `SVGWriter`

...

package clockfaceimport ("fmt""io""time")const (secondHandLength = 90clockCentreX = 150clockCentreY = 150)//SVGWriter writes an SVG representation of an analogue clock, showing the time t, to the writer wfunc SVGWriter(w io.Writer, t time.Time) {io.WriteString(w, svgStart)io.WriteString(w, bezel)secondHand(w, t)io.WriteString(w, svgEnd)}func secondHand(w io.Writer, t time.Time) {p := secondHandPoint(t)p = Point{p.X * secondHandLength, p.Y * secondHandLength} // scalep = Point{p.X, -p.Y} // flipp = Point{p.X + clockCentreX, p.Y + clockCentreY} // translatefmt.Fprintf(w, `<line x1="150" y1="150" x2="%.3f" y2="%.3f" style="fill:none;stroke:#f00;stroke-width:3px;"/>`, p.X, p.Y)}const svgStart = `<?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">`const bezel = `<circle cx="150" cy="150" r="100" style="fill:#fff;stroke:#000;stroke-width:5px;"/>`const svgEnd = `</svg>`

The most beautiful SVG writer? No. But hopefully it'll do the job...

--- FAIL: TestSVGWriterAtMidnight (0.00s)clockface_acceptance_test.go:56: Expected to find the second hand with x2 of 150 and y2 of 60, in the SVG output <?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"><circle cx="150" cy="150" r="100" style="fill:#fff;stroke:#000;stroke-width:5px;"/><line x1="150" y1="150" x2="150.000000" y2="60.000000" style="fill:none;stroke:#f00;stroke-width:3px;"/></svg>FAILexit status 1FAIL github.com/gypsydave5/learn-go-with-tests/math/v7b/clockface 0.008s

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.

s := fmt.Sprintf(`<line x1="150" y1="150" x2="%.3f" y2="%.3f" style="fill:none;stroke:#f00;stroke-width:3px;"/>`, p.X, p.Y)

And after we update our expectations in the test

x2 := "150.000"y2 := "60.000"

We get:

PASSok github.com/gypsydave5/learn-go-with-tests/math/v7b/clockface 0.006s

We can now shorten our `main`

function:

package mainimport ("os""time""github.com/gypsydave5/learn-go-with-tests/math/v7b/clockface")func main() {t := time.Now()clockface.SVGWriter(os.Stdout, t)}

And we can write a test for another time following the same pattern, but not before...

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 really`strings`

are they? They'renumbers!

Do I really care about the

`style`

of the hand? Or, for that matter, theempty

`Text`

node that's been generated by`zak`

?

We can do better. Let's make a few adjustments to the `Svg`

struct, and the tests, to sharpen everything up.

type SVG struct {XMLName xml.Name `xml:"svg"`Xmlns string `xml:"xmlns,attr"`Width float64 `xml:"width,attr"`Height float64 `xml:"height,attr"`ViewBox string `xml:"viewBox,attr"`Version string `xml:"version,attr"`Circle Circle `xml:"circle"`Line []Line `xml:"line"`}type Circle struct {Cx float64 `xml:"cx,attr"`Cy float64 `xml:"cy,attr"`R float64 `xml:"r,attr"`}type Line struct {X1 float64 `xml:"x1,attr"`Y1 float64 `xml:"y1,attr"`X2 float64 `xml:"x2,attr"`Y2 float64 `xml:"y2,attr"`}

Here I've

Made the important parts of the struct named types -- the

`Line`

and the`Circle`

Turned the numeric attributes into

`float64`

s instead of`string`

s.Deleted unused attributes like

`Style`

and`Text`

Renamed

`Svg`

to`SVG`

because*it's the right thing to do*.

This will let us assert more precisely on the line we're looking for:

func TestSVGWriterAtMidnight(t *testing.T) {tm := time.Date(1337, time.January, 1, 0, 0, 0, 0, time.UTC)b := bytes.Buffer{}clockface.SVGWriter(&b, tm)svg := SVG{}xml.Unmarshal(b.Bytes(), &svg)want := Line{150, 150, 150, 60}for _, line := range svg.Line {if line == want {return}}t.Errorf("Expected to find the second hand line %+v, in the SVG lines %+v", want, svg.Line)}

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:

func TestSVGWriterSecondHand(t *testing.T) {cases := []struct {time time.Timeline Line}{{simpleTime(0, 0, 0),Line{150, 150, 150, 60},},{simpleTime(0, 0, 30),Line{150, 150, 150, 240},},}for _, c := range cases {t.Run(testName(c.time), func(t *testing.T) {b := bytes.Buffer{}clockface.SVGWriter(&b, c.time)svg := SVG{}xml.Unmarshal(b.Bytes(), &svg)if !containsLine(c.line, svg.Line) {t.Errorf("Expected to find the second hand line %+v, in the SVG lines %+v", c.line, svg.Line)}})}}

Now *that's* what I call an acceptance test!

So that's the second hand done. Now let's get started on the minute hand.

func TestSVGWriterMinutedHand(t *testing.T) {cases := []struct {time time.Timeline Line}{{simpleTime(0, 0, 0),Line{150, 150, 150, 70},},}for _, c := range cases {t.Run(testName(c.time), func(t *testing.T) {b := bytes.Buffer{}clockface.SVGWriter(&b, c.time)svg := SVG{}xml.Unmarshal(b.Bytes(), &svg)if !containsLine(c.line, svg.Line) {t.Errorf("Expected to find the minute hand line %+v, in the SVG lines %+v", c.line, svg.Line)}})}}

--- FAIL: TestSVGWriterMinutedHand (0.00s)--- FAIL: TestSVGWriterMinutedHand/00:00:00 (0.00s)clockface_acceptance_test.go:87: Expected to find the minute hand line {X1:150 Y1:150 X2:150 Y2:70}, in the SVG lines [{X1:150 Y1:150 X2:150 Y2:60}]FAILexit status 1FAIL github.com/gypsydave5/learn-go-with-tests/math/v8/clockface 0.007s

We'd better start building some other clockhands, 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:

func TestMinutesInRadians(t *testing.T) {cases := []struct {time time.Timeangle float64}{{simpleTime(0, 30, 0), math.Pi},}for _, c := range cases {t.Run(testName(c.time), func(t *testing.T) {got := minutesInRadians(c.time)if got != c.angle {t.Fatalf("Wanted %v radians, but got %v", c.angle, got)}})}}

# github.com/gypsydave5/learn-go-with-tests/math/v8/clockface [github.com/gypsydave5/learn-go-with-tests/math/v8/clockface.test]./clockface_test.go:59:11: undefined: minutesInRadiansFAIL github.com/gypsydave5/learn-go-with-tests/math/v8/clockface [build failed]

func minutesInRadians(t time.Time) float64 {return math.Pi}

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.

func TestMinutesInRadians(t *testing.T) {cases := []struct {time time.Timeangle float64}{{simpleTime(0, 30, 0), math.Pi},{simpleTime(0, 0, 7), 7 * (math.Pi / (30 * 60))},}for _, c := range cases {t.Run(testName(c.time), func(t *testing.T) {got := minutesInRadians(c.time)if got != c.angle {t.Fatalf("Wanted %v radians, but got %v", c.angle, got)}})}}

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.

--- FAIL: TestMinutesInRadians (0.00s)--- FAIL: TestMinutesInRadians/00:00:07 (0.00s)clockface_test.go:62: Wanted 0.012217304763960306 radians, but got 3.141592653589793FAILexit status 1FAIL github.com/gypsydave5/learn-go-with-tests/math/v8/clockface 0.009s

In the immortal words of Jennifer Aniston: Here comes the science bit

func minutesInRadians(t time.Time) float64 {return (secondsInRadians(t) / 60) +(math.Pi / (30 / float64(t.Minute())))}

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.

secondsInRadians(t) / 60

Then we just add on the movement for the minutes - similar to the movement of the second hand.

math.Pi / (30 / float64(t.Minute()))

And...

PASSok github.com/gypsydave5/learn-go-with-tests/math/v8/clockface 0.007s

Nice and easy.

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.

func TestMinuteHandPoint(t *testing.T) {cases := []struct {time time.Timepoint Point}{{simpleTime(0, 30, 0), Point{0, -1}},}for _, c := range cases {t.Run(testName(c.time), func(t *testing.T) {got := minuteHandPoint(c.time)if !roughlyEqualPoint(got, c.point) {t.Fatalf("Wanted %v Point, but got %v", c.point, got)}})}}

# github.com/gypsydave5/learn-go-with-tests/math/v9/clockface [github.com/gypsydave5/learn-go-with-tests/math/v9/clockface.test]./clockface_test.go:79:11: undefined: minuteHandPointFAIL github.com/gypsydave5/learn-go-with-tests/math/v9/clockface [build failed]

func minuteHandPoint(t time.Time) Point {return Point{}}

--- FAIL: TestMinuteHandPoint (0.00s)--- FAIL: TestMinuteHandPoint/00:30:00 (0.00s)clockface_test.go:80: Wanted {0 -1} Point, but got {0 0}FAILexit status 1FAIL github.com/gypsydave5/learn-go-with-tests/math/v9/clockface 0.007s

func minuteHandPoint(t time.Time) Point {return Point{0, -1}}

PASSok github.com/gypsydave5/learn-go-with-tests/math/v9/clockface 0.007s

And now for some actual work

func TestMinuteHandPoint(t *testing.T) {cases := []struct {time time.Timepoint Point}{{simpleTime(0, 30, 0), Point{0, -1}},{simpleTime(0, 45, 0), Point{-1, 0}},}for _, c := range cases {t.Run(testName(c.time), func(t *testing.T) {got := minuteHandPoint(c.time)if !roughlyEqualPoint(got, c.point) {t.Fatalf("Wanted %v Point, but got %v", c.point, got)}})}}

--- FAIL: TestMinuteHandPoint (0.00s)--- FAIL: TestMinuteHandPoint/00:45:00 (0.00s)clockface_test.go:81: Wanted {-1 0} Point, but got {0 -1}FAILexit status 1FAIL github.com/gypsydave5/learn-go-with-tests/math/v9/clockface 0.007s

A quick copy and paste of the `secondHandPoint`

function with some minor changes ought to do it...

func minuteHandPoint(t time.Time) Point {angle := minutesInRadians(t)x := math.Sin(angle)y := math.Cos(angle)return Point{x, y}}

PASSok github.com/gypsydave5/learn-go-with-tests/math/v9/clockface 0.009s

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.

func angleToPoint(angle float64) Point {x := math.Sin(angle)y := math.Cos(angle)return Point{x, y}}

and we can rewrite `minuteHandPoint`

and `secondHandPoint`

as one liners:

func minuteHandPoint(t time.Time) Point {return angleToPoint(minutesInRadians(t))}

func secondHandPoint(t time.Time) Point {return angleToPoint(secondsInRadians(t))}

PASSok github.com/gypsydave5/learn-go-with-tests/math/v9/clockface 0.007s

Now we can uncomment the acceptance test and get to work drawing the minute hand

Another quick copy-and-paste with some minor adjustments

func minuteHand(w io.Writer, t time.Time) {p := minuteHandPoint(t)p = Point{p.X * minuteHandLength, p.Y * minuteHandLength}p = Point{p.X, -p.Y}p = Point{p.X + clockCentreX, p.Y + clockCentreY}fmt.Fprintf(w, `<line x1="150" y1="150" x2="%.3f" y2="%.3f" style="fill:none;stroke:#000;stroke-width:3px;"/>`, p.X, p.Y)}

PASSok github.com/gypsydave5/learn-go-with-tests/math/v9/clockface 0.006s

But the proof of the pudding is in the eating - if we now compile and run our `clockface`

program, we should see something like

Let's remove the duplication from the `secondHand`

and `minuteHand`

functions, putting all of that scale, flip and translate logic all in one place.

func secondHand(w io.Writer, t time.Time) {p := makeHand(secondHandPoint(t), secondHandLength)fmt.Fprintf(w, `<line x1="150" y1="150" x2="%.3f" y2="%.3f" style="fill:none;stroke:#f00;stroke-width:3px;"/>`, p.X, p.Y)}func minuteHand(w io.Writer, t time.Time) {p := makeHand(minuteHandPoint(t), minuteHandLength)fmt.Fprintf(w, `<line x1="150" y1="150" x2="%.3f" y2="%.3f" style="fill:none;stroke:#000;stroke-width:3px;"/>`, p.X, p.Y)}func makeHand(p Point, length float64) Point {p = Point{p.X * length, p.Y * length}p = Point{p.X, -p.Y}return Point{p.X + clockCentreX, p.Y + clockCentreY}}

PASSok github.com/gypsydave5/learn-go-with-tests/math/v9/clockface 0.007s

There... now it's just the hour hand to do!

func TestSVGWriterHourHand(t *testing.T) {cases := []struct {time time.Timeline Line}{{simpleTime(6, 0, 0),Line{150, 150, 150, 200},},}for _, c := range cases {t.Run(testName(c.time), func(t *testing.T) {b := bytes.Buffer{}clockface.SVGWriter(&b, c.time)svg := SVG{}xml.Unmarshal(b.Bytes(), &svg)if !containsLine(c.line, svg.Line) {t.Errorf("Expected to find the hour hand line %+v, in the SVG lines %+v", c.line, svg.Line)}})}}

--- FAIL: TestSVGWriterHourHand (0.00s)--- FAIL: TestSVGWriterHourHand/06:00:00 (0.00s)clockface_acceptance_test.go:113: Expected to find the hour hand line {X1:150 Y1:150 X2:150 Y2:200}, in the SVG lines [{X1:150 Y1:150 X2:150 Y2:60} {X1:150 Y1:150 X2:150 Y2:70}]FAILexit status 1FAIL github.com/gypsydave5/learn-go-with-tests/math/v10/clockface 0.013s

Again, let's comment this one out until we've got the some coverage with the lower level tests:

func TestHoursInRadians(t *testing.T) {cases := []struct {time time.Timeangle float64}{{simpleTime(6, 0, 0), math.Pi},}for _, c := range cases {t.Run(testName(c.time), func(t *testing.T) {got := hoursInRadians(c.time)if got != c.angle {t.Fatalf("Wanted %v radians, but got %v", c.angle, got)}})}}

# github.com/gypsydave5/learn-go-with-tests/math/v10/clockface [github.com/gypsydave5/learn-go-with-tests/math/v10/clockface.test]./clockface_test.go:97:11: undefined: hoursInRadiansFAIL github.com/gypsydave5/learn-go-with-tests/math/v10/clockface [build failed]

func hoursInRadians(t time.Time) float64 {return math.Pi}

PASSok github.com/gypsydave5/learn-go-with-tests/math/v10/clockface 0.007s

func TestHoursInRadians(t *testing.T) {cases := []struct {time time.Timeangle float64}{{simpleTime(6, 0, 0), math.Pi},{simpleTime(0, 0, 0), 0},}for _, c := range cases {t.Run(testName(c.time), func(t *testing.T) {got := hoursInRadians(c.time)if got != c.angle {t.Fatalf("Wanted %v radians, but got %v", c.angle, got)}})}}

--- FAIL: TestHoursInRadians (0.00s)--- FAIL: TestHoursInRadians/00:00:00 (0.00s)clockface_test.go:100: Wanted 0 radians, but got 3.141592653589793FAILexit status 1FAIL github.com/gypsydave5/learn-go-with-tests/math/v10/clockface 0.007s

func hoursInRadians(t time.Time) float64 {return (math.Pi / (6 / float64(t.Hour())))}

func TestHoursInRadians(t *testing.T) {cases := []struct {time time.Timeangle float64}{{simpleTime(6, 0, 0), math.Pi},{simpleTime(0, 0, 0), 0},{simpleTime(21, 0, 0), math.Pi * 1.5},}for _, c := range cases {t.Run(testName(c.time), func(t *testing.T) {got := hoursInRadians(c.time)if got != c.angle {t.Fatalf(</