Compare commits

...

44 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
7a32f26bd5 context: implement a spy response writer to check if anything is written 2024-09-21 20:46:07 +02:00
c5582275e7 context: realistic usage of context.
Use a go routine for the work logic. Here sleep and append to string.
Inside the go routine, select for ctx.Done(). If happens, just stop and
return.

In the outside function, select for ctx.Done() too. If it happens, that
means the work logic has not finished before canceled.
2024-09-21 20:35:43 +02:00
c0db9ab22b context: refactorize assert functions 2024-09-21 20:26:22 +02:00
0b181ccf0f context: setup timeout context 2024-09-21 20:20:15 +02:00
d49f952035 context: add basic context test 2024-09-21 20:07:09 +02:00
436bcad0e1 counter: Use a constructor 2024-09-21 19:48:58 +02:00
6ded953ec6 counter: fix mutex copy issue 2024-09-21 19:47:27 +02:00
572c8033f6 counter: handle racing situation with mutex 2024-09-21 19:43:09 +02:00
ea6f225992 counter: refactor, add assertCounter func 2024-09-21 18:21:56 +02:00
41ae820a21 counter: add baisics 2024-09-21 17:51:58 +02:00
15 changed files with 916 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)
}

23
context/context.go Normal file
View 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
View 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")
}
})
}

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

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

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