この章のすべてのコードはここにあります
現代のコンピューターのすべての能力が驚異的な速さで膨大な合計を実行するために、普通の開発者は自分の仕事を行うために数学を使用することはほとんどありません。
でも今日はだめ!
今日は、数学を使用して実際の問題を解決します。退屈な数学ではありません。 私たちは三角法やベクトルなど、高校の後で使う必要はないと言っていたあらゆる種類のものを使用します。
問題
時計のSVGを作成したい。
デジタル時計ではありません。きっと、それは簡単でしょう。アナログ時計です。 あなたが求めているのは、time
パッケージからTime
を受け取り、時、分、秒のすべての針が正しい方向を向いている時計の SVG を出力するだけの素敵な関数です。
これがどれほど難しいことなのでしょうか?
最初に、遊ぶために時計の SVG が必要になります。 SVGは、XMLで記述された一連の形状として記述されているため、プログラムで操作するための素晴らしい画像フォーマットです。
このように記述されています。
コピー <?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">
<!-- bezel -->
<circle cx="150" cy="150" r="100" style="fill:#fff;stroke:#000;stroke-width:5px;"/>
<!-- hour hand -->
<line x1="150" y1="150" x2="114.150000" y2="132.260000"
style="fill:none;stroke:#000;stroke-width:7px;"/>
<!-- minute hand -->
<line x1="150" y1="150" x2="101.290000" y2="99.730000"
style="fill:none;stroke:#000;stroke-width:7px;"/>
<!-- second hand -->
<line x1="150" y1="150" x2="77.190000" y2="202.900000"
style="fill:none;stroke:#f00;stroke-width:3px;"/>
</svg>
これは、3本の線が描かれた円で、各線は円の真ん中にあり、(x=150、y=150)あり、少し離れています。
だから私たちがやろうとしていることは、上記をどうにかして再構築することですが、線を変更して、それらが所定の時間に適切な方向を指すようにします。
受け入れテスト
行き詰まる前に、受け入れテストについて考えてみましょう。時計の例があるので、重要なパラメータがどうなるかを考えてみましょう。
コピー <line x1="150" y1="150" x2="114.150000" y2="132.260000"
style="fill:none;stroke:#000;stroke-width:7px;"/>
時計の中心(この線の属性x1
およびy1
)は、時計の各針で同じです。 時計の針ごとに変更する必要がある数値(SVGを構築するためのパラメーター)は、x2
およびy2
属性です。時計の針ごとにX
とY
が必要です。
私はより多くのパラメーターについて考えることができました。 文字盤の円の半径、SVGのサイズ、手の色、その形状など...しかし、シンプルで具体的な問題を解決することから始めるのが良いです。具体的な解決策、そしてそれを一般化するためにパラメーターを追加し始めます。
ので、箇条書きにします。
SVGに関する注意点:原点(0, 0)-は、予想される 左下 ではなく、 左上 です。どの番号をラインに接続するかを検討しているときは、これを覚えておくことが重要です。
最後に、SVGを構築する how は決めていません。 text/template
パッケージのテンプレートを使用するか、単にバイトをbytes.Buffer
またはライターに入れます。しかし、これらの数値が必要になることはわかっているので、それらを作成するもののテストに焦点を合わせましょう。
最初にテストを書く
だから私の最初のテストは次のようになります。
コピー package clockface_test
import (
"testing"
"time"
"github.com/gypsydave5/learn-go-with-tests/math/v1/clockface"
)
func TestSecondHandAtMidnight (t * testing . T ) {
tm := time. Date ( 1337 , time.January, 1 , 0 , 0 , 0 , 0 , time.UTC)
want := clockface . Point {X: 150 , Y: 150 - 90 }
got := clockface. SecondHand (tm)
if got != want {
t. Errorf ( "Got %v , wanted %v " , got, want)
}
}
SVGが左上から座標をプロットする方法を覚えていますか? 秒針を真夜中に配置するには、X軸(まだ150)の文字盤の中心から移動しておらず、Y軸は中心からの「上」方向の長さです。 150 マイナス 90。
テストを実行してみてください
これにより、不足している関数と型の周囲で予想される失敗が排除されます。
コピー --- FAIL: TestSecondHandAtMidnight (0.00s)
# github.com/gypsydave5/learn-go-with-tests/math/v1/clockface_test [github.com/gypsydave5/learn-go-with-tests/math/v1/clockface.test]
./clockface_test.go:13:10: undefined: clockface.Point
./clockface_test.go:14:9: undefined: clockface.SecondHand
FAIL github.com/gypsydave5/learn-go-with-tests/math/v1/clockface [build failed]
だから秒針の先が行くはずのPoint
とそれを取得する関数。
テストを実行するための最小限のコードを記述し、失敗したテスト出力を確認します
これらの型を実装して、コードをコンパイルしてみましょう
コピー package clockface
import "time"
// A Point represents a two dimensional Cartesian coordinate
type Point struct {
X float64
Y float64
}
// SecondHand is the unit vector of the second hand of an analogue clock at time `t`
// represented as a Point.
func SecondHand (t time . Time ) Point {
return Point {}
}
そして今、私たちは
コピー --- FAIL: TestSecondHandAtMidnight (0.00s)
clockface_test.go:17: Got {0 0}, wanted {150 60}
FAIL
exit status 1
FAIL github.com/gypsydave5/learn-go-with-tests/math/v1/clockface 0.006s
成功させるのに十分なコードを書く
予想される失敗が発生したら、HandsAt
の戻り値を入力できます。
コピー // SecondHand is the unit vector of the second hand of an analogue clock at time `t`
// represented as a Point.
func SecondHand (t time . Time ) Point {
return Point { 150 , 60 }
}
見てください。テストが成功しました。
Behold, a passing test.
コピー PASS
ok github.com/gypsydave5/learn-go-with-tests/math/v1/clockface 0.006s
リファクタリング♪
まだリファクタリングする必要はありません。コードはほとんどありません!
新しい要件について繰り返します
毎回真夜中を示す時計を返すだけではなく、おそらくここでいくつかの作業を行う必要があります...
最初にテストを書く
コピー func TestSecondHandAt30Seconds (t * testing . T ) {
tm := time. Date ( 1337 , time.January, 1 , 0 , 0 , 30 , 0 , time.UTC)
want := clockface . Point {X: 150 , Y: 150 + 90 }
got := clockface. SecondHand (tm)
if got != want {
t. Errorf ( "Got %v , wanted %v " , got, want)
}
}
同じアイデアですが、今度は秒針が downwards を指しているため、Y軸の長さを add します。
これはコンパイルされます...しかし、どのように成功したのでしょうか?
思考時間
この問題をどのように解決しますか?
秒針は毎分同じ60の状態を通過し、60の異なる方向を指しています。 0秒の場合は文字盤の上部を指し、30秒の場合は文字盤の下部を指します。簡単です。
つまり、秒針がどの方向を向いているか、たとえば37秒を考えたい場合は、円の周りの12時から37/60度の間の角度が必要です。 度数では、これは (360 / 60 ) * 37 = 222
ですが、完全なローテーションの37/60
であることを覚えているだけの方が簡単です。
しかし、角度はストーリーの半分にすぎません。秒針の先端が指しているX座標とY座標を知る必要があります。どうすればそれを解決できますか?
数学
原点の周りに描かれた半径1の円を想像してください。座標0, 0
。
これは「単位円(unit circle
)」と呼ばれます。半径が1単位だからです。
円の円周は、グリッド上のポイントから作られます。 より多くの座標。これらの各座標のxおよびyコンポーネントは三角形を形成し、その斜辺は常に1。 円の半径です
三角測量では、原点との角度がわかっている場合、各三角形のXとYの長さを計算できます。 X座標はcos(a)になり、Y座標はsin(a)になります。ここで、aは線と(positive)x軸とのなす角度です。
(これを信じない場合は、ウィキペディアを見て... )
最後のひねりX軸からではなく12時からの角度を測定したいので、(3時)、軸を交換する必要があります。ここで、x = sin(a)およびy = cos(a)です。
これで、秒針の角度(1秒ごとに円の1/60)とX座標とY座標を取得する方法がわかりました。sin
と cos
の両方に関数が必要です。
math
幸い、Goのmath
パッケージには両方があり、1つの小さな問題が頭に浮かぶ必要があります。 math.Cos
の説明を見ると
Cosは、ラジアン引数xの余弦を返します。
角度をラジアンにする必要があります。では、ラジアンとは何ですか?円の完全な回転を360度で構成するのではなく、完全な回転を2πラジアンとして定義します。これを実行する理由はありません。
読んだり、学習したり、考えたりしたので、次のテストを書くことができます。
最初にテストを書く
このすべての数学は困難で混乱を招きます。私は何が起こっているのか理解していると確信していません。 それではテストを書きましょう!問題全体を一度に解決する必要はありません。特定の時間の秒針について、ラジアン単位で正しい角度を計算することから始めましょう。
私はこれらのテストをclockface
パッケージ内で記述します。それらがエクスポートされることはなく、何が起こっているのかをしっかり把握すると、または移動されて削除される可能性があります。
また、これらのテストに取り組んでいる間に取り組んでいた受け入れテストをコメントアウトします 。これに合格している間、そのテストに気を取られたくありません。
コピー package clockface
import (
"math"
"testing"
"time"
)
func TestSecondsInRadians (t * testing . T ) {
thirtySeconds := time. Date ( 312 , time.October, 28 , 0 , 0 , 30 , 0 , time.UTC)
want := math.Pi
got := secondsInRadians (thirtySeconds)
if want != got {
t. Fatalf ( "Wanted %v radians, but got %v " , want, got)
}
}
ここでは、1分の30秒後に秒針が24時間半になることをテストしています。そして、それはmath
パッケージの最初の使用です!円の1回転が2πラジアンである場合、途中のラウンドはちょうどπラジアンになるはずです。math.Pi
はπの値を提供します。
テストを実行してみてください
コピー # github.com/gypsydave5/learn-go-with-tests/math/v2/clockface [github.com/gypsydave5/learn-go-with-tests/math/v2/clockface.test]
./clockface_test.go:12:9: undefined: secondsInRadians
FAIL github.com/gypsydave5/learn-go-with-tests/math/v2/clockface [build failed]
テストを実行するための最小限のコードを記述し、失敗したテスト出力を確認します
コピー func secondsInRadians (t time . Time ) float64 {
return 0
}
コピー --- FAIL: TestSecondsInRadians (0.00s)
clockface_test.go:15: Wanted 3.141592653589793 radians, but got 0
FAIL
exit status 1
FAIL github.com/gypsydave5/learn-go-with-tests/math/v2/clockface 0.007s
成功させるのに十分なコードを書く
コピー func secondsInRadians (t time . Time ) float64 {
return math.Pi
}
コピー PASS
ok github.com/gypsydave5/learn-go-with-tests/math/v2/clockface 0.011s
リファクタリング♪
まだリファクタリングは必要ありません。
新しい要件について繰り返します
テストを拡張して、さらにいくつかのシナリオをカバーできます。 少し先にスキップして、すでにリファクタリングされたテストコードをいくつか示します。目的の場所に到達する方法は十分に明確になっているはずです。
コピー func TestSecondsInRadians (t * testing . T ) {
cases := [] struct {
time time . Time
angle float64
}{
{ simpleTime ( 0 , 0 , 30 ), math.Pi},
{ simpleTime ( 0 , 0 , 0 ), 0 },
{ simpleTime ( 0 , 0 , 45 ), (math.Pi / 2 ) * 3 },
{ simpleTime ( 0 , 0 , 7 ), (math.Pi / 30 ) * 7 },
}
for _, c := range cases {
t. Run ( testName (c.time), func (t * testing . T ) {
got := secondsInRadians (c.time)
if got != c.angle {
t. Fatalf ( "Wanted %v radians, but got %v " , c.angle, got)
}
})
}
}
いくつかのヘルパー関数を追加して、このテーブルベースのテストの作成を少し面倒にしました。 testName
は時刻をデジタル時計形式(HH:MM:SS)に変換し、simpleTime
は実際に気にする部分のみを使用してtime.Time
を構築します(再び、時間、分、秒)。
コピー func simpleTime (hours, minutes, seconds int ) time . Time {
return time. Date ( 312 , time.October, 28 , hours, minutes, seconds, 0 , time.UTC)
}
func testName (t time . Time ) string {
return t. Format ( "15:04:05" )
}
これらの2つの関数は、これらのテスト(および将来のテスト)の記述と保守を少し簡単にするのに役立ちます。
これにより、優れたテスト出力が得られます。
コピー --- FAIL: TestSecondsInRadians (0.00s)
--- FAIL: TestSecondsInRadians/00:00:00 (0.00s)
clockface_test.go:24: Wanted 0 radians, but got 3.141592653589793
--- FAIL: TestSecondsInRadians/00:00:45 (0.00s)
clockface_test.go:24: Wanted 4.71238898038469 radians, but got 3.141592653589793
--- FAIL: TestSecondsInRadians/00:00:07 (0.00s)
clockface_test.go:24: Wanted 0.7330382858376184 radians, but got 3.141592653589793
FAIL
exit status 1
FAIL github.com/gypsydave5/learn-go-with-tests/math/v3/clockface 0.007s
上で話していた数学のすべてを実装するお時間です。
コピー func secondsInRadians (t time . Time ) float64 {
return float64 (t. Second ()) * (math.Pi / 30 )
}
1秒は(2π/60)ラジアンです... 2をキャンセルすると、π/30ラジアンになります。これに秒数( float64
として)を掛けると、すべてのテストに合格するはずです...
コピー --- FAIL: TestSecondsInRadians (0.00s)
--- FAIL: TestSecondsInRadians/00:00:30 (0.00s)
clockface_test.go:24: Wanted 3.141592653589793 radians, but got 3.1415926535897936
FAIL
exit status 1
FAIL github.com/gypsydave5/learn-go-with-tests/math/v3/clockface 0.006s
待って、なになに?
浮動小数点数(Floats
)は恐ろしい
浮動小数点演算は悪名高いほど不正確 です。 コンピュータは本当に整数とある程度の有理数しか扱えません。特に、secondsInRadians
関数のように10進数を上下に因数分解すると、10進数は不正確になり始めます。 math.Pi
を30で除算してから30を掛けることで、math.Pi
と同じではない数値になりました。
これを回避するには2つの方法があります。
方程式をリファクタリングして関数をリファクタリングする
(1)はそれほど魅力的ではないように思われるかもしれませんが、浮動小数点の等価性を機能させる唯一の方法であることがよくあります。時計の文字盤を描画するために、ごくわずかな分数で不正確であることは率直に言って問題にならないので、角度に対して「十分に近い」等式を定義する関数を書くことができます。
しかし、精度を取り戻すには簡単な方法があります。方程式を並べ替えて、分割して乗算しないようにします。分割するだけですべてが可能です。
だから代わりに
コピー numberOfSeconds * π / 30
私たちは書けます。
コピー π / (30 / numberOfSeconds)
これは同等です。
In Go:
コピー func secondsInRadians (t time . Time ) float64 {
return (math.Pi / ( 30 / ( float64 (t. Second ()))))
}
そして、パスが成功します。
コピー PASS
ok github.com/gypsydave5/learn-go-with-tests/math/v2/clockface 0.005s
新しい要件について繰り返します
したがって、ここで最初の部分をカバーしました。 秒針がラジアンで指し示す角度はわかっています。次に、座標を計算する必要があります。
繰り返しますが、これは可能な限り単純にして、単位円 でのみ機能するようにします。半径1の円です。これは、手の長さがすべて1であることを意味しますが、明るい面では、数学が飲み込みやすくなります。
最初にテストを書く
コピー func TestSecondHandVector (t * testing . T ) {
cases := [] struct {
time time . Time
point Point
}{
{ simpleTime ( 0 , 0 , 30 ), Point { 0 , - 1 }},
}
for _, c := range cases {
t. Run ( testName (c.time), func (t * testing . T ) {
got := secondHandPoint (c.time)
if got != c.point {
t. Fatalf ( "Wanted %v Point, but got %v " , c.point, got)
}
})
}
}
テストを実行してみてください
コピー # github.com/gypsydave5/learn-go-with-tests/math/v4/clockface [github.com/gypsydave5/learn-go-with-tests/math/v4/clockface.test]
./clockface_test.go:40:11: undefined: secondHandPoint
FAIL github.com/gypsydave5/learn-go-with-tests/math/v4/clockface [build failed]
テストを実行するための最小限のコードを記述し、失敗したテスト出力を確認します
コピー func secondHandPoint (t time . Time ) Point {
return Point {}
}
コピー --- FAIL: TestSecondHandPoint (0.00s)
--- FAIL: TestSecondHandPoint/00:00:30 (0.00s)
clockface_test.go:42: Wanted {0 -1} Point, but got {0 0}
FAIL
exit status 1
FAIL github.com/gypsydave5/learn-go-with-tests/math/v4/clockface 0.010s
成功させるのに十分なコードを書く
コピー func secondHandPoint (t time . Time ) Point {
return Point { 0 , - 1 }
}
コピー PASS
ok github.com/gypsydave5/learn-go-with-tests/math/v4/clockface 0.007s
新しい要件について繰り返します
コピー func TestSecondHandPoint (t * testing . T ) {
cases := [] struct {
time time . Time
point Point
}{
{ simpleTime ( 0 , 0 , 30 ), Point { 0 , - 1 }},
{ simpleTime ( 0 , 0 , 45 ), Point { - 1 , 0 }},
}
for _, c := range cases {
t. Run ( testName (c.time), func (t * testing . T ) {
got := secondHandPoint (c.time)
if got != c.point {
t. Fatalf ( "Wanted %v Point, but got %v " , c.point, got)
}
})
}
}
テストを実行してみてください
コピー --- FAIL: TestSecondHandPoint (0.00s)
--- FAIL: TestSecondHandPoint/00:00:45 (0.00s)
clockface_test.go:43: Wanted {-1 0} Point, but got {0 -1}
FAIL
exit status 1
FAIL github.com/gypsydave5/learn-go-with-tests/math/v4/clockface 0.006s
成功させるのに十分なコードを書く
ここで、XとYを生成する方程式が必要です。秒に書きましょう。
コピー func secondHandPoint (t time . Time ) Point {
angle := secondsInRadians (t)
x := math. Sin (angle)
y := math. Cos (angle)
return Point {x, y}
}
これなら、いけるでしょうか。
コピー --- FAIL: TestSecondHandPoint (0.00s)
--- FAIL: TestSecondHandPoint/00:00:30 (0.00s)
clockface_test.go:43: Wanted {0 -1} Point, but got {1.2246467991473515e-16 -1}
--- FAIL: TestSecondHandPoint/00:00:45 (0.00s)
clockface_test.go:43: Wanted {-1 0} Point, but got {-1 -1.8369701987210272e-16}
FAIL
exit status 1
FAIL github.com/gypsydave5/learn-go-with-tests/math/v4/clockface 0.007s
待って。。もう一度、フロートにもう一度呪われているようです。 予期しない数値は両方とも、小数点以下16桁で「無限」 infinitesimal のようです。 したがって、ここでも、精度を上げるか、ほぼ同じであると言って私たちの生活を続けるかを選択できます。
これらの角度の精度を向上させる1つのオプションは、math/big
パッケージの合理的なタイプRat
を使用することです。 しかし、目的が月着陸ではなくSVGを描画することであることを考えると、少しぼやけて暮らせると思います。
コピー func TestSecondHandPoint (t * testing . T ) {
cases := [] struct {
time time . Time
point Point
}{
{ simpleTime ( 0 , 0 , 30 ), Point { 0 , - 1 }},
{ simpleTime ( 0 , 0 , 45 ), Point { - 1 , 0 }},
}
for _, c := range cases {
t. Run ( testName (c.time), func (t * testing . T ) {
got := secondHandPoint (c.time)
if ! roughlyEqualPoint (got, c.point) {
t. Fatalf ( "Wanted %v Point, but got %v " , c.point, got)
}
})
}
}
func roughlyEqualFloat64 (a, b float64 ) bool {
const equalityThreshold = 1 e- 7
return math. Abs (a - b) < equalityThreshold
}
func roughlyEqualPoint (a, b Point ) bool {
return roughlyEqualFloat64 (a.X, b.X) &&
roughlyEqualFloat64 (a.Y, b.Y)
}
2つのPoints
間のおおよその等価性を定義する2つの関数を定義しました。
X要素とY要素が互いに0.0000001以内にある場合に機能します。
それはまだかなり正確です。
そして今、私たちはテストに成功します。
コピー PASS
ok github.com/gypsydave5/learn-go-with-tests/math/v4/clockface 0.007s
リファクタリング♪
これでもかなり満足しています。
新しい要件について繰り返します
まあ、 new と言っても完全に正確というわけではありません。 実際にできることは、受け入れテストに合格することです!それがどのように見えるかを思い出してみましょう
コピー func TestSecondHandAt30Seconds (t * testing . T ) {
tm := time. Date ( 1337 , time.January, 1 , 0 , 0 , 30 , 0 , time.UTC)
want := clockface . Point {X: 150 , Y: 150 + 90 }
got := clockface. SecondHand (tm)
if got != want {
t. Errorf ( "Got %v , wanted %v " , got, want)
}
}
テストを実行してみてください
コピー --- FAIL: TestSecondHandAt30Seconds (0.00s)
clockface_acceptance_test.go:28: Got {150 60}, wanted {150 240}
FAIL
exit status 1
FAIL github.com/gypsydave5/learn-go-with-tests/math/v5/clockface 0.007s
成功させるのに十分なコードを書く
単位ベクトルをSVG上の点に変換するには、3つのことを行う必要があります。
SVGに原点があるSVGを考慮するため、X軸上で反転します
左上隅
それを正しい位置に移動します(それが
(150, 150))
楽しい時間ですね!
コピー // SecondHand is the unit vector of the second hand of an analogue clock at time `t`
// represented as a Point.
func SecondHand (t time . Time ) Point {
p := secondHandPoint (t)
p = Point {p.X * 90 , p.Y * 90 } // scale
p = Point {p.X, - p.Y} // flip
p = Point {p.X + 150 , p.Y + 150 } // translate
return p
}
正確にその順序でスケーリング、反転、変換されます。やったー!
コピー PASS
ok github.com/gypsydave5/learn-go-with-tests/math/v5/clockface 0.007s
リファクタリング♪
ここに定数として取り出されるべきいくつかの魔法の数字があるので、それをやってみましょう
コピー const secondHandLength = 90
const clockCentreX = 150
const clockCentreY = 150
// SecondHand is the unit vector of the second hand of an analogue clock at time `t`
// represented as a Point.
func SecondHand (t time . Time ) Point {
p := secondHandPoint (t)
p = Point {p.X * secondHandLength, p.Y * secondHandLength}
p = Point {p.X, - p.Y}
p = Point {p.X + clockCentreX, p.Y + clockCentreY} //translate
return p
}
時計を描く
さて...とにかく秒針...
これをやってみましょう! そこに座って人々を魅了するために世界に出て行くのを待っているだけで、価値を提供しないよりも悪いことはありません。秒針を描いてみよう!
メインのclockface
パッケージディレクトリの下に、(confusingly)、clockface
という新しいディレクトリを追加します。そこにSVGをビルドするバイナリを作成するmain
パッケージを置きます。
コピー ├── clockface
│ └── main.go
├── clockface.go
├── clockface_acceptance_test.go
└── clockface_test.go
main.go
の中
コピー package main
import (
"fmt"
"io"
"os"
"time"
"github.com/gypsydave5/learn-go-with-tests/math/v6/clockface"
)
func main () {
t := time. Now ()
sh := clockface. SecondHand (t)
io. WriteString (os.Stdout, svgStart)
io. WriteString (os.Stdout, bezel)
io. WriteString (os.Stdout, secondHandTag (sh))
io. WriteString (os.Stdout, svgEnd)
}
func secondHandTag (p clockface . Point ) string {
return fmt. Sprintf ( `<line x1="150" y1="150" x2=" %f " y2=" %f " style="fill:none;stroke:#f00;stroke-width:3px;"/>` , p.X, p.Y)
}
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>`
ああ、少年よ、私はこの混乱の美しいコードのために賞を獲得しようとしているわけではありません。 しかし、これは仕事をします。これはos.Stdout
に SVG を書き出しています。一度に一文字ずつ。
これを構築すると
そしてそれを実行し、出力をファイルに送信します
コピー ./clockface > clock.svg
私たちは次のようなものを見るはずです
リファクタリング♪
これは臭い。まあ、それはまったく悪臭はしませんが、私はそれについて満足していません。
そのSecondHand
関数全体は、SVGであることに結びついています...
SVGについて言及するか、実際にSVGを作成する...
... 同時に、私はSVGコードをテストしていません。
ええ、私は失敗したと思います。これは間違っていると感じます。 よりSVG中心のテストで回復してみましょう。
私たちのオプションは何ですか? さて、SVGWriter
から吐き出される文字に、特定の時間に予想されるSVGタグのようなものが含まれていることをテストしてみることができます。例えば:
コピー func TestSVGWriterAtMidnight (t * testing . T ) {
tm := time. Date ( 1337 , time.January, 1 , 0 , 0 , 0 , 0 , time.UTC)
var b strings . Builder
clockface. SVGWriter ( & b, tm)
got := b. String ()
want := `<line x1="150" y1="150" x2="150" y2="60"`
if ! strings. Contains (got, want) {
t. Errorf ( "Expected to find the second hand %v , in the SVG output %v " , want, got)
}
}
しかし、これは本当に改善されたのでしょうか?
有効なSVGを生成しなかった場合でも(パスが出力に文字列が表示されることをテストするだけなので)成功するだけでなく、その文字列に最小の重要でない変更を加えた場合も失敗します。たとえば、属性の間にスペースを追加します。
最大のにおいは、実際には、データ構造(XML)を一連の文字としての(文字列としての)表示でテストすることです。これは never 、 ever です。これは、上で概説したのと同じような問題を引き起こすためです。脆弱すぎて感度が不十分なテストです。間違ったことをテストするテスト!
したがって、唯一の解決策は、出力をXMLとしてテストすることです。そのためには、それを解析する必要があります。
XMLの解析
encoding/xml
は、シンプルなXML解析で行うすべてのことを処理できるGoパッケージです。
関数xml.Unmarshall
は、XMLデータの[]byte
と、非整列化する構造体へのポインターを受け取ります。
したがって、XMLを非整列化するための構造体が必要になります。 すべてのノードと属性の正しい名前と正しい構造の書き方を検討するのに少し時間を費やすことができましたが、幸い、誰かがzek
そのハードワークのすべてを自動化するプログラム。 さらに良いことに、https://www.onlinetool.io/xmltogo/ にオンラインバージョンがあります。ファイルの上部から1つのボックスにSVGを貼り付けるだけです。
コピー type Svg struct {
XMLName xml . Name `xml:"svg"`
Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"`
Width string `xml:"width,attr"`
Height string `xml:"height,attr"`
ViewBox string `xml:"viewBox,attr"`
Version string `xml:"version,attr"`
Circle struct {
Text string `xml:",chardata"`
Cx string `xml:"cx,attr"`
Cy string `xml:"cy,attr"`
R string `xml:"r,attr"`
Style string `xml:"style,attr"`
} `xml:"circle"`
Line [] struct {
Text string `xml:",chardata"`
X1 string `xml:"x1,attr"`
Y1 string `xml:"y1,attr"`
X2 string `xml:"x2,attr"`
Y2 string `xml:"y2,attr"`
Style string `xml:"style,attr"`
} `xml:"line"`
}
(構造体の名前をSVG
に変更するなど)必要がある場合は、これを調整できますが、最初から十分です。
コピー func TestSVGWriterAtMidnight (t * testing . T ) {
tm := time. Date ( 1337 , time.January, 1 , 0 , 0 , 0 , 0 , time.UTC)
b := bytes . Buffer {}
clockface. SVGWriter ( & b, tm)
svg := Svg {}
xml. Unmarshal (b. Bytes (), & svg)
x2 := "150"
y2 := "60"
for _, line := range svg.Line {
if line.X2 == x2 && line.Y2 == y2 {
return
}
}
t. Errorf ( "Expected to find the second hand with x2 of %+v and y2 of %+v , in the SVG output %v " , x2, y2, b. String ())
}
clockface.SVGWriter
の出力をbytes.Buffer
に書き込み、次にUnmarshall
をSvg
に書き込みます。次に、Svg
内の各Line
を見て、それらのいずれかが期待されるX2
およびY2
値を持っているかどうかを確認します。 一致した場合、早期に(テストに合格)します。そうでない場合、(うまくいけば)情報メッセージで失敗します。
コピー # github.com/gypsydave5/learn-go-with-tests/math/v7b/clockface_test [github.com/gypsydave5/learn-go-with-tests/math/v7b/clockface.test]
./clockface_acceptance_test.go:41:2: undefined: clockface.SVGWriter
FAIL github.com/gypsydave5/learn-go-with-tests/math/v7b/clockface [build failed]
そのSVGWriter
を書くほうがいいようです...
コピー package clockface
import (
"fmt"
"io"
"time"
)
const (
secondHandLength = 90
clockCentreX = 150
clockCentreY = 150
)
//SVGWriter writes an SVG representation of an analogue clock, showing the time t, to the writer w
func SVGWriter (w io . Writer , t time . Time ) {
io. WriteString (w, svgStart)
io. WriteString (w, bezel)
secondHand (w, t)
io. WriteString (w, svgEnd)
}
func secondHand (w io . Writer , t time . Time ) {
p := secondHandPoint (t)
p = Point {p.X * secondHandLength, p.Y * secondHandLength} // 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)
}
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>`
最も美しいSVGの書き方ですか?いいえ。でもうまくいけば、うまくいきます...
コピー --- FAIL: TestSVGWriterAtMidnight (0.00s)
clockface_acceptance_test.go:56: Expected to find the second hand with x2 of 150 and y2 of 60, in the SVG output <?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"><circle cx="150" cy="150" r="100" style="fill:#fff;stroke:#000;stroke-width:5px;"/><line x1="150" y1="150" x2="150.000000" y2="60.000000" style="fill:none;stroke:#f00;stroke-width:3px;"/></svg>
FAIL
exit status 1
FAIL github.com/gypsydave5/learn-go-with-tests/math/v7b/clockface 0.008s
おっと!
%f
フォーマットディレクティブは、座標をデフォルトの精度レベル(小数点以下6桁)に出力します。座標に期待する精度のレベルを明示する必要があります。小数点第3位としましょう。
コピー s := fmt. Sprintf ( `<line x1="150" y1="150" x2=" %.3f " y2=" %.3f " style="fill:none;stroke:#f00;stroke-width:3px;"/>` , p.X, p.Y)
テストでの期待を更新した後
コピー x2 := "150.000"
y2 := "60.000"
我々はテストの成功を得られます
コピー PASS
ok github.com/gypsydave5/learn-go-with-tests/math/v7b/clockface 0.006s
main
関数を短くすることができます。
コピー package main
import (
"os"
"time"
"github.com/gypsydave5/learn-go-with-tests/math/v7b/clockface"
)
func main () {
t := time. Now ()
clockface. SVGWriter (os.Stdout, t)
}
そして、同じパターンに従って別の時間のテストを書くことができますが、前にはできません...
リファクタリング♪
3つのことが突き出ています。
確認する必要があるすべての情報を実際にテストしているわけではありません。
現在-たとえば、x1
値はどうですか?
また、x1
などの属性は実際には「文字列」ではないのですか?彼らは
数字!
私は本当に手の「スタイル」を気にしますか?または、そのことについては、
zak
によって生成された空のText
ノード?
私たちはもっとうまくやることができます。 すべてをシャープにするために、Svg
構造体とテストにいくつかの調整を加えましょう。
コピー 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"`
}
ここで私は
名前付き型構造体の重要な部分 -- Line
と
Circle
数値属性をstring
ではなくfloat64
に変更しました。
これにより、探している行でより正確に評価できます。
コピー func TestSVGWriterAtMidnight (t * testing . T ) {
tm := time. Date ( 1337 , time.January, 1 , 0 , 0 , 0 , 0 , time.UTC)
b := bytes . Buffer {}
clockface. SVGWriter ( & b, tm)
svg := SVG {}
xml. Unmarshal (b. Bytes (), & svg)
want := Line { 150 , 150 , 150 , 60 }
for _, line := range svg.Line {
if line == want {
return
}
}