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 47e825446b
commit 2186162cd7
2 changed files with 326 additions and 30 deletions

View File

@ -256,36 +256,155 @@ func (v *R8) Unmarshal(s string) error {
return err
}
// FloatFixed14_4 maps a float64 to the SOAP "fixed.14.4" type.
type FloatFixed14_4 float64
const Fixed14_4Denominator = 1e4
const Fixed14_4MaxInteger = 1e14 - 1
const Fixed14_4MaxFractional = 1e18 - 1
var _ SOAPValue = new(FloatFixed14_4)
func NewFloatFixed14_4(v float64) *FloatFixed14_4 {
v2 := FloatFixed14_4(v)
return &v2
// 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
// after the decimal point (fractional part).
//
// 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 {
return strconv.FormatFloat(float64(*v), 'f', 4, 64)
var _ SOAPValue = &Fixed14_4{}
// 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) {
if *v >= 1e14 || *v <= -1e14 {
return "", fmt.Errorf("soap fixed14.4: value %v out of bounds", v)
// SetFromParts sets the value based on the integer component and the fractional component.
// Bounds:
// * 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
}
func (v *FloatFixed14_4) Unmarshal(s string) error {
v2, err := strconv.ParseFloat(s, 64)
func (v *Fixed14_4) Unmarshal(s string) error {
parts := strings.SplitN(s, ".", 2)
intPart, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil {
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)
}
}
// 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 = FloatFixed14_4(v2)
v.SetParts(intPart, int16(fracPart))
return nil
}
@ -609,6 +728,62 @@ func (v *TimeOfDayTZ) Unmarshal(s string) error {
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
// the local time zone. The time of day components are ignored when
// marshalling.
@ -862,3 +1037,10 @@ func (v *URI) Unmarshal(s string) error {
*v = URI(*v2)
return nil
}
func min(a, b int) int {
if a < b {
return a
}
return b
}

View File

@ -3,10 +3,19 @@ package types
import (
"bytes"
"fmt"
"math"
"testing"
"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 typeTestCase struct {
@ -135,20 +144,37 @@ func Test(t *testing.T) {
},
{
makeValue: func() SOAPValue { return new(FloatFixed14_4) },
isEqual: func(got, want SOAPValue) bool { return *got.(*FloatFixed14_4) == *want.(*FloatFixed14_4) },
marshalTests: []marshalCase{
{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"},
makeValue: func() SOAPValue { return &Fixed14_4{} },
isEqual: func(got, want SOAPValue) bool {
return got.(*Fixed14_4).Fractional == want.(*Fixed14_4).Fractional
},
marshalErrs: []SOAPValue{
NewFloatFixed14_4(1e14),
NewFloatFixed14_4(-1e14),
marshalTests: []marshalCase{
{newFixed14_4Parts(0, 0), "0.0000"},
{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)
}
})
}
})
}