diff --git a/v2/soap/types/types.go b/v2/soap/types/types.go index 974cadb..e999941 100644 --- a/v2/soap/types/types.go +++ b/v2/soap/types/types.go @@ -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 +} diff --git a/v2/soap/types/types_test.go b/v2/soap/types/types_test.go index 7d6e979..4dbd7bf 100644 --- a/v2/soap/types/types_test.go +++ b/v2/soap/types/types_test.go @@ -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) + } + }) + } + }) +}