Compare commits

..

34 Commits

Author SHA1 Message Date
3923758807 blogposts: add some comments 2024-09-24 22:18:01 +02:00
3cdca7cc94 blogposts: handle file type 2024-09-24 22:13:33 +02:00
e8b8166ae9 blogposts: finish up 2024-09-24 21:58:06 +02:00
48df8ace6e blogposts: read body 2024-09-24 21:51:09 +02:00
8228bcdce3 blogposts: read tags 2024-09-24 21:38:21 +02:00
a33d490896 blogposts: read metadata 2024-09-24 21:26:42 +02:00
00596a7c52 blogposts: create new file for post and a helper func 2024-09-24 21:10:16 +02:00
1153a0a5bd blogposts: use io.Reader interface to decouple 2024-09-24 21:06:57 +02:00
dec5f045a7 blogposts: refactor, create newPost func 2024-09-24 21:03:33 +02:00
6292463e90 blogposts: test get blog content 2024-09-24 20:57:28 +02:00
048e6cccac blogposts: add the first test 2024-09-23 23:09:10 +02:00
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
8629b537b0 roman: check number range 2024-09-22 22:00:50 +02:00
abc546178d roman: fix the number range 2024-09-22 21:51:40 +02:00
650ec27ec9 roman: add property based test with quick.check 2024-09-22 21:47:11 +02:00
52825e49b3 ***roman: add convert to arabic, the real one 2024-09-22 21:36:03 +02:00
715ea6477a roman: add convert to arabic 2024-09-22 21:28:24 +02:00
6da23dc641 roman: add more roman letters and test 2024-09-22 21:24:24 +02:00
7d751cab3d ***roman: refactor in the OOP way 2024-09-22 21:12:56 +02:00
b37f8a8d4c roman: add more tests 2024-09-22 21:09:51 +02:00
6f853c25d0 roman: add 9 and 10 2024-09-22 21:07:05 +02:00
0a75d86850 roman: add 5 and refactor 2024-09-22 21:02:27 +02:00
2515644d4f roman: refactor to use string building pattern 2024-09-22 20:59:11 +02:00
3a0808c599 roman: add 4 2024-09-22 20:57:12 +02:00
accb2c51dc roman: use stringBuild for better performance(?) 2024-09-22 20:54:22 +02:00
5f93a0b843 roman: get start with the roman numerals 2024-09-22 20:53:13 +02:00
11 changed files with 729 additions and 3 deletions

2
.gitignore vendored Normal file
View File

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

39
blogposts/blogposts.go Normal file
View 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
}

View 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
View 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
View 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
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 package main
import ( import (
"gobytest/countdown" "gobytest/clockface"
"os" "os"
"time" "time"
) )
func main() { func main() {
// greet.Greet(os.Stdout, "Elodie") // 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)
} }

58
roman/roman.go Normal file
View 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
View 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)
}
}