Replace Fixed14_4Float with a integer version.

This type avoids loss of precision, and is likely what was
intended by having 14.4 fixed point.

Conversions to/from float64 are provided.
This commit is contained in:
John Beisley 2021-07-10 15:53:22 +01:00 committed by Huin
parent 688314d831
commit 01b23aa7e6
2 changed files with 326 additions and 30 deletions

View File

@ -256,36 +256,155 @@ func (v *R8) Unmarshal(s string) error {
return err return err
} }
// FloatFixed14_4 maps a float64 to the SOAP "fixed.14.4" type. const Fixed14_4Denominator = 1e4
type FloatFixed14_4 float64 const Fixed14_4MaxInteger = 1e14 - 1
const Fixed14_4MaxFractional = 1e18 - 1
var _ SOAPValue = new(FloatFixed14_4) // Fixed14_4 represents a fixed point number with up to 14 decimal digits
// before the decimal point (integer part), and up to 4 decimal digits
func NewFloatFixed14_4(v float64) *FloatFixed14_4 { // after the decimal point (fractional part).
v2 := FloatFixed14_4(v) //
return &v2 // Corresponds to the SOAP "fixed.14.4" type.
//
// This is a struct to avoid accidentally using the value directly as an
// integer.
type Fixed14_4 struct {
// Fractional divided by 1e4 is the fixed point value. Take care setting
// this directly, it should only contain values in the range (-1e18, 1e18).
Fractional int64
} }
func (v *FloatFixed14_4) String() string { var _ SOAPValue = &Fixed14_4{}
return strconv.FormatFloat(float64(*v), 'f', 4, 64)
// Fixed14_4FromParts creates a Fixed14_4 from components.
// Bounds:
// * Both intPart and fracPart must have the same sign.
// * -1e14 < intPart < 1e14
// * -1e4 < fracPart < 1e4
func Fixed14_4FromParts(intPart int64, fracPart int16) (Fixed14_4, error) {
var v Fixed14_4
err := v.SetParts(intPart, fracPart)
return v, err
} }
func (v *FloatFixed14_4) Marshal() (string, error) { // SetFromParts sets the value based on the integer component and the fractional component.
if *v >= 1e14 || *v <= -1e14 { // Bounds:
return "", fmt.Errorf("soap fixed14.4: value %v out of bounds", v) // * Both intPart and fracPart must have the same sign.
// * -1e14 < intPart < 1e14
// * -1e4 < fracPart < 1e4
func (v *Fixed14_4) SetParts(intPart int64, fracPart int16) error {
if (intPart < 0) != (fracPart < 0) {
return fmt.Errorf("want intPart and fracPart with same sign, got %d and %d",
intPart, fracPart)
} }
if intPart < -Fixed14_4MaxInteger || intPart > Fixed14_4MaxInteger {
return fmt.Errorf("want intPart in range (-1e14,1e14), got %d", intPart)
}
if fracPart < -Fixed14_4Denominator || fracPart > Fixed14_4Denominator {
return fmt.Errorf("want fracPart in range (-1e4,1e4), got %d", fracPart)
}
v.Fractional = intPart*Fixed14_4Denominator + int64(fracPart)
return nil
}
// Returns the integer part and fractional part of the fixed point number.
func (v Fixed14_4) Parts() (int64, int16) {
return v.Fractional / Fixed14_4Denominator, int16(v.Fractional % Fixed14_4Denominator)
}
// Fixed14_4FromFractional creates a Fixed14_4 from an integer, where the
// parameter divided by 1e4 is the fixed point value.
func Fixed14_4FromFractional(fracValue int64) (Fixed14_4, error) {
var v Fixed14_4
err := v.SetFractional(fracValue)
return v, err
}
// SetFromFractional sets the value of the fixed point number, where fracValue
// divided by 1e4 is the fixed point value. Unlike setting v.Fractional
// directly, this checks the value.
func (v *Fixed14_4) SetFractional(fracValue int64) error {
if fracValue < -Fixed14_4MaxFractional || fracValue > Fixed14_4MaxFractional {
return fmt.Errorf("want intPart in range (-1e18,1e18), got %d", fracValue)
}
v.Fractional = fracValue
return nil
}
// Fixed14_4FromFloat creates a Fixed14_4 from a float64. Returns error if the
// float is outside the range.
func Fixed14_4FromFloat(f float64) (Fixed14_4, error) {
i := int64(f * Fixed14_4Denominator)
return Fixed14_4FromFractional(i)
}
// SetFloat64 sets the value of the fixed point number from a float64. Returns
// error if the float is outside the range.
func (v *Fixed14_4) SetFloat64(f float64) error {
i := int64(f * Fixed14_4Denominator)
return v.SetFractional(i)
}
func (v Fixed14_4) Float64() float64 {
return float64(v.Fractional) / Fixed14_4Denominator
}
func (v Fixed14_4) String() string {
intPart, fracPart := v.Parts()
if fracPart < 0 {
fracPart = -fracPart
}
return fmt.Sprintf("%d.%04d", intPart, fracPart)
}
func (v *Fixed14_4) Marshal() (string, error) {
return v.String(), nil return v.String(), nil
} }
func (v *FloatFixed14_4) Unmarshal(s string) error { func (v *Fixed14_4) Unmarshal(s string) error {
v2, err := strconv.ParseFloat(s, 64) parts := strings.SplitN(s, ".", 2)
intPart, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil { if err != nil {
return err return err
} }
if v2 >= 1e14 || v2 <= -1e14 {
return fmt.Errorf("soap fixed14.4: value %q out of bounds", s) var fracPart int64
if len(parts) >= 2 && len(parts[1]) > 0 {
fracStr := parts[1]
for _, r := range fracStr {
if r < '0' || r > '9' {
return fmt.Errorf("found non-digit in fractional component of %q", s)
} }
*v = FloatFixed14_4(v2) }
// Take only the 4 most significant digits of the fractional component.
fracStr = fracStr[:min(len(fracStr), 4)]
fracPart, err = strconv.ParseInt(fracStr, 10, 16)
if err != nil {
return err
}
if fracPart < 0 {
// This shouldn't happen by virtue of earlier digit-only check.
return fmt.Errorf("got negative fractional component in %q", s)
}
switch len(fracStr) {
case 1:
fracPart *= 1000
case 2:
fracPart *= 100
case 3:
fracPart *= 10
case 4:
fracPart *= 1
}
if intPart < 0 {
fracPart = -fracPart
}
}
v.SetParts(intPart, int16(fracPart))
return nil return nil
} }
@ -609,6 +728,62 @@ func (v *TimeOfDayTZ) Unmarshal(s string) error {
return nil return nil
} }
// Date maps to the SOAP "date" type. Marshaling and Unmarshalling does *not*
// check if components are in range.
type Date struct {
Year int
Month time.Month
Day int
}
var _ SOAPValue = &Date{}
func DateFromTime(t time.Time) Date {
var d Date
d.SetFromTime(t)
return d
}
func (d *Date) SetFromTime(t time.Time) {
d.Year = t.Year()
d.Month = t.Month()
d.Day = t.Day()
}
// ToTime returns a time.Time from the date components, at midnight, and using
// the given location.
func (d *Date) ToTime(loc *time.Location) time.Time {
return time.Date(d.Year, d.Month, d.Day, 0, 0, 0, 0, loc)
}
func (d *Date) String() string {
return fmt.Sprintf("%04d-%02d-%02d", d.Year, d.Month, d.Day)
}
// CheckValid returns an error if the date components are out of range.
func (d *Date) CheckValid() error {
y, m, day := d.ToTime(time.UTC).Date()
if y != d.Year || m != d.Month || day != d.Day {
return fmt.Errorf("SOAP date component(s) out of range in %v", d)
}
return nil
}
func (d *Date) Marshal() (string, error) {
return d.String(), nil
}
func (d *Date) Unmarshal(s string) error {
year, month, day, err := parseDateParts(s)
if err != nil {
return err
}
d.Year = year
d.Month = time.Month(month)
d.Day = day
return nil
}
// DateLocal maps time.Time to the SOAP "date" type. Dates map to midnight in // DateLocal maps time.Time to the SOAP "date" type. Dates map to midnight in
// the local time zone. The time of day components are ignored when // the local time zone. The time of day components are ignored when
// marshalling. // marshalling.
@ -862,3 +1037,10 @@ func (v *URI) Unmarshal(s string) error {
*v = URI(*v2) *v = URI(*v2)
return nil return nil
} }
func min(a, b int) int {
if a < b {
return a
}
return b
}

