Skip to content

Commit

Permalink
feat(core): custom duration type (#291)
Browse files Browse the repository at this point in the history
  • Loading branch information
kindermoumoute authored Jan 14, 2020
1 parent 51f964a commit 5148814
Show file tree
Hide file tree
Showing 2 changed files with 247 additions and 20 deletions.
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)
}
})
}
}

0 comments on commit 5148814

Please sign in to comment.