Skip to content

Commit

Permalink
time & date conversion
Browse files Browse the repository at this point in the history
  • Loading branch information
jraymakers committed Aug 25, 2024
1 parent 22885cc commit 2c3e5a5
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 7 deletions.
14 changes: 12 additions & 2 deletions bindings/pkgs/@duckdb/node-bindings/duckdb.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,18 @@ export interface TimeParts {
}

export interface TimeTZ {
/** 40 bits for micros, 24 bits for offset */
bits: number; // or bigint, or buffer?
/**
* 40 bits for micros, then 24 bits for encoded offset in seconds.
*
* Max absolute unencoded offset = 15:59:59 = 60 * (60 * 15 + 59) + 59 = 57599.
*
* Encoded offset is unencoded offset inverted then shifted (by +57599) to unsigned.
*
* Max unencoded offset = 57599 -> -57599 -> 0 encoded.
*
* Min unencoded offset = -57599 -> 57599 -> 115198 encoded.
*/
bits: bigint;
}
export interface TimeTZParts {
time: TimeParts;
Expand Down
49 changes: 44 additions & 5 deletions bindings/src/duckdb_node_bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1033,7 +1033,7 @@ class DuckDBNodeAddon : public Napi::Addon<DuckDBNodeAddon> {
int32_t year = date_parts_obj.Get("year").As<Napi::Number>().Int32Value();
int8_t month = date_parts_obj.Get("month").As<Napi::Number>().Int32Value();
int8_t day = date_parts_obj.Get("day").As<Napi::Number>().Int32Value();
duckdb_date_struct date_parts { year, month, day };
duckdb_date_struct date_parts = { year, month, day };
auto date = duckdb_to_date(date_parts);
auto result = Napi::Object::New(env);
result.Set("days", Napi::Number::New(env, date.days));
Expand All @@ -1055,28 +1055,67 @@ class DuckDBNodeAddon : public Napi::Addon<DuckDBNodeAddon> {
// function from_time(time: Time): TimeParts
Napi::Value from_time(const Napi::CallbackInfo& info) {
auto env = info.Env();
throw Napi::Error::New(env, "Not implemented yet");
auto time_obj = info[0].As<Napi::Object>();
auto micros = time_obj.Get("micros").As<Napi::Number>().Int64Value();
duckdb_time time = { micros };
auto time_parts = duckdb_from_time(time);
auto result = Napi::Object::New(env);
result.Set("hour", Napi::Number::New(env, time_parts.hour));
result.Set("min", Napi::Number::New(env, time_parts.min));
result.Set("sec", Napi::Number::New(env, time_parts.sec));
result.Set("micros", Napi::Number::New(env, time_parts.micros));
return result;
}

// DUCKDB_API duckdb_time_tz duckdb_create_time_tz(int64_t micros, int32_t offset);
// function create_time_tz(micros: number, offset: number): TimeTZ
Napi::Value create_time_tz(const Napi::CallbackInfo& info) {
auto env = info.Env();
throw Napi::Error::New(env, "Not implemented yet");
auto micros = info[0].As<Napi::Number>().Int64Value();
auto offset = info[1].As<Napi::Number>().Int32Value();
auto time_tz = duckdb_create_time_tz(micros, offset);
auto result = Napi::Object::New(env);
result.Set("bits", Napi::BigInt::New(env, time_tz.bits));
return result;
}

// DUCKDB_API duckdb_time_tz_struct duckdb_from_time_tz(duckdb_time_tz micros);
// function from_time_tz(time_tz: TimeTZ): TimeTZParts
Napi::Value from_time_tz(const Napi::CallbackInfo& info) {
auto env = info.Env();
throw Napi::Error::New(env, "Not implemented yet");
auto time_tz_obj = info[0].As<Napi::Object>();
bool lossless;
auto bits = time_tz_obj.Get("bits").As<Napi::BigInt>().Uint64Value(&lossless);
if (!lossless) {
throw Napi::Error::New(env, "bits out of uint64 range");
}
duckdb_time_tz time_tz = { bits };
auto time_tz_parts = duckdb_from_time_tz(time_tz);
auto result = Napi::Object::New(env);
auto time = Napi::Object::New(env);
time.Set("hour", Napi::Number::New(env, time_tz_parts.time.hour));
time.Set("min", Napi::Number::New(env, time_tz_parts.time.min));
time.Set("sec", Napi::Number::New(env, time_tz_parts.time.sec));
time.Set("micros", Napi::Number::New(env, time_tz_parts.time.micros));
result.Set("time", time);
result.Set("offset", Napi::Number::New(env, time_tz_parts.offset));
return result;
}

// DUCKDB_API duckdb_time duckdb_to_time(duckdb_time_struct time);
// function to_time(parts: TimeParts): Time
Napi::Value to_time(const Napi::CallbackInfo& info) {
auto env = info.Env();
throw Napi::Error::New(env, "Not implemented yet");
auto time_parts_obj = info[0].As<Napi::Object>();
int8_t hour = time_parts_obj.Get("hour").As<Napi::Number>().Int32Value();
int8_t min = time_parts_obj.Get("min").As<Napi::Number>().Int32Value();
int8_t sec = time_parts_obj.Get("sec").As<Napi::Number>().Int32Value();
int32_t micros = time_parts_obj.Get("micros").As<Napi::Number>().Int32Value();
duckdb_time_struct time_parts = { hour, min, sec, micros };
auto time = duckdb_to_time(time_parts);
auto result = Napi::Object::New(env);
result.Set("micros", Napi::Number::New(env, time.micros));
return result;
}

// DUCKDB_API duckdb_timestamp_struct duckdb_from_timestamp(duckdb_timestamp ts);
Expand Down
58 changes: 58 additions & 0 deletions bindings/test/conversion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,62 @@ suite('conversion', () => {
expect(duckdb.is_finite_date({ days: -0x7FFFFFFF })).toBe(false);
});
});
suite('from_time', () => {
test('mid-range', () => {
// 45296789123 = 1000000 * (60 * (60 * 12 + 34) + 56) + 789123 = 12:34:56.789123
expect(duckdb.from_time({ micros: 45296789123 })).toStrictEqual({ hour: 12, min: 34, sec: 56, micros: 789123 });
});
test('min', () => {
expect(duckdb.from_time({ micros: 0 })).toStrictEqual({ hour: 0, min: 0, sec: 0, micros: 0 });
});
test('max', () => {
// 86400000000 = 1000000 * (60 * (60 * 24 + 0) + 0) + 0 = 24:00:00.000000
expect(duckdb.from_time({ micros: 86400000000 })).toStrictEqual({ hour: 24, min: 0, sec: 0, micros: 0 });
});
});
suite('create_time_tz', () => {
// See datetime.hpp for format of "bits" field. Summary:
// 40 bits for micros, then 24 bits for encoded offset in seconds.
// Max absolute unencoded offset = 15:59:59 = 60 * (60 * 15 + 59) + 59 = 57599.
// Encoded offset is unencoded offset inverted then shifted (by +57599) to unsigned.
// Max unencoded offset = 57599 -> -57599 -> 0 encoded.
// Min unencoded offset = -57599 -> 57599 -> 115198 encoded.
test('mid-range', () => {
// 45296789123 = 1000000 * (60 * (60 * 12 + 34) + 56) + 789123 = 12:34:56.789123
// 759954015223079167n = (45296789123n << 24n) + 57599n
expect(duckdb.create_time_tz(45296789123, 0)).toStrictEqual({ bits: 759954015223079167n });
});
test('min', () => {
expect(duckdb.create_time_tz(0, 57599)).toStrictEqual({ bits: 0n });
});
test('max', () => {
// 1449551462400115198n = (86400000000n << 24n) + 2n * 57599n
expect(duckdb.create_time_tz(86400000000, -57599)).toStrictEqual({ bits: 1449551462400115198n });
});
});
suite('from_time_tz', () => {
test('mid-range', () => {
expect(duckdb.from_time_tz({ bits: 759954015223079167n })).toStrictEqual({ time: { hour: 12, min: 34, sec: 56, micros: 789123 }, offset: 0 });
});
test('min', () => {
expect(duckdb.from_time_tz({ bits: 0n })).toStrictEqual({ time: { hour: 0, min: 0, sec: 0, micros: 0 }, offset: 57599 });
});
test('max', () => {
expect(duckdb.from_time_tz({ bits: 1449551462400115198n })).toStrictEqual({ time: { hour: 24, min: 0, sec: 0, micros: 0 }, offset: -57599 });
});
test('out of range', () => {
expect(() => duckdb.from_time_tz({ bits: 2n ** 64n })).toThrowError('bits out of uint64 range');
});
});
suite('to_time', () => {
test('mid-range', () => {
expect(duckdb.to_time({ hour: 12, min: 34, sec: 56, micros: 789123 })).toStrictEqual({ micros: 45296789123 });
});
test('min', () => {
expect(duckdb.to_time({ hour: 0, min: 0, sec: 0, micros: 0 })).toStrictEqual({ micros: 0 });
});
test('max', () => {
expect(duckdb.to_time({ hour: 24, min: 0, sec: 0, micros: 0 })).toStrictEqual({ micros: 86400000000 });
});
});
});

0 comments on commit 2c3e5a5

Please sign in to comment.