Compare commits
80 Commits
fc67cf14ff
...
main
Author | SHA1 | Date | |
---|---|---|---|
3923758807 | |||
3cdca7cc94 | |||
e8b8166ae9 | |||
48df8ace6e | |||
8228bcdce3 | |||
a33d490896 | |||
00596a7c52 | |||
1153a0a5bd | |||
dec5f045a7 | |||
6292463e90 | |||
048e6cccac | |||
ef646bf98e | |||
6837cd9fc1 | |||
c6ef156d88 | |||
a4531bde4c | |||
5a62fef4f3 | |||
b58e2436cd | |||
7b3fb0f91c | |||
423c9e9567 | |||
c9c9b9bf7b | |||
8629b537b0 | |||
abc546178d | |||
650ec27ec9 | |||
52825e49b3 | |||
715ea6477a | |||
6da23dc641 | |||
7d751cab3d | |||
b37f8a8d4c | |||
6f853c25d0 | |||
0a75d86850 | |||
2515644d4f | |||
3a0808c599 | |||
accb2c51dc | |||
5f93a0b843 | |||
7a32f26bd5 | |||
c5582275e7 | |||
c0db9ab22b | |||
0b181ccf0f | |||
d49f952035 | |||
436bcad0e1 | |||
6ded953ec6 | |||
572c8033f6 | |||
ea6f225992 | |||
41ae820a21 | |||
a0bad4d6e9 | |||
ab7818e9ea | |||
574532f484 | |||
10a93aeb27 | |||
61a0d0ff03 | |||
f64d75ca45 | |||
799d730d14 | |||
1c3ad9b9d8 | |||
049c346e63 | |||
80959f822b | |||
30a408c9e4 | |||
8026475424 | |||
122770ae5c | |||
766d7449e8 | |||
b332088638 | |||
3556c5b805 | |||
ee89092ea4 | |||
3cc29f8383 | |||
2dc7275f15 | |||
5042f1048e | |||
3538818769 | |||
491d2c1b22 | |||
d83f1c6f4c | |||
cd4f6fc6c7 | |||
cb8e6d23f3 | |||
93d9a14858 | |||
05226985c7 | |||
72e6ac9b77 | |||
56be76b275 | |||
d7840e9cb7 | |||
5f02a8fbde | |||
f573268be9 | |||
c04a2e1d65 | |||
4be80f4477 | |||
58efd33b20 | |||
7e713f54de |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
clock.svg
|
||||
gobytest
|
39
blogposts/blogposts.go
Normal file
39
blogposts/blogposts.go
Normal file
@ -0,0 +1,39 @@
|
||||
package blogposts
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var markdownSuffix = map[string]struct{}{
|
||||
"md": {},
|
||||
}
|
||||
|
||||
var ErrUnknownFileType = errors.New("unknown file type, must be markdown")
|
||||
|
||||
func NewPostsFromFS(fileSystem fs.FS) ([]Post, error) {
|
||||
dir, err := fs.ReadDir(fileSystem, ".")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var posts []Post
|
||||
for _, f := range dir {
|
||||
fileName := f.Name()
|
||||
if !isMarkdownFile(fileName) {
|
||||
return nil, ErrUnknownFileType
|
||||
}
|
||||
post, err := getPost(fileSystem, fileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
posts = append(posts, post)
|
||||
}
|
||||
return posts, nil
|
||||
}
|
||||
|
||||
func isMarkdownFile(fileName string) bool {
|
||||
splitted := strings.Split(fileName, ".")
|
||||
_, ok := markdownSuffix[splitted[len(splitted)-1:][0]]
|
||||
return ok
|
||||
}
|
75
blogposts/blogposts_test.go
Normal file
75
blogposts/blogposts_test.go
Normal file
@ -0,0 +1,75 @@
|
||||
package blogposts
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
)
|
||||
|
||||
// NOTE: This should be a black box test outside blogposts package.
|
||||
|
||||
// NOTE: If we want to test the return error
|
||||
//
|
||||
// type StubFailingFS struct{}
|
||||
//
|
||||
// func (s StubFailingFS) Open(name string) (fs.File, error) {
|
||||
// return nil, errors.New("fail")
|
||||
// }
|
||||
|
||||
func TestNewBlogPosts(t *testing.T) {
|
||||
const (
|
||||
firstBody = `Title: Post 1
|
||||
Description: Description 1
|
||||
Tags: tdd, go
|
||||
---
|
||||
Hello
|
||||
World`
|
||||
secondBody = `Title: Post 2
|
||||
Description: Description 2
|
||||
Tags: rust, borrow-checker
|
||||
---
|
||||
B
|
||||
L
|
||||
M`
|
||||
)
|
||||
|
||||
fs := fstest.MapFS{
|
||||
"hello world.md": {Data: []byte(firstBody)},
|
||||
"hello-world2.md": {Data: []byte(secondBody)},
|
||||
}
|
||||
|
||||
posts, err := NewPostsFromFS(fs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(posts) != len(fs) {
|
||||
t.Errorf("got %d posts, wanted %d posts", len(posts), len(fs))
|
||||
}
|
||||
|
||||
assertPost(t, posts[0], Post{
|
||||
Title: "Post 1",
|
||||
Description: "Description 1",
|
||||
Tags: []string{"tdd", "go"},
|
||||
Body: `Hello
|
||||
World`,
|
||||
})
|
||||
}
|
||||
|
||||
func TestWrongFile(t *testing.T) {
|
||||
fs := fstest.MapFS{
|
||||
"hello world.txt": {Data: []byte("Yolo")},
|
||||
}
|
||||
|
||||
_, err := NewPostsFromFS(fs)
|
||||
if err == nil {
|
||||
t.Errorf("should be an error but not")
|
||||
}
|
||||
}
|
||||
|
||||
func assertPost(t testing.TB, got Post, want Post) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %+v, want %+v", got, want)
|
||||
}
|
||||
}
|
7
blogposts/hello_world.md
Normal file
7
blogposts/hello_world.md
Normal file
@ -0,0 +1,7 @@
|
||||
Title: Hello, TDD world!
|
||||
Description: First post on our wonderful blog
|
||||
Tags: tdd, go
|
||||
---
|
||||
Hello world!
|
||||
|
||||
The body of posts starts after the `---`
|
73
blogposts/post.go
Normal file
73
blogposts/post.go
Normal file
@ -0,0 +1,73 @@
|
||||
package blogposts
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"io/fs"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Post struct {
|
||||
Title, Description, Body string
|
||||
Tags []string
|
||||
}
|
||||
|
||||
// TODO: Split a metadata line into 2 parts, one is the key, another is the
|
||||
// value, check if the key exists in the key map (ex. Title, Description etc.)
|
||||
// If not exist, then ignore this metadata line.
|
||||
//
|
||||
// WARN: What if a file doesn't have metadata, just pure Markdown ??
|
||||
// We can control if we at least found title line. If at the end, it is not
|
||||
// present, we consider that the post has an error.
|
||||
const (
|
||||
titleLine = "Title: "
|
||||
descriptionLine = "Description: "
|
||||
tagsLine = "Tags: "
|
||||
tagSeparator = ", "
|
||||
bodySeparator = "---"
|
||||
)
|
||||
|
||||
func getPost(fileSystem fs.FS, fileName string) (Post, error) {
|
||||
postFile, err := fileSystem.Open(fileName)
|
||||
if err != nil {
|
||||
return Post{}, err
|
||||
}
|
||||
defer postFile.Close()
|
||||
|
||||
return newPost(postFile)
|
||||
}
|
||||
|
||||
// NOTE: Does newPost have to be coupled to an fs.File ?
|
||||
// Do we use all the methods and data from this type? What do we really need?
|
||||
func newPost(postFile io.Reader) (Post, error) {
|
||||
scanner := bufio.NewScanner(postFile)
|
||||
|
||||
readMetaLine := func(tagName string) string {
|
||||
scanner.Scan()
|
||||
return strings.TrimPrefix(scanner.Text(), tagName)
|
||||
}
|
||||
|
||||
post := Post{
|
||||
Title: readMetaLine(titleLine),
|
||||
Description: readMetaLine(descriptionLine),
|
||||
Tags: strings.Split(readMetaLine(tagsLine), tagSeparator),
|
||||
Body: readBody(scanner),
|
||||
}
|
||||
return post, nil
|
||||
}
|
||||
|
||||
func readBody(scanner *bufio.Scanner) string {
|
||||
for scanner.Scan() {
|
||||
if strings.TrimSpace(scanner.Text()) == bodySeparator {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// The rest is the body
|
||||
var buf strings.Builder
|
||||
for scanner.Scan() {
|
||||
buf.Write(scanner.Bytes())
|
||||
buf.Write([]byte{'\n'})
|
||||
}
|
||||
return strings.TrimSuffix(buf.String(), "\n")
|
||||
}
|
113
clockface/clockface.go
Normal file
113
clockface/clockface.go
Normal 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}
|
||||
}
|
123
clockface/clockface_acceptance_test.go
Normal file
123
clockface/clockface_acceptance_test.go
Normal 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
145
clockface/clockface_test.go
Normal 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)
|
||||
}
|
31
concurrency/concurrency.go
Normal file
31
concurrency/concurrency.go
Normal file
@ -0,0 +1,31 @@
|
||||
package concurrency
|
||||
|
||||
type WebsiteChecker func(string) bool
|
||||
|
||||
type result struct {
|
||||
string
|
||||
bool
|
||||
}
|
||||
|
||||
func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool {
|
||||
results := make(map[string]bool)
|
||||
resultChannel := make(chan result)
|
||||
|
||||
for _, url := range urls {
|
||||
go func(u string) {
|
||||
resultChannel <- result{u, wc(u)}
|
||||
}(url)
|
||||
}
|
||||
// XXX: it seems that this works on Go 1.23
|
||||
// for _, u := range urls {
|
||||
// go func() {
|
||||
// resultChannel <- result{u, wc(u)}
|
||||
// }()
|
||||
// }
|
||||
for i := 0; i < len(urls); i++ {
|
||||
r := <-resultChannel
|
||||
results[r.string] = r.bool
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
47
concurrency/concurrency_test.go
Normal file
47
concurrency/concurrency_test.go
Normal file
@ -0,0 +1,47 @@
|
||||
package concurrency
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func mockWebsiteChecker(url string) bool {
|
||||
return url != "waat://furhurterwe.geds"
|
||||
}
|
||||
|
||||
func TestCheckWebsites(t *testing.T) {
|
||||
websites := []string{
|
||||
"http://google.com",
|
||||
"http://blog.abc.com",
|
||||
"waat://furhurterwe.geds",
|
||||
}
|
||||
|
||||
want := map[string]bool{
|
||||
"http://google.com": true,
|
||||
"http://blog.abc.com": true,
|
||||
"waat://furhurterwe.geds": false,
|
||||
}
|
||||
|
||||
got := CheckWebsites(mockWebsiteChecker, websites)
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("wanted %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func slowStubWebsiteChecker(_ string) bool {
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
return true
|
||||
}
|
||||
|
||||
func BenchmarkCheckWebsites(b *testing.B) {
|
||||
urls := make([]string, 100)
|
||||
for i := 0; i < len(urls); i++ {
|
||||
urls[i] = "a url"
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
CheckWebsites(slowStubWebsiteChecker, urls)
|
||||
}
|
||||
}
|
23
context/context.go
Normal file
23
context/context.go
Normal file
@ -0,0 +1,23 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
Fetch(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
func Server(store Store) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := store.Fetch(r.Context())
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Fprint(w, data)
|
||||
}
|
||||
}
|
98
context/context_test.go
Normal file
98
context/context_test.go
Normal file
@ -0,0 +1,98 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SpyStore struct {
|
||||
response string
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func (s *SpyStore) Fetch(ctx context.Context) (string, error) {
|
||||
data := make(chan string, 1)
|
||||
go func() {
|
||||
var result string
|
||||
for _, c := range s.response {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("spy store got cancelled")
|
||||
return
|
||||
default:
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
result += string(c)
|
||||
}
|
||||
}
|
||||
data <- result
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return "", ctx.Err()
|
||||
case res := <-data:
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
type SpyResponseWriter struct {
|
||||
written bool
|
||||
}
|
||||
|
||||
func (s *SpyResponseWriter) Header() http.Header {
|
||||
s.written = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SpyResponseWriter) Write([]byte) (int, error) {
|
||||
s.written = true
|
||||
return 0, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (s *SpyResponseWriter) WriteHeader(statusCode int) {
|
||||
s.written = true
|
||||
}
|
||||
|
||||
func TestServer(t *testing.T) {
|
||||
t.Run("basic get store", func(t *testing.T) {
|
||||
data := "hello, world"
|
||||
store := &SpyStore{response: data, t: t}
|
||||
srv := Server(store)
|
||||
|
||||
request := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
srv.ServeHTTP(response, request)
|
||||
|
||||
if response.Body.String() != data {
|
||||
t.Errorf(`got "%s", want "%s"`, response.Body.String(), data)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("tells store to cancel work if request is cancelled", func(t *testing.T) {
|
||||
data := "hello, world"
|
||||
store := &SpyStore{response: data, t: t}
|
||||
srv := Server(store)
|
||||
|
||||
request := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
|
||||
cancellingCtx, cancel := context.WithCancel(request.Context())
|
||||
// Wait 5ms to call cancel
|
||||
time.AfterFunc(5*time.Millisecond, cancel)
|
||||
|
||||
request = request.WithContext(cancellingCtx)
|
||||
|
||||
response := &SpyResponseWriter{}
|
||||
|
||||
srv.ServeHTTP(response, request)
|
||||
|
||||
if response.written {
|
||||
t.Error("a response should not have been written")
|
||||
}
|
||||
})
|
||||
}
|
45
countdown/countdown.go
Normal file
45
countdown/countdown.go
Normal file
@ -0,0 +1,45 @@
|
||||
package countdown
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
finalWord = "Go!"
|
||||
countdownStart = 3
|
||||
)
|
||||
|
||||
type Sleeper interface {
|
||||
Sleep()
|
||||
}
|
||||
|
||||
type ConfigurableSleeper struct {
|
||||
duration time.Duration
|
||||
sleep func(time.Duration)
|
||||
}
|
||||
|
||||
func NewConfigurableSleeper(
|
||||
duration time.Duration,
|
||||
sleep func(time.Duration),
|
||||
) *ConfigurableSleeper {
|
||||
return &ConfigurableSleeper{
|
||||
duration: duration,
|
||||
sleep: sleep,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ConfigurableSleeper) Sleep() {
|
||||
s.sleep(s.duration)
|
||||
}
|
||||
|
||||
func Countdown(out io.Writer, sleeper Sleeper) {
|
||||
// XXX: The sequence is not tested
|
||||
for i := countdownStart; i > 0; i-- {
|
||||
fmt.Fprintf(out, "%d\n", i)
|
||||
// This is difficult to test!
|
||||
sleeper.Sleep()
|
||||
}
|
||||
fmt.Fprint(out, finalWord)
|
||||
}
|
77
countdown/countdown_test.go
Normal file
77
countdown/countdown_test.go
Normal file
@ -0,0 +1,77 @@
|
||||
package countdown
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
write = "write"
|
||||
sleep = "sleep"
|
||||
)
|
||||
|
||||
type SpyCountdownOperations struct {
|
||||
Calls []string
|
||||
}
|
||||
|
||||
func (s *SpyCountdownOperations) Sleep() {
|
||||
s.Calls = append(s.Calls, sleep)
|
||||
}
|
||||
|
||||
func (s *SpyCountdownOperations) Write(p []byte) (n int, err error) {
|
||||
s.Calls = append(s.Calls, write)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
type SpyTime struct {
|
||||
durationSlept time.Duration
|
||||
}
|
||||
|
||||
func (s *SpyTime) Sleep(duration time.Duration) {
|
||||
s.durationSlept = duration
|
||||
}
|
||||
|
||||
func TestCountdown(t *testing.T) {
|
||||
t.Run("print 3 to Go!", func(t *testing.T) {
|
||||
buffer := &bytes.Buffer{}
|
||||
Countdown(buffer, &SpyCountdownOperations{})
|
||||
|
||||
got := buffer.String()
|
||||
want := `3
|
||||
2
|
||||
1
|
||||
Go!`
|
||||
|
||||
if got != want {
|
||||
t.Errorf("got %q want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("sleep before every print", func(t *testing.T) {
|
||||
spySleepPrinter := &SpyCountdownOperations{}
|
||||
|
||||
Countdown(spySleepPrinter, spySleepPrinter)
|
||||
|
||||
want := []string{
|
||||
write, sleep, write, sleep, write, sleep, write,
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(want, spySleepPrinter.Calls) {
|
||||
t.Errorf("wanted %v got %v", want, spySleepPrinter.Calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestConfigurableSleeper(t *testing.T) {
|
||||
sleepTime := 5 * time.Second
|
||||
|
||||
spyTime := &SpyTime{}
|
||||
sleeper := NewConfigurableSleeper(sleepTime, spyTime.Sleep)
|
||||
sleeper.Sleep()
|
||||
|
||||
if spyTime.durationSlept != sleepTime {
|
||||
t.Errorf("should have slept for %v but slept for %v", sleepTime, spyTime.durationSlept)
|
||||
}
|
||||
}
|
22
counter/counter.go
Normal file
22
counter/counter.go
Normal file
@ -0,0 +1,22 @@
|
||||
package counter
|
||||
|
||||
import "sync"
|
||||
|
||||
type Counter struct {
|
||||
count int
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
func NewCounter() *Counter {
|
||||
return &Counter{}
|
||||
}
|
||||
|
||||
func (c *Counter) Inc() {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
c.count++
|
||||
}
|
||||
|
||||
func (c *Counter) Value() int {
|
||||
return c.count
|
||||
}
|
44
counter/counter_test.go
Normal file
44
counter/counter_test.go
Normal file
@ -0,0 +1,44 @@
|
||||
package counter
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// assertCounter
|
||||
// XXX: a copy of Counter is created, but we shouldn't copy a mutex. Use pointer instead.
|
||||
func assertCounter(t testing.TB, got *Counter, want int) {
|
||||
t.Helper()
|
||||
if got.Value() != want {
|
||||
t.Errorf("got %d, want %d", got.Value(), want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCounter(t *testing.T) {
|
||||
t.Run("incrementing the counter 3 times leaves it at 3", func(t *testing.T) {
|
||||
counter := NewCounter()
|
||||
|
||||
counter.Inc()
|
||||
counter.Inc()
|
||||
counter.Inc()
|
||||
|
||||
assertCounter(t, counter, 3)
|
||||
})
|
||||
|
||||
t.Run("it runs safely concurrently", func(t *testing.T) {
|
||||
wantedCount := 1000
|
||||
counter := NewCounter()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(wantedCount)
|
||||
|
||||
for i := 0; i < wantedCount; i++ {
|
||||
go func() {
|
||||
counter.Inc()
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
assertCounter(t, counter, wantedCount)
|
||||
})
|
||||
}
|
@ -1,10 +1,19 @@
|
||||
package dictionary
|
||||
|
||||
import "errors"
|
||||
|
||||
type Dictionary map[string]string
|
||||
|
||||
var ErrNotFound = errors.New("could not find the word you were looking for")
|
||||
const (
|
||||
ErrNotFound = DictionaryErr("could not find the word you were looking for")
|
||||
ErrWordExists = DictionaryErr("word exists")
|
||||
ErrWordDoesNotExist = DictionaryErr("word does not exist")
|
||||
)
|
||||
|
||||
type DictionaryErr string
|
||||
|
||||
// implements the Error interface, makes it an "error"
|
||||
func (e DictionaryErr) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (d Dictionary) Search(word string) (string, error) {
|
||||
result, ok := d[word]
|
||||
@ -13,3 +22,33 @@ func (d Dictionary) Search(word string) (string, error) {
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (d Dictionary) Add(word, definition string) error {
|
||||
_, err := d.Search(word)
|
||||
switch err {
|
||||
case ErrNotFound:
|
||||
d[word] = definition
|
||||
case nil:
|
||||
return ErrWordExists
|
||||
default:
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d Dictionary) Update(word, definition string) error {
|
||||
_, err := d.Search(word)
|
||||
switch err {
|
||||
case ErrNotFound:
|
||||
return ErrWordDoesNotExist
|
||||
case nil:
|
||||
d[word] = definition
|
||||
default:
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d Dictionary) Delete(word string) {
|
||||
delete(d, word)
|
||||
}
|
||||
|
@ -1,12 +1,22 @@
|
||||
package dictionary
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func assertStrings(t testing.TB, got, want string) {
|
||||
func assertStrings(t testing.TB, got, want, given string) {
|
||||
t.Helper()
|
||||
|
||||
if got != want {
|
||||
t.Errorf("got %q want %q given %q", got, want, "test")
|
||||
t.Errorf("got %q want %q given %q", got, want, given)
|
||||
}
|
||||
}
|
||||
|
||||
func assertError(t testing.TB, got, want error) {
|
||||
t.Helper()
|
||||
|
||||
if got != want {
|
||||
t.Errorf("got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,16 +27,82 @@ func TestSearch(t *testing.T) {
|
||||
got, _ := dictionary.Search("test")
|
||||
want := "this is just a test"
|
||||
|
||||
assertStrings(t, got, want)
|
||||
assertStrings(t, got, want, "test")
|
||||
})
|
||||
|
||||
t.Run("unknown word", func(t *testing.T) {
|
||||
_, err := dictionary.Search("undefined")
|
||||
want := "could not find the word you were looking for"
|
||||
if err == nil {
|
||||
t.Fatal("expected to get an error")
|
||||
}
|
||||
|
||||
assertStrings(t, err.Error(), want)
|
||||
assertError(t, err, ErrNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func assertDefinition(t testing.TB, dict Dictionary, word, def string) {
|
||||
t.Helper()
|
||||
|
||||
got, err := dict.Search(word)
|
||||
if err != nil {
|
||||
t.Fatal("Should find added word:", err)
|
||||
}
|
||||
assertStrings(t, got, def, word)
|
||||
}
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
t.Run("new word", func(t *testing.T) {
|
||||
dict := Dictionary{}
|
||||
|
||||
word := "test"
|
||||
def := "this is just a test"
|
||||
|
||||
dict.Add(word, def)
|
||||
assertDefinition(t, dict, word, def)
|
||||
})
|
||||
|
||||
t.Run("existing word", func(t *testing.T) {
|
||||
word := "test"
|
||||
def := "this is just a test"
|
||||
dict := Dictionary{word: def}
|
||||
|
||||
err := dict.Add(word, def)
|
||||
assertError(t, err, ErrWordExists)
|
||||
assertDefinition(t, dict, word, def)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
t.Run("update an existing word", func(t *testing.T) {
|
||||
word := "test"
|
||||
def := "this is just a test"
|
||||
updated := "updated definition"
|
||||
dict := Dictionary{word: def}
|
||||
|
||||
dict.Update(word, updated)
|
||||
assertDefinition(t, dict, word, updated)
|
||||
})
|
||||
|
||||
t.Run("update an non-existing word", func(t *testing.T) {
|
||||
word := "test"
|
||||
updated := "updated definition"
|
||||
dict := Dictionary{}
|
||||
|
||||
err := dict.Update(word, updated)
|
||||
assertError(t, err, ErrWordDoesNotExist)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
t.Run("delete a word", func(t *testing.T) {
|
||||
word := "test"
|
||||
def := "this is just a test"
|
||||
dict := Dictionary{word: def}
|
||||
|
||||
dict.Delete(word)
|
||||
_, err := dict.Search(word)
|
||||
if err == nil {
|
||||
t.Fatal("expected to get an error")
|
||||
}
|
||||
assertError(t, err, ErrNotFound)
|
||||
})
|
||||
}
|
||||
|
17
greet/greet_test.go
Normal file
17
greet/greet_test.go
Normal file
@ -0,0 +1,17 @@
|
||||
package greet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGreet(t *testing.T) {
|
||||
buffer := bytes.Buffer{}
|
||||
Greet(&buffer, "Chris")
|
||||
got := buffer.String()
|
||||
want := "Hello, Chris"
|
||||
|
||||
if got != want {
|
||||
t.Errorf("got %q want %q", got, want)
|
||||
}
|
||||
}
|
19
greet/main.go
Normal file
19
greet/main.go
Normal file
@ -0,0 +1,19 @@
|
||||
package greet
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func Greet(writer io.Writer, name string) {
|
||||
fmt.Fprintf(writer, "Hello, %s", name)
|
||||
}
|
||||
|
||||
func MyGreeterHandler(w http.ResponseWriter, r *http.Request) {
|
||||
Greet(w, "world")
|
||||
}
|
||||
|
||||
// func main() {
|
||||
// log.Fatal(http.ListenAndServe(":5001", http.HandlerFunc(MyGreeterHandler)))
|
||||
// }
|
20
main.go
Normal file
20
main.go
Normal file
@ -0,0 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"gobytest/clockface"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// greet.Greet(os.Stdout, "Elodie")
|
||||
// sleeper := countdown.NewConfigurableSleeper(1*time.Second, time.Sleep)
|
||||
|
||||
// countdown
|
||||
// countdown.Countdown(os.Stdout, sleeper)
|
||||
|
||||
// clockface
|
||||
t := time.Now()
|
||||
|
||||
clockface.SVGWriter(os.Stdout, t)
|
||||
}
|
45
racer/racer.go
Normal file
45
racer/racer.go
Normal file
@ -0,0 +1,45 @@
|
||||
package racer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrRacerTimeout = errors.New("timeout")
|
||||
|
||||
const tenSecondTimeout = 10 * time.Second
|
||||
|
||||
func ConfigurableRacer(a, b string, timeout time.Duration) (string, error) {
|
||||
select {
|
||||
// wait until channel closed
|
||||
case <-ping(a):
|
||||
return a, nil
|
||||
case <-ping(b):
|
||||
return b, nil
|
||||
case <-time.After(timeout):
|
||||
return "", ErrRacerTimeout
|
||||
}
|
||||
}
|
||||
|
||||
func Racer(a, b string) (string, error) {
|
||||
return ConfigurableRacer(a, b, tenSecondTimeout)
|
||||
}
|
||||
|
||||
func ping(url string) chan struct{} {
|
||||
ch := make(chan struct{})
|
||||
go func() {
|
||||
http.Get(url)
|
||||
// if got, close channel
|
||||
close(ch)
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
// func measureResponseTime(url string) time.Duration {
|
||||
// start := time.Now()
|
||||
// http.Get(url)
|
||||
// duration := time.Since(start)
|
||||
// fmt.Println(duration)
|
||||
// return duration
|
||||
// }
|
55
racer/racer_test.go
Normal file
55
racer/racer_test.go
Normal file
@ -0,0 +1,55 @@
|
||||
package racer
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SpyRacerTimeout struct{}
|
||||
|
||||
func (s *SpyRacerTimeout) Timeout() <-chan time.Time {
|
||||
return time.After(1 * time.Millisecond)
|
||||
}
|
||||
|
||||
func TestRacer(t *testing.T) {
|
||||
t.Run("compares speeds of servers, returning the url of the fasted one", func(t *testing.T) {
|
||||
slowServer := makeDelayedServer(20 * time.Millisecond)
|
||||
defer slowServer.Close()
|
||||
|
||||
fastServer := makeDelayedServer(0 * time.Millisecond)
|
||||
defer fastServer.Close()
|
||||
|
||||
slowURL := slowServer.URL
|
||||
fastURL := fastServer.URL
|
||||
|
||||
want := fastURL
|
||||
got, _ := Racer(slowURL, fastURL)
|
||||
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns an error if a server doesn't respond within 10s", func(t *testing.T) {
|
||||
serverA := makeDelayedServer(25 * time.Millisecond)
|
||||
defer serverA.Close()
|
||||
|
||||
serverB := makeDelayedServer(25 * time.Millisecond)
|
||||
defer serverB.Close()
|
||||
|
||||
_, err := ConfigurableRacer(serverA.URL, serverB.URL, 20*time.Millisecond)
|
||||
if err == nil {
|
||||
t.Error("expected an error but didn't got one")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func makeDelayedServer(delay time.Duration) *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(delay)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
}
|
58
roman/roman.go
Normal file
58
roman/roman.go
Normal file
@ -0,0 +1,58 @@
|
||||
package roman
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type RomanNumeral struct {
|
||||
Value uint16
|
||||
Symbol string
|
||||
}
|
||||
|
||||
var allRomanNumerals = []RomanNumeral{
|
||||
{1000, "M"},
|
||||
{900, "CM"},
|
||||
{500, "D"},
|
||||
{400, "CD"},
|
||||
{900, "CM"},
|
||||
{100, "C"},
|
||||
{90, "XC"},
|
||||
{50, "L"},
|
||||
{40, "XL"},
|
||||
{10, "X"},
|
||||
{9, "IX"},
|
||||
{5, "V"},
|
||||
{4, "IV"},
|
||||
{1, "I"},
|
||||
}
|
||||
|
||||
var ErrNumberOutOfRange = errors.New("numbers should not be bigger than 3999")
|
||||
|
||||
func ConvertToRoman(arabic uint16) (string, error) {
|
||||
if arabic > 3999 {
|
||||
return "", ErrNumberOutOfRange
|
||||
}
|
||||
var converted strings.Builder
|
||||
for _, numeral := range allRomanNumerals {
|
||||
for arabic >= numeral.Value {
|
||||
converted.WriteString(numeral.Symbol)
|
||||
arabic -= numeral.Value
|
||||
}
|
||||
}
|
||||
|
||||
return converted.String(), nil
|
||||
}
|
||||
|
||||
// XXX: This convert function doesn't check the validity of the roman numerals string
|
||||
func ConvertToArabic(roman string) uint16 {
|
||||
var converted uint16
|
||||
for _, numeral := range allRomanNumerals {
|
||||
for strings.HasPrefix(roman, numeral.Symbol) {
|
||||
converted += numeral.Value
|
||||
// roman = roman[len(numeral.Symbol):]
|
||||
roman = strings.TrimPrefix(roman, numeral.Symbol)
|
||||
}
|
||||
}
|
||||
return converted
|
||||
}
|
85
roman/roman_test.go
Normal file
85
roman/roman_test.go
Normal file
@ -0,0 +1,85 @@
|
||||
package roman
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"testing/quick"
|
||||
)
|
||||
|
||||
var cases = []struct {
|
||||
Arabic uint16
|
||||
Roman string
|
||||
}{
|
||||
{Arabic: 1, Roman: "I"},
|
||||
{Arabic: 2, Roman: "II"},
|
||||
{Arabic: 3, Roman: "III"},
|
||||
{Arabic: 4, Roman: "IV"},
|
||||
{Arabic: 5, Roman: "V"},
|
||||
{Arabic: 6, Roman: "VI"},
|
||||
{Arabic: 7, Roman: "VII"},
|
||||
{Arabic: 8, Roman: "VIII"},
|
||||
{Arabic: 9, Roman: "IX"},
|
||||
{Arabic: 10, Roman: "X"},
|
||||
{Arabic: 14, Roman: "XIV"},
|
||||
{Arabic: 18, Roman: "XVIII"},
|
||||
{Arabic: 20, Roman: "XX"},
|
||||
{Arabic: 39, Roman: "XXXIX"},
|
||||
{Arabic: 40, Roman: "XL"},
|
||||
{Arabic: 47, Roman: "XLVII"},
|
||||
{Arabic: 49, Roman: "XLIX"},
|
||||
{Arabic: 50, Roman: "L"},
|
||||
{Arabic: 100, Roman: "C"},
|
||||
{Arabic: 90, Roman: "XC"},
|
||||
{Arabic: 400, Roman: "CD"},
|
||||
{Arabic: 500, Roman: "D"},
|
||||
{Arabic: 900, Roman: "CM"},
|
||||
{Arabic: 1000, Roman: "M"},
|
||||
{Arabic: 1984, Roman: "MCMLXXXIV"},
|
||||
{Arabic: 3999, Roman: "MMMCMXCIX"},
|
||||
{Arabic: 2014, Roman: "MMXIV"},
|
||||
{Arabic: 1006, Roman: "MVI"},
|
||||
{Arabic: 798, Roman: "DCCXCVIII"},
|
||||
}
|
||||
|
||||
func TestRomanNemerals(t *testing.T) {
|
||||
for _, test := range cases {
|
||||
t.Run(fmt.Sprintf("%d gets converted to %q", test.Arabic, test.Roman), func(t *testing.T) {
|
||||
got, _ := ConvertToRoman(test.Arabic)
|
||||
want := test.Roman
|
||||
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertingToArabic(t *testing.T) {
|
||||
for _, test := range cases {
|
||||
t.Run(fmt.Sprintf("%q gets converted to %d", test.Roman, test.Arabic), func(t *testing.T) {
|
||||
got := ConvertToArabic(test.Roman)
|
||||
want := test.Arabic
|
||||
|
||||
if got != want {
|
||||
t.Errorf("got %d, want %d", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPropertiesOfConversion(t *testing.T) {
|
||||
assertion := func(arabic uint16) bool {
|
||||
roman, err := ConvertToRoman(arabic)
|
||||
if arabic > 3999 {
|
||||
return err == ErrNumberOutOfRange
|
||||
}
|
||||
fromRoman := ConvertToArabic(roman)
|
||||
return fromRoman == arabic
|
||||
}
|
||||
|
||||
if err := quick.Check(assertion, &quick.Config{
|
||||
MaxCount: 1000,
|
||||
}); err != nil {
|
||||
t.Error("failed checks", err)
|
||||
}
|
||||
}
|
52
walk/walk.go
Normal file
52
walk/walk.go
Normal file
@ -0,0 +1,52 @@
|
||||
package walk
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func Walk(x interface{}, fn func(string)) {
|
||||
val := getValue(x)
|
||||
|
||||
walkValue := func(value reflect.Value) {
|
||||
Walk(value.Interface(), fn)
|
||||
}
|
||||
|
||||
switch val.Kind() {
|
||||
case reflect.String:
|
||||
fn(val.String())
|
||||
case reflect.Struct:
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
// XXX: Interface() to get interface
|
||||
walkValue(val.Field(i))
|
||||
}
|
||||
case reflect.Slice, reflect.Array:
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
walkValue(val.Index(i))
|
||||
}
|
||||
case reflect.Map:
|
||||
for _, key := range val.MapKeys() {
|
||||
walkValue(val.MapIndex(key))
|
||||
}
|
||||
case reflect.Chan:
|
||||
for {
|
||||
if v, ok := val.Recv(); ok {
|
||||
walkValue(v)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
case reflect.Func:
|
||||
valFnResult := val.Call(nil)
|
||||
for _, res := range valFnResult {
|
||||
walkValue(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getValue(x interface{}) reflect.Value {
|
||||
val := reflect.ValueOf(x)
|
||||
if val.Kind() == reflect.Pointer {
|
||||
return val.Elem()
|
||||
}
|
||||
return val
|
||||
}
|
147
walk/walk_test.go
Normal file
147
walk/walk_test.go
Normal file
@ -0,0 +1,147 @@
|
||||
package walk
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type Profile struct {
|
||||
Age int
|
||||
City string
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Profile Profile
|
||||
}
|
||||
|
||||
func TestWalk(t *testing.T) {
|
||||
cases := []struct {
|
||||
Name string
|
||||
Input interface{}
|
||||
ExpectedCalls []string
|
||||
}{
|
||||
{
|
||||
"struct with one string field",
|
||||
struct {
|
||||
Name string
|
||||
}{"Chris"},
|
||||
[]string{"Chris"},
|
||||
},
|
||||
{
|
||||
"struct with two string fields",
|
||||
struct {
|
||||
Name string
|
||||
City string
|
||||
}{"Chris", "London"},
|
||||
[]string{"Chris", "London"},
|
||||
},
|
||||
{
|
||||
"struct with non string field",
|
||||
struct {
|
||||
Name string
|
||||
Age int
|
||||
}{"Chris", 29},
|
||||
[]string{"Chris"},
|
||||
},
|
||||
{
|
||||
"nested fields",
|
||||
Person{"Chris", Profile{29, "London"}},
|
||||
[]string{"Chris", "London"},
|
||||
},
|
||||
{
|
||||
"pointer to things",
|
||||
&Person{"Chris", Profile{29, "London"}},
|
||||
[]string{"Chris", "London"},
|
||||
},
|
||||
{
|
||||
"slices",
|
||||
[]Profile{
|
||||
{29, "London"},
|
||||
{33, "Paris"},
|
||||
},
|
||||
[]string{"London", "Paris"},
|
||||
},
|
||||
{
|
||||
"arrays",
|
||||
[2]Profile{
|
||||
{29, "London"},
|
||||
{33, "Paris"},
|
||||
},
|
||||
[]string{"London", "Paris"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range cases {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
var got []string
|
||||
Walk(test.Input, func(input string) {
|
||||
got = append(got, input)
|
||||
})
|
||||
|
||||
if !reflect.DeepEqual(got, test.ExpectedCalls) {
|
||||
t.Errorf("got %v want %v", got, test.ExpectedCalls)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("with maps", func(t *testing.T) {
|
||||
aMap := map[string]string{
|
||||
"Cow": "meuh",
|
||||
"Sheep": "meh",
|
||||
}
|
||||
var got []string
|
||||
Walk(aMap, func(input string) {
|
||||
got = append(got, input)
|
||||
})
|
||||
|
||||
assertContains(t, got, "meuh")
|
||||
assertContains(t, got, "meh")
|
||||
})
|
||||
|
||||
t.Run("with channels", func(t *testing.T) {
|
||||
aChannel := make(chan Profile)
|
||||
go func() {
|
||||
aChannel <- Profile{21, "Berlin"}
|
||||
aChannel <- Profile{28, "Beijing"}
|
||||
close(aChannel)
|
||||
}()
|
||||
var got []string
|
||||
want := []string{"Berlin", "Beijing"}
|
||||
Walk(aChannel, func(input string) {
|
||||
got = append(got, input)
|
||||
})
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with functions", func(t *testing.T) {
|
||||
aFunction := func() (Profile, Profile) {
|
||||
return Profile{21, "Berlin"}, Profile{28, "Beijing"}
|
||||
}
|
||||
|
||||
var got []string
|
||||
want := []string{"Berlin", "Beijing"}
|
||||
Walk(aFunction, func(input string) {
|
||||
got = append(got, input)
|
||||
})
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func assertContains(t testing.TB, haystack []string, needle string) {
|
||||
t.Helper()
|
||||
contains := false
|
||||
|
||||
for _, x := range haystack {
|
||||
if x == needle {
|
||||
contains = true
|
||||
}
|
||||
}
|
||||
if !contains {
|
||||
t.Errorf("expected %v to contain %q but it didn't", haystack, needle)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user