Compare commits

...

9 Commits

Author SHA1 Message Date
ef646bf98e clockface: refactor magic numbers 2024-09-23 22:40:35 +02:00
6837cd9fc1 clockface: add hours 2024-09-23 22:35:02 +02:00
c6ef156d88 clockface: refactor 2024-09-23 22:18:46 +02:00
a4531bde4c clockface: add minute hand tests 2024-09-23 22:16:06 +02:00
5a62fef4f3 clockface: add minute hand 2024-09-23 22:09:10 +02:00
b58e2436cd clockface: refactorize acceptance test 2024-09-23 18:01:35 +02:00
7b3fb0f91c clockface: xml 2024-09-23 17:24:40 +02:00
423c9e9567 clockface: add main func 2024-09-23 16:57:24 +02:00
c9c9b9bf7b clockface: part1 2024-09-23 16:49:29 +02:00
5 changed files with 392 additions and 3 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
clock.svg
gobytest

113
clockface/clockface.go Normal file
View File

@ -0,0 +1,113 @@
package clockface
import (
"fmt"
"io"
"math"
"time"
)
// Point represents a two-dimentional Cartesian coordinate
type Point struct {
X float64
Y float64
}
const (
hourHandLength = 50
minuteHandLength = 80
secondHandLength = 90
clockCentreX = 150
clockCentreY = 150
)
const (
secondsInHalfClock = 30
secondsInClock = 2 * secondsInHalfClock
minutesInHalfClock = 30
minutesInClock = 2 * minutesInHalfClock
hoursInHalfClock = 6
hoursInClock = 2 * hoursInHalfClock
)
const svgStart = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 300 300"
version="2.0">`
const bezel = `<circle cx="150" cy="150" r="100" style="fill:#fff;stroke:#000;stroke-width:5px;"/>`
const svgEnd = `</svg>`
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)
}
// SecondHand is the unit vector of the second hand of an analogue clock at the time `t` represented as a Point
func secondHand(w io.Writer, t time.Time) {
p := secondHandPoint(t)
makeHand(w, secondHandLength, p)
}
// MinuteHand is the unit vector of the minute hand of an analogue clock at the time `t` represented as a Point
func minuteHand(w io.Writer, t time.Time) {
p := minuteHandPoint(t)
makeHand(w, minuteHandLength, p)
}
// MinuteHand is the unit vector of the minute hand of an analogue clock at the time `t` represented as a Point
func hourHand(w io.Writer, t time.Time) {
p := hourHandPoint(t)
makeHand(w, hourHandLength, p)
}
func makeHand(w io.Writer, length float64, p Point) {
p = Point{p.X * length, p.Y * length} // scale
p = Point{p.X, -p.Y} // flip
p = Point{p.X + clockCentreX, p.Y + clockCentreY} // translate
fmt.Fprintf(
w,
`<line x1="150" y1="150" x2="%f" y2="%f" style="fill:none;stroke:#f00;stroke-width:3px;"/>`,
p.X,
p.Y,
)
}
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 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 secondsInRadians(t time.Time) float64 {
return math.Pi / (secondsInHalfClock / float64(t.Second()))
}
func secondHandPoint(t time.Time) Point {
return angleToPoint(secondsInRadians(t))
}
func angleToPoint(angle float64) Point {
x := math.Sin(angle)
y := math.Cos(angle)
return Point{x, y}
}

View File

@ -0,0 +1,123 @@
package clockface
import (
"bytes"
"encoding/xml"
"testing"
"time"
)
type Svg struct {
XMLName xml.Name `xml:"svg"`
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 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"`
}
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{}
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{}
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{}
SVGWriter(&b, c.time)
svg := Svg{}
xml.Unmarshal(b.Bytes(), &svg)
if !containsLine(c.line, svg.Line) {
t.Errorf(
"Expected to find the hour hand line %+v in the SVG lines %v",
c.line,
svg.Line,
)
}
})
}
}
func containsLine(l Line, ls []Line) bool {
for _, line := range ls {
if line == l {
return true
}
}
return false
}

145
clockface/clockface_test.go Normal file
View File

@ -0,0 +1,145 @@
package clockface
import (
"math"
"testing"
"time"
)
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(c.angle, got) {
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(9, 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 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 c.angle != got {
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 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 c.angle != got {
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 simpleTime(hours, minutes, seconds int) time.Time {
return time.Date(321, time.October, 28, hours, minutes, seconds, 0, time.UTC)
}
func testName(t time.Time) string {
return t.Format("15:04:05")
}
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)
}

12
main.go
View File

@ -1,14 +1,20 @@
package main
import (
"gobytest/countdown"
"gobytest/clockface"
"os"
"time"
)
func main() {
// greet.Greet(os.Stdout, "Elodie")
sleeper := countdown.NewConfigurableSleeper(1*time.Second, time.Sleep)
// sleeper := countdown.NewConfigurableSleeper(1*time.Second, time.Sleep)
countdown.Countdown(os.Stdout, sleeper)
// countdown
// countdown.Countdown(os.Stdout, sleeper)
// clockface
t := time.Now()
clockface.SVGWriter(os.Stdout, t)
}