Skip to content
Draft
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
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
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
171 changes: 171 additions & 0 deletions datetime/interval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,174 @@
})
}
}

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",

Check failure on line 230 in datetime/interval_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

The line is 105 characters long, which exceeds the maximum of 100 characters. (lll)

Check failure on line 230 in datetime/interval_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

The line is 105 characters long, which exceeds the maximum of 100 characters. (lll)
},
{
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}

// Проверяем разные форматы fmt
result := fmt.Sprintf("%s", ival)

Check failure on line 262 in datetime/interval_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

S1025: should use String() instead of fmt.Sprintf (gosimple)

Check failure on line 262 in datetime/interval_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

S1025: should use String() instead of fmt.Sprintf (gosimple)
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