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:
parent
688314d831
commit
01b23aa7e6
@ -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)
|
||||
}
|
||||
*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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user