Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ includes some improvements to logging.
- Implemented stringer methods for pool.Role (#405).
- Support the IPROTO_INSERT_ARROW request (#399).
- A simple implementation of using the box interface (#410).
- Function String() for types datetime, interval, decimal (#322).

### Changed

Expand Down
5 changes: 5 additions & 0 deletions datetime/datetime.go
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,11 @@ func datetimeDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error {
return ptr.UnmarshalMsgpack(b)
}

// This method converts Datetime to String - formats to ISO8601.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The go-idiomatic way to comment a method.

Suggested change
// This method converts Datetime to String - formats to ISO8601.
// String converts Datetime to String - formats to ISO8601.

func (d Datetime) String() string {
return d.time.Format(time.RFC3339Nano)
}

func init() {
msgpack.RegisterExtDecoder(datetimeExtID, Datetime{}, datetimeDecoder)
msgpack.RegisterExtEncoder(datetimeExtID, Datetime{}, datetimeEncoder)
Expand Down
17 changes: 17 additions & 0 deletions datetime/datetime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1174,6 +1174,23 @@ func runTestMain(m *testing.M) int {
return m.Run()
}

func TestDatetimeString(t *testing.T) {

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

tm, _ := time.Parse(time.RFC3339Nano, "2010-05-24T17:51:56.000000009Z")
dt, err := MakeDatetime(tm)
if err != nil {
t.Fatalf("Unable to create Datetime from %s: %s", tm, err)
}

result := dt.String()
t.Logf("Result: %s", result)

expected := "2010-05-24T17:51:56.000000009Z"
if result != expected {
t.Errorf("Expected %s, got %s", expected, result)
}

}
func TestMain(m *testing.M) {
code := runTestMain(m)
os.Exit(code)
Expand Down
6 changes: 3 additions & 3 deletions datetime/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ func ExampleDatetime_Interval() {
ival := dtFirst.Interval(dtSecond)
fmt.Printf("%v", ival)
// Output:
// {2 2 0 -11 0 -1 0 0 0}
// 2 years, 2 months, -11 days and -1 minute
}

// ExampleDatetime_Add demonstrates how to add an Interval to a Datetime value.
Expand Down Expand Up @@ -278,7 +278,7 @@ func ExampleInterval_Add() {

fmt.Printf("%v", ival)
// Output:
// {11 2 3 0 0 30 10 0 1}
// 11 years, 2 months, 3 weeks, 30 minutes and 10 seconds
}

// ExampleInterval_Sub demonstrates how to subtract two intervals.
Expand All @@ -298,5 +298,5 @@ func ExampleInterval_Sub() {

fmt.Printf("%v", ival)
// Output:
// {-9 2 3 0 0 -30 10 0 1}
// -9 years, 2 months, 3 weeks, -30 minutes and 10 seconds
}
81 changes: 81 additions & 0 deletions datetime/interval.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package datetime

import (
"bytes"
"fmt"
"reflect"
"strings"

"github.com/vmihailenco/msgpack/v5"
)
Expand Down Expand Up @@ -216,6 +218,85 @@ func decodeInterval(d *msgpack.Decoder, v reflect.Value) (err error) {
return nil
}

// Returns a human-readable string representation of the interval.
func (ival Interval) String() string {
if ival.countNonZeroFields() == 0 {
return "0 seconds"
}

parts := make([]string, 0, 9)

// Helper function for adding components.
addPart := func(value int64, singular, plural string) {
if value == 0 {
return
}
if value == 1 || value == -1 {
parts = append(parts, fmt.Sprintf("%d %s", value, singular))
} else {
parts = append(parts, fmt.Sprintf("%d %s", value, plural))
}
}

addPart(ival.Year, "year", "years")
addPart(ival.Month, "month", "months")
addPart(ival.Week, "week", "weeks")
addPart(ival.Day, "day", "days")
addPart(ival.Hour, "hour", "hours")
addPart(ival.Min, "minute", "minutes")

// Processing seconds and nanoseconds - combine if both are present.
if ival.Sec != 0 && ival.Nsec != 0 {
// Define a common symbol for proper formatting.
secSign := ival.Sec < 0
nsecSign := ival.Nsec < 0

if secSign == nsecSign {
// Same signs - combine them.
absSec := ival.Sec
absNsec := ival.Nsec
if secSign {
absSec = -absSec
absNsec = -absNsec
}
parts = append(parts, fmt.Sprintf("%s%d.%09d seconds",
boolToSign(secSign), absSec, absNsec))
} else {
// Different characters - output separately.
addPart(ival.Sec, "second", "seconds")
addPart(ival.Nsec, "nanosecond", "nanoseconds")
}
} else {
// Only seconds or only nanoseconds.
addPart(ival.Sec, "second", "seconds")
addPart(ival.Nsec, "nanosecond", "nanoseconds")
}

return joinIntervalParts(parts)
}

// Returns "-" for true and an empty string for false.
func boolToSign(negative bool) string {
if negative {
return "-"
}
return ""
}

// Combines parts of an interval into a readable string.
func joinIntervalParts(parts []string) string {
switch len(parts) {
case 0:
return "0 seconds"
case 1:
return parts[0]
case 2:
return parts[0] + " and " + parts[1]
default:
return strings.Join(parts[:len(parts)-1], ", ") + " and " + parts[len(parts)-1]
}
}

func init() {
msgpack.RegisterExtEncoder(interval_extId, Interval{},
func(e *msgpack.Encoder, v reflect.Value) (ret []byte, err error) {
Expand Down
169 changes: 169 additions & 0 deletions datetime/interval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,172 @@ func TestIntervalTarantoolEncoding(t *testing.T) {
})
}
}

func TestIntervalString(t *testing.T) {
tests := []struct {
name string
interval Interval
expected string
}{
{
name: "empty interval",
interval: Interval{},
expected: "0 seconds",
},
{
name: "single component - years",
interval: Interval{
Year: 1,
},
expected: "1 year",
},
{
name: "multiple years",
interval: Interval{
Year: 5,
},
expected: "5 years",
},
{
name: "multiple components",
interval: Interval{
Year: 1,
Month: 2,
Day: 3,
},
expected: "1 year, 2 months and 3 days",
},
{
name: "time components",
interval: Interval{
Hour: 1,
Min: 30,
Sec: 45,
},
expected: "1 hour, 30 minutes and 45 seconds",
},
{
name: "seconds with nanoseconds same sign",
interval: Interval{
Sec: 5,
Nsec: 123456789,
},
expected: "5.123456789 seconds",
},
{
name: "negative seconds with nanoseconds",
interval: Interval{
Sec: -5,
Nsec: -123456789,
},
expected: "-5.123456789 seconds",
},
{
name: "seconds and nanoseconds different signs",
interval: Interval{
Sec: 5,
Nsec: -123456789,
},
expected: "5 seconds and -123456789 nanoseconds",
},
{
name: "only nanoseconds",
interval: Interval{
Nsec: 500000000,
},
expected: "500000000 nanoseconds",
},
{
name: "weeks",
interval: Interval{
Week: 2,
},
expected: "2 weeks",
},
{
name: "complex interval",
interval: Interval{
Year: 1,
Month: 6,
Week: 2,
Day: 3,
Hour: 12,
Min: 30,
Sec: 45,
Nsec: 123456789,
},
expected: "1 year, 6 months, 2 weeks, 3 days, 12 hours, 30 minutes " +
"and 45.123456789 seconds",
},
{
name: "negative components",
interval: Interval{
Year: -1,
Day: -2,
Hour: -3,
},
expected: "-1 year, -2 days and -3 hours",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.interval.String()
if result != tt.expected {
t.Errorf("Interval.String() = %v, want %v", result, tt.expected)
}
})
}
}

func TestIntervalStringIntegration(t *testing.T) {
t.Run("implements Stringer", func(t *testing.T) {
var _ fmt.Stringer = Interval{}
})

t.Run("works with fmt package", func(t *testing.T) {
ival := Interval{Hour: 2, Min: 30}
result := ival.String()
expected := "2 hours and 30 minutes"
if result != expected {
t.Errorf("fmt.Sprintf('%%s') = %v, want %v", result, expected)
}

result = fmt.Sprintf("%v", ival)
if result != expected {
t.Errorf("fmt.Sprintf('%%v') = %v, want %v", result, expected)
}
})
}

func TestIntervalStringEdgeCases(t *testing.T) {
tests := []struct {
name string
interval Interval
}{
{
name: "max values",
interval: Interval{Year: 1<<63 - 1, Month: 1<<63 - 1},
},
{
name: "min values",
interval: Interval{Year: -1 << 63, Month: -1 << 63},
},
{
name: "mixed signs complex",
interval: Interval{Year: 1, Month: -1, Day: 1, Hour: -1},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.interval.String()
if result == "" {
t.Error("Interval.String() returned empty string")
}
if len(result) > 1000 { // Разумный лимит
t.Error("Interval.String() returned unexpectedly long string")
}
})
}
}
Loading
Loading