Skip to content

Commit 2900ed6

Browse files
authored
fix(arrow/array): update timestamp json format (#450)
### Rationale for this change resolves #445 ### What changes are included in this PR? Use `time.RFC3339Nano` for the format to output the time value ### Are these changes tested? Yes ### Are there any user-facing changes? Changes the format of timestamp strings for `ValueStr` and marshalling to JSON to match JSON and Go conventions.
1 parent a7d23a7 commit 2900ed6

File tree

3 files changed

+53
-13
lines changed

3 files changed

+53
-13
lines changed

arrow/array/timestamp.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ func (a *Timestamp) ValueStr(i int) string {
9393
}
9494

9595
toTime, _ := a.DataType().(*arrow.TimestampType).GetToTimeFunc()
96-
return toTime(a.values[i]).Format("2006-01-02 15:04:05.999999999Z0700")
96+
return toTime(a.values[i]).Format(time.RFC3339Nano)
9797
}
9898

9999
func (a *Timestamp) GetOneForMarshal(i int) interface{} {

arrow/array/timestamp_test.go

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ import (
2323
"github.com/apache/arrow-go/v18/arrow"
2424
"github.com/apache/arrow-go/v18/arrow/array"
2525
"github.com/apache/arrow-go/v18/arrow/memory"
26+
"github.com/apache/arrow-go/v18/internal/json"
2627
"github.com/stretchr/testify/assert"
28+
"github.com/stretchr/testify/require"
2729
)
2830

2931
func TestTimestampStringRoundTrip(t *testing.T) {
@@ -248,8 +250,8 @@ func TestTimestampValueStr(t *testing.T) {
248250
arr := b.NewArray()
249251
defer arr.Release()
250252

251-
assert.Equal(t, "1968-11-30 13:30:45-0700", arr.ValueStr(0))
252-
assert.Equal(t, "2016-02-29 10:42:23-0700", arr.ValueStr(1))
253+
assert.Equal(t, "1968-11-30T13:30:45-07:00", arr.ValueStr(0))
254+
assert.Equal(t, "2016-02-29T10:42:23-07:00", arr.ValueStr(1))
253255
}
254256

255257
func TestTimestampEquality(t *testing.T) {
@@ -278,16 +280,16 @@ func TestTimestampEquality(t *testing.T) {
278280

279281
// No timezone, "wall clock" semantics
280282
// These timestamps have no actual timezone, but we still represent as UTC per Go conventions
281-
assert.Equal(t, "1968-11-30 20:30:45Z", arrs[0].ValueStr(0))
282-
assert.Equal(t, "2016-02-29 17:42:23Z", arrs[0].ValueStr(1))
283+
assert.Equal(t, "1968-11-30T20:30:45Z", arrs[0].ValueStr(0))
284+
assert.Equal(t, "2016-02-29T17:42:23Z", arrs[0].ValueStr(1))
283285

284286
// UTC timezone, "instant" semantics
285-
assert.Equal(t, "1968-11-30 20:30:45Z", arrs[1].ValueStr(0))
286-
assert.Equal(t, "2016-02-29 17:42:23Z", arrs[1].ValueStr(1))
287+
assert.Equal(t, "1968-11-30T20:30:45Z", arrs[1].ValueStr(0))
288+
assert.Equal(t, "2016-02-29T17:42:23Z", arrs[1].ValueStr(1))
287289

288290
// America/Phoenix timezone, "instant" semantics
289-
assert.Equal(t, "1968-11-30 13:30:45-0700", arrs[2].ValueStr(0))
290-
assert.Equal(t, "2016-02-29 10:42:23-0700", arrs[2].ValueStr(1))
291+
assert.Equal(t, "1968-11-30T13:30:45-07:00", arrs[2].ValueStr(0))
292+
assert.Equal(t, "2016-02-29T10:42:23-07:00", arrs[2].ValueStr(1))
291293

292294
// Despite timezone and semantics, the physical values are equivalent
293295
assert.Equal(t, arrs[0].Value(0), arrs[1].Value(0))
@@ -298,3 +300,41 @@ func TestTimestampEquality(t *testing.T) {
298300
assert.Equal(t, arrs[0].Value(1), arrs[2].Value(1))
299301
assert.Equal(t, arrs[1].Value(1), arrs[2].Value(1))
300302
}
303+
304+
func TestTimestampArrayJSONRoundTrip(t *testing.T) {
305+
mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
306+
defer mem.AssertSize(t, 0)
307+
308+
tz, _ := time.LoadLocation("America/Phoenix")
309+
dt := &arrow.TimestampType{Unit: arrow.Second, TimeZone: tz.String()}
310+
b := array.NewTimestampBuilder(mem, dt)
311+
defer b.Release()
312+
313+
b.Append(-34226955)
314+
b.Append(1456767743)
315+
316+
arr := b.NewArray()
317+
defer arr.Release()
318+
319+
assert.Equal(t, "1968-11-30T13:30:45-07:00", arr.ValueStr(0))
320+
assert.Equal(t, "2016-02-29T10:42:23-07:00", arr.ValueStr(1))
321+
assert.Equal(t, 2, arr.Len())
322+
assert.Equal(t, 0, arr.NullN())
323+
324+
json_bytes, err := arr.MarshalJSON()
325+
require.NoError(t, err)
326+
327+
expectedJSON := `["1968-11-30T13:30:45-07:00","2016-02-29T10:42:23-07:00"]`
328+
require.Equal(t, expectedJSON, string(json_bytes))
329+
330+
var timestamps []time.Time
331+
err = json.Unmarshal(json_bytes, &timestamps)
332+
require.NoError(t, err)
333+
require.Len(t, timestamps, 2)
334+
335+
expectedTime1 := time.Date(1968, time.November, 30, 13, 30, 45, 0, tz)
336+
expectedTime2 := time.Date(2016, time.February, 29, 10, 42, 23, 0, tz)
337+
338+
assert.Equal(t, expectedTime1.Unix(), timestamps[0].Unix())
339+
assert.Equal(t, expectedTime2.Unix(), timestamps[1].Unix())
340+
}

arrow/avro/testdata/testdata.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,16 @@ func (b ByteArray) MarshalJSON() ([]byte, error) {
5050
type TimestampMicros int64
5151

5252
func (t TimestampMicros) MarshalJSON() ([]byte, error) {
53-
ts := time.Unix(0, int64(t)*int64(time.Microsecond)).UTC().Format("2006-01-02 15:04:05.000000")
53+
ts := time.Unix(0, int64(t)*int64(time.Microsecond)).UTC().Format(time.RFC3339Nano)
5454
// arrow record marshaller trims trailing zero digits from timestamp so we do the same
55-
return json.Marshal(fmt.Sprintf("%sZ", strings.TrimRight(ts, "0.")))
55+
return json.Marshal(ts)
5656
}
5757

5858
type TimestampMillis int64
5959

6060
func (t TimestampMillis) MarshalJSON() ([]byte, error) {
61-
ts := time.Unix(0, int64(t)*int64(time.Millisecond)).UTC().Format("2006-01-02 15:04:05.000")
62-
return json.Marshal(fmt.Sprintf("%sZ", strings.TrimRight(ts, "0.")))
61+
ts := time.Unix(0, int64(t)*int64(time.Millisecond)).UTC().Format(time.RFC3339Nano)
62+
return json.Marshal(ts)
6363
}
6464

6565
type TimeMillis time.Duration

0 commit comments

Comments
 (0)