Skip to content

Commit 679282c

Browse files
committed
msgpack: add string() for decimal, datetime. interval
Added a benchmark, which shows that the code for decimal is optimized two or more times for string conversion than the code from the library. Added a datetime, Interval type conversion function to a string, added tests for this function. Added #322
1 parent 8384443 commit 679282c

File tree

7 files changed

+987
-1
lines changed

7 files changed

+987
-1
lines changed

datetime/datetime.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,11 @@ func datetimeDecoder(d *msgpack.Decoder, v reflect.Value, extLen int) error {
350350
return ptr.UnmarshalMsgpack(b)
351351
}
352352

353+
// This method converts Datetime to String - formats to ISO8601.
354+
func (d Datetime) String() string {
355+
return d.time.Format(time.RFC3339Nano)
356+
}
357+
353358
func init() {
354359
msgpack.RegisterExtDecoder(datetimeExtID, Datetime{}, datetimeDecoder)
355360
msgpack.RegisterExtEncoder(datetimeExtID, Datetime{}, datetimeEncoder)

datetime/datetime_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1174,6 +1174,23 @@ func runTestMain(m *testing.M) int {
11741174
return m.Run()
11751175
}
11761176

1177+
func TestDatetimeString(t *testing.T) {
1178+
1179+
tm, _ := time.Parse(time.RFC3339Nano, "2010-05-24T17:51:56.000000009Z")
1180+
dt, err := MakeDatetime(tm)
1181+
if err != nil {
1182+
t.Fatalf("Unable to create Datetime from %s: %s", tm, err)
1183+
}
1184+
1185+
result := dt.String()
1186+
t.Logf("Result: %s", result)
1187+
1188+
expected := "2010-05-24T17:51:56.000000009Z"
1189+
if result != expected {
1190+
t.Errorf("Expected %s, got %s", expected, result)
1191+
}
1192+
1193+
}
11771194
func TestMain(m *testing.M) {
11781195
code := runTestMain(m)
11791196
os.Exit(code)

datetime/interval.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package datetime
22

33
import (
44
"bytes"
5+
"fmt"
56
"reflect"
7+
"strings"
68

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

221+
// Returns a human-readable string representation of the interval.
222+
func (ival Interval) String() string {
223+
if ival.countNonZeroFields() == 0 {
224+
return "0 seconds"
225+
}
226+
227+
parts := make([]string, 0, 9)
228+
229+
// Helper function for adding components.
230+
addPart := func(value int64, singular, plural string) {
231+
if value == 0 {
232+
return
233+
}
234+
if value == 1 || value == -1 {
235+
parts = append(parts, fmt.Sprintf("%d %s", value, singular))
236+
} else {
237+
parts = append(parts, fmt.Sprintf("%d %s", value, plural))
238+
}
239+
}
240+
241+
addPart(ival.Year, "year", "years")
242+
addPart(ival.Month, "month", "months")
243+
addPart(ival.Week, "week", "weeks")
244+
addPart(ival.Day, "day", "days")
245+
addPart(ival.Hour, "hour", "hours")
246+
addPart(ival.Min, "minute", "minutes")
247+
248+
// Processing seconds and nanoseconds - combine if both are present.
249+
if ival.Sec != 0 && ival.Nsec != 0 {
250+
// Define a common symbol for proper formatting.
251+
secSign := ival.Sec < 0
252+
nsecSign := ival.Nsec < 0
253+
254+
if secSign == nsecSign {
255+
// Same signs - combine them.
256+
absSec := ival.Sec
257+
absNsec := ival.Nsec
258+
if secSign {
259+
absSec = -absSec
260+
absNsec = -absNsec
261+
}
262+
parts = append(parts, fmt.Sprintf("%s%d.%09d seconds",
263+
boolToSign(secSign), absSec, absNsec))
264+
} else {
265+
// Different characters - output separately.
266+
addPart(ival.Sec, "second", "seconds")
267+
addPart(ival.Nsec, "nanosecond", "nanoseconds")
268+
}
269+
} else {
270+
// Only seconds or only nanoseconds.
271+
addPart(ival.Sec, "second", "seconds")
272+
addPart(ival.Nsec, "nanosecond", "nanoseconds")
273+
}
274+
275+
return joinIntervalParts(parts)
276+
}
277+
278+
// Returns "-" for true and an empty string for false.
279+
func boolToSign(negative bool) string {
280+
if negative {
281+
return "-"
282+
}
283+
return ""
284+
}
285+
286+
// Combines parts of an interval into a readable string.
287+
func joinIntervalParts(parts []string) string {
288+
switch len(parts) {
289+
case 0:
290+
return "0 seconds"
291+
case 1:
292+
return parts[0]
293+
case 2:
294+
return parts[0] + " and " + parts[1]
295+
default:
296+
return strings.Join(parts[:len(parts)-1], ", ") + " and " + parts[len(parts)-1]
297+
}
298+
}
299+
219300
func init() {
220301
msgpack.RegisterExtEncoder(interval_extId, Interval{},
221302
func(e *msgpack.Encoder, v reflect.Value) (ret []byte, err error) {

datetime/interval_test.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,171 @@ func TestIntervalTarantoolEncoding(t *testing.T) {
133133
})
134134
}
135135
}
136+
137+
func TestIntervalString(t *testing.T) {
138+
tests := []struct {
139+
name string
140+
interval Interval
141+
expected string
142+
}{
143+
{
144+
name: "empty interval",
145+
interval: Interval{},
146+
expected: "0 seconds",
147+
},
148+
{
149+
name: "single component - years",
150+
interval: Interval{
151+
Year: 1,
152+
},
153+
expected: "1 year",
154+
},
155+
{
156+
name: "multiple years",
157+
interval: Interval{
158+
Year: 5,
159+
},
160+
expected: "5 years",
161+
},
162+
{
163+
name: "multiple components",
164+
interval: Interval{
165+
Year: 1,
166+
Month: 2,
167+
Day: 3,
168+
},
169+
expected: "1 year, 2 months and 3 days",
170+
},
171+
{
172+
name: "time components",
173+
interval: Interval{
174+
Hour: 1,
175+
Min: 30,
176+
Sec: 45,
177+
},
178+
expected: "1 hour, 30 minutes and 45 seconds",
179+
},
180+
{
181+
name: "seconds with nanoseconds same sign",
182+
interval: Interval{
183+
Sec: 5,
184+
Nsec: 123456789,
185+
},
186+
expected: "5.123456789 seconds",
187+
},
188+
{
189+
name: "negative seconds with nanoseconds",
190+
interval: Interval{
191+
Sec: -5,
192+
Nsec: -123456789,
193+
},
194+
expected: "-5.123456789 seconds",
195+
},
196+
{
197+
name: "seconds and nanoseconds different signs",
198+
interval: Interval{
199+
Sec: 5,
200+
Nsec: -123456789,
201+
},
202+
expected: "5 seconds and -123456789 nanoseconds",
203+
},
204+
{
205+
name: "only nanoseconds",
206+
interval: Interval{
207+
Nsec: 500000000,
208+
},
209+
expected: "500000000 nanoseconds",
210+
},
211+
{
212+
name: "weeks",
213+
interval: Interval{
214+
Week: 2,
215+
},
216+
expected: "2 weeks",
217+
},
218+
{
219+
name: "complex interval",
220+
interval: Interval{
221+
Year: 1,
222+
Month: 6,
223+
Week: 2,
224+
Day: 3,
225+
Hour: 12,
226+
Min: 30,
227+
Sec: 45,
228+
Nsec: 123456789,
229+
},
230+
expected: "1 year, 6 months, 2 weeks, 3 days, 12 hours, 30 minutes and 45.123456789 seconds",
231+
},
232+
{
233+
name: "negative components",
234+
interval: Interval{
235+
Year: -1,
236+
Day: -2,
237+
Hour: -3,
238+
},
239+
expected: "-1 year, -2 days and -3 hours",
240+
},
241+
}
242+
243+
for _, tt := range tests {
244+
t.Run(tt.name, func(t *testing.T) {
245+
result := tt.interval.String()
246+
if result != tt.expected {
247+
t.Errorf("Interval.String() = %v, want %v", result, tt.expected)
248+
}
249+
})
250+
}
251+
}
252+
253+
func TestIntervalStringIntegration(t *testing.T) {
254+
t.Run("implements Stringer", func(t *testing.T) {
255+
var _ fmt.Stringer = Interval{}
256+
})
257+
258+
t.Run("works with fmt package", func(t *testing.T) {
259+
ival := Interval{Hour: 2, Min: 30}
260+
result := fmt.Sprintf("%s", ival)
261+
expected := "2 hours and 30 minutes"
262+
if result != expected {
263+
t.Errorf("fmt.Sprintf('%%s') = %v, want %v", result, expected)
264+
}
265+
266+
result = fmt.Sprintf("%v", ival)
267+
if result != expected {
268+
t.Errorf("fmt.Sprintf('%%v') = %v, want %v", result, expected)
269+
}
270+
})
271+
}
272+
273+
func TestIntervalStringEdgeCases(t *testing.T) {
274+
tests := []struct {
275+
name string
276+
interval Interval
277+
}{
278+
{
279+
name: "max values",
280+
interval: Interval{Year: 1<<63 - 1, Month: 1<<63 - 1},
281+
},
282+
{
283+
name: "min values",
284+
interval: Interval{Year: -1 << 63, Month: -1 << 63},
285+
},
286+
{
287+
name: "mixed signs complex",
288+
interval: Interval{Year: 1, Month: -1, Day: 1, Hour: -1},
289+
},
290+
}
291+
292+
for _, tt := range tests {
293+
t.Run(tt.name, func(t *testing.T) {
294+
result := tt.interval.String()
295+
if result == "" {
296+
t.Error("Interval.String() returned empty string")
297+
}
298+
if len(result) > 1000 { // Разумный лимит
299+
t.Error("Interval.String() returned unexpectedly long string")
300+
}
301+
})
302+
}
303+
}

0 commit comments

Comments
 (0)