Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 100 additions & 20 deletions scw/custom_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"
"time"

"github.com/scaleway/scaleway-sdk-go/internal/errors"
"github.com/scaleway/scaleway-sdk-go/logger"
)

Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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
}
147 changes: 147 additions & 0 deletions scw/custom_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}