diff --git a/math.md b/math.md
new file mode 100644
index 000000000..16768a8c7
--- /dev/null
+++ b/math.md
@@ -0,0 +1,2173 @@
+# Mathematics
+
+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:
+
+![an svg of a clock](math/example_clock.svg)
+
+is described like this:
+
+```xml
+
+
+
+```
+
+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. 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`][texttemplate] 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:
+
+```go
+package clockface_test
+
+import (
+ "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.
+
+### Try to run the test
+
+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.SecondHand
+FAIL 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.
+
+### 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
+
+```go
+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 github.com/gypsydave5/learn-go-with-tests/math/v1/clockface 0.006s
+```
+
+### Write enough code to make it pass
+
+When we get the expected failure, we can fill in the return value of `HandsAt`:
+
+```go
+// 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 github.com/gypsydave5/learn-go-with-tests/math/v1/clockface 0.006s
+```
+
+### 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
+
+```go
+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?
+
+## 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`.
+
+![picture of the unit circle](math/images/unit_circle.png)
+
+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
+
+![picture of the unit circle with a point defined on the circumference](math/images/unit_circle_coords.png)
+
+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](math/images/unit_circle_params.png)
+
+(If you don't believe this, [go and look at Wikipedia...][circle])
+
+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](math/images/unit_circle_12_oclock.png)
+
+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`
+
+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`][mathcos]:
+
+> 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.[^2]
+
+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 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.
+
+```go
+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 π.
+
+### Try to run the test
+
+```
+# 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: secondsInRadians
+FAIL github.com/gypsydave5/learn-go-with-tests/math/v2/clockface [build failed]
+```
+
+### Write the minimal amount of code for the test to run and check the failing test output
+
+```go
+func secondsInRadians(t time.Time) float64 {
+ return 0
+}
+```
+
+```
+--- FAIL: TestSecondsInRadians (0.00s)
+ clockface_test.go:15: Wanted 3.141592653589793 radians, but got 0
+FAIL
+exit status 1
+FAIL github.com/gypsydave5/learn-go-with-tests/math/v2/clockface 0.007s
+```
+
+### Write enough code to make it pass
+
+```go
+func secondsInRadians(t time.Time) float64 {
+ return math.Pi
+}
+```
+
+```
+PASS
+ok github.com/gypsydave5/learn-go-with-tests/math/v2/clockface 0.011s
+```
+
+### 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.
+
+```go
+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).[^1]
+
+```go
+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.141592653589793
+FAIL
+exit status 1
+FAIL 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:
+
+```go
+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.1415926535897936
+FAIL
+exit status 1
+FAIL github.com/gypsydave5/learn-go-with-tests/math/v3/clockface 0.006s
+```
+
+Wait, what?
+
+### Floats are horrible
+
+Floating point arithmetic is [notoriously inaccurate][floatingpoint]. 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 the 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 infinitessimal 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:
+
+```go
+func secondsinradians(t time.time) float64 {
+ return (math.Pi / (30 / (float64(t.Second()))))
+}
+```
+
+And we get a pass.
+
+```
+PASS
+ok github.com/gypsydave5/learn-go-with-tests/math/v2/clockface 0.005s
+```
+
+
+
+### 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
+
+```go
+func TestSecondHandVector(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ point 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)
+ }
+ })
+ }
+}
+```
+
+### Try to run the test
+
+```
+# 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: secondHandPoint
+FAIL github.com/gypsydave5/learn-go-with-tests/math/v4/clockface [build failed]
+```
+
+### Write the minimal amount of code for the test to run and check the failing test output
+
+```go
+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}
+FAIL
+exit status 1
+FAIL github.com/gypsydave5/learn-go-with-tests/math/v4/clockface 0.010s
+```
+
+### Write enough code to make it pass
+
+```go
+func secondHandPoint(t time.Time) Point {
+ return Point{0, -1}
+}
+```
+
+```
+PASS
+ok github.com/gypsydave5/learn-go-with-tests/math/v4/clockface 0.007s
+```
+
+### Repeat for new requirements
+
+```go
+func TestSecondHandPoint(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ point 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)
+ }
+ })
+ }
+}
+```
+
+### Try to run the test
+
+```
+--- FAIL: TestSecondHandPoint (0.00s)
+ --- FAIL: TestSecondHandPoint/00:00:45 (0.00s)
+ clockface_test.go:43: Wanted {-1 0} Point, but got {0 -1}
+FAIL
+exit status 1
+FAIL github.com/gypsydave5/learn-go-with-tests/math/v4/clockface 0.006s
+```
+
+### Write enough code to make it pass
+
+Remember our unit circle picture?
+
+![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](math/images/unit_circle_params.png)
+
+We now want the equation that produces X and Y. Let's write it into seconds:
+
+```go
+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}
+FAIL
+exit status 1
+FAIL 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 _infinitessimal_ - 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.
+
+```go
+func TestSecondHandPoint(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ point 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-7
+ return 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
+
+```
+PASS
+ok github.com/gypsydave5/learn-go-with-tests/math/v4/clockface 0.007s
+```
+
+### Refactor
+
+I'm still pretty happy with this.
+
+
+
+
+### Repeat for new requirements
+
+Well, saying _new_ isn't enirely accurate - really what we can do now is get
+that acceptance test passing! Let's remind ourselves of what it looks like:
+
+
+```go
+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)
+ }
+}
+```
+
+### Try to run the test
+
+
+```
+--- FAIL: TestSecondHandAt30Seconds (0.00s)
+ clockface_acceptance_test.go:28: Got {150 60}, wanted {150 240}
+FAIL
+exit status 1
+FAIL github.com/gypsydave5/learn-go-with-tests/math/v5/clockface 0.007s
+```
+
+### Write enough code to make it pass
+
+We need to do three things to convert our unit vector into a point on the SVG:
+
+1. Scale it to the length of the hand
+2. Flip it over the X axis because to account for the SVG having an origin in
+ the top left hand corner
+3. Translate it to the right position (so that it's coming from an origin of
+ (150,150))
+
+Fun times!
+
+```go
+// 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} // scale
+ p = Point{p.X, -p.Y} // flip
+ p = Point{p.X + 150, p.Y + 150} // translate
+ return p
+}
+```
+
+Scale, flip, and translated in exactly that order. Hooray maths!
+
+```
+PASS
+ok github.com/gypsydave5/learn-go-with-tests/math/v5/clockface 0.007s
+```
+
+### Refactor
+
+There's a few magic numbers here that should get pulled out as constants, so
+let's do that
+
+```go
+const secondHandLength = 90
+const clockCentreX = 150
+const 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} //translate
+ return p
+}
+```
+
+## 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:
+
+```
+├── clockface
+│ └── main.go
+├── clockface.go
+├── clockface_acceptance_test.go
+└── clockface_test.go
+```
+
+and inside `main.go`
+
+```go
+package main
+
+import (
+ "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(``, p.X, p.Y)
+}
+
+const svgStart = `
+
+`
+```
+
+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
+
+![a clock with only a second hand](math/v6/clockface/clockface/clock.svg)
+
+
+
+
+### Refactor
+
+This stinks. Well, it doesn't quite _stink_ stink, but I'm not happy about it.
+
+1. That whole `SecondHand` function is _super_ tied to being an SVG... without
+ mentioning SVGs or actually producing an SVG...
+2. ... 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:
+
+```go
+func TestSVGWriterAtMidnight(t *testing.T) {
+ tm := time.Date(1337, time.January, 1, 0, 0, 0, 0, time.UTC)
+
+ var b strings.Builder
+ clockface.SVGWriter(&b, tm)
+ got := b.String()
+
+ want := ``, p.X, p.Y)
+}
+
+const svgStart = `
+
+`
+```
+
+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
+
+
+FAIL
+exit status 1
+FAIL 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.
+
+```go
+s := fmt.Sprintf(``, p.X, p.Y)
+```
+
+And after we update our expectations in the test
+
+```go
+ x2 := "150.000"
+ y2 := "60.000"
+```
+
+We get:
+
+```
+PASS
+ok github.com/gypsydave5/learn-go-with-tests/math/v7b/clockface 0.006s
+```
+
+We can now shorten our `main` function:
+
+```go
+package main
+
+import (
+ "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...
+
+
+
+### Refactor
+
+Three things stick out:
+
+1. We're not really testing for all of the information we need to ensure is
+ present - what about the `x1` values, for instance?
+2. Also, those attributes for `x1` etc. aren't really `strings` are they? They're
+ numbers!
+3. Do I really care about the `style` of the hand? Or, for that matter, the
+ empty `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.
+
+```go
+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:
+
+```go
+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:
+
+```go
+func TestSVGWriterSecondHand(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ line 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!
+
+
+
+### Write the test first
+
+So that's the second hand done. Now let's get started on the minute hand.
+
+```go
+func TestSVGWriterMinutedHand(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ line 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)
+ }
+ })
+ }
+}
+```
+
+### Try to run the test
+
+```
+--- 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}]
+FAIL
+exit status 1
+FAIL 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:
+
+```go
+func TestMinutesInRadians(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ angle 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)
+ }
+ })
+ }
+}
+```
+
+### Try to run the test
+
+```
+# 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: minutesInRadians
+FAIL github.com/gypsydave5/learn-go-with-tests/math/v8/clockface [build failed]
+```
+
+### Write the minimal amount of code for the test to run and check the failing test output
+
+```go
+func minutesInRadians(t time.Time) float64 {
+ return math.Pi
+}
+```
+
+### 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.
+
+```go
+func TestMinutesInRadians(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ angle 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.
+
+### Try to run the test
+
+```go
+--- FAIL: TestMinutesInRadians (0.00s)
+ --- FAIL: TestMinutesInRadians/00:00:07 (0.00s)
+ clockface_test.go:62: Wanted 0.012217304763960306 radians, but got 3.141592653589793
+FAIL
+exit status 1
+FAIL github.com/gypsydave5/learn-go-with-tests/math/v8/clockface 0.009s
+```
+
+### Write enough code to make it pass
+
+In the immortal words of Jennifer Aniston: [Here comes the science
+bit](https://www.youtube.com/watch?v=29Im23SPNok)
+
+```go
+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.
+
+```go
+secondsInRadians(t) / 60
+```
+
+Then we just add on the movement for the minutes - similar to the movement of
+the second hand.
+
+```go
+math.Pi / (30 / float64(t.Minute()))
+```
+
+And...
+
+```go
+PASS
+ok github.com/gypsydave5/learn-go-with-tests/math/v8/clockface 0.007s
+```
+
+Nice and easy.
+
+### 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,[^3] 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
+
+```go
+func TestMinuteHandPoint(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ point 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)
+ }
+ })
+ }
+}
+```
+
+### Try to run the test
+
+```
+# 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: minuteHandPoint
+FAIL github.com/gypsydave5/learn-go-with-tests/math/v9/clockface [build failed]
+```
+
+### Write the minimal amount of code for the test to run and check the failing test output
+
+```go
+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}
+FAIL
+exit status 1
+FAIL github.com/gypsydave5/learn-go-with-tests/math/v9/clockface 0.007s
+```
+
+### Write enough code to make it pass
+
+```go
+func minuteHandPoint(t time.Time) Point {
+ return Point{0, -1}
+}
+```
+
+```
+PASS
+ok github.com/gypsydave5/learn-go-with-tests/math/v9/clockface 0.007s
+```
+
+### Repeat for new requirements
+
+And now for some actual work
+
+```go
+func TestMinuteHandPoint(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ point 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}
+FAIL
+exit status 1
+FAIL github.com/gypsydave5/learn-go-with-tests/math/v9/clockface 0.007s
+```
+
+### Write enough code to make it pass
+
+A quick copy and paste of the `secondHandPoint` function with some minor changes
+ought to do it...
+
+```go
+func minuteHandPoint(t time.Time) Point {
+ angle := minutesInRadians(t)
+ x := math.Sin(angle)
+ y := math.Cos(angle)
+
+ return Point{x, y}
+}
+```
+
+```
+PASS
+ok github.com/gypsydave5/learn-go-with-tests/math/v9/clockface 0.009s
+```
+
+### 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.
+
+```go
+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:
+
+```go
+func minuteHandPoint(t time.Time) Point {
+ return angleToPoint(minutesInRadians(t))
+}
+```
+
+```go
+func secondHandPoint(t time.Time) Point {
+ return angleToPoint(secondsInRadians(t))
+}
+```
+
+```
+PASS
+ok 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
+
+### Write enough code to make it pass
+
+Another quick copy-and-paste with some minor adjustments
+
+```go
+func minuteHand(w io.Writer, t time.Time) {
+ p := minuteHandPoint(t)
+ p = Point{p.X * secondHandLength, p.Y * secondHandLength}
+ p = Point{p.X, -p.Y}
+ p = Point{p.X + clockCentreX, p.Y + clockCentreY}
+ fmt.Fprintf(w, ``, p.X, p.Y)
+}
+```
+
+```
+PASS
+ok 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
+
+![a clock with only a second hand](math/v9/clockface/clockface/clock.svg)
+
+### 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.
+
+```go
+func secondHand(w io.Writer, t time.Time) {
+ p := makeHand(secondHandPoint(t), secondHandLength)
+ fmt.Fprintf(w, ``, p.X, p.Y)
+}
+
+func minuteHand(w io.Writer, t time.Time) {
+ p := makeHand(minuteHandPoint(t), minuteHandLength)
+ fmt.Fprintf(w, ``, 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}
+}
+```
+
+```
+PASS
+ok github.com/gypsydave5/learn-go-with-tests/math/v9/clockface 0.007s
+```
+
+There... now it's just the hour hand to do!
+
+
+
+### Write the test first
+
+```go
+func TestSVGWriterHourHand(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ line 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 minute hand line %+v, in the SVG lines %+v", c.line, svg.Line)
+ }
+ })
+ }
+}
+```
+
+### Try to run the test
+
+```
+--- FAIL: TestSVGWriterHourHand (0.00s)
+ --- FAIL: TestSVGWriterHourHand/06:00:00 (0.00s)
+ clockface_acceptance_test.go:113: Expected to find the minute 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}]
+FAIL
+exit status 1
+FAIL 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:
+
+### Write the test first
+
+```go
+func TestHoursInRadians(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ angle 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)
+ }
+ })
+ }
+}
+```
+
+### Try to run the test
+
+```
+# 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: hoursInRadians
+FAIL github.com/gypsydave5/learn-go-with-tests/math/v10/clockface [build failed]
+```
+
+### Write the minimal amount of code for the test to run and check the failing test output
+
+```go
+func hoursInRadians(t time.Time) float64 {
+ return math.Pi
+}
+```
+
+```
+PASS
+ok github.com/gypsydave5/learn-go-with-tests/math/v10/clockface 0.007s
+```
+
+### Repeat for new requirements
+
+```go
+func TestHoursInRadians(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ angle 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)
+ }
+ })
+ }
+}
+```
+
+### Try to run the test
+
+```
+--- FAIL: TestHoursInRadians (0.00s)
+ --- FAIL: TestHoursInRadians/00:00:00 (0.00s)
+ clockface_test.go:100: Wanted 0 radians, but got 3.141592653589793
+FAIL
+exit status 1
+FAIL github.com/gypsydave5/learn-go-with-tests/math/v10/clockface 0.007s
+```
+
+### Write enough code to make it pass
+
+```go
+func hoursInRadians(t time.Time) float64 {
+ return (math.Pi / (6 / float64(t.Hour())))
+}
+```
+
+### Repeat for new requirements
+
+```
+func TestHoursInRadians(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ angle 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("Wanted %v radians, but got %v", c.angle, got)
+ }
+ })
+ }
+}
+```
+
+### Try to run the test
+
+```
+--- FAIL: TestHoursInRadians (0.00s)
+ --- FAIL: TestHoursInRadians/21:00:00 (0.00s)
+ clockface_test.go:101: Wanted 4.71238898038469 radians, but got 10.995574287564276
+FAIL
+exit status 1
+FAIL github.com/gypsydave5/learn-go-with-tests/math/v10/clockface 0.014s
+```
+
+### Write enough code to make it pass
+
+```go
+func hoursInRadians(t time.Time) float64 {
+ return (math.Pi / (6 / (float64(t.Hour() % 12))))
+}
+```
+
+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.
+
+```
+PASS
+ok github.com/gypsydave5/learn-go-with-tests/math/v10/clockface 0.008s
+```
+### Write the test first
+
+Now let's try and move the hour hand around the clocface based on the minutes
+and the seconds that have passed.
+
+```go
+func TestHoursInRadians(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ angle float64
+ }{
+ {simpleTime(6, 0, 0), math.Pi},
+ {simpleTime(0, 0, 0), 0},
+ {simpleTime(21, 0, 0), math.Pi * 1.5},
+ {simpleTime(0, 1, 30), math.Pi / ((6 * 60 * 60) / 90)},
+ }
+
+ 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)
+ }
+ })
+ }
+}
+```
+
+### Try to run the test
+
+```
+--- FAIL: TestHoursInRadians (0.00s)
+ --- FAIL: TestHoursInRadians/00:01:30 (0.00s)
+ clockface_test.go:102: Wanted 0.013089969389957472 radians, but got 0
+FAIL
+exit status 1
+FAIL github.com/gypsydave5/learn-go-with-tests/math/v10/clockface 0.007s
+```
+
+### 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:
+
+```go
+func hoursInRadians(t time.Time) float64 {
+ return (minutesInRadians(t) / 12) +
+ (math.Pi / (6 / float64(t.Hour()%12)))
+}
+```
+
+and behold:
+
+```
+--- FAIL: TestHoursInRadians (0.00s)
+ --- FAIL: TestHoursInRadians/00:01:30 (0.00s)
+ clockface_test.go:104: Wanted 0.013089969389957472 radians, but got 0.01308996938995747
+FAIL
+exit status 1
+FAIL github.com/gypsydave5/learn-go-with-tests/math/v10/clockface 0.007s
+```
+
+AAAAARGH BLOODY FLOATING POINT ARITHMETIC!
+
+Let's update our test to use `roughlyEqualFloat64` for the comparison of the
+angles.
+
+```go
+func TestHoursInRadians(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ angle float64
+ }{
+ {simpleTime(6, 0, 0), math.Pi},
+ {simpleTime(0, 0, 0), 0},
+ {simpleTime(21, 0, 0), math.Pi * 1.5},
+ {simpleTime(0, 1, 30), math.Pi / ((6 * 60 * 60) / 90)},
+ }
+
+ for _, c := range cases {
+ t.Run(testName(c.time), func(t *testing.T) {
+ got := hoursInRadians(c.time)
+ if !roughlyEqualFloat64(got, c.angle) {
+ t.Fatalf("Wanted %v radians, but got %v", c.angle, got)
+ }
+ })
+ }
+}
+```
+
+```
+PASS
+ok github.com/gypsydave5/learn-go-with-tests/math/v10/clockface 0.007s
+```
+
+### Refactor
+
+If we're going to use `roughlyEqualFloat64` in _one_ of our radians tests, we
+should probably use it for _all_ of them. That's a nice and simple refactor.
+
+
+
+## Hour Hand Point
+
+Right, it's time to calculate where the hour hand point is going to go by
+working out the unit vector.
+
+### Write the test first
+
+```go
+func TestHourHandPoint(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ point Point
+ }{
+ {simpleTime(6, 0, 0), Point{0, -1}},
+ {simpleTime(21, 0, 0), Point{-1, 0}},
+ }
+
+ for _, c := range cases {
+ t.Run(testName(c.time), func(t *testing.T) {
+ got := hourHandPoint(c.time)
+ if !roughlyEqualPoint(got, c.point) {
+ t.Fatalf("Wanted %v Point, but got %v", c.point, got)
+ }
+ })
+ }
+}
+```
+
+Wait, am I just going to throw _two_ test cases out there _at once_? Isn't this _bad TDD_?
+
+### On TDD Zealotry
+
+Test driven development is not a religion. Some people might
+act like it is - usually people who don't do TDD but who are happy to moan
+on Twitter or Dev.to that it's only done by zealots and that they're 'being
+pragmatic' when they don't write tests. But it's not a religion. It's tool.
+
+I _know_ what the two tests are going to be - I've tested two other clock hands
+in exactly the same way - and I already know what my implementation is going to
+be - I wrote a function for the general case of changing an angle into a point
+in the minute hand iteration.
+
+I'm not going to plough through TDD ceremony for the sake of it. Tests are
+a tool to help me write better code. TDD is a technique to help me write better
+code. Neither tests nor TDD are an end in themselves.
+
+My confidence has increased, so I feel I can make larger strides forward. I'm
+going to 'skip' a few steps, because I know where I am, I know where I'm going
+and I've been down this road before.
+
+But also note: I'm not skipping writing the tests entirely.
+
+### Try to run the test
+
+```
+# github.com/gypsydave5/learn-go-with-tests/math/v11/clockface [github.com/gypsydave5/learn-go-with-tests/math/v11/clockface.test]
+./clockface_test.go:119:11: undefined: hourHandPoint
+FAIL github.com/gypsydave5/learn-go-with-tests/math/v11/clockface [build failed]
+```
+
+### Write enough code to make it pass
+
+```go
+func hourHandPoint(t time.Time) Point {
+ return angleToPoint(hoursInRadians(t))
+}
+```
+
+As I said, I know where I am and I know where I'm going. Why pretend otherwise?
+The tests will soon tell me if I'm wrong.
+
+```
+PASS
+ok github.com/gypsydave5/learn-go-with-tests/math/v11/clockface 0.009s
+```
+
+
+## Draw the hour hand
+
+And finally we get to draw in the hour hand. We can bring in that acceptance
+test by uncommenting it:
+
+```go
+func TestSVGWriterHourHand(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ line 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 minute hand line %+v, in the SVG lines %+v", c.line, svg.Line)
+ }
+ })
+ }
+}
+```
+
+### Try to run the test
+
+```
+--- FAIL: TestSVGWriterHourHand (0.00s)
+ --- FAIL: TestSVGWriterHourHand/06:00:00 (0.00s)
+ clockface_acceptance_test.go:113: Expected to find the minute 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}]
+FAIL
+exit status 1
+FAIL github.com/gypsydave5/learn-go-with-tests/math/v10/clockface 0.013s
+```
+
+### Write enough code to make it pass
+
+And we can now make our final adjustments to `svgWriter.go`
+
+```go
+const (
+ secondHandLength = 90
+ minuteHandLength = 80
+ hourHandLength = 50
+ clockCentreX = 150
+ clockCentreY = 150
+)
+
+//SVGWriter writes an SVG representation of an analogue clock, showing the time t, to the writer w
+func SVGWriter(w io.Writer, t time.Time) {
+ io.WriteString(w, svgStart)
+ io.WriteString(w, bezel)
+ secondHand(w, t)
+ minuteHand(w, t)
+ hourHand(w, t)
+ io.WriteString(w, svgEnd)
+}
+
+// ...
+
+func hourHand(w io.Writer, t time.Time) {
+ p := makeHand(hourHandPoint(t), hourHandLength)
+ fmt.Fprintf(w, ``, p.X, p.Y)
+}
+
+```
+
+and so...
+
+```
+PASS
+ok github.com/gypsydave5/learn-go-with-tests/math/v12/clockface 0.007s
+```
+
+Let's just check by compiling and running our `clockface` program.
+
+![a clock](math/v12/clockface/clockface/clock.svg)
+
+### Refactor
+
+Looking at `clockface.go`, there are a few 'magic numbers' floating about. They
+are all based around how many hours/minutes/seconds there are in a half-turn
+around a clockface. Let's refactor so that we make explicit their meaning.
+
+```go
+const (
+ secondsInHalfClock = 30
+ secondsInClock = 2 * secondsInHalfClock
+ minutesInHalfClock = 30
+ minutesInClock = 2 * minutesInHalfClock
+ hoursInHalfClock = 6
+ hoursInClock = 2 * hoursInHalfClock
+)
+```
+
+Why do this? Well, it makes explicit what each number _means_ in the equation.
+If - _when_ - we come back to this code, these names will help us to understand
+what's going on.
+
+Moreover, should we ever want to make some really, really WEIRD clocks - ones
+with 4 hours for the hour hand, and 20 seconds for the second hand say - these
+constants could easily become parameters. We're helping to leave that door open
+(even if we never go through it).
+
+## Wrapping up
+
+Do we need to do anything else?
+
+First, let's pat ourselves on the back - we've written a program that makes an
+SVG clockface. It works and it's great. It will only ever make one sort of
+clockface - but that's fine! Maybe you only _want_ one sort of clockface.
+There's nothing wrong with a program that solves a specific problem and nothing
+else.
+
+### A Program... and a Library
+
+But the code we've written _does_ solve a more general set of problems to do
+with drawing a clockface. Because we used tests to think about each small part
+of the problem in isolation, and because we codified that isolation with
+functions, we've built a very reasonable little API for clockface calculations.
+
+We can work on this project and turn it into something more general - a library
+for calculating clockface angles and/or vectors.
+
+In fact, providing the library along with the program is _a really good idea_.
+It costs us nothing, while increasing the utility of our program and helping to
+document how it works.
+
+> APIs should come with programs, and vice versa. An API that you must write C
+> code to use, which cannot be invoked easily from the command line, is harder to
+> learn and use. And contrariwise, it's a royal pain to have interfaces whose
+> only open, documented form is a program, so you cannot invoke them easily from
+> a C program.
+> -- Henry Spencer, in _The Art of Unix Programming_
+
+In [my final take on this program](math/vFinal/clockface), I've made the
+unexported functions within `clockface` into a public API for the library, with
+functions to calculate the angle and unit vector for each of the clock hands.
+I've also split the SVG generation part into its own package, `svg`, which is
+then used by the `clockface` program directly. Naturally I've documented each of
+the functions and packages.
+
+Talking about SVGs...
+
+### The Most Valuable Test
+
+I'm sure you've noticed that the most sophisticated piece of code for handling
+SVGs isn't in our application code at all; it's in the test code. Should this
+make us feel uncomfortable? Shouldn't we do something like
+
+- use a template from `text/template`?
+- use an XML library (much as we're doing in our test)?
+- use an SVG library?
+
+We could refactor our code to do any of these things, and we can do so because
+because it doesn't matter _how_ we produce our SVG, what's important is _that
+it's an SVG that we produce_. As such, the part of our system that needs to know
+the most about SVGs - that needs to be the strictest about what constitutes an
+SVG - is the test for the SVG output; it needs to have enough context and
+knowledge about SVGs for us to be confident that we're outputting an SVG.
+
+Wee may have felt odd that we were pouring a lot of time and effort into those
+SVG tests - importing an XML library, parsing XML, refactoring the structs - but
+that test code is a valuable part of our codebase - possibly more valuable than
+the current production code. It will help guarantee that the output is always
+a valid SVG, no matter what we choose to use to produce it.
+
+Tests are not second class citizens - they are not 'throwaway' code. Good tests
+will last a lot longer than the particular version of the code they are
+testing. You should never feel like you're spending 'too much time' writing your
+tests. It's usually a wise investment.
+
+[^1]: This is a lot easier than writing a name out by hand as a string and then having to keep it in sync with the actual time. Believe me you don't want to do that...
+
+[^2]: In short it makes it easier to do calculus with circles as π just keeps coming up as an angle if you use normal degrees, so if you count your angles in πs it makes all the equations simpler.
+
+[^3]: Missattributed because, like all great authors, Kent Beck is more quoted than read. Beck himself attributes it to [Phlip][phlip].
+
+[texttemplate]: https://golang.org/pkg/text/template/
+[circle]: https://en.wikipedia.org/wiki/Sine#Unit_circle_definition
+[mathcos]: https://golang.org/pkg/math/#Cos
+[floatingpoint]: https://0.30000000000000004.com/
+[phlip]: http://wiki.c2.com/?PhlIp
+[xml]: https://godoc.org/encoding/xml
diff --git a/math/clock.template.svg b/math/clock.template.svg
new file mode 100644
index 000000000..b83880aa2
--- /dev/null
+++ b/math/clock.template.svg
@@ -0,0 +1,21 @@
+
+
+
diff --git a/math/example_clock.svg b/math/example_clock.svg
new file mode 100644
index 000000000..df03ec534
--- /dev/null
+++ b/math/example_clock.svg
@@ -0,0 +1,21 @@
+
+
+
diff --git a/math/images/unit_circle.png b/math/images/unit_circle.png
new file mode 100644
index 000000000..99f19d42f
Binary files /dev/null and b/math/images/unit_circle.png differ
diff --git a/math/images/unit_circle_12_oclock.png b/math/images/unit_circle_12_oclock.png
new file mode 100644
index 000000000..fc05f7333
Binary files /dev/null and b/math/images/unit_circle_12_oclock.png differ
diff --git a/math/images/unit_circle_coords.png b/math/images/unit_circle_coords.png
new file mode 100644
index 000000000..244128c49
Binary files /dev/null and b/math/images/unit_circle_coords.png differ
diff --git a/math/images/unit_circle_params.png b/math/images/unit_circle_params.png
new file mode 100644
index 000000000..3d8a2debc
Binary files /dev/null and b/math/images/unit_circle_params.png differ
diff --git a/math/v1/clockface/clockface.go b/math/v1/clockface/clockface.go
new file mode 100644
index 000000000..5e3154f45
--- /dev/null
+++ b/math/v1/clockface/clockface.go
@@ -0,0 +1,15 @@
+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{150, 60}
+}
diff --git a/math/v1/clockface/clockface_test.go b/math/v1/clockface/clockface_test.go
new file mode 100644
index 000000000..24280fa64
--- /dev/null
+++ b/math/v1/clockface/clockface_test.go
@@ -0,0 +1,19 @@
+package clockface_test
+
+import (
+ "testing"
+ "time"
+
+ "github.com/quii/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)
+ }
+}
diff --git a/math/v10/clockface/clockface.go b/math/v10/clockface/clockface.go
new file mode 100644
index 000000000..6e5b8135a
--- /dev/null
+++ b/math/v10/clockface/clockface.go
@@ -0,0 +1,41 @@
+package clockface
+
+import (
+ "math"
+ "time"
+)
+
+// A Point represents a two dimensional Cartesian coordinate
+type Point struct {
+ X float64
+ Y float64
+}
+
+func secondsInRadians(t time.Time) float64 {
+ return (math.Pi / (30 / float64(t.Second())))
+}
+
+func secondHandPoint(t time.Time) Point {
+ return angleToPoint(secondsInRadians(t))
+}
+
+func minutesInRadians(t time.Time) float64 {
+ return (secondsInRadians(t) / 60) +
+ (math.Pi / (30 / float64(t.Minute())))
+}
+
+func minuteHandPoint(t time.Time) Point {
+ return angleToPoint(minutesInRadians(t))
+}
+
+func hoursInRadians(t time.Time) float64 {
+ return (minutesInRadians(t) / 12) +
+ (math.Pi / (6 / float64(t.Hour()%12)))
+}
+
+func angleToPoint(angle float64) Point {
+ x := math.Sin(angle)
+ y := math.Cos(angle)
+
+ return Point{x, y}
+}
diff --git a/math/v10/clockface/clockface/clock.svg b/math/v10/clockface/clockface/clock.svg
new file mode 100644
index 000000000..cdf9aae10
--- /dev/null
+++ b/math/v10/clockface/clockface/clock.svg
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/math/v10/clockface/clockface/main.go b/math/v10/clockface/clockface/main.go
new file mode 100644
index 000000000..3274bcf7c
--- /dev/null
+++ b/math/v10/clockface/clockface/main.go
@@ -0,0 +1,13 @@
+package main
+
+import (
+ "os"
+ "time"
+
+ "github.com/quii/learn-go-with-tests/math/v10/clockface"
+)
+
+func main() {
+ t := time.Now()
+ clockface.SVGWriter(os.Stdout, t)
+}
diff --git a/math/v10/clockface/clockface_acceptance_test.go b/math/v10/clockface/clockface_acceptance_test.go
new file mode 100644
index 000000000..afd5e03b7
--- /dev/null
+++ b/math/v10/clockface/clockface_acceptance_test.go
@@ -0,0 +1,134 @@
+package clockface_test
+
+import (
+ "bytes"
+ "encoding/xml"
+ "testing"
+ "time"
+
+ "github.com/quii/learn-go-with-tests/math/v10/clockface"
+)
+
+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 Circle `xml:"circle"`
+ Line []Line `xml:"line"`
+}
+
+type Line struct {
+ X1 float64 `xml:"x1,attr"`
+ Y1 float64 `xml:"y1,attr"`
+ X2 float64 `xml:"x2,attr"`
+ Y2 float64 `xml:"y2,attr"`
+}
+
+type Circle struct {
+ Cx float64 `xml:"cx,attr"`
+ Cy float64 `xml:"cy,attr"`
+ R float64 `xml:"r,attr"`
+}
+
+func TestSVGWriterSecondHand(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ line 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)
+ }
+ })
+ }
+}
+
+func TestSVGWriterMinutedHand(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ line 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)
+ }
+ })
+ }
+}
+
+func TestSVGWriterHourHand(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ line 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 minute hand line %+v, in the SVG lines %+v", c.line, svg.Line)
+ }
+ })
+ }
+}
+
+func containsLine(l Line, ls []Line) bool {
+ for _, line := range ls {
+ if line == l {
+ return true
+ }
+ }
+ return false
+}
+
+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")
+}
diff --git a/math/v10/clockface/clockface_test.go b/math/v10/clockface/clockface_test.go
new file mode 100644
index 000000000..aeb58449b
--- /dev/null
+++ b/math/v10/clockface/clockface_test.go
@@ -0,0 +1,124 @@
+package clockface
+
+import (
+ "math"
+ "testing"
+ "time"
+)
+
+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 !roughlyEqualFloat64(got, c.angle) {
+ t.Fatalf("Wanted %v radians, but got %v", c.angle, got)
+ }
+ })
+ }
+}
+
+func TestSecondHandPoint(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ point 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 TestMinutesInRadians(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ angle 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 !roughlyEqualFloat64(got, c.angle) {
+ t.Fatalf("Wanted %v radians, but got %v", c.angle, got)
+ }
+ })
+ }
+}
+
+func TestMinuteHandPoint(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ point 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)
+ }
+ })
+ }
+}
+
+func TestHoursInRadians(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ angle float64
+ }{
+ {simpleTime(6, 0, 0), math.Pi},
+ {simpleTime(0, 0, 0), 0},
+ {simpleTime(21, 0, 0), math.Pi * 1.5},
+ {simpleTime(0, 1, 30), math.Pi / ((6 * 60 * 60) / 90)},
+ }
+
+ for _, c := range cases {
+ t.Run(testName(c.time), func(t *testing.T) {
+ got := hoursInRadians(c.time)
+ if !roughlyEqualFloat64(got, c.angle) {
+ t.Fatalf("Wanted %v radians, but got %v", c.angle, got)
+ }
+ })
+ }
+}
+
+func roughlyEqualFloat64(a, b float64) bool {
+ const equalityThreshold = 1e-7
+ return math.Abs(a-b) < equalityThreshold
+}
+
+func roughlyEqualPoint(a, b Point) bool {
+ return roughlyEqualFloat64(a.X, b.X) &&
+ roughlyEqualFloat64(a.Y, b.Y)
+}
+
+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")
+}
diff --git a/math/v10/clockface/svgWriter.go b/math/v10/clockface/svgWriter.go
new file mode 100644
index 000000000..e9d9b0108
--- /dev/null
+++ b/math/v10/clockface/svgWriter.go
@@ -0,0 +1,51 @@
+package clockface
+
+import (
+ "fmt"
+ "io"
+ "time"
+)
+
+const (
+ secondHandLength = 90
+ minuteHandLength = 80
+ clockCentreX = 150
+ clockCentreY = 150
+)
+
+//SVGWriter writes an SVG representation of an analogue clock, showing the time t, to the writer w
+func SVGWriter(w io.Writer, t time.Time) {
+ io.WriteString(w, svgStart)
+ io.WriteString(w, bezel)
+ secondHand(w, t)
+ minuteHand(w, t)
+ io.WriteString(w, svgEnd)
+}
+
+func secondHand(w io.Writer, t time.Time) {
+ p := makeHand(secondHandPoint(t), secondHandLength)
+ fmt.Fprintf(w, ``, p.X, p.Y)
+}
+
+func minuteHand(w io.Writer, t time.Time) {
+ p := makeHand(minuteHandPoint(t), minuteHandLength)
+ fmt.Fprintf(w, ``, 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}
+}
+
+const svgStart = `
+
+`
diff --git a/math/v11/clockface/clockface.go b/math/v11/clockface/clockface.go
new file mode 100644
index 000000000..869fea799
--- /dev/null
+++ b/math/v11/clockface/clockface.go
@@ -0,0 +1,45 @@
+package clockface
+
+import (
+ "math"
+ "time"
+)
+
+// A Point represents a two dimensional Cartesian coordinate
+type Point struct {
+ X float64
+ Y float64
+}
+
+func secondsInRadians(t time.Time) float64 {
+ return (math.Pi / (30 / float64(t.Second())))
+}
+
+func secondHandPoint(t time.Time) Point {
+ return angleToPoint(secondsInRadians(t))
+}
+
+func minutesInRadians(t time.Time) float64 {
+ return (secondsInRadians(t) / 60) +
+ (math.Pi / (30 / float64(t.Minute())))
+}
+
+func minuteHandPoint(t time.Time) Point {
+ return angleToPoint(minutesInRadians(t))
+}
+
+func hoursInRadians(t time.Time) float64 {
+ return (minutesInRadians(t) / 12) +
+ (math.Pi / (6 / float64(t.Hour()%12)))
+}
+
+func hourHandPoint(t time.Time) Point {
+ return angleToPoint(hoursInRadians(t))
+}
+
+func angleToPoint(angle float64) Point {
+ x := math.Sin(angle)
+ y := math.Cos(angle)
+
+ return Point{x, y}
+}
diff --git a/math/v11/clockface/clockface/clock.svg b/math/v11/clockface/clockface/clock.svg
new file mode 100644
index 000000000..cdf9aae10
--- /dev/null
+++ b/math/v11/clockface/clockface/clock.svg
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/math/v11/clockface/clockface/main.go b/math/v11/clockface/clockface/main.go
new file mode 100644
index 000000000..fc47f6c8c
--- /dev/null
+++ b/math/v11/clockface/clockface/main.go
@@ -0,0 +1,13 @@
+package main
+
+import (
+ "os"
+ "time"
+
+ "github.com/quii/learn-go-with-tests/math/v9/clockface"
+)
+
+func main() {
+ t := time.Now()
+ clockface.SVGWriter(os.Stdout, t)
+}
diff --git a/math/v11/clockface/clockface_acceptance_test.go b/math/v11/clockface/clockface_acceptance_test.go
new file mode 100644
index 000000000..d39d822d8
--- /dev/null
+++ b/math/v11/clockface/clockface_acceptance_test.go
@@ -0,0 +1,134 @@
+package clockface_test
+
+import (
+ "bytes"
+ "encoding/xml"
+ "testing"
+ "time"
+
+ "github.com/quii/learn-go-with-tests/math/v11/clockface"
+)
+
+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 Circle `xml:"circle"`
+ Line []Line `xml:"line"`
+}
+
+type Line struct {
+ X1 float64 `xml:"x1,attr"`
+ Y1 float64 `xml:"y1,attr"`
+ X2 float64 `xml:"x2,attr"`
+ Y2 float64 `xml:"y2,attr"`
+}
+
+type Circle struct {
+ Cx float64 `xml:"cx,attr"`
+ Cy float64 `xml:"cy,attr"`
+ R float64 `xml:"r,attr"`
+}
+
+func TestSVGWriterSecondHand(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ line 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)
+ }
+ })
+ }
+}
+
+func TestSVGWriterMinutedHand(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ line 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)
+ }
+ })
+ }
+}
+
+func TestSVGWriterHourHand(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ line 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 minute hand line %+v, in the SVG lines %+v", c.line, svg.Line)
+ }
+ })
+ }
+}
+
+func containsLine(l Line, ls []Line) bool {
+ for _, line := range ls {
+ if line == l {
+ return true
+ }
+ }
+ return false
+}
+
+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")
+}
diff --git a/math/v11/clockface/clockface_test.go b/math/v11/clockface/clockface_test.go
new file mode 100644
index 000000000..db8d818ab
--- /dev/null
+++ b/math/v11/clockface/clockface_test.go
@@ -0,0 +1,143 @@
+package clockface
+
+import (
+ "math"
+ "testing"
+ "time"
+)
+
+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 !roughlyEqualFloat64(got, c.angle) {
+ t.Fatalf("Wanted %v radians, but got %v", c.angle, got)
+ }
+ })
+ }
+}
+
+func TestSecondHandPoint(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ point 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 TestMinutesInRadians(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ angle 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 !roughlyEqualFloat64(got, c.angle) {
+ t.Fatalf("Wanted %v radians, but got %v", c.angle, got)
+ }
+ })
+ }
+}
+
+func TestMinuteHandPoint(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ point 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)
+ }
+ })
+ }
+}
+
+func TestHoursInRadians(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ angle float64
+ }{
+ {simpleTime(6, 0, 0), math.Pi},
+ {simpleTime(0, 0, 0), 0},
+ {simpleTime(21, 0, 0), math.Pi * 1.5},
+ {simpleTime(0, 1, 30), math.Pi / ((6 * 60 * 60) / 90)},
+ }
+
+ for _, c := range cases {
+ t.Run(testName(c.time), func(t *testing.T) {
+ got := hoursInRadians(c.time)
+ if !roughlyEqualFloat64(got, c.angle) {
+ t.Fatalf("Wanted %v radians, but got %v", c.angle, got)
+ }
+ })
+ }
+}
+
+func TestHourHandPoint(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ point Point
+ }{
+ {simpleTime(6, 0, 0), Point{0, -1}},
+ {simpleTime(21, 0, 0), Point{-1, 0}},
+ }
+
+ for _, c := range cases {
+ t.Run(testName(c.time), func(t *testing.T) {
+ got := hourHandPoint(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-7
+ return math.Abs(a-b) < equalityThreshold
+}
+
+func roughlyEqualPoint(a, b Point) bool {
+ return roughlyEqualFloat64(a.X, b.X) &&
+ roughlyEqualFloat64(a.Y, b.Y)
+}
+
+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")
+}
diff --git a/math/v11/clockface/svgWriter.go b/math/v11/clockface/svgWriter.go
new file mode 100644
index 000000000..e9d9b0108
--- /dev/null
+++ b/math/v11/clockface/svgWriter.go
@@ -0,0 +1,51 @@
+package clockface
+
+import (
+ "fmt"
+ "io"
+ "time"
+)
+
+const (
+ secondHandLength = 90
+ minuteHandLength = 80
+ clockCentreX = 150
+ clockCentreY = 150
+)
+
+//SVGWriter writes an SVG representation of an analogue clock, showing the time t, to the writer w
+func SVGWriter(w io.Writer, t time.Time) {
+ io.WriteString(w, svgStart)
+ io.WriteString(w, bezel)
+ secondHand(w, t)
+ minuteHand(w, t)
+ io.WriteString(w, svgEnd)
+}
+
+func secondHand(w io.Writer, t time.Time) {
+ p := makeHand(secondHandPoint(t), secondHandLength)
+ fmt.Fprintf(w, ``, p.X, p.Y)
+}
+
+func minuteHand(w io.Writer, t time.Time) {
+ p := makeHand(minuteHandPoint(t), minuteHandLength)
+ fmt.Fprintf(w, ``, 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}
+}
+
+const svgStart = `
+
+`
diff --git a/math/v12/clockface/clockface.go b/math/v12/clockface/clockface.go
new file mode 100644
index 000000000..cbd5334eb
--- /dev/null
+++ b/math/v12/clockface/clockface.go
@@ -0,0 +1,54 @@
+package clockface
+
+import (
+ "math"
+ "time"
+)
+
+const (
+ secondsInHalfClock = 30
+ secondsInClock = 2 * secondsInHalfClock
+ minutesInHalfClock = 30
+ minutesInClock = 2 * minutesInHalfClock
+ hoursInHalfClock = 6
+ hoursInClock = 2 * hoursInHalfClock
+)
+
+// A Point represents a two dimensional Cartesian coordinate
+type Point struct {
+ X float64
+ Y float64
+}
+
+func secondsInRadians(t time.Time) float64 {
+ return (math.Pi / (secondsInHalfClock / float64(t.Second())))
+}
+
+func secondHandPoint(t time.Time) Point {
+ return angleToPoint(secondsInRadians(t))
+}
+
+func minutesInRadians(t time.Time) float64 {
+ return (secondsInRadians(t) / minutesInClock) +
+ (math.Pi / (minutesInHalfClock / float64(t.Minute())))
+}
+
+func minuteHandPoint(t time.Time) Point {
+ return angleToPoint(minutesInRadians(t))
+}
+
+func hoursInRadians(t time.Time) float64 {
+ return (minutesInRadians(t) / hoursInClock) +
+ (math.Pi / (hoursInHalfClock / float64(t.Hour()%hoursInClock)))
+}
+
+func hourHandPoint(t time.Time) Point {
+ return angleToPoint(hoursInRadians(t))
+}
+
+func angleToPoint(angle float64) Point {
+ x := math.Sin(angle)
+ y := math.Cos(angle)
+
+ return Point{x, y}
+}
diff --git a/math/v12/clockface/clockface/clock.svg b/math/v12/clockface/clockface/clock.svg
new file mode 100644
index 000000000..cae4d9072
--- /dev/null
+++ b/math/v12/clockface/clockface/clock.svg
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/math/v12/clockface/clockface/main.go b/math/v12/clockface/clockface/main.go
new file mode 100644
index 000000000..cdba952bd
--- /dev/null
+++ b/math/v12/clockface/clockface/main.go
@@ -0,0 +1,13 @@
+package main
+
+import (
+ "os"
+ "time"
+
+ "github.com/quii/learn-go-with-tests/math/v12/clockface"
+)
+
+func main() {
+ t := time.Now()
+ clockface.SVGWriter(os.Stdout, t)
+}
diff --git a/math/v12/clockface/clockface_acceptance_test.go b/math/v12/clockface/clockface_acceptance_test.go
new file mode 100644
index 000000000..3c39d74a0
--- /dev/null
+++ b/math/v12/clockface/clockface_acceptance_test.go
@@ -0,0 +1,134 @@
+package clockface_test
+
+import (
+ "bytes"
+ "encoding/xml"
+ "testing"
+ "time"
+
+ "github.com/quii/learn-go-with-tests/math/v12/clockface"
+)
+
+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 Circle `xml:"circle"`
+ Line []Line `xml:"line"`
+}
+
+type Line struct {
+ X1 float64 `xml:"x1,attr"`
+ Y1 float64 `xml:"y1,attr"`
+ X2 float64 `xml:"x2,attr"`
+ Y2 float64 `xml:"y2,attr"`
+}
+
+type Circle struct {
+ Cx float64 `xml:"cx,attr"`
+ Cy float64 `xml:"cy,attr"`
+ R float64 `xml:"r,attr"`
+}
+
+func TestSVGWriterSecondHand(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ line 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)
+ }
+ })
+ }
+}
+
+func TestSVGWriterMinutedHand(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ line 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)
+ }
+ })
+ }
+}
+
+func TestSVGWriterHourHand(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ line 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 minute hand line %+v, in the SVG lines %+v", c.line, svg.Line)
+ }
+ })
+ }
+}
+
+func containsLine(l Line, ls []Line) bool {
+ for _, line := range ls {
+ if line == l {
+ return true
+ }
+ }
+ return false
+}
+
+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")
+}
diff --git a/math/v12/clockface/clockface_test.go b/math/v12/clockface/clockface_test.go
new file mode 100644
index 000000000..db8d818ab
--- /dev/null
+++ b/math/v12/clockface/clockface_test.go
@@ -0,0 +1,143 @@
+package clockface
+
+import (
+ "math"
+ "testing"
+ "time"
+)
+
+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 !roughlyEqualFloat64(got, c.angle) {
+ t.Fatalf("Wanted %v radians, but got %v", c.angle, got)
+ }
+ })
+ }
+}
+
+func TestSecondHandPoint(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ point 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 TestMinutesInRadians(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ angle 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 !roughlyEqualFloat64(got, c.angle) {
+ t.Fatalf("Wanted %v radians, but got %v", c.angle, got)
+ }
+ })
+ }
+}
+
+func TestMinuteHandPoint(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ point 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)
+ }
+ })
+ }
+}
+
+func TestHoursInRadians(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ angle float64
+ }{
+ {simpleTime(6, 0, 0), math.Pi},
+ {simpleTime(0, 0, 0), 0},
+ {simpleTime(21, 0, 0), math.Pi * 1.5},
+ {simpleTime(0, 1, 30), math.Pi / ((6 * 60 * 60) / 90)},
+ }
+
+ for _, c := range cases {
+ t.Run(testName(c.time), func(t *testing.T) {
+ got := hoursInRadians(c.time)
+ if !roughlyEqualFloat64(got, c.angle) {
+ t.Fatalf("Wanted %v radians, but got %v", c.angle, got)
+ }
+ })
+ }
+}
+
+func TestHourHandPoint(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ point Point
+ }{
+ {simpleTime(6, 0, 0), Point{0, -1}},
+ {simpleTime(21, 0, 0), Point{-1, 0}},
+ }
+
+ for _, c := range cases {
+ t.Run(testName(c.time), func(t *testing.T) {
+ got := hourHandPoint(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-7
+ return math.Abs(a-b) < equalityThreshold
+}
+
+func roughlyEqualPoint(a, b Point) bool {
+ return roughlyEqualFloat64(a.X, b.X) &&
+ roughlyEqualFloat64(a.Y, b.Y)
+}
+
+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")
+}
diff --git a/math/v12/clockface/svgWriter.go b/math/v12/clockface/svgWriter.go
new file mode 100644
index 000000000..b3832d955
--- /dev/null
+++ b/math/v12/clockface/svgWriter.go
@@ -0,0 +1,58 @@
+package clockface
+
+import (
+ "fmt"
+ "io"
+ "time"
+)
+
+const (
+ secondHandLength = 90
+ minuteHandLength = 80
+ hourHandLength = 50
+ clockCentreX = 150
+ clockCentreY = 150
+)
+
+//SVGWriter writes an SVG representation of an analogue clock, showing the time t, to the writer w
+func SVGWriter(w io.Writer, t time.Time) {
+ io.WriteString(w, svgStart)
+ io.WriteString(w, bezel)
+ secondHand(w, t)
+ minuteHand(w, t)
+ hourHand(w, t)
+ io.WriteString(w, svgEnd)
+}
+
+func secondHand(w io.Writer, t time.Time) {
+ p := makeHand(secondHandPoint(t), secondHandLength)
+ fmt.Fprintf(w, ``, p.X, p.Y)
+}
+
+func minuteHand(w io.Writer, t time.Time) {
+ p := makeHand(minuteHandPoint(t), minuteHandLength)
+ fmt.Fprintf(w, ``, p.X, p.Y)
+}
+
+func hourHand(w io.Writer, t time.Time) {
+ p := makeHand(hourHandPoint(t), hourHandLength)
+ fmt.Fprintf(w, ``, 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}
+}
+
+const svgStart = `
+
+`
diff --git a/math/v2/clockface/clockface.go b/math/v2/clockface/clockface.go
new file mode 100644
index 000000000..eb3301a3d
--- /dev/null
+++ b/math/v2/clockface/clockface.go
@@ -0,0 +1,22 @@
+package clockface
+
+import (
+ "math"
+ "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{150, 60}
+}
+
+func secondsInRadians(t time.Time) float64 {
+ return math.Pi
+}
diff --git a/math/v2/clockface/clockface_acceptance_test.go b/math/v2/clockface/clockface_acceptance_test.go
new file mode 100644
index 000000000..f523b4716
--- /dev/null
+++ b/math/v2/clockface/clockface_acceptance_test.go
@@ -0,0 +1,30 @@
+package clockface_test
+
+import (
+ "testing"
+ "time"
+
+ "github.com/quii/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)
+ }
+}
+
+// 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)
+// }
+// }
diff --git a/math/v2/clockface/clockface_test.go b/math/v2/clockface/clockface_test.go
new file mode 100644
index 000000000..a77039e68
--- /dev/null
+++ b/math/v2/clockface/clockface_test.go
@@ -0,0 +1,17 @@
+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)
+ }
+}
diff --git a/math/v3/clockface/clockface.go b/math/v3/clockface/clockface.go
new file mode 100644
index 000000000..dbef18816
--- /dev/null
+++ b/math/v3/clockface/clockface.go
@@ -0,0 +1,22 @@
+package clockface
+
+import (
+ "math"
+ "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{150, 60}
+}
+
+func secondsInRadians(t time.Time) float64 {
+ return (math.Pi / (30 / (float64(t.Second()))))
+}
diff --git a/math/v3/clockface/clockface_acceptance_test.go b/math/v3/clockface/clockface_acceptance_test.go
new file mode 100644
index 000000000..f523b4716
--- /dev/null
+++ b/math/v3/clockface/clockface_acceptance_test.go
@@ -0,0 +1,30 @@
+package clockface_test
+
+import (
+ "testing"
+ "time"
+
+ "github.com/quii/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)
+ }
+}
+
+// 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)
+// }
+// }
diff --git a/math/v3/clockface/clockface_test.go b/math/v3/clockface/clockface_test.go
new file mode 100644
index 000000000..76a21f8f3
--- /dev/null
+++ b/math/v3/clockface/clockface_test.go
@@ -0,0 +1,36 @@
+package clockface
+
+import (
+ "math"
+ "testing"
+ "time"
+)
+
+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)
+ }
+ })
+ }
+}
+
+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")
+}
diff --git a/math/v4/clockface/clockface.go b/math/v4/clockface/clockface.go
new file mode 100644
index 000000000..b0a32f6d5
--- /dev/null
+++ b/math/v4/clockface/clockface.go
@@ -0,0 +1,30 @@
+package clockface
+
+import (
+ "math"
+ "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{150, 60}
+}
+
+func secondsInRadians(t time.Time) float64 {
+ return (math.Pi / (30 / (float64(t.Second()))))
+}
+
+func secondHandPoint(t time.Time) Point {
+ angle := secondsInRadians(t)
+ x := math.Sin(angle)
+ y := math.Cos(angle)
+
+ return Point{x, y}
+}
diff --git a/math/v4/clockface/clockface_acceptance_test.go b/math/v4/clockface/clockface_acceptance_test.go
new file mode 100644
index 000000000..f523b4716
--- /dev/null
+++ b/math/v4/clockface/clockface_acceptance_test.go
@@ -0,0 +1,30 @@
+package clockface_test
+
+import (
+ "testing"
+ "time"
+
+ "github.com/quii/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)
+ }
+}
+
+// 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)
+// }
+// }
diff --git a/math/v4/clockface/clockface_test.go b/math/v4/clockface/clockface_test.go
new file mode 100644
index 000000000..6bed993c5
--- /dev/null
+++ b/math/v4/clockface/clockface_test.go
@@ -0,0 +1,65 @@
+package clockface
+
+import (
+ "math"
+ "testing"
+ "time"
+)
+
+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)
+ }
+ })
+ }
+}
+
+func TestSecondHandPoint(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ point 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-7
+ return math.Abs(a-b) < equalityThreshold
+}
+
+func roughlyEqualPoint(a, b Point) bool {
+ return roughlyEqualFloat64(a.X, b.X) &&
+ roughlyEqualFloat64(a.Y, b.Y)
+}
+
+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")
+}
diff --git a/math/v5/clockface/clockface.go b/math/v5/clockface/clockface.go
new file mode 100644
index 000000000..5e45b198e
--- /dev/null
+++ b/math/v5/clockface/clockface.go
@@ -0,0 +1,38 @@
+package clockface
+
+import (
+ "math"
+ "time"
+)
+
+// A Point represents a two dimensional Cartesian coordinate
+type Point struct {
+ X float64
+ Y float64
+}
+
+const secondHandLength = 90
+const clockCentreX = 150
+const 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} //translate
+ return p
+}
+
+func secondsInRadians(t time.Time) float64 {
+ return (math.Pi / (30 / (float64(t.Second()))))
+}
+
+func secondHandPoint(t time.Time) Point {
+ angle := secondsInRadians(t)
+ x := math.Sin(angle)
+ y := math.Cos(angle)
+
+ return Point{x, y}
+}
diff --git a/math/v5/clockface/clockface_acceptance_test.go b/math/v5/clockface/clockface_acceptance_test.go
new file mode 100644
index 000000000..536a82a54
--- /dev/null
+++ b/math/v5/clockface/clockface_acceptance_test.go
@@ -0,0 +1,30 @@
+package clockface_test
+
+import (
+ "testing"
+ "time"
+
+ "github.com/quii/learn-go-with-tests/math/v5/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)
+ }
+}
+
+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)
+ }
+}
diff --git a/math/v5/clockface/clockface_test.go b/math/v5/clockface/clockface_test.go
new file mode 100644
index 000000000..6bed993c5
--- /dev/null
+++ b/math/v5/clockface/clockface_test.go
@@ -0,0 +1,65 @@
+package clockface
+
+import (
+ "math"
+ "testing"
+ "time"
+)
+
+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)
+ }
+ })
+ }
+}
+
+func TestSecondHandPoint(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ point 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-7
+ return math.Abs(a-b) < equalityThreshold
+}
+
+func roughlyEqualPoint(a, b Point) bool {
+ return roughlyEqualFloat64(a.X, b.X) &&
+ roughlyEqualFloat64(a.Y, b.Y)
+}
+
+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")
+}
diff --git a/math/v6/clockface/clockface.go b/math/v6/clockface/clockface.go
new file mode 100644
index 000000000..5e45b198e
--- /dev/null
+++ b/math/v6/clockface/clockface.go
@@ -0,0 +1,38 @@
+package clockface
+
+import (
+ "math"
+ "time"
+)
+
+// A Point represents a two dimensional Cartesian coordinate
+type Point struct {
+ X float64
+ Y float64
+}
+
+const secondHandLength = 90
+const clockCentreX = 150
+const 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} //translate
+ return p
+}
+
+func secondsInRadians(t time.Time) float64 {
+ return (math.Pi / (30 / (float64(t.Second()))))
+}
+
+func secondHandPoint(t time.Time) Point {
+ angle := secondsInRadians(t)
+ x := math.Sin(angle)
+ y := math.Cos(angle)
+
+ return Point{x, y}
+}
diff --git a/math/v6/clockface/clockface/clock.svg b/math/v6/clockface/clockface/clock.svg
new file mode 100644
index 000000000..6314d9f70
--- /dev/null
+++ b/math/v6/clockface/clockface/clock.svg
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/math/v6/clockface/clockface/main.go b/math/v6/clockface/clockface/main.go
new file mode 100644
index 000000000..844367dba
--- /dev/null
+++ b/math/v6/clockface/clockface/main.go
@@ -0,0 +1,35 @@
+package main
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "time"
+
+ "github.com/quii/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(``, p.X, p.Y)
+}
+
+const svgStart = `
+
+`
diff --git a/math/v6/clockface/clockface_acceptance_test.go b/math/v6/clockface/clockface_acceptance_test.go
new file mode 100644
index 000000000..cee19ee76
--- /dev/null
+++ b/math/v6/clockface/clockface_acceptance_test.go
@@ -0,0 +1,30 @@
+package clockface_test
+
+import (
+ "testing"
+ "time"
+
+ "github.com/quii/learn-go-with-tests/math/v6/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)
+ }
+}
+
+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)
+ }
+}
diff --git a/math/v6/clockface/clockface_test.go b/math/v6/clockface/clockface_test.go
new file mode 100644
index 000000000..6bed993c5
--- /dev/null
+++ b/math/v6/clockface/clockface_test.go
@@ -0,0 +1,65 @@
+package clockface
+
+import (
+ "math"
+ "testing"
+ "time"
+)
+
+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)
+ }
+ })
+ }
+}
+
+func TestSecondHandPoint(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ point 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-7
+ return math.Abs(a-b) < equalityThreshold
+}
+
+func roughlyEqualPoint(a, b Point) bool {
+ return roughlyEqualFloat64(a.X, b.X) &&
+ roughlyEqualFloat64(a.Y, b.Y)
+}
+
+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")
+}
diff --git a/math/v7/clockface/clockface.go b/math/v7/clockface/clockface.go
new file mode 100644
index 000000000..60fae7af4
--- /dev/null
+++ b/math/v7/clockface/clockface.go
@@ -0,0 +1,24 @@
+package clockface
+
+import (
+ "math"
+ "time"
+)
+
+// A Point represents a two dimensional Cartesian coordinate
+type Point struct {
+ X float64
+ Y float64
+}
+
+func secondsInRadians(t time.Time) float64 {
+ return (math.Pi / (30 / (float64(t.Second()))))
+}
+
+func secondHandPoint(t time.Time) Point {
+ angle := secondsInRadians(t)
+ x := math.Sin(angle)
+ y := math.Cos(angle)
+
+ return Point{x, y}
+}
diff --git a/math/v7/clockface/clockface/clock.svg b/math/v7/clockface/clockface/clock.svg
new file mode 100644
index 000000000..a140ce766
--- /dev/null
+++ b/math/v7/clockface/clockface/clock.svg
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/math/v7/clockface/clockface/main.go b/math/v7/clockface/clockface/main.go
new file mode 100644
index 000000000..c579cbb9c
--- /dev/null
+++ b/math/v7/clockface/clockface/main.go
@@ -0,0 +1,13 @@
+package main
+
+import (
+ "os"
+ "time"
+
+ "github.com/quii/learn-go-with-tests/math/v7/clockface"
+)
+
+func main() {
+ t := time.Now()
+ clockface.SVGWriter(os.Stdout, t)
+}
diff --git a/math/v7/clockface/clockface_acceptance_test.go b/math/v7/clockface/clockface_acceptance_test.go
new file mode 100644
index 000000000..967565204
--- /dev/null
+++ b/math/v7/clockface/clockface_acceptance_test.go
@@ -0,0 +1,37 @@
+package clockface_test
+
+import (
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/quii/learn-go-with-tests/math/v7/clockface"
+)
+
+func TestSVGWriterAtMidnight(t *testing.T) {
+ tm := time.Date(1337, time.January, 1, 0, 0, 0, 0, time.UTC)
+
+ var b strings.Builder
+ clockface.SVGWriter(&b, tm)
+ got := b.String()
+
+ want := ``, p.X, p.Y)
+}
+
+const svgStart = `
+
+`
diff --git a/math/v7b/clockface/clockface.go b/math/v7b/clockface/clockface.go
new file mode 100644
index 000000000..60fae7af4
--- /dev/null
+++ b/math/v7b/clockface/clockface.go
@@ -0,0 +1,24 @@
+package clockface
+
+import (
+ "math"
+ "time"
+)
+
+// A Point represents a two dimensional Cartesian coordinate
+type Point struct {
+ X float64
+ Y float64
+}
+
+func secondsInRadians(t time.Time) float64 {
+ return (math.Pi / (30 / (float64(t.Second()))))
+}
+
+func secondHandPoint(t time.Time) Point {
+ angle := secondsInRadians(t)
+ x := math.Sin(angle)
+ y := math.Cos(angle)
+
+ return Point{x, y}
+}
diff --git a/math/v7b/clockface/clockface/clock.svg b/math/v7b/clockface/clockface/clock.svg
new file mode 100644
index 000000000..a140ce766
--- /dev/null
+++ b/math/v7b/clockface/clockface/clock.svg
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/math/v7b/clockface/clockface/main.go b/math/v7b/clockface/clockface/main.go
new file mode 100644
index 000000000..c579cbb9c
--- /dev/null
+++ b/math/v7b/clockface/clockface/main.go
@@ -0,0 +1,13 @@
+package main
+
+import (
+ "os"
+ "time"
+
+ "github.com/quii/learn-go-with-tests/math/v7/clockface"
+)
+
+func main() {
+ t := time.Now()
+ clockface.SVGWriter(os.Stdout, t)
+}
diff --git a/math/v7b/clockface/clockface_acceptance_test.go b/math/v7b/clockface/clockface_acceptance_test.go
new file mode 100644
index 000000000..c5dd1bae8
--- /dev/null
+++ b/math/v7b/clockface/clockface_acceptance_test.go
@@ -0,0 +1,57 @@
+package clockface_test
+
+import (
+ "bytes"
+ "encoding/xml"
+ "testing"
+ "time"
+
+ "github.com/quii/learn-go-with-tests/math/v7b/clockface"
+)
+
+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"`
+}
+
+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.000"
+ y2 := "60.000"
+
+ 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())
+}
diff --git a/math/v7b/clockface/clockface_test.go b/math/v7b/clockface/clockface_test.go
new file mode 100644
index 000000000..6bed993c5
--- /dev/null
+++ b/math/v7b/clockface/clockface_test.go
@@ -0,0 +1,65 @@
+package clockface
+
+import (
+ "math"
+ "testing"
+ "time"
+)
+
+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)
+ }
+ })
+ }
+}
+
+func TestSecondHandPoint(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ point 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-7
+ return math.Abs(a-b) < equalityThreshold
+}
+
+func roughlyEqualPoint(a, b Point) bool {
+ return roughlyEqualFloat64(a.X, b.X) &&
+ roughlyEqualFloat64(a.Y, b.Y)
+}
+
+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")
+}
diff --git a/math/v7b/clockface/svgWriter.go b/math/v7b/clockface/svgWriter.go
new file mode 100644
index 000000000..9a30256ca
--- /dev/null
+++ b/math/v7b/clockface/svgWriter.go
@@ -0,0 +1,41 @@
+package clockface
+
+import (
+ "fmt"
+ "io"
+ "time"
+)
+
+const (
+ secondHandLength = 90
+ clockCentreX = 150
+ clockCentreY = 150
+)
+
+//SVGWriter writes an SVG representation of an analogue clock, showing the time t, to the writer w
+func 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}
+ p = Point{p.X, -p.Y}
+ p = Point{p.X + clockCentreX, p.Y + clockCentreY} //translate
+ fmt.Fprintf(w, ``, p.X, p.Y)
+}
+
+const svgStart = `
+
+`
diff --git a/math/v7c/clockface/clockface.go b/math/v7c/clockface/clockface.go
new file mode 100644
index 000000000..60fae7af4
--- /dev/null
+++ b/math/v7c/clockface/clockface.go
@@ -0,0 +1,24 @@
+package clockface
+
+import (
+ "math"
+ "time"
+)
+
+// A Point represents a two dimensional Cartesian coordinate
+type Point struct {
+ X float64
+ Y float64
+}
+
+func secondsInRadians(t time.Time) float64 {
+ return (math.Pi / (30 / (float64(t.Second()))))
+}
+
+func secondHandPoint(t time.Time) Point {
+ angle := secondsInRadians(t)
+ x := math.Sin(angle)
+ y := math.Cos(angle)
+
+ return Point{x, y}
+}
diff --git a/math/v7c/clockface/clockface/clock.svg b/math/v7c/clockface/clockface/clock.svg
new file mode 100644
index 000000000..a140ce766
--- /dev/null
+++ b/math/v7c/clockface/clockface/clock.svg
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/math/v7c/clockface/clockface/main.go b/math/v7c/clockface/clockface/main.go
new file mode 100644
index 000000000..c579cbb9c
--- /dev/null
+++ b/math/v7c/clockface/clockface/main.go
@@ -0,0 +1,13 @@
+package main
+
+import (
+ "os"
+ "time"
+
+ "github.com/quii/learn-go-with-tests/math/v7/clockface"
+)
+
+func main() {
+ t := time.Now()
+ clockface.SVGWriter(os.Stdout, t)
+}
diff --git a/math/v7c/clockface/clockface_acceptance_test.go b/math/v7c/clockface/clockface_acceptance_test.go
new file mode 100644
index 000000000..820fe3077
--- /dev/null
+++ b/math/v7c/clockface/clockface_acceptance_test.go
@@ -0,0 +1,82 @@
+package clockface_test
+
+import (
+ "bytes"
+ "encoding/xml"
+ "testing"
+ "time"
+
+ "github.com/quii/learn-go-with-tests/math/v7c/clockface"
+)
+
+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 Circle `xml:"circle"`
+ Line []Line `xml:"line"`
+}
+
+type Line struct {
+ X1 float64 `xml:"x1,attr"`
+ Y1 float64 `xml:"y1,attr"`
+ X2 float64 `xml:"x2,attr"`
+ Y2 float64 `xml:"y2,attr"`
+}
+
+type Circle struct {
+ Cx float64 `xml:"cx,attr"`
+ Cy float64 `xml:"cy,attr"`
+ R float64 `xml:"r,attr"`
+}
+
+func TestSVGWriterSecondHand(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ line 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)
+ }
+ })
+ }
+}
+
+func containsLine(l Line, ls []Line) bool {
+ for _, line := range ls {
+ if line == l {
+ return true
+ }
+ }
+ return false
+}
+
+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")
+}
diff --git a/math/v7c/clockface/clockface_test.go b/math/v7c/clockface/clockface_test.go
new file mode 100644
index 000000000..6bed993c5
--- /dev/null
+++ b/math/v7c/clockface/clockface_test.go
@@ -0,0 +1,65 @@
+package clockface
+
+import (
+ "math"
+ "testing"
+ "time"
+)
+
+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)
+ }
+ })
+ }
+}
+
+func TestSecondHandPoint(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ point 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-7
+ return math.Abs(a-b) < equalityThreshold
+}
+
+func roughlyEqualPoint(a, b Point) bool {
+ return roughlyEqualFloat64(a.X, b.X) &&
+ roughlyEqualFloat64(a.Y, b.Y)
+}
+
+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")
+}
diff --git a/math/v7c/clockface/svgWriter.go b/math/v7c/clockface/svgWriter.go
new file mode 100644
index 000000000..9a30256ca
--- /dev/null
+++ b/math/v7c/clockface/svgWriter.go
@@ -0,0 +1,41 @@
+package clockface
+
+import (
+ "fmt"
+ "io"
+ "time"
+)
+
+const (
+ secondHandLength = 90
+ clockCentreX = 150
+ clockCentreY = 150
+)
+
+//SVGWriter writes an SVG representation of an analogue clock, showing the time t, to the writer w
+func 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}
+ p = Point{p.X, -p.Y}
+ p = Point{p.X + clockCentreX, p.Y + clockCentreY} //translate
+ fmt.Fprintf(w, ``, p.X, p.Y)
+}
+
+const svgStart = `
+
+`
diff --git a/math/v8/clockface/clockface.go b/math/v8/clockface/clockface.go
new file mode 100644
index 000000000..f8518e13d
--- /dev/null
+++ b/math/v8/clockface/clockface.go
@@ -0,0 +1,29 @@
+package clockface
+
+import (
+ "math"
+ "time"
+)
+
+// A Point represents a two dimensional Cartesian coordinate
+type Point struct {
+ X float64
+ Y float64
+}
+
+func secondsInRadians(t time.Time) float64 {
+ return (math.Pi / (30 / float64(t.Second())))
+}
+
+func secondHandPoint(t time.Time) Point {
+ angle := secondsInRadians(t)
+ x := math.Sin(angle)
+ y := math.Cos(angle)
+
+ return Point{x, y}
+}
+
+func minutesInRadians(t time.Time) float64 {
+ return (secondsInRadians(t) / 60) +
+ (math.Pi / (30 / float64(t.Minute())))
+}
diff --git a/math/v8/clockface/clockface/clock.svg b/math/v8/clockface/clockface/clock.svg
new file mode 100644
index 000000000..a140ce766
--- /dev/null
+++ b/math/v8/clockface/clockface/clock.svg
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/math/v8/clockface/clockface/main.go b/math/v8/clockface/clockface/main.go
new file mode 100644
index 000000000..c579cbb9c
--- /dev/null
+++ b/math/v8/clockface/clockface/main.go
@@ -0,0 +1,13 @@
+package main
+
+import (
+ "os"
+ "time"
+
+ "github.com/quii/learn-go-with-tests/math/v7/clockface"
+)
+
+func main() {
+ t := time.Now()
+ clockface.SVGWriter(os.Stdout, t)
+}
diff --git a/math/v8/clockface/clockface_acceptance_test.go b/math/v8/clockface/clockface_acceptance_test.go
new file mode 100644
index 000000000..8b54727c4
--- /dev/null
+++ b/math/v8/clockface/clockface_acceptance_test.go
@@ -0,0 +1,108 @@
+package clockface_test
+
+import (
+ "bytes"
+ "encoding/xml"
+ "testing"
+ "time"
+
+ "github.com/quii/learn-go-with-tests/math/v8/clockface"
+)
+
+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 Circle `xml:"circle"`
+ Line []Line `xml:"line"`
+}
+
+type Line struct {
+ X1 float64 `xml:"x1,attr"`
+ Y1 float64 `xml:"y1,attr"`
+ X2 float64 `xml:"x2,attr"`
+ Y2 float64 `xml:"y2,attr"`
+}
+
+type Circle struct {
+ Cx float64 `xml:"cx,attr"`
+ Cy float64 `xml:"cy,attr"`
+ R float64 `xml:"r,attr"`
+}
+
+func TestSVGWriterSecondHand(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ line 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)
+ }
+ })
+ }
+}
+
+func TestSVGWriterMinuteHand(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ line 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)
+ }
+ })
+ }
+}
+
+func containsLine(l Line, ls []Line) bool {
+ for _, line := range ls {
+ if line == l {
+ return true
+ }
+ }
+ return false
+}
+
+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")
+}
diff --git a/math/v8/clockface/clockface_test.go b/math/v8/clockface/clockface_test.go
new file mode 100644
index 000000000..6e18cf138
--- /dev/null
+++ b/math/v8/clockface/clockface_test.go
@@ -0,0 +1,84 @@
+package clockface
+
+import (
+ "math"
+ "testing"
+ "time"
+)
+
+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)
+ }
+ })
+ }
+}
+
+func TestSecondHandPoint(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ point 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 TestMinutesInRadians(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ angle 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)
+ }
+ })
+ }
+}
+
+func roughlyEqualFloat64(a, b float64) bool {
+ const equalityThreshold = 1e-7
+ return math.Abs(a-b) < equalityThreshold
+}
+
+func roughlyEqualPoint(a, b Point) bool {
+ return roughlyEqualFloat64(a.X, b.X) &&
+ roughlyEqualFloat64(a.Y, b.Y)
+}
+
+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")
+}
diff --git a/math/v8/clockface/svgWriter.go b/math/v8/clockface/svgWriter.go
new file mode 100644
index 000000000..9a30256ca
--- /dev/null
+++ b/math/v8/clockface/svgWriter.go
@@ -0,0 +1,41 @@
+package clockface
+
+import (
+ "fmt"
+ "io"
+ "time"
+)
+
+const (
+ secondHandLength = 90
+ clockCentreX = 150
+ clockCentreY = 150
+)
+
+//SVGWriter writes an SVG representation of an analogue clock, showing the time t, to the writer w
+func 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}
+ p = Point{p.X, -p.Y}
+ p = Point{p.X + clockCentreX, p.Y + clockCentreY} //translate
+ fmt.Fprintf(w, ``, p.X, p.Y)
+}
+
+const svgStart = `
+
+`
diff --git a/math/v9/clockface/clockface.go b/math/v9/clockface/clockface.go
new file mode 100644
index 000000000..f1345b15f
--- /dev/null
+++ b/math/v9/clockface/clockface.go
@@ -0,0 +1,36 @@
+package clockface
+
+import (
+ "math"
+ "time"
+)
+
+// A Point represents a two dimensional Cartesian coordinate
+type Point struct {
+ X float64
+ Y float64
+}
+
+func secondsInRadians(t time.Time) float64 {
+ return (math.Pi / (30 / float64(t.Second())))
+}
+
+func secondHandPoint(t time.Time) Point {
+ return angleToPoint(secondsInRadians(t))
+}
+
+func minutesInRadians(t time.Time) float64 {
+ return (secondsInRadians(t) / 60) +
+ (math.Pi / (30 / float64(t.Minute())))
+}
+
+func minuteHandPoint(t time.Time) Point {
+ return angleToPoint(minutesInRadians(t))
+}
+
+func angleToPoint(angle float64) Point {
+ x := math.Sin(angle)
+ y := math.Cos(angle)
+
+ return Point{x, y}
+}
diff --git a/math/v9/clockface/clockface/clock.svg b/math/v9/clockface/clockface/clock.svg
new file mode 100644
index 000000000..cdf9aae10
--- /dev/null
+++ b/math/v9/clockface/clockface/clock.svg
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/math/v9/clockface/clockface/main.go b/math/v9/clockface/clockface/main.go
new file mode 100644
index 000000000..fc47f6c8c
--- /dev/null
+++ b/math/v9/clockface/clockface/main.go
@@ -0,0 +1,13 @@
+package main
+
+import (
+ "os"
+ "time"
+
+ "github.com/quii/learn-go-with-tests/math/v9/clockface"
+)
+
+func main() {
+ t := time.Now()
+ clockface.SVGWriter(os.Stdout, t)
+}
diff --git a/math/v9/clockface/clockface_acceptance_test.go b/math/v9/clockface/clockface_acceptance_test.go
new file mode 100644
index 000000000..43e7e9ca6
--- /dev/null
+++ b/math/v9/clockface/clockface_acceptance_test.go
@@ -0,0 +1,108 @@
+package clockface_test
+
+import (
+ "bytes"
+ "encoding/xml"
+ "testing"
+ "time"
+
+ "github.com/quii/learn-go-with-tests/math/v9/clockface"
+)
+
+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 Circle `xml:"circle"`
+ Line []Line `xml:"line"`
+}
+
+type Line struct {
+ X1 float64 `xml:"x1,attr"`
+ Y1 float64 `xml:"y1,attr"`
+ X2 float64 `xml:"x2,attr"`
+ Y2 float64 `xml:"y2,attr"`
+}
+
+type Circle struct {
+ Cx float64 `xml:"cx,attr"`
+ Cy float64 `xml:"cy,attr"`
+ R float64 `xml:"r,attr"`
+}
+
+func TestSVGWriterSecondHand(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ line 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)
+ }
+ })
+ }
+}
+
+func TestSVGWriterMinutedHand(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ line 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)
+ }
+ })
+ }
+}
+
+func containsLine(l Line, ls []Line) bool {
+ for _, line := range ls {
+ if line == l {
+ return true
+ }
+ }
+ return false
+}
+
+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")
+}
diff --git a/math/v9/clockface/clockface_test.go b/math/v9/clockface/clockface_test.go
new file mode 100644
index 000000000..3106c4450
--- /dev/null
+++ b/math/v9/clockface/clockface_test.go
@@ -0,0 +1,103 @@
+package clockface
+
+import (
+ "math"
+ "testing"
+ "time"
+)
+
+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)
+ }
+ })
+ }
+}
+
+func TestSecondHandPoint(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ point 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 TestMinutesInRadians(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ angle 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)
+ }
+ })
+ }
+}
+
+func TestMinuteHandPoint(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ point 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)
+ }
+ })
+ }
+}
+
+func roughlyEqualFloat64(a, b float64) bool {
+ const equalityThreshold = 1e-7
+ return math.Abs(a-b) < equalityThreshold
+}
+
+func roughlyEqualPoint(a, b Point) bool {
+ return roughlyEqualFloat64(a.X, b.X) &&
+ roughlyEqualFloat64(a.Y, b.Y)
+}
+
+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")
+}
diff --git a/math/v9/clockface/svgWriter.go b/math/v9/clockface/svgWriter.go
new file mode 100644
index 000000000..e9d9b0108
--- /dev/null
+++ b/math/v9/clockface/svgWriter.go
@@ -0,0 +1,51 @@
+package clockface
+
+import (
+ "fmt"
+ "io"
+ "time"
+)
+
+const (
+ secondHandLength = 90
+ minuteHandLength = 80
+ clockCentreX = 150
+ clockCentreY = 150
+)
+
+//SVGWriter writes an SVG representation of an analogue clock, showing the time t, to the writer w
+func SVGWriter(w io.Writer, t time.Time) {
+ io.WriteString(w, svgStart)
+ io.WriteString(w, bezel)
+ secondHand(w, t)
+ minuteHand(w, t)
+ io.WriteString(w, svgEnd)
+}
+
+func secondHand(w io.Writer, t time.Time) {
+ p := makeHand(secondHandPoint(t), secondHandLength)
+ fmt.Fprintf(w, ``, p.X, p.Y)
+}
+
+func minuteHand(w io.Writer, t time.Time) {
+ p := makeHand(minuteHandPoint(t), minuteHandLength)
+ fmt.Fprintf(w, ``, 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}
+}
+
+const svgStart = `
+
+`
diff --git a/math/vFinal/clockface/clockface.go b/math/vFinal/clockface/clockface.go
new file mode 100644
index 000000000..1f20db340
--- /dev/null
+++ b/math/vFinal/clockface/clockface.go
@@ -0,0 +1,66 @@
+// Package clockface provides functions that calculate the positions of the hands
+// of an analogue clock,
+package clockface
+
+import (
+ "math"
+ "time"
+)
+
+const (
+ secondsInHalfClock = 30
+ secondsInClock = 2 * secondsInHalfClock
+ minutesInHalfClock = 30
+ minutesInClock = 2 * minutesInHalfClock
+ hoursInHalfClock = 6
+ hoursInClock = 2 * hoursInHalfClock
+)
+
+// A Point is a Cartesian coordinate. They are used in the package
+// to represent the unit vector from the origin of a clock hand.
+type Point struct {
+ X float64
+ Y float64
+}
+
+// SecondsInRadians returns the angle of the second hand from 12 o'clock in radians
+func SecondsInRadians(t time.Time) float64 {
+ return (math.Pi / (secondsInHalfClock / float64(t.Second())))
+}
+
+// SecondHandPoint is the unit vector of the second hand at time `t`,
+// represented a Point.
+func SecondHandPoint(t time.Time) Point {
+ return angleToPoint(SecondsInRadians(t))
+}
+
+// MinutesInRadians returns the angle of the minute hand from 12 o'clock in radians
+func MinutesInRadians(t time.Time) float64 {
+ return (SecondsInRadians(t) / minutesInClock) +
+ (math.Pi / (minutesInHalfClock / float64(t.Minute())))
+}
+
+// MinuteHandPoint is the unit vector of the minute hand at time `t`,
+// represented a Point.
+func MinuteHandPoint(t time.Time) Point {
+ return angleToPoint(MinutesInRadians(t))
+}
+
+// HoursInRadians returns the angle of the hour hand from 12 o'clock in radians
+func HoursInRadians(t time.Time) float64 {
+ return (MinutesInRadians(t) / hoursInClock) +
+ (math.Pi / (hoursInHalfClock / float64(t.Hour()%hoursInClock)))
+}
+
+// HourHandPoint is the unit vector of the hour hand at time `t`,
+// represented a Point.
+func HourHandPoint(t time.Time) Point {
+ return angleToPoint(HoursInRadians(t))
+}
+
+func angleToPoint(angle float64) Point {
+ x := math.Sin(angle)
+ y := math.Cos(angle)
+
+ return Point{x, y}
+}
diff --git a/math/vFinal/clockface/clockface/clock.svg b/math/vFinal/clockface/clockface/clock.svg
new file mode 100644
index 000000000..174305bb3
--- /dev/null
+++ b/math/vFinal/clockface/clockface/clock.svg
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/math/vFinal/clockface/clockface/main.go b/math/vFinal/clockface/clockface/main.go
new file mode 100644
index 000000000..34a39d95a
--- /dev/null
+++ b/math/vFinal/clockface/clockface/main.go
@@ -0,0 +1,14 @@
+// Writes an SVG clockface of the current time to Stdout
+package main
+
+import (
+ "os"
+ "time"
+
+ "github.com/quii/learn-go-with-tests/math/vFinal/clockface/svg"
+)
+
+func main() {
+ t := time.Now()
+ svg.Write(os.Stdout, t)
+}
diff --git a/math/vFinal/clockface/clockface_test.go b/math/vFinal/clockface/clockface_test.go
new file mode 100644
index 000000000..aa7ba7f13
--- /dev/null
+++ b/math/vFinal/clockface/clockface_test.go
@@ -0,0 +1,145 @@
+package clockface_test
+
+import (
+ "math"
+ "testing"
+ "time"
+
+ . "github.com/quii/learn-go-with-tests/math/vFinal/clockface"
+)
+
+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 !roughlyEqualFloat64(got, c.angle) {
+ t.Fatalf("Wanted %v radians, but got %v", c.angle, got)
+ }
+ })
+ }
+}
+
+func TestSecondHandPoint(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ point Point
+ }{
+ {simpleTime(0, 0, 30), Point{X: 0, Y: -1}},
+ {simpleTime(0, 0, 45), Point{X: -1, Y: 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 TestMinutesInRadians(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ angle 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 !roughlyEqualFloat64(got, c.angle) {
+ t.Fatalf("Wanted %v radians, but got %v", c.angle, got)
+ }
+ })
+ }
+}
+
+func TestMinuteHandPoint(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ point Point
+ }{
+ {simpleTime(0, 30, 0), Point{X: 0, Y: -1}},
+ {simpleTime(0, 45, 0), Point{X: -1, Y: 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)
+ }
+ })
+ }
+}
+
+func TestHoursInRadians(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ angle float64
+ }{
+ {simpleTime(6, 0, 0), math.Pi},
+ {simpleTime(0, 0, 0), 0},
+ {simpleTime(21, 0, 0), math.Pi * 1.5},
+ {simpleTime(0, 1, 30), math.Pi / ((6 * 60 * 60) / 90)},
+ }
+
+ for _, c := range cases {
+ t.Run(testName(c.time), func(t *testing.T) {
+ got := HoursInRadians(c.time)
+ if !roughlyEqualFloat64(got, c.angle) {
+ t.Fatalf("Wanted %v radians, but got %v", c.angle, got)
+ }
+ })
+ }
+}
+
+func TestHourHandPoint(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ point Point
+ }{
+ {simpleTime(6, 0, 0), Point{X: 0, Y: -1}},
+ {simpleTime(21, 0, 0), Point{X: -1, Y: 0}},
+ }
+
+ for _, c := range cases {
+ t.Run(testName(c.time), func(t *testing.T) {
+ got := HourHandPoint(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-7
+ return math.Abs(a-b) < equalityThreshold
+}
+
+func roughlyEqualPoint(a, b Point) bool {
+ return roughlyEqualFloat64(a.X, b.X) &&
+ roughlyEqualFloat64(a.Y, b.Y)
+}
+
+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")
+}
diff --git a/math/vFinal/clockface/svg/svg.go b/math/vFinal/clockface/svg/svg.go
new file mode 100644
index 000000000..f2af8f434
--- /dev/null
+++ b/math/vFinal/clockface/svg/svg.go
@@ -0,0 +1,61 @@
+// Package svg produces an SVG clockface representation of a time.
+package svg
+
+import (
+ "fmt"
+ "io"
+ "time"
+
+ cf "github.com/quii/learn-go-with-tests/math/vFinal/clockface"
+)
+
+const (
+ secondHandLength = 90
+ minuteHandLength = 80
+ hourHandLength = 50
+ clockCentreX = 150
+ clockCentreY = 150
+)
+
+// Write writes an SVG representation of an analogue clock, showing the time t, to the writer w
+func Write(w io.Writer, t time.Time) {
+ io.WriteString(w, svgStart)
+ io.WriteString(w, bezel)
+ secondHand(w, t)
+ minuteHand(w, t)
+ hourHand(w, t)
+ io.WriteString(w, svgEnd)
+}
+
+func secondHand(w io.Writer, t time.Time) {
+ p := makeHand(cf.SecondHandPoint(t), secondHandLength)
+ fmt.Fprintf(w, ``, p.X, p.Y)
+}
+
+func minuteHand(w io.Writer, t time.Time) {
+ p := makeHand(cf.MinuteHandPoint(t), minuteHandLength)
+ fmt.Fprintf(w, ``, p.X, p.Y)
+}
+
+func hourHand(w io.Writer, t time.Time) {
+ p := makeHand(cf.HourHandPoint(t), hourHandLength)
+ fmt.Fprintf(w, ``, p.X, p.Y)
+}
+
+func makeHand(p cf.Point, length float64) cf.Point {
+ p = cf.Point{X: p.X * length, Y: p.Y * length}
+ p = cf.Point{X: p.X, Y: -p.Y}
+ return cf.Point{X: p.X + clockCentreX, Y: p.Y + clockCentreY}
+}
+
+const svgStart = `
+
+`
diff --git a/math/vFinal/clockface/svg/svg_test.go b/math/vFinal/clockface/svg/svg_test.go
new file mode 100644
index 000000000..ef7799bf9
--- /dev/null
+++ b/math/vFinal/clockface/svg/svg_test.go
@@ -0,0 +1,134 @@
+package svg_test
+
+import (
+ "bytes"
+ "encoding/xml"
+ "testing"
+ "time"
+
+ . "github.com/quii/learn-go-with-tests/math/vFinal/clockface/svg"
+)
+
+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 Circle `xml:"circle"`
+ Line []Line `xml:"line"`
+}
+
+type Line struct {
+ X1 float64 `xml:"x1,attr"`
+ Y1 float64 `xml:"y1,attr"`
+ X2 float64 `xml:"x2,attr"`
+ Y2 float64 `xml:"y2,attr"`
+}
+
+type Circle struct {
+ Cx float64 `xml:"cx,attr"`
+ Cy float64 `xml:"cy,attr"`
+ R float64 `xml:"r,attr"`
+}
+
+func TestSVGWriterSecondHand(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ line 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{}
+ Write(&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)
+ }
+ })
+ }
+}
+
+func TestSVGWriterMinutedHand(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ line 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{}
+ Write(&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)
+ }
+ })
+ }
+}
+
+func TestSVGWriterHourHand(t *testing.T) {
+ cases := []struct {
+ time time.Time
+ line 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{}
+ Write(&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)
+ }
+ })
+ }
+}
+
+func containsLine(l Line, ls []Line) bool {
+ for _, line := range ls {
+ if line == l {
+ return true
+ }
+ }
+ return false
+}
+
+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")
+}