Add geo pkg

This commit is contained in:
dwrz
2026-10-23 14:10:22 +00:00
parent 68623bb4e6
commit 936e7d38cb
2 changed files with 217 additions and 0 deletions
+183
View File
@@ -0,0 +1,183 @@
package geo
import (
"math"
"strconv"
"strings"
"testing"
)
// parseCase holds one input to ParseCoordinates and its expectation.
// expected is nil for inputs that must fail; errPart is then the
// text the error message must contain.
type parseCase struct {
in string
expected *Coordinates
errPart string
}
func TestParseCoordinates(t *testing.T) {
var cases = []parseCase{
{in: `52.37,4.90`, expected: &Coordinates{52.37, 4.90}},
{in: `-33.87,151.21`, expected: &Coordinates{-33.87, 151.21}},
{in: `1,2`, expected: &Coordinates{1, 2}},
{in: `+1.5,-2.5`, expected: &Coordinates{1.5, -2.5}},
{in: `1e1,2.5e0`, expected: &Coordinates{10, 2.5}},
{in: `0x1p2,3`, expected: &Coordinates{4, 3}},
// ParseCoordinates does not check value ranges.
{in: `inf,1`, expected: &Coordinates{math.Inf(1), 1}},
{in: ``, errPart: `invalid string`},
{in: `52.37`, errPart: `invalid string`},
{in: `52.37,4.90,1`, errPart: `invalid string`},
{in: `a,b`, errPart: `latitude`},
{in: `1,b`, errPart: `longitude`},
{in: `1,`, errPart: `longitude`},
{in: `,2`, errPart: `latitude`},
// ParseFloat does not skip whitespace.
{in: ` 52.37 , 4.90 `, errPart: `latitude`},
// ParseFloat rejects values outside the float64 range.
{in: `1e999,2`, errPart: `latitude`},
}
for _, c := range cases {
t.Run(c.in, c.testParseCoordinates)
}
}
// testParseCoordinates checks the expected result or error for one parse case.
func (c parseCase) testParseCoordinates(t *testing.T) {
got, err := ParseCoordinates(c.in)
if c.expected == nil {
if err == nil {
t.Fatalf("got %v, want error", got)
}
if got != nil {
t.Fatalf("got %v and error %v, want nil", got, err)
}
if !strings.Contains(err.Error(), c.errPart) {
t.Fatalf("error %q, want %q", err.Error(), c.errPart)
}
return
}
if err != nil {
t.Fatalf("error = %v", err)
}
if got == nil {
t.Fatalf("got nil, want %v", c.expected)
}
if got.Latitude != c.expected.Latitude {
t.Errorf(
"Latitude = %v, want %v",
got.Latitude, c.expected.Latitude,
)
}
if got.Longitude != c.expected.Longitude {
t.Errorf(
"Longitude = %v, want %v",
got.Longitude, c.expected.Longitude,
)
}
}
// TestParseCoordinatesNaN checks that ParseCoordinates accepts nan.
// It does not check value ranges.
func TestParseCoordinatesNaN(t *testing.T) {
got, err := ParseCoordinates("nan,2")
if err != nil {
t.Fatalf("error = %v", err)
}
if got == nil {
t.Fatal("got nil, want coordinates")
}
if !math.IsNaN(got.Latitude) {
t.Errorf("Latitude = %v, want NaN", got.Latitude)
}
if got.Longitude != 2 {
t.Errorf("Longitude = %v, want 2", got.Longitude)
}
}
// FuzzParseCoordinates checks that ParseCoordinates handles any input
// without panicking, that it does not return a result and an error
// together, and that a successful result matches strconv.ParseFloat
// on each comma-separated part.
func FuzzParseCoordinates(f *testing.F) {
f.Add(`52.37,4.90`)
f.Add(`-33.8688,151.2093`)
f.Add(`nan,inf`)
f.Add(``)
f.Add(`52.37`)
f.Add(`52.37,4.90,1`)
f.Add(`a,b`)
f.Add(`1,`)
f.Add(`,2`)
f.Add(`1e999,2`)
f.Fuzz(fuzzParseCoordinates)
}
func fuzzParseCoordinates(t *testing.T, s string) {
got, err := ParseCoordinates(s)
if err != nil && got != nil {
t.Fatalf(
"ParseCoordinates(%q) = %v with error %v, want nil",
s, got, err,
)
}
// The input must hold exactly two comma-separated parts.
parts := strings.Split(s, `,`)
if len(parts) != 2 {
if err == nil {
t.Fatalf(
"ParseCoordinates(%q) = %v, want error",
s, got,
)
}
return
}
// Each part must hold a valid float64 value.
expectedLat, latErr := strconv.ParseFloat(parts[0], 64)
expectedLon, lonErr := strconv.ParseFloat(parts[1], 64)
if latErr != nil || lonErr != nil {
if err == nil {
t.Fatalf(
"ParseCoordinates(%q) = %v, want error",
s, got,
)
}
return
}
if err != nil {
t.Fatalf("ParseCoordinates(%q) error = %v", s, err)
}
if got == nil {
t.Fatalf("ParseCoordinates(%q) = nil, want coordinates", s)
}
// Both values come from strconv.ParseFloat. The bit comparison
// reports NaN as equal; the == operator does not.
if math.Float64bits(got.Latitude) !=
math.Float64bits(expectedLat) {
t.Fatalf(
"ParseCoordinates(%q).Latitude = %v, want %v",
s, got.Latitude, expectedLat,
)
}
if math.Float64bits(got.Longitude) !=
math.Float64bits(expectedLon) {
t.Fatalf(
"ParseCoordinates(%q).Longitude = %v, want %v",
s, got.Longitude, expectedLon,
)
}
}