View File

@ -3,10 +3,19 @@ package types
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"math"
"testing" "testing"
"time" "time"
) )
func newFixed14_4Parts(intPart int64, fracPart int16) *Fixed14_4 {
v, err := Fixed14_4FromParts(intPart, fracPart)
if err != nil {
panic(err)
}
return &v
}
type isEqual func(got, want SOAPValue) bool type isEqual func(got, want SOAPValue) bool
type typeTestCase struct { type typeTestCase struct {
@ -135,20 +144,37 @@ func Test(t *testing.T) {
}, },
{ {
makeValue: func() SOAPValue { return new(FloatFixed14_4) }, makeValue: func() SOAPValue { return &Fixed14_4{} },
isEqual: func(got, want SOAPValue) bool { return *got.(*FloatFixed14_4) == *want.(*FloatFixed14_4) }, isEqual: func(got, want SOAPValue) bool {
marshalTests: []marshalCase{ return got.(*Fixed14_4).Fractional == want.(*Fixed14_4).Fractional
{NewFloatFixed14_4(0), "0.0000"},
{NewFloatFixed14_4(1), "1.0000"},
{NewFloatFixed14_4(1.2346), "1.2346"},
{NewFloatFixed14_4(-1), "-1.0000"},
{NewFloatFixed14_4(-1.2346), "-1.2346"},
{NewFloatFixed14_4(1e13), "10000000000000.0000"},
{NewFloatFixed14_4(-1e13), "-10000000000000.0000"},
}, },
marshalErrs: []SOAPValue{ marshalTests: []marshalCase{
NewFloatFixed14_4(1e14), {newFixed14_4Parts(0, 0), "0.0000"},
NewFloatFixed14_4(-1e14), {newFixed14_4Parts(1, 2), "1.0002"},
{newFixed14_4Parts(1, 20), "1.0020"},
{newFixed14_4Parts(1, 200), "1.0200"},
{newFixed14_4Parts(1, 2000), "1.2000"},
{newFixed14_4Parts(-1, -2), "-1.0002"},
{newFixed14_4Parts(1234, 5678), "1234.5678"},
{newFixed14_4Parts(-1234, -5678), "-1234.5678"},
{newFixed14_4Parts(9999_99999_99999, 9999), "99999999999999.9999"},
{newFixed14_4Parts(-9999_99999_99999, -9999), "-99999999999999.9999"},
},
unmarshalErrs: append([]string{
"", ".", "0.00000000abc", "0.-5",
}, badNumbers...),
unmarshalTests: []unmarshalCase{
{"010", newFixed14_4Parts(10, 0)},
{"0", newFixed14_4Parts(0, 0)},
{"0.", newFixed14_4Parts(0, 0)},
{"0.000005", newFixed14_4Parts(0, 0)},
{"1.2", newFixed14_4Parts(1, 2000)},
{"1.20", newFixed14_4Parts(1, 2000)},
{"1.200", newFixed14_4Parts(1, 2000)},
{"1.02", newFixed14_4Parts(1, 200)},
{"1.020", newFixed14_4Parts(1, 200)},
{"1.002", newFixed14_4Parts(1, 20)},
{"1.00200005", newFixed14_4Parts(1, 20)},
}, },
}, },
@ -422,3 +448,91 @@ func Test(t *testing.T) {
}) })
} }
} }
func TestFixed14_4(t *testing.T) {
t.Run("Parts", func(t *testing.T) {
tests := []struct {
intPart int64
fracPart int16
fractional int64
}{
{0, 0, 0},
{1, 2, 1_0002},
{-1, -2, -1_0002},
{1234, 5678, 1234_5678},
{-1234, -5678, -1234_5678},
{9999_99999_99999, 9999, 9999_99999_99999_9999},
{-9999_99999_99999, -9999, -9999_99999_99999_9999},
}
for _, test := range tests {
test := test
t.Run(fmt.Sprintf("FromParts(%d,%d)", test.intPart, test.fracPart), func(t *testing.T) {
got, err := Fixed14_4FromParts(test.intPart, test.fracPart)
if err != nil {
t.Errorf("got error %v, want success", err)
}
if got.Fractional != test.fractional {
t.Errorf("got %d, want %d", got.Fractional, test.fractional)
}
})
t.Run(fmt.Sprintf("%d.Parts()", test.fractional), func(t *testing.T) {
v, err := Fixed14_4FromFractional(test.fractional)
if err != nil {
t.Errorf("got error %v, want success", err)
}
gotIntPart, gotFracPart := v.Parts()
if gotIntPart != test.intPart {
t.Errorf("got %d, want %d", gotIntPart, test.intPart)
}
if gotFracPart != test.fracPart {
t.Errorf("got %d, want %d", gotFracPart, test.fracPart)
}
})
}
})
t.Run("Float", func(t *testing.T) {
tests := []struct {
flt float64
fix *Fixed14_4
}{
{0, newFixed14_4Parts(0, 0)},
{1234.5678, newFixed14_4Parts(1234, 5678)},
{-1234.5678, newFixed14_4Parts(-1234, -5678)},
}
for _, test := range tests {
t.Run(fmt.Sprintf("To/FromFloat(%v)", test.fix), func(t *testing.T) {
gotFix, err := Fixed14_4FromFloat(test.flt)
if err != nil {
t.Errorf("got error %v, want success", err)
}
if gotFix.Fractional != test.fix.Fractional {
t.Errorf("got %v, want %v", gotFix, test.fix)
}
gotFlt := test.fix.Float64()
if math.Abs(gotFlt-test.flt) > 1e-6 {
t.Errorf("got %f, want %f", gotFlt, test.flt)
}
})
}
errTests := []float64{
1e50,
-1e50,
1e14,
-1e14,
math.NaN(),
math.Inf(1),
math.Inf(-1),
}
for _, test := range errTests {
t.Run(fmt.Sprintf("ErrorFromFloat(%f)", test), func(t *testing.T) {
got, err := Fixed14_4FromFloat(test)
if err == nil {
t.Errorf("got success and %v, want error", got)
}
})
}
})
}