diff --git a/tt/option_convert.go b/tt/option_convert.go index c635f502..0786c11f 100644 --- a/tt/option_convert.go +++ b/tt/option_convert.go @@ -72,31 +72,26 @@ func FromCsv(val any, strv string) error { return p } -func formatFloat(v float64) string { - value := "" - if math.IsNaN(v) { - value = "" - } else if v > -100_000 && v < 100_000 { - // use pretty %g formatting but avoid exponents - value = fmt.Sprintf("%g", v) - } else { - value = fmt.Sprintf("%0.5f", v) - } - return value -} - // ToCsv converts any value to a CSV string representation func ToCsv(val any) (string, error) { value := "" switch v := val.(type) { case nil: value = "" + case canCsvString: + value = v.ToCsv() + case canValue: + a, err := v.Value() + if err != nil { + return "", err + } + return ToCsv(a) case string: value = v - case int: - value = strconv.Itoa(v) case int64: - value = strconv.Itoa(int(v)) + value = strconv.FormatInt(v, 10) + case int: + value = strconv.FormatInt(int64(v), 10) case bool: if v { value = "true" @@ -115,16 +110,6 @@ func ToCsv(val any) (string, error) { } case []byte: value = string(v) - case canCsvString: - value = v.ToCsv() - case canValue: - a, err := v.Value() - if err != nil { - return "", err - } - return ToCsv(a) - case canString: - value = v.String() case int8, int16, int32, uint, uint8, uint16, uint32, uint64: value = fmt.Sprintf("%d", v) default: @@ -155,12 +140,12 @@ func convertAssign(dest any, src any) (bool, error) { *d = s case []byte: *d = string(s) - case int: - *d = strconv.Itoa(s) case int64: - *d = strconv.Itoa(int(s)) + *d = strconv.FormatInt(s, 10) + case int: + *d = strconv.FormatInt(int64(s), 10) case float64: - *d = fmt.Sprintf("%0.5f", s) + *d = formatFloat(s) case time.Time: *d = s.Format(time.RFC3339) case canString: @@ -286,3 +271,26 @@ func parseTime(d string) (time.Time, error) { } return s, err } + +func formatFloat(v float64) string { + if math.IsNaN(v) || math.IsInf(v, 0) || math.IsInf(v, -1) { + return "" + } + return trimZeroAfterDecimal(strconv.FormatFloat(v, 'f', 5, 64)) +} + +func trimZeroAfterDecimal(value string) string { + i := 0 + j := len(value) - 1 + for ; i < len(value); i++ { + if value[i] == '.' { + break + } + } + for ; j > i+1; j-- { + if value[j] != '0' { + break + } + } + return value[0 : j+1] +} diff --git a/tt/option_test.go b/tt/option_test.go index 7c4baeda..a2384d91 100644 --- a/tt/option_test.go +++ b/tt/option_test.go @@ -2,12 +2,72 @@ package tt import ( "database/sql/driver" + "math" "testing" "time" "github.com/stretchr/testify/assert" ) +func TestToCsv(t *testing.T) { + tcs := []struct { + name string + val any + expectString string + expectError bool + }{ + // ints + {name: "int:1", val: 1, expectString: "1"}, + {name: "int:-1", val: -1, expectString: "-1"}, + {name: "int:0", val: 0, expectString: "0"}, + // Ints + {name: "Int:1", val: NewInt(1), expectString: "1"}, + {name: "Int:-1", val: NewInt(-1), expectString: "-1"}, + {name: "Int:0", val: NewInt(0), expectString: "0"}, + {name: "Int:empty", val: Int{}, expectString: ""}, + // floats + {name: "float:1.0", val: 1.0, expectString: "1.0"}, + {name: "float:NaN", val: math.NaN(), expectString: ""}, + {name: "float:+Inf", val: math.Inf(0), expectString: ""}, + {name: "float:-Inf", val: math.Inf(-1), expectString: ""}, + {name: "float:1.2", val: 1.2, expectString: "1.2"}, + {name: "float:1.23", val: 1.23, expectString: "1.23"}, + // Floats + {name: "Float:1.0", val: NewFloat(1.0), expectString: "1.0"}, + {name: "Float:empty", val: Float{}, expectString: ""}, + {name: "Float:+Inf", val: NewFloat(math.Inf(0)), expectString: ""}, + {name: "Float:-Inf", val: NewFloat(math.Inf(-1)), expectString: ""}, + {name: "Float:NaN", val: NewFloat(math.NaN()), expectString: ""}, + {name: "Float:1.2", val: NewFloat(1.2), expectString: "1.2"}, + {name: "Float:-1.2", val: NewFloat(-1.2), expectString: "-1.2"}, + {name: "Float:1.23", val: NewFloat(1.23), expectString: "1.23"}, + {name: "Float:1.234", val: NewFloat(1.234), expectString: "1.234"}, + {name: "Float:1.2345", val: NewFloat(1.23456), expectString: "1.23456"}, + {name: "Float:1.123456", val: NewFloat(1.123456), expectString: "1.12346"}, + {name: "Float:-1.123456", val: NewFloat(-1.123456), expectString: "-1.12346"}, + {name: "Float:1000.0", val: NewFloat(1000.0), expectString: "1000.0"}, + {name: "Float:1000.12345", val: NewFloat(1000.12345), expectString: "1000.12345"}, + {name: "Float:1000.1234567890", val: NewFloat(1000.1234567890), expectString: "1000.12346"}, + {name: "Float:123_456_789_000", val: NewFloat(123_456_789_000), expectString: "123456789000.0"}, + {name: "Float:123_456_789_000.123`", val: NewFloat(123_456_789_000.123), expectString: "123456789000.123"}, + {name: "Float:123_456_789_000.123456`", val: NewFloat(123_456_789_000.123456), expectString: "123456789000.12346"}, + } + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + s, ferr := ToCsv(tc.val) + if tc.expectError && ferr != nil { + // ok + return + } else if tc.expectError && ferr == nil { + t.Error("expected error") + } else if !tc.expectError && ferr != nil { + t.Error(ferr) + } + assert.Equal(t, tc.expectString, s) + }) + } +} + func TestOptionString(t *testing.T) { testStr := "hello" quote := func(v string) string { return "\"" + v + "\"" } @@ -43,7 +103,7 @@ func TestOptionString(t *testing.T) { true: nil, "true": "true", "nil": "nil", - 1.23: "1.23000", + 1.23: "1.23", 1.234567: "1.23457", nil: nil, }, @@ -53,7 +113,7 @@ func TestOptionString(t *testing.T) { 1: "1", nil: "", true: "", - 1.23: "1.23000", + 1.23: "1.23", 1.234567: "1.23457", }, uj: map[string]any{ @@ -113,9 +173,9 @@ func TestOptionString(t *testing.T) { "1.234": 1.234, }, str: map[any]any{ - 1234: "1234.00000", - 1.234: "1.23400", - 1.567: "1.56700", + 1234: "1234.0", + 1.234: "1.234", + 1.567: "1.567", "fail": "", }, uj: map[string]any{