diff --git a/scw/custom_types.go b/scw/custom_types.go index f2161f4b9..3358a9600 100644 --- a/scw/custom_types.go +++ b/scw/custom_types.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/scaleway/scaleway-sdk-go/internal/errors" "github.com/scaleway/scaleway-sdk-go/logger" ) @@ -92,28 +93,16 @@ func NewMoneyFromFloat(value float64, currencyCode string, precision int) *Money } strValue := strconv.FormatFloat(value, 'f', precision, 64) - parts := strings.Split(strValue, ".") - - money := &Money{ - CurrencyCode: currencyCode, - Units: int64(value), - Nanos: 0, + units, nanos, err := splitFloatString(strValue) + if err != nil { + panic(err) } - // Handle nanos. - if len(parts) == 2 { - // Add leading zeros. - strNanos := parts[1] + "000000000"[len(parts[1]):] - - n, err := strconv.ParseInt(strNanos, 10, 32) - if err != nil { - panic(fmt.Errorf("invalid nanos %s", strNanos)) - } - - money.Nanos = int32(n) + return &Money{ + CurrencyCode: currencyCode, + Units: units, + Nanos: nanos, } - - return money } // String returns the string representation of Money. @@ -137,7 +126,7 @@ func (m *Money) String() string { // ToFloat converts a Money object to a float. func (m *Money) ToFloat() float64 { - return float64(m.Units) + float64(m.Nanos)/1000000000 + return float64(m.Units) + float64(m.Nanos)/1e9 } // Money represents a size in bytes. @@ -257,3 +246,94 @@ func (n *IPNet) UnmarshalJSON(b []byte) error { return nil } + +// Duration represents a signed, fixed-length span of time represented as a +// count of seconds and fractions of seconds at nanosecond resolution. It is +// independent of any calendar and concepts like "day" or "month". It is related +// to Timestamp in that the difference between two Timestamp values is a Duration +// and it can be added or subtracted from a Timestamp. +// Range is approximately +-10,000 years. +type Duration struct { + Seconds int64 + Nanos int32 +} + +func (d *Duration) ToTimeDuration() *time.Duration { + if d == nil { + return nil + } + timeDuration := time.Duration(d.Nanos) + time.Duration(d.Seconds/1e9) + return &timeDuration +} + +func (d Duration) MarshalJSON() ([]byte, error) { + nanos := d.Nanos + if nanos < 0 { + nanos = -nanos + } + + return []byte(`"` + fmt.Sprintf("%d.%09d", d.Seconds, nanos) + `s"`), nil +} + +func (d *Duration) UnmarshalJSON(b []byte) error { + if string(b) == "null" { + return nil + } + var str string + + err := json.Unmarshal(b, &str) + if err != nil { + return err + } + if str == "" { + *d = Duration{} + return nil + } + + seconds, nanos, err := splitFloatString(strings.TrimRight(str, "s")) + if err != nil { + return err + } + + *d = Duration{ + Seconds: seconds, + Nanos: nanos, + } + + return nil +} + +// splitFloatString splits a float represented in a string, and returns its units (left-coma part) and nanos (right-coma part). +// E.g.: +// "3" ==> units = 3 | nanos = 0 +// "3.14" ==> units = 3 | nanos = 14*1e7 +// "-3.14" ==> units = -3 | nanos = -14*1e7 +func splitFloatString(input string) (units int64, nanos int32, err error) { + parts := strings.SplitN(input, ".", 2) + + // parse units as int64 + units, err = strconv.ParseInt(parts[0], 10, 64) + if err != nil { + return 0, 0, errors.Wrap(err, "invalid units") + } + + // handle nanos + if len(parts) == 2 { + // add leading zeros + strNanos := parts[1] + "000000000"[len(parts[1]):] + + // parse nanos as int32 + n, err := strconv.ParseUint(strNanos, 10, 32) + if err != nil { + return 0, 0, errors.Wrap(err, "invalid nanos") + } + + nanos = int32(n) + } + + if units < 0 { + nanos = -nanos + } + + return units, nanos, nil +} diff --git a/scw/custom_types_test.go b/scw/custom_types_test.go index d9b860964..b3ccac986 100644 --- a/scw/custom_types_test.go +++ b/scw/custom_types_test.go @@ -409,3 +409,150 @@ func TestIPNet_UnmarshalJSON(t *testing.T) { }) } } + +func TestDuration_MarshallJSON(t *testing.T) { + cases := []struct { + name string + duration Duration + want string + err error + }{ + { + name: "small seconds", + duration: Duration{Seconds: 3, Nanos: 0}, + want: `"3.000000000s"`, + }, + { + name: "small seconds, small nanos", + duration: Duration{Seconds: 3, Nanos: 12e7}, + want: `"3.120000000s"`, + }, + { + name: "small seconds, big nanos", + duration: Duration{Seconds: 3, Nanos: 123456789}, + want: `"3.123456789s"`, + }, + { + name: "big seconds, big nanos", + duration: Duration{Seconds: 345679384, Nanos: 123456789}, + want: `"345679384.123456789s"`, + }, + { + name: "negative small seconds", + duration: Duration{Seconds: -3, Nanos: 0}, + want: `"-3.000000000s"`, + }, + { + name: "negative small seconds, small nanos", + duration: Duration{Seconds: -3, Nanos: -12e7}, + want: `"-3.120000000s"`, + }, + { + name: "negative small seconds, big nanos", + duration: Duration{Seconds: -3, Nanos: -123456789}, + want: `"-3.123456789s"`, + }, + { + name: "negative big seconds, big nanos", + duration: Duration{Seconds: -345679384, Nanos: -123456789}, + want: `"-345679384.123456789s"`, + }, + { + name: "negative big seconds, big nanos", + duration: Duration{}, + want: `"0.000000000s"`, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got, err := json.Marshal(c.duration) + + testhelpers.Equals(t, c.err, err) + if c.err == nil { + testhelpers.Equals(t, c.want, string(got)) + } + }) + } +} + +func TestDuration_UnmarshalJSON(t *testing.T) { + cases := []struct { + name string + json string + want *Duration + err string + }{ + { + name: "error negative nanos", + json: `{"duration":"a.12s"}`, + want: nil, + err: "scaleway-sdk-go: invalid units: strconv.ParseInt: parsing \"a\": invalid syntax", + }, + { + name: "error negative nanos", + json: `{"duration":"3.-12s"}`, + want: nil, + err: "scaleway-sdk-go: invalid nanos: strconv.ParseUint: parsing \"-12000000\": invalid syntax", + }, + { + name: "null", + json: `{"duration":null}`, + want: nil, + }, + { + name: "small seconds", + json: `{"duration":"3.00s"}`, + want: &Duration{Seconds: 3, Nanos: 0}, + }, + { + name: "small seconds, small nanos", + json: `{"duration":"3.12s"}`, + want: &Duration{Seconds: 3, Nanos: 12e7}, + }, + { + name: "bug seconds", + json: `{"duration":"987654321.00s"}`, + want: &Duration{Seconds: 987654321, Nanos: 0}, + }, + { + name: "big seconds, big nanos", + json: `{"duration":"987654321.123456789s"}`, + want: &Duration{Seconds: 987654321, Nanos: 123456789}, + }, + { + name: "negative small seconds", + json: `{"duration":"-3.00s"}`, + want: &Duration{Seconds: -3, Nanos: 0}, + }, + { + name: "negative small seconds, small nanos", + json: `{"duration":"-3.12s"}`, + want: &Duration{Seconds: -3, Nanos: -12e7}, + }, + { + name: "negative bug seconds", + json: `{"duration":"-987654321.00s"}`, + want: &Duration{Seconds: -987654321, Nanos: 0}, + }, + { + name: "negative big seconds, big nanos", + json: `{"duration":"-987654321.123456789s"}`, + want: &Duration{Seconds: -987654321, Nanos: -123456789}, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var testType struct { + Duration *Duration + } + err := json.Unmarshal([]byte(c.json), &testType) + if err != nil { + testhelpers.Equals(t, c.err, err.Error()) + } else { + testhelpers.Equals(t, c.want, testType.Duration) + } + }) + } +}