Skip to content

Commit 2c3e5a5

Browse files
committed
time & date conversion
1 parent 22885cc commit 2c3e5a5

File tree

3 files changed

+114
-7
lines changed

3 files changed

+114
-7
lines changed

bindings/pkgs/@duckdb/node-bindings/duckdb.d.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,18 @@ export interface TimeParts {
127127
}
128128

129129
export interface TimeTZ {
130-
/** 40 bits for micros, 24 bits for offset */
131-
bits: number; // or bigint, or buffer?
130+
/**
131+
* 40 bits for micros, then 24 bits for encoded offset in seconds.
132+
*
133+
* Max absolute unencoded offset = 15:59:59 = 60 * (60 * 15 + 59) + 59 = 57599.
134+
*
135+
* Encoded offset is unencoded offset inverted then shifted (by +57599) to unsigned.
136+
*
137+
* Max unencoded offset = 57599 -> -57599 -> 0 encoded.
138+
*
139+
* Min unencoded offset = -57599 -> 57599 -> 115198 encoded.
140+
*/
141+
bits: bigint;
132142
}
133143
export interface TimeTZParts {
134144
time: TimeParts;

bindings/src/duckdb_node_bindings.cpp

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,7 +1033,7 @@ class DuckDBNodeAddon : public Napi::Addon<DuckDBNodeAddon> {
10331033
int32_t year = date_parts_obj.Get("year").As<Napi::Number>().Int32Value();
10341034
int8_t month = date_parts_obj.Get("month").As<Napi::Number>().Int32Value();
10351035
int8_t day = date_parts_obj.Get("day").As<Napi::Number>().Int32Value();
1036-
duckdb_date_struct date_parts { year, month, day };
1036+
duckdb_date_struct date_parts = { year, month, day };
10371037
auto date = duckdb_to_date(date_parts);
10381038
auto result = Napi::Object::New(env);
10391039
result.Set("days", Napi::Number::New(env, date.days));
@@ -1055,28 +1055,67 @@ class DuckDBNodeAddon : public Napi::Addon<DuckDBNodeAddon> {
10551055
// function from_time(time: Time): TimeParts
10561056
Napi::Value from_time(const Napi::CallbackInfo& info) {
10571057
auto env = info.Env();
1058-
throw Napi::Error::New(env, "Not implemented yet");
1058+
auto time_obj = info[0].As<Napi::Object>();
1059+
auto micros = time_obj.Get("micros").As<Napi::Number>().Int64Value();
1060+
duckdb_time time = { micros };
1061+
auto time_parts = duckdb_from_time(time);
1062+
auto result = Napi::Object::New(env);
1063+
result.Set("hour", Napi::Number::New(env, time_parts.hour));
1064+
result.Set("min", Napi::Number::New(env, time_parts.min));
1065+
result.Set("sec", Napi::Number::New(env, time_parts.sec));
1066+
result.Set("micros", Napi::Number::New(env, time_parts.micros));
1067+
return result;
10591068
}
10601069

10611070
// DUCKDB_API duckdb_time_tz duckdb_create_time_tz(int64_t micros, int32_t offset);
10621071
// function create_time_tz(micros: number, offset: number): TimeTZ
10631072
Napi::Value create_time_tz(const Napi::CallbackInfo& info) {
10641073
auto env = info.Env();
1065-
throw Napi::Error::New(env, "Not implemented yet");
1074+
auto micros = info[0].As<Napi::Number>().Int64Value();
1075+
auto offset = info[1].As<Napi::Number>().Int32Value();
1076+
auto time_tz = duckdb_create_time_tz(micros, offset);
1077+
auto result = Napi::Object::New(env);
1078+
result.Set("bits", Napi::BigInt::New(env, time_tz.bits));
1079+
return result;
10661080
}
10671081

10681082
// DUCKDB_API duckdb_time_tz_struct duckdb_from_time_tz(duckdb_time_tz micros);
10691083
// function from_time_tz(time_tz: TimeTZ): TimeTZParts
10701084
Napi::Value from_time_tz(const Napi::CallbackInfo& info) {
10711085
auto env = info.Env();
1072-
throw Napi::Error::New(env, "Not implemented yet");
1086+
auto time_tz_obj = info[0].As<Napi::Object>();
1087+
bool lossless;
1088+
auto bits = time_tz_obj.Get("bits").As<Napi::BigInt>().Uint64Value(&lossless);
1089+
if (!lossless) {
1090+
throw Napi::Error::New(env, "bits out of uint64 range");
1091+
}
1092+
duckdb_time_tz time_tz = { bits };
1093+
auto time_tz_parts = duckdb_from_time_tz(time_tz);
1094+
auto result = Napi::Object::New(env);
1095+
auto time = Napi::Object::New(env);
1096+
time.Set("hour", Napi::Number::New(env, time_tz_parts.time.hour));
1097+
time.Set("min", Napi::Number::New(env, time_tz_parts.time.min));
1098+
time.Set("sec", Napi::Number::New(env, time_tz_parts.time.sec));
1099+
time.Set("micros", Napi::Number::New(env, time_tz_parts.time.micros));
1100+
result.Set("time", time);
1101+
result.Set("offset", Napi::Number::New(env, time_tz_parts.offset));
1102+
return result;
10731103
}
10741104

10751105
// DUCKDB_API duckdb_time duckdb_to_time(duckdb_time_struct time);
10761106
// function to_time(parts: TimeParts): Time
10771107
Napi::Value to_time(const Napi::CallbackInfo& info) {
10781108
auto env = info.Env();
1079-
throw Napi::Error::New(env, "Not implemented yet");
1109+
auto time_parts_obj = info[0].As<Napi::Object>();
1110+
int8_t hour = time_parts_obj.Get("hour").As<Napi::Number>().Int32Value();
1111+
int8_t min = time_parts_obj.Get("min").As<Napi::Number>().Int32Value();
1112+
int8_t sec = time_parts_obj.Get("sec").As<Napi::Number>().Int32Value();
1113+
int32_t micros = time_parts_obj.Get("micros").As<Napi::Number>().Int32Value();
1114+
duckdb_time_struct time_parts = { hour, min, sec, micros };
1115+
auto time = duckdb_to_time(time_parts);
1116+
auto result = Napi::Object::New(env);
1117+
result.Set("micros", Napi::Number::New(env, time.micros));
1118+
return result;
10801119
}
10811120

10821121
// DUCKDB_API duckdb_timestamp_struct duckdb_from_timestamp(duckdb_timestamp ts);

bindings/test/conversion.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,62 @@ suite('conversion', () => {
3636
expect(duckdb.is_finite_date({ days: -0x7FFFFFFF })).toBe(false);
3737
});
3838
});
39+
suite('from_time', () => {
40+
test('mid-range', () => {
41+
// 45296789123 = 1000000 * (60 * (60 * 12 + 34) + 56) + 789123 = 12:34:56.789123
42+
expect(duckdb.from_time({ micros: 45296789123 })).toStrictEqual({ hour: 12, min: 34, sec: 56, micros: 789123 });
43+
});
44+
test('min', () => {
45+
expect(duckdb.from_time({ micros: 0 })).toStrictEqual({ hour: 0, min: 0, sec: 0, micros: 0 });
46+
});
47+
test('max', () => {
48+
// 86400000000 = 1000000 * (60 * (60 * 24 + 0) + 0) + 0 = 24:00:00.000000
49+
expect(duckdb.from_time({ micros: 86400000000 })).toStrictEqual({ hour: 24, min: 0, sec: 0, micros: 0 });
50+
});
51+
});
52+
suite('create_time_tz', () => {
53+
// See datetime.hpp for format of "bits" field. Summary:
54+
// 40 bits for micros, then 24 bits for encoded offset in seconds.
55+
// Max absolute unencoded offset = 15:59:59 = 60 * (60 * 15 + 59) + 59 = 57599.
56+
// Encoded offset is unencoded offset inverted then shifted (by +57599) to unsigned.
57+
// Max unencoded offset = 57599 -> -57599 -> 0 encoded.
58+
// Min unencoded offset = -57599 -> 57599 -> 115198 encoded.
59+
test('mid-range', () => {
60+
// 45296789123 = 1000000 * (60 * (60 * 12 + 34) + 56) + 789123 = 12:34:56.789123
61+
// 759954015223079167n = (45296789123n << 24n) + 57599n
62+
expect(duckdb.create_time_tz(45296789123, 0)).toStrictEqual({ bits: 759954015223079167n });
63+
});
64+
test('min', () => {
65+
expect(duckdb.create_time_tz(0, 57599)).toStrictEqual({ bits: 0n });
66+
});
67+
test('max', () => {
68+
// 1449551462400115198n = (86400000000n << 24n) + 2n * 57599n
69+
expect(duckdb.create_time_tz(86400000000, -57599)).toStrictEqual({ bits: 1449551462400115198n });
70+
});
71+
});
72+
suite('from_time_tz', () => {
73+
test('mid-range', () => {
74+
expect(duckdb.from_time_tz({ bits: 759954015223079167n })).toStrictEqual({ time: { hour: 12, min: 34, sec: 56, micros: 789123 }, offset: 0 });
75+
});
76+
test('min', () => {
77+
expect(duckdb.from_time_tz({ bits: 0n })).toStrictEqual({ time: { hour: 0, min: 0, sec: 0, micros: 0 }, offset: 57599 });
78+
});
79+
test('max', () => {
80+
expect(duckdb.from_time_tz({ bits: 1449551462400115198n })).toStrictEqual({ time: { hour: 24, min: 0, sec: 0, micros: 0 }, offset: -57599 });
81+
});
82+
test('out of range', () => {
83+
expect(() => duckdb.from_time_tz({ bits: 2n ** 64n })).toThrowError('bits out of uint64 range');
84+
});
85+
});
86+
suite('to_time', () => {
87+
test('mid-range', () => {
88+
expect(duckdb.to_time({ hour: 12, min: 34, sec: 56, micros: 789123 })).toStrictEqual({ micros: 45296789123 });
89+
});
90+
test('min', () => {
91+
expect(duckdb.to_time({ hour: 0, min: 0, sec: 0, micros: 0 })).toStrictEqual({ micros: 0 });
92+
});
93+
test('max', () => {
94+
expect(duckdb.to_time({ hour: 24, min: 0, sec: 0, micros: 0 })).toStrictEqual({ micros: 86400000000 });
95+
});
96+
});
3997
});

0 commit comments

Comments
 (0)