From b2a82260076e1304043c8abf464248b0a13eb847 Mon Sep 17 00:00:00 2001 From: Anton Date: Wed, 7 Aug 2024 23:56:18 +0500 Subject: [PATCH] time: new extension --- .github/workflows/build.yml | 1 + Makefile | 11 + docs/time.md | 912 +++++++++++++++++++++++++++++++++++ src/sqlite3-time.c | 26 + src/time/duration.c | 117 +++++ src/time/extension.c | 798 +++++++++++++++++++++++++++++++ src/time/extension.h | 13 + src/time/time.c | 876 ++++++++++++++++++++++++++++++++++ src/time/timex.h | 270 +++++++++++ test/time.sql | 284 +++++++++++ test/time/duration.test.c | 249 ++++++++++ test/time/time.test.c | 929 ++++++++++++++++++++++++++++++++++++ 12 files changed, 4486 insertions(+) create mode 100644 docs/time.md create mode 100644 src/sqlite3-time.c create mode 100644 src/time/duration.c create mode 100644 src/time/extension.c create mode 100644 src/time/extension.h create mode 100644 src/time/time.c create mode 100644 src/time/timex.h create mode 100644 test/time.sql create mode 100644 test/time/duration.test.c create mode 100644 test/time/time.test.c diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3bd4f08e..26923ac1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,6 +42,7 @@ jobs: if: matrix.os == 'ubuntu-20.04' run: | make compile-linux + make ctest-all make test-all - name: Build for Windows diff --git a/Makefile b/Makefile index 3b840323..8ac8babb 100644 --- a/Makefile +++ b/Makefile @@ -52,6 +52,7 @@ compile-linux-x86: gcc -O3 $(LINIX_FLAGS) -include src/regexp/constants.h src/sqlite3-regexp.c src/regexp/*.c src/regexp/pcre2/*.c -o dist/x86/regexp.so gcc -O3 $(LINIX_FLAGS) src/sqlite3-stats.c src/stats/*.c -o dist/x86/stats.so -lm gcc -O3 $(LINIX_FLAGS) src/sqlite3-text.c src/text/*.c src/text/*/*.c -o dist/x86/text.so + gcc -O3 $(LINIX_FLAGS) src/sqlite3-time.c src/time/*.c -o dist/x86/time.so gcc -O3 $(LINIX_FLAGS) src/sqlite3-unicode.c src/unicode/*.c -o dist/x86/unicode.so gcc -O3 $(LINIX_FLAGS) src/sqlite3-uuid.c src/uuid/*.c -o dist/x86/uuid.so gcc -O3 $(LINIX_FLAGS) src/sqlite3-vsv.c src/vsv/*.c -o dist/x86/vsv.so -lm @@ -68,6 +69,7 @@ compile-linux-arm64: aarch64-linux-gnu-gcc -O3 $(LINIX_FLAGS) -include src/regexp/constants.h src/sqlite3-regexp.c src/regexp/*.c src/regexp/pcre2/*.c -o dist/arm64/regexp.so aarch64-linux-gnu-gcc -O3 $(LINIX_FLAGS) src/sqlite3-stats.c src/stats/*.c -o dist/arm64/stats.so -lm aarch64-linux-gnu-gcc -O3 $(LINIX_FLAGS) src/sqlite3-text.c src/text/*.c src/text/*/*.c -o dist/arm64/text.so + aarch64-linux-gnu-gcc -O3 $(LINIX_FLAGS) src/sqlite3-time.c src/time/*.c -o dist/arm64/time.so aarch64-linux-gnu-gcc -O3 $(LINIX_FLAGS) src/sqlite3-unicode.c src/unicode/*.c -o dist/arm64/unicode.so aarch64-linux-gnu-gcc -O3 $(LINIX_FLAGS) src/sqlite3-uuid.c src/uuid/*.c -o dist/arm64/uuid.so aarch64-linux-gnu-gcc -O3 $(LINIX_FLAGS) src/sqlite3-vsv.c src/vsv/*.c -o dist/arm64/vsv.so -lm @@ -86,6 +88,7 @@ compile-windows: gcc -O3 $(WINDO_FLAGS) src/sqlite3-regexp.c -include src/regexp/constants.h src/regexp/*.c src/regexp/pcre2/*.c -o dist/regexp.dll gcc -O3 $(WINDO_FLAGS) src/sqlite3-stats.c src/stats/*.c -o dist/stats.dll -lm gcc -O3 $(WINDO_FLAGS) src/sqlite3-text.c src/text/*.c src/text/*/*.c -o dist/text.dll + gcc -O3 $(WINDO_FLAGS) src/sqlite3-time.c src/time/*.c -o dist/time.dll gcc -O3 $(WINDO_FLAGS) src/sqlite3-unicode.c src/unicode/*.c -o dist/unicode.dll gcc -O3 $(WINDO_FLAGS) src/sqlite3-uuid.c src/uuid/*.c -o dist/uuid.dll gcc -O3 $(WINDO_FLAGS) src/sqlite3-vsv.c src/vsv/*.c -o dist/vsv.dll -lm @@ -120,6 +123,7 @@ compile-macos: gcc -O3 $(MACOS_FLAGS) -include src/regexp/constants.h src/sqlite3-regexp.c src/regexp/*.c src/regexp/pcre2/*.c -o dist/regexp.dylib gcc -O3 $(MACOS_FLAGS) src/sqlite3-stats.c src/stats/*.c -o dist/stats.dylib -lm gcc -O3 $(MACOS_FLAGS) src/sqlite3-text.c src/text/*.c src/text/*/*.c -o dist/text.dylib + gcc -O3 $(MACOS_FLAGS) src/sqlite3-time.c src/time/*.c -o dist/time.dylib gcc -O3 $(MACOS_FLAGS) src/sqlite3-unicode.c src/unicode/*.c -o dist/unicode.dylib gcc -O3 $(MACOS_FLAGS) src/sqlite3-uuid.c src/uuid/*.c -o dist/uuid.dylib gcc -O3 $(MACOS_FLAGS) src/sqlite3-vsv.c src/vsv/*.c -o dist/vsv.dylib -lm @@ -136,6 +140,7 @@ compile-macos-x86: gcc -O3 $(MACOS_FLAGS) -include src/regexp/constants.h src/sqlite3-regexp.c src/regexp/*.c src/regexp/pcre2/*.c -o dist/x86/regexp.dylib -target x86_64-apple-macos10.12 gcc -O3 $(MACOS_FLAGS) src/sqlite3-stats.c src/stats/*.c -o dist/x86/stats.dylib -target x86_64-apple-macos10.12 -lm gcc -O3 $(MACOS_FLAGS) src/sqlite3-text.c src/text/*.c src/text/*/*.c -o dist/x86/text.dylib -target x86_64-apple-macos10.12 + gcc -O3 $(MACOS_FLAGS) src/sqlite3-time.c src/time/*.c -o dist/x86/time.dylib -target x86_64-apple-macos10.12 gcc -O3 $(MACOS_FLAGS) src/sqlite3-unicode.c src/unicode/*.c -o dist/x86/unicode.dylib -target x86_64-apple-macos10.12 gcc -O3 $(MACOS_FLAGS) src/sqlite3-uuid.c src/uuid/*.c -o dist/x86/uuid.dylib -target x86_64-apple-macos10.12 gcc -O3 $(MACOS_FLAGS) src/sqlite3-vsv.c src/vsv/*.c -o dist/x86/vsv.dylib -target x86_64-apple-macos10.12 -lm @@ -152,6 +157,7 @@ compile-macos-arm64: gcc -O3 $(MACOS_FLAGS) -include src/regexp/constants.h src/sqlite3-regexp.c src/regexp/*.c src/regexp/pcre2/*.c -o dist/arm64/regexp.dylib -target arm64-apple-macos11 gcc -O3 $(MACOS_FLAGS) src/sqlite3-stats.c src/stats/*.c -o dist/arm64/stats.dylib -target arm64-apple-macos11 -lm gcc -O3 $(MACOS_FLAGS) src/sqlite3-text.c src/text/*.c src/text/*/*.c -o dist/arm64/text.dylib -target arm64-apple-macos11 + gcc -O3 $(MACOS_FLAGS) src/sqlite3-time.c src/time/*.c -o dist/arm64/time.dylib -target arm64-apple-macos11 gcc -O3 $(MACOS_FLAGS) src/sqlite3-unicode.c src/unicode/*.c -o dist/arm64/unicode.dylib -target arm64-apple-macos11 gcc -O3 $(MACOS_FLAGS) src/sqlite3-uuid.c src/uuid/*.c -o dist/arm64/uuid.dylib -target arm64-apple-macos11 gcc -O3 $(MACOS_FLAGS) src/sqlite3-vsv.c src/vsv/*.c -o dist/arm64/vsv.dylib -target arm64-apple-macos11 -lm @@ -170,6 +176,7 @@ test-all: make test suite=math make test suite=regexp make test suite=stats + make test suite=time make test suite=text make test suite=unicode make test suite=uuid @@ -189,6 +196,10 @@ ctest-all: make ctest package=text module=rstring gcc -Wall -Isrc test/text/utf8.test.c src/text/utf8/*.c -o text.utf8 make ctest package=text module=utf8 + gcc -Wall -Isrc test/time/time.test.c src/time/*.c -o time.time + make ctest package=time module=time + gcc -Wall -Isrc test/time/duration.test.c src/time/*.c -o time.duration + make ctest package=time module=duration ctest: @chmod +x $(package).$(module) diff --git a/docs/time.md b/docs/time.md new file mode 100644 index 00000000..a3c9b829 --- /dev/null +++ b/docs/time.md @@ -0,0 +1,912 @@ +# time: High-precision date/time in SQLite + +The `sqlean-time` extension provides functionality for working with time and duration with nanosecond precision. + +[Concepts](#concepts) • +[Creating values](#creating-time-values) • +[Extracting fields](#extracting-time-fields) • +[Unix time](#unix-time) • +[Time comparison](#comparing-time) • +[Time arithmetic](#time-arithmetic) • +[Rounding](#rounding) • +[Formatting](#formatting) • +[Acknowledgements](#acknowledgements) • +[Installation and usage](#installation-and-usage) + +## Concepts + +This extension works with two types of values: Time and Duration. + +```text + Time + + since within + 0-time second +┌─────────┬─────────────┐ +│ seconds │ nanoseconds │ +└─────────┴─────────────┘ + 64 bit 32 bit +``` + +Time is a pair (seconds, nanoseconds), where `seconds` is the 64-bit number of seconds since zero time (0001-01-01 00:00:00 UTC) and `nanoseconds` is the number of nanoseconds within the current second (0-999999999). + +For maximum flexibility, you can store time values in their internal representation (a 13-byte BLOB). This allows you to represent dates for billions of years in the past and future with nanosecond precision. + +Alternatively, you can store time values as a NUMBER (64-bit integer) of seconds (milli-, micro- or nanoseconds) since the Unix epoch (1970-01-01 00:00:00 UTC). In this case, the range of representable dates depends on the unit of time used: + +- Seconds: billions of years into the past or future with second precision. +- Milliseconds: 292 million years before or after 1970 with millisecond precision. +- Microseconds: years from -290307 to 294246 with microsecond precision. +- Nanoseconds: years from 1678 to 2262 with nanosecond precision. + +Time is always stored and operated in UTC, but you can convert it from/to a specific timezone. + +```text + Duration +┌─────────────┐ +│ nanoseconds │ +└─────────────┘ + 64 bit +``` + +Duration is a 64-bit number of nanoseconds, so it can represent values up to about 290 years. You can store duration values as NUMBER. + +The calendrical calculations always assume a Gregorian calendar, with no leap seconds. + +## Creating time values + +There are two basic constructors — one for the current time and one for a specific date/time. + +### time_now + +```text +time_now() +``` + +Returns the current time in UTC. + +```sql +select time_fmt_iso(time_now()); +-- 2024-08-06T21:22:15.431295000Z +``` + +Aliased as Postgres-like `now()`. + +### time_date + +```text +time_date(year, month, day[, hour, min, sec[, nsec[, offset_sec]]]) +``` + +Returns the Time corresponding to a given date/time. The time part (hour+minute+second), the nanosecond part, and the timezone offset part are all optional. + +The `month`, `day`, `hour`, `min`, `sec`, and `nsec` values may be outside their usual ranges and will be normalized during the conversion. For example, October 32 converts to November 1. + +If `offset_sec` is not 0, the source time is treated as being in a given timezone (with an offset in seconds east of UTC) and converted back to UTC. + +```sql +select time_fmt_iso(time_date(2011, 11, 18)); +-- 2011-11-18T00:00:00Z + +select time_fmt_iso(time_date(2011, 11, 18, 15, 56, 35)); +-- 2011-11-18T15:56:35Z + +select time_fmt_iso(time_date(2011, 11, 18, 15, 56, 35, 666777888)); +-- 2011-11-18T15:56:35.666777888Z + +select time_fmt_iso(time_date(2011, 11, 18, 15, 56, 35, 0, 3*3600)); +-- 2011-11-18T12:56:35Z + +select time_fmt_iso(time_date(2011, 11, 18, 15, 56, 35, 666777888, 3*3600)); +-- 2011-11-18T12:56:35.666777888Z +``` + +Aliased as Postgres-like `make_date` and `make_timestamp`: + +```text +make_date(year, month, day) +make_timestamp(year, month, day, hour, min, sec) +``` + +## Extracting time fields + +There are a number of functions for extracting different time fields, and a generic function for extracting time fields by name. + +### time_get_year + +```text +time_get_year(t) +``` + +Returns the year in which t occurs. + +```sql +select time_get_year(time_now()); +-- 2024 +``` + +### time_get_month + +```text +time_get_month(t) +``` + +Returns the month of the year specified by t (1-12). + +```sql +select time_get_month(time_now()); +-- 8 +``` + +### time_get_day + +```text +time_get_day(t) +``` + +Returns the day of the month specified by t (1-31). + +```sql +select time_get_day(time_now()); +-- 6 +``` + +### time_get_hour + +```text +time_get_hour(t) +``` + +Returns the hour within the day specified by t (0-23). + +```sql +select time_get_hour(time_now()); +-- 21 +``` + +### time_get_minute + +```text +time_get_minute(t) +``` + +Returns the minute offset within the hour specified by t. + +```sql +select time_get_minute(time_now()); +-- 22 +``` + +### time_get_second + +```text +time_get_second(t) +``` + +Returns the second offset within the minute specified by t (0-59). + +```sql +select time_get_second(time_now()); +-- 15 +``` + +### time_get_nano + +```text +time_get_nano(t) +``` + +Returns the nanosecond offset within the second specified by t (0-999999999). + +```sql +select time_get_nano(time_now()); +-- 431295000 +``` + +### time_get_weekday + +```text +time_get_weekday(t) +``` + +Returns the day of the week specified by t (0-6, Sunday = 0). + +```sql +select time_get_weekday(time_now()); +-- 2 +``` + +### time_get_yearday + +```text +time_get_yearday(t) +``` + +Returns the day of the year specified by t (1-366). + +```sql +select time_get_yearday(time_now()); +-- 219 +``` + +### time_get_isoyear + +```text +time_get_isoyear(t) +``` + +Returns the ISO 8601 year in which t occurs. + +```sql +select time_get_isoyear(time_now()); +-- 2024 +``` + +### time_get_isoweek + +```text +time_get_isoweek(t) +``` + +Returns the ISO 8601 week number of the year specified by t (1-53). + +```sql +select time_get_isoweek(time_now()); +-- 32 +``` + +### time_get + +```text +time_get(t, field) +``` + +Returns the value of a given field from a time value specified by t. + +Supported fields: + +```text +millennium hour isoyear +century minute isoweek +decade second isodow +year milli[second] yearday +quarter micro[second] weekday +month nano[second] epoch +day +``` + +```sql +select time_get(time_now(), 'millennium'); +-- 2 +select time_get(time_now(), 'century'); +-- 20 +select time_get(time_now(), 'decade'); +-- 202 + +select time_get(time_now(), 'year'); +-- 2024 +select time_get(time_now(), 'quarter'); +-- 3 +select time_get(time_now(), 'month'); +-- 8 +select time_get(time_now(), 'day'); +-- 6 + +select time_get(time_now(), 'hour'); +-- 21 +select time_get(time_now(), 'minute'); +-- 22 +select time_get(time_now(), 'second'); +-- 15.431295 + +select time_get(time_now(), 'milli'); +-- 431 +select time_get(time_now(), 'micro'); +-- 431295 +select time_get(time_now(), 'nano'); +-- 431295000 + +select time_get(time_now(), 'isoyear'); +-- 2024 +select time_get(time_now(), 'isoweek'); +-- 32 +select time_get(time_now(), 'isodow'); +-- 2 + +select time_get(time_now(), 'yearday'); +-- 219 +select time_get(time_now(), 'weekday'); +-- 2 + +select time_get(time_now(), 'epoch'); +-- 1722979335.43129 +``` + +Aliased as Postgres-like `date_part(field, t)`. + +## Unix time + +These are functions for converting time values to/from Unix time (time since the Unix epoch - January 1, 1970 UTC). + +### time_unix + +```text +time_unix(sec[, nsec]) +``` + +Returns the Time corresponding to the given Unix time, `sec` seconds and `nsec` nanoseconds since January 1, 1970 UTC. + +```sql +select time_fmt_iso(time_unix(1321631795)); +-- 2011-11-18T15:56:35Z +select time_fmt_iso(time_unix(1321631795, 666777888)); +-- 2011-11-18T15:56:35.666777888Z +``` + +Aliased as Postgres-like `to_timestamp(sec)`. + +### time_milli + +```text +time_milli(msec) +``` + +Returns the Time corresponding to the given Unix time, `msec` milliseconds since January 1, 1970 UTC. + +```sql +select time_fmt_iso(time_milli(1321631795666)); +-- 2011-11-18T15:56:35.666000000Z +``` + +### time_micro + +```text +time_micro(usec) +``` + +Returns the Time corresponding to the given Unix time, `usec` microseconds since January 1, 1970 UTC. + +```sql +select time_fmt_iso(time_micro(1321631795666777)); +-- 2011-11-18T15:56:35.666777000Z +``` + +### time_nano + +```text +time_nano(nsec) +``` + +Returns the Time corresponding to the given Unix time, `nsec` nanoseconds since January 1, 1970 UTC. + +```sql +select time_fmt_iso(time_nano(1321631795666777888)); +-- 2011-11-18T15:56:35.666777888Z +``` + +### time_to_unix + +```text +time_to_unix(t) +``` + +Returns t as a Unix time, the number of seconds elapsed since January 1, 1970 UTC. + +Unix-like operating systems often record time as a 32-bit number of seconds, but since `time_to_unix` returns a 64-bit value, it is valid for billions of years into the past or future. + +```sql +select time_to_unix(time_now())); +-- 1722979335 +``` + +### time_to_milli + +```text +time_to_milli(t) +``` + +Returns t as a Unix time, the number of milliseconds elapsed since January 1, 1970 UTC. + +The result is undefined if the Unix time in milliseconds cannot be represented by a 64-bit integer (a date more than 292 million years before or after 1970). + +```sql +select time_to_milli(time_now())); +-- 1722979335431 +``` + +### time_to_micro + +```text +time_to_micro(t) +``` + +Returns t as a Unix time, the number of microseconds elapsed since January 1, 1970 UTC. + +The result is undefined if the Unix time in microseconds cannot be represented by a 64-bit integer (a date before year -290307 or after year 294246). + +```sql +select time_to_micro(time_now())); +-- 1722979335431295 +``` + +### time_to_nano + +```text +time_to_nano(t) +``` + +Returns t as a Unix time, the number of nanoseconds elapsed since January 1, 1970 UTC. + +The result is undefined if the Unix time in nanoseconds cannot be represented by a 64-bit integer (a date before the year 1678 or after 2262). + +```sql +select time_to_nano(time_now())); +-- 1722979335431295000 +``` + +## Time comparison + +These are functions for comparing time values. + +### time_after + +```text +time_after(t, u) +``` + +Reports whether the time instant t is after u. + +```sql +select time_after(time_now(), time_date(2011, 11, 18)); +-- 1 +``` + +### time_before + +```text +time_before(t, u) +``` + +Reports whether the time instant t is before u. + +```sql +select time_before(time_now(), time_date(2011, 11, 18)); +-- 0 +``` + +### time_compare + +```text +time_compare(t, u) +``` + +Compares the time instant t with u: + +- if t is before u, it returns -1; +- if t is after u, it returns +1; +- if they're the same, it returns 0. + +```sql +select time_compare(time_now(), time_date(2011, 11, 18)); +-- 1 +select time_compare(time_date(2011, 11, 18), time_now()); +-- -1 +select time_compare(time_date(2011, 11, 18), time_date(2011, 11, 18)); +-- 0 +``` + +### time_equal + +```text +time_before(t, u) +``` + +Reports whether t and u represent the same time instant. + +```sql +select time_equal(time_now(), time_date(2011, 11, 18)); +-- 0 +select time_equal(time_date(2011, 11, 18), time_date(2011, 11, 18)); +-- 1 +``` + +## Time arithmetic + +These are functions for adding time and duration values, and functions for subtracting time values. + +### time_add + +```text +time_add(t, d) +``` + +Returns the time t plus the duration d. Use negative d to subtract duration. + +You can use the following duration constants: + +- `dur_us()` - 1 microsecond; +- `dur_ms()` - 1 millisecond; +- `dur_s()` - 1 second; +- `dur_m()` - 1 minute; +- `dur_h()` - 1 hour. + +```sql +select time_fmt_iso(time_add(time_now(), 24*dur_h())); +-- 2024-08-07T21:22:15.431295000Z + +select time_fmt_iso(time_add(time_now(), 60*dur_m())); +-- 2024-08-06T22:22:15.431295000Z + +select time_fmt_iso(time_add(time_now(), 5*dur_m()+30*dur_s())); +-- 2024-08-06T21:27:45.431295000Z +``` + +Do not use `time_add` to add days, months or years. Use `time_add_date` instead. + +Aliased as Postgres-like `date_add(t, d)`. + +### time_add_date + +```text +time_add_date(t, years[, months[, days]]) +``` + +Returns the time corresponding to adding the given number of years, months, and days to t. +For example, `time_add_date(-1, 2, 3)` applied to January 1, 2011 returns March 4, 2010. + +Normalizes its result in the same way that `time_date` does, so, for example, adding one month to October 31 yields December 1, the normalized form for November 31. + +Use negative values to subtract years, months, and days. + +```sql +select time_fmt_date(time_add_date(time_date(2011, 11, 18), 5)); +-- 2016-11-18 +select time_fmt_date(time_add_date(time_date(2011, 11, 18), 3, 5)); +-- 2015-04-18 +select time_fmt_date(time_add_date(time_date(2011, 11, 18), 3, 5, -10)); +-- 2015-04-08 +``` + +### time_sub + +```text +time_sub(t, u) +``` + +Returns the duration between two time values t and u (in nanoseconds). If the result exceeds the maximum (or minimum) value that can be stored in a Duration, the maximum (or minimum) duration will be returned. + +```sql +select time_sub(time_date(2011, 11, 19), time_date(2011, 11, 18)); +-- 86400000000000 + +select time_sub( + time_date(2011, 11, 18, 16, 56, 35), + time_date(2011, 11, 18, 15, 56, 35) +); +-- 3600000000000 + +select time_sub(time_unix(1321631795, 5000000), time_unix(1321631795, 0)); +-- 5000000 +``` + +Aliased as Postgres-like `age(t, u)`. + +### time_since + +```text +time_since(t) +``` + +Returns the time elapsed since t (in nanoseconds). It is shorthand for `time_sub(time_now(), t)`. + +```sql +select time_since(time_now()); +-- 5000 +``` + +### time_until + +```text +time_until(t) +``` + +Returns the duration until t (in nanoseconds). It is shorthand for `time_sub(t, time_now())`. + +```sql +select time_until(time_date(2024, 9, 1)); +-- 2144479530297000 +``` + +## Rounding + +These are functions for truncating and rounding the time. + +### time_trunc + +```text +time_trunc(t, field) +time_trunc(t, d) +``` + +Truncates t to a precision specified by field, or rounds down t to a multiple of the duration d. + +Supported fields: + +```text +millennium hour +century minute +decade second +year milli[second] +quarter micro[second] +month +week +day +``` + +```sql +with t as ( + select time_date(2011, 11, 18, 15, 56, 35, 666777888) as v +) +select 'original = ' || time_fmt_iso(t.v) from t union all +select 'millennium = ' || time_fmt_iso(time_trunc(t.v, 'millennium')) from t union all +select 'century = ' || time_fmt_iso(time_trunc(t.v, 'century')) from t union all +select 'decade = ' || time_fmt_iso(time_trunc(t.v, 'decade')) from t union all +select 'year = ' || time_fmt_iso(time_trunc(t.v, 'year')) from t union all +select 'quarter = ' || time_fmt_iso(time_trunc(t.v, 'quarter')) from t union all +select 'month = ' || time_fmt_iso(time_trunc(t.v, 'month')) from t union all +select 'week = ' || time_fmt_iso(time_trunc(t.v, 'week')) from t union all +select 'day = ' || time_fmt_iso(time_trunc(t.v, 'day')) from t union all +select 'hour = ' || time_fmt_iso(time_trunc(t.v, 'hour')) from t union all +select 'minute = ' || time_fmt_iso(time_trunc(t.v, 'minute')) from t union all +select 'second = ' || time_fmt_iso(time_trunc(t.v, 'second')) from t union all +select 'milli = ' || time_fmt_iso(time_trunc(t.v, 'milli')) from t union all +select 'micro = ' || time_fmt_iso(time_trunc(t.v, 'micro')) from t; +``` + +```text +original = 2011-11-18T15:56:35.666777888Z +millennium = 2000-01-01T00:00:00Z +century = 2000-01-01T00:00:00Z +decade = 2010-01-01T00:00:00Z +year = 2011-01-01T00:00:00Z +quarter = 2011-10-01T00:00:00Z +month = 2011-11-01T00:00:00Z +week = 2011-11-12T00:00:00Z +day = 2011-11-18T00:00:00Z +hour = 2011-11-18T15:00:00Z +minute = 2011-11-18T15:56:00Z +second = 2011-11-18T15:56:35Z +milli = 2011-11-18T15:56:35.666000000Z +micro = 2011-11-18T15:56:35.666777000Z +``` + +Supported durations: any duration that is a multiple of 1 second. If d <= 0, returns t unchanged. + +```sql +with t as ( + select time_date(2011, 11, 18, 15, 56, 35, 666777888) as v +) +select 't = ' || time_fmt_iso(t.v) from t union all +select '12h = ' || time_fmt_iso(time_trunc(t.v, 12*dur_h())) from t union all +select '1h = ' || time_fmt_iso(time_trunc(t.v, dur_h())) from t union all +select '30m = ' || time_fmt_iso(time_trunc(t.v, 30*dur_m())) from t union all +select '1m = ' || time_fmt_iso(time_trunc(t.v, dur_m())) from t union all +select '30s = ' || time_fmt_iso(time_trunc(t.v, 30*dur_s())) from t union all +select '1s = ' || time_fmt_iso(time_trunc(t.v, dur_s())) from t; +``` + +```text +t = 2011-11-18T15:56:35.666777888Z +12h = 2011-11-18T12:00:00Z +1h = 2011-11-18T15:00:00Z +30m = 2011-11-18T15:30:00Z +1m = 2011-11-18T15:56:00Z +30s = 2011-11-18T15:56:30Z +1s = 2011-11-18T15:56:35Z +``` + +Aliased as Postgres-like `date_trunc(field, t)`. + +### time_round + +```text +time_round(t, d) +``` + +Rounds t to the nearest multiple of the duration d. + +Supports any duration that is a multiple of 1 second. The rounding behavior for halfway values is to round up. If d <= 0, returns t unchanged. + +```sql +with t as ( + select time_date(2011, 11, 18, 15, 56, 35, 666777888) as v +) +select 't = ' || time_fmt_iso(t.v) from t union all +select '12h = ' || time_fmt_iso(time_round(t.v, 12*dur_h())) from t union all +select '1h = ' || time_fmt_iso(time_round(t.v, dur_h())) from t union all +select '30m = ' || time_fmt_iso(time_round(t.v, 30*dur_m())) from t union all +select '1m = ' || time_fmt_iso(time_round(t.v, dur_m())) from t union all +select '30s = ' || time_fmt_iso(time_round(t.v, 30*dur_s())) from t union all +select '1s = ' || time_fmt_iso(time_round(t.v, dur_s())) from t; +``` + +```text +t = 2011-11-18T15:56:35.666777888Z +12h = 2011-11-18T12:00:00Z +1h = 2011-11-18T16:00:00Z +30m = 2011-11-18T16:00:00Z +1m = 2011-11-18T15:57:00Z +30s = 2011-11-18T15:56:30Z +1s = 2011-11-18T15:56:36Z +``` + +## Formatting + +These are functions for formatting and parsing the time. + +### time_fmt_iso + +```text +time_fmt_iso(t[, offset_sec]) +``` + +Returns an ISO 8601 time string for the given time value. Optionally converts the time value to the given timezone offset before formatting. + +Chooses the most compact representation: + +```text +2006-01-02T15:04:05.999999999+07:00 +2006-01-02T15:04:05.999999999Z +2006-01-02T15:04:05+07:00 +2006-01-02T15:04:05Z +``` + +```sql +select time_fmt_iso(time_date(2011, 11, 18, 15, 56, 35, 666777888), 3*3600); +-- 2011-11-18T18:56:35.666777888+03:00 + +select time_fmt_iso(time_date(2011, 11, 18, 15, 56, 35, 666777888)); +-- 2011-11-18T15:56:35.666777888Z + +select time_fmt_iso(time_date(2011, 11, 18, 15, 56, 35), 3*3600); +-- 2011-11-18T18:56:35+03:00 + +select time_fmt_iso(time_date(2011, 11, 18, 15, 56, 35)); +-- 2011-11-18T15:56:35Z +``` + +### time_fmt_datetime + +```text +time_fmt_datetime(t[, offset_sec]) +``` + +Returns a datetime string (`2006-01-02 15:04:05`) for the given time value. Optionally converts the time value to the given timezone offset before formatting. + +```sql +select time_fmt_datetime(time_date(2011, 11, 18, 15, 56, 35), 3*3600); +-- 2011-11-18 18:56:35 + +select time_fmt_datetime(time_date(2011, 11, 18, 15, 56, 35)); +-- 2011-11-18 15:56:35 + +select time_fmt_datetime(time_date(2011, 11, 18)); +-- 2011-11-18 00:00:00 +``` + +### time_fmt_date + +```text +time_fmt_date(t[, offset_sec]) +``` + +Returns a date string (`2006-01-02`) for the given time value. Optionally converts the time value to the given timezone offset before formatting. + +```sql +select time_fmt_date(time_date(2011, 11, 18, 15, 56, 35), 12*3600); +-- 2011-11-19 + +select time_fmt_date(time_date(2011, 11, 18, 15, 56, 35)); +-- 2011-11-18 + +select time_fmt_date(time_date(2011, 11, 18)); +-- 2011-11-18 +``` + +### time_fmt_time + +```text +time_fmt_time(t[, offset_sec]) +``` + +Returns a time string (`15:04:05`) for the given time value. Optionally converts the time value to the given timezone offset before formatting. + +```sql +select time_fmt_time(time_date(2011, 11, 18, 15, 56, 35), 3*3600); +-- 18:56:35 + +select time_fmt_time(time_date(2011, 11, 18, 15, 56, 35)); +-- 15:56:35 + +select time_fmt_time(time_date(2011, 11, 18)); +-- 00:00:00 +``` + +### time_parse + +```text +time_parse(s) +``` + +Parses a formatted string and returns the time value it represents. + +Supports a limited set of layouts: + +```text +2006-01-02T15:04:05.999999999+07:00 ISO 8601 with nanoseconds and timezone +2006-01-02T15:04:05.999999999Z ISO 8601 with nanoseconds, UTC +2006-01-02T15:04:05+07:00 ISO 8601 with timezone +2006-01-02T15:04:05Z ISO 8601, UTC +2006-01-02 15:04:05 Date and time, UTC +2006-01-02 Date only, UTC +15:04:05 Time only, UTC +``` + +```sql +select time_parse('2011-11-18T15:56:35.666777888Z') = time_unix(1321631795, 666777888); +select time_parse('2011-11-18T19:26:35.666777888+03:30') = time_unix(1321631795, 666777888); +select time_parse('2011-11-18T12:26:35.666777888-03:30') = time_unix(1321631795, 666777888); +select time_parse('2011-11-18T15:56:35Z') = time_unix(1321631795, 0); +select time_parse('2011-11-18T19:26:35+03:30') = time_unix(1321631795, 0); +select time_parse('2011-11-18T12:26:35-03:30') = time_unix(1321631795, 0); +select time_parse('2011-11-18 15:56:35') = time_unix(1321631795, 0); +select time_parse('2011-11-18') = time_date(2011, 11, 18); +select time_parse('15:56:35') = time_date(1, 1, 1, 15, 56, 35); +``` + +## Duration constants + +These functions return durations in nanoseconds: + +- `dur_ns()` = 1 nanosecond; +- `dur_us()` = 1 microsecond = 10³ ns; +- `dur_ms()` = 1 millisecond = 10⁶ ns; +- `dur_s()` = 1 second = 10⁹ ns; +- `dur_m()` = 1 minute = 60\*10⁹ ns; +- `dur_h()` = 1 hour = 3600\*10⁹ ns. + +```sql +select dur_ns(); +-- 1 +select dur_us(); +-- 1000 +select dur_ms(); +-- 1000000 +select dur_s(); +-- 1000000000 +select dur_m(); +-- 60000000000 +select dur_h(); +-- 3600000000000 +``` + +## Acknowledgements + +While this extension is implemented in C, its design and implementation is largely based on Go's stdlib [time](https://github.com/golang/go/tree/master/src/time) package (BSD 3-Clause License), which I think is awesome (except for the formatting). + +## Installation and usage + +SQLite command-line interface: + +``` +sqlite> .load ./time +sqlite> select time_now(); +``` + +See [How to install an extension](install.md) for usage with IDE, Python, etc. + +↓ [Download](https://github.com/nalgeon/sqlean/releases/latest) the extension. + +⛱ [Explore](https://github.com/nalgeon/sqlean) other extensions. + +★ [Subscribe](https://antonz.org/subscribe/) to stay on top of new features. diff --git a/src/sqlite3-time.c b/src/sqlite3-time.c new file mode 100644 index 00000000..b0e99a3f --- /dev/null +++ b/src/sqlite3-time.c @@ -0,0 +1,26 @@ +// Copyright (c) 2024 Anton Zhiyanov, MIT License +// https://github.com/nalgeon/sqlean + +// SQLite extension for working with time. + +#include "sqlite3ext.h" +SQLITE_EXTENSION_INIT1 + +#include "sqlean.h" +#include "time/extension.h" + +// sqlean_version returns the current Sqlean version. +static void sqlean_version(sqlite3_context* context, int argc, sqlite3_value** argv) { + sqlite3_result_text(context, SQLEAN_VERSION, -1, SQLITE_STATIC); +} + +#ifdef _WIN32 +__declspec(dllexport) +#endif + int sqlite3_time_init(sqlite3* db, char** errmsg_ptr, const sqlite3_api_routines* api) { + (void)errmsg_ptr; + SQLITE_EXTENSION_INIT2(api); + static const int flags = SQLITE_UTF8 | SQLITE_INNOCUOUS | SQLITE_DETERMINISTIC; + sqlite3_create_function(db, "sqlean_version", 0, flags, 0, sqlean_version, 0, 0); + return time_init(db); +} diff --git a/src/time/duration.c b/src/time/duration.c new file mode 100644 index 00000000..4cee7b48 --- /dev/null +++ b/src/time/duration.c @@ -0,0 +1,117 @@ +// Copyright (c) 2024 Anton Zhiyanov, MIT License +// https://github.com/nalgeon/sqlean + +// Based on Go's time package, BSD 3-Clause License +// https://github.com/golang/go + +// Duration methods. + +#include +#include +#include +#include "time/timex.h" + +// Common durations. +const Duration Nanosecond = 1; +const Duration Microsecond = 1000 * Nanosecond; +const Duration Millisecond = 1000 * Microsecond; +const Duration Second = 1000 * Millisecond; +const Duration Minute = 60 * Second; +const Duration Hour = 60 * Minute; + +#pragma region Conversion + +// dur_to_micro returns the duration as an integer microsecond count. +int64_t dur_to_micro(Duration d) { + return d / Microsecond; +} + +// dur_to_milli returns the duration as an integer millisecond count. +int64_t dur_to_milli(Duration d) { + return d / Millisecond; +} + +// dur_to_seconds returns the duration as a floating point number of seconds. +double dur_to_seconds(Duration d) { + int64_t sec = d / Second; + int64_t nsec = d % Second; + return (double)sec + (double)nsec / 1e9; +} + +// dur_to_minutes returns the duration as a floating point number of minutes. +double dur_to_minutes(Duration d) { + int64_t min = d / Minute; + int64_t nsec = d % Minute; + return (double)min + (double)nsec / (60 * 1e9); +} + +// dur_to_hours returns the duration as a floating point number of hours. +double dur_to_hours(Duration d) { + int64_t hour = d / Hour; + int64_t nsec = d % Hour; + return (double)hour + (double)nsec / (60 * 60 * 1e9); +} + +#pragma endregion + +#pragma region Rounding + +// less_than_half reports whether x+x < y but avoids overflow, +// assuming x and y are both positive (Duration is signed). +static bool less_than_half(Duration x, Duration y) { + return x < y - x; +} + +// dur_truncate returns the result of rounding d toward zero to a multiple of m. +// If m <= 0, Truncate returns d unchanged. +Duration dur_truncate(Duration d, Duration m) { + if (m <= 0) { + return d; + } + return d - d % m; +} + +// dur_round returns the result of rounding d to the nearest multiple of m. +// The rounding behavior for halfway values is to round away from zero. +// If the result exceeds the maximum (or minimum) +// value that can be stored in a Duration, +// Round returns the maximum (or minimum) duration. +// If m <= 0, Round returns d unchanged. +Duration dur_round(Duration d, Duration m) { + if (m <= 0) { + return d; + } + int64_t r = d % m; + + if (d < 0) { + r = -r; + if (less_than_half(r, m)) { + return d + r; + } + int64_t d1 = d - m + r; + if (d1 < d) { + return d1; + } + return MIN_DURATION; // overflow + } + + if (less_than_half(r, m)) { + return d - r; + } + int64_t d1 = d + m - r; + if (d1 > d) { + return d1; + } + return MAX_DURATION; // overflow +} + +// dur_abs returns the absolute value of d. +// As a special case, MIN_DURATION is converted to MAX_DURATION. +Duration dur_abs(Duration d) { + if (d == MIN_DURATION) { + return MAX_DURATION; + } + return d < 0 ? -d : d; +} + +#pragma endregion diff --git a/src/time/extension.c b/src/time/extension.c new file mode 100644 index 00000000..676c042a --- /dev/null +++ b/src/time/extension.c @@ -0,0 +1,798 @@ +// Copyright (c) 2024 Anton Zhiyanov, MIT License +// https://github.com/nalgeon/sqlean + +// SQLite extension for working with time. + +#include +#include +#include +#include +#include + +#include "sqlite3ext.h" +SQLITE_EXTENSION_INIT3 + +#include "time/timex.h" + +// result_blob converts a Time value to a blob and sets it as the result. +static void result_blob(sqlite3_context* context, Time t) { + uint8_t buf[TIMEX_BLOB_SIZE]; + time_to_blob(t, buf); + sqlite3_result_blob(context, buf, sizeof(buf), SQLITE_TRANSIENT); +} + +// time_now() +static void fn_now(sqlite3_context* context, int argc, sqlite3_value** argv) { + assert(argc == 0); + Time t = time_now(); + result_blob(context, t); +} + +// time_date(year, month, day[, hour, min, sec[, nsec[, offset_sec]]]) +static void fn_date(sqlite3_context* context, int argc, sqlite3_value** argv) { + assert(argc == 3 || argc == 6 || argc == 7 || argc == 8); + for (int i = 0; i < argc; i++) { + if (sqlite3_value_type(argv[i]) != SQLITE_INTEGER) { + sqlite3_result_error(context, "all parameters should be integers", -1); + return; + } + } + int year = sqlite3_value_int(argv[0]); + int month = sqlite3_value_int(argv[1]); + int day = sqlite3_value_int(argv[2]); + + int hour = 0; + int min = 0; + int sec = 0; + if (argc >= 6) { + hour = sqlite3_value_int(argv[3]); + min = sqlite3_value_int(argv[4]); + sec = sqlite3_value_int(argv[5]); + } + + int nsec = 0; + if (argc >= 7) { + nsec = sqlite3_value_int(argv[6]); + } + + int offset_sec = 0; + if (argc == 8) { + offset_sec = sqlite3_value_int(argv[7]); + } + + Time t = time_date(year, month, day, hour, min, sec, nsec, offset_sec); + result_blob(context, t); +} + +// time_get_year(t) +// time_get_month(t) +// time_get_day(t) +// time_get_hour(t) +// time_get_minute(t) +// time_get_second(t) +// time_get_nano(t) +// time_get_weekday(t) +// time_get_yearday(t) +static void fn_extract(sqlite3_context* context, int argc, sqlite3_value** argv) { + assert(argc == 1); + if (sqlite3_value_type(argv[0]) != SQLITE_BLOB) { + sqlite3_result_error(context, "parameter should be a time blob", -1); + return; + } + if (sqlite3_value_bytes(argv[0]) != TIMEX_BLOB_SIZE) { + sqlite3_result_error(context, "invalid time blob size", -1); + return; + } + int (*extract)(Time t) = (int (*)(Time t))sqlite3_user_data(context); + Time t = time_blob(sqlite3_value_blob(argv[0])); + sqlite3_result_int(context, extract(t)); +} + +// time_get_isoyear(t) +static void fn_get_isoyear(sqlite3_context* context, int argc, sqlite3_value** argv) { + assert(argc == 1); + if (sqlite3_value_type(argv[0]) != SQLITE_BLOB) { + sqlite3_result_error(context, "parameter should be a time blob", -1); + return; + } + if (sqlite3_value_bytes(argv[0]) != TIMEX_BLOB_SIZE) { + sqlite3_result_error(context, "invalid time blob size", -1); + return; + } + Time t = time_blob(sqlite3_value_blob(argv[0])); + int year, week; + time_get_isoweek(t, &year, &week); + sqlite3_result_int(context, year); +} + +// time_get_isoweek(t) +static void fn_get_isoweek(sqlite3_context* context, int argc, sqlite3_value** argv) { + assert(argc == 1); + if (sqlite3_value_type(argv[0]) != SQLITE_BLOB) { + sqlite3_result_error(context, "parameter should be a time blob", -1); + return; + } + if (sqlite3_value_bytes(argv[0]) != TIMEX_BLOB_SIZE) { + sqlite3_result_error(context, "invalid time blob size", -1); + return; + } + Time t = time_blob(sqlite3_value_blob(argv[0])); + int year, week; + time_get_isoweek(t, &year, &week); + sqlite3_result_int(context, week); +} + +// get_field returns a part of the t according to a given field +static void get_field(sqlite3_context* context, Time t, const char* field) { + // millennium, century, decade + if (strcmp(field, "millennium") == 0) { + int millennium = time_get_year(t) / 1000; + sqlite3_result_int(context, millennium); + return; + } + if (strcmp(field, "century") == 0) { + int century = time_get_year(t) / 100; + sqlite3_result_int(context, century); + return; + } + if (strncmp(field, "decade", 6) == 0) { + int decade = time_get_year(t) / 10; + sqlite3_result_int(context, decade); + return; + } + + // year, quarter, month, day + if (strcmp(field, "year") == 0 || strcmp(field, "years") == 0) { + sqlite3_result_int(context, time_get_year(t)); + return; + } + if (strncmp(field, "quarter", 7) == 0) { + int quarter = (time_get_month(t) - 1) / 3 + 1; + sqlite3_result_int(context, quarter); + return; + } + if (strncmp(field, "month", 5) == 0) { + sqlite3_result_int(context, time_get_month(t)); + return; + } + if (strcmp(field, "day") == 0 || strcmp(field, "days") == 0) { + sqlite3_result_int(context, time_get_day(t)); + return; + } + + // hour, minute, second + if (strncmp(field, "hour", 4) == 0) { + sqlite3_result_int(context, time_get_hour(t)); + return; + } + if (strncmp(field, "minute", 6) == 0) { + sqlite3_result_int(context, time_get_minute(t)); + return; + } + if (strncmp(field, "second", 6) == 0) { + // including fractional part + double sec = time_get_second(t) + t.nsec / 1e9; + sqlite3_result_double(context, sec); + return; + } + + // millisecond, microsecond, nanosecond + if (strncmp(field, "milli", 5) == 0) { + int msec = time_get_nano(t) / 1000000; + sqlite3_result_int(context, msec); + return; + } + if (strncmp(field, "micro", 5) == 0) { + int usec = time_get_nano(t) / 1000; + sqlite3_result_int(context, usec); + return; + } + if (strncmp(field, "nano", 4) == 0) { + sqlite3_result_int(context, time_get_nano(t)); + return; + } + + // isoyear, isoweek, isodow, yearday, weekday + if (strcmp(field, "isoyear") == 0) { + int year, week; + time_get_isoweek(t, &year, &week); + sqlite3_result_int(context, year); + return; + } + if (strcmp(field, "isoweek") == 0 || strcmp(field, "week") == 0) { + int year, week; + time_get_isoweek(t, &year, &week); + sqlite3_result_int(context, week); + return; + } + if (strcmp(field, "isodow") == 0) { + int isodow = time_get_weekday(t) == 0 ? 7 : time_get_weekday(t); + sqlite3_result_int(context, isodow); + return; + } + if (strcmp(field, "yearday") == 0 || strcmp(field, "doy") == 0 || + strcmp(field, "dayofyear") == 0) { + sqlite3_result_int(context, time_get_yearday(t)); + return; + } + if (strcmp(field, "weekday") == 0 || strcmp(field, "dow") == 0 || + strcmp(field, "dayofweek") == 0) { + sqlite3_result_int(context, time_get_weekday(t)); + return; + } + + // epoch + if (strcmp(field, "epoch") == 0) { + // including fractional part + double epoch = time_to_unix(t) + t.nsec / 1e9; + sqlite3_result_double(context, epoch); + return; + } + + sqlite3_result_error(context, "unknown field", -1); +} + +// time_get(t, field) +static void fn_get(sqlite3_context* context, int argc, sqlite3_value** argv) { + assert(argc == 2); + + if (sqlite3_value_type(argv[0]) != SQLITE_BLOB) { + sqlite3_result_error(context, "1st parameter: should be a time blob", -1); + return; + } + if (sqlite3_value_bytes(argv[0]) != TIMEX_BLOB_SIZE) { + sqlite3_result_error(context, "1st parameter: invalid time blob size", -1); + return; + } + Time t = time_blob(sqlite3_value_blob(argv[0])); + + if (sqlite3_value_type(argv[1]) != SQLITE_TEXT) { + sqlite3_result_error(context, "2nd parameter: should be a field name", -1); + return; + } + const char* field = (const char*)sqlite3_value_text(argv[1]); + + get_field(context, t, field); +} + +// date_part(field, t) +// Postgres-compatible. +static void date_part(sqlite3_context* context, int argc, sqlite3_value** argv) { + assert(argc == 2); + + if (sqlite3_value_type(argv[0]) != SQLITE_TEXT) { + sqlite3_result_error(context, "1st parameter: should be a field name", -1); + return; + } + const char* field = (const char*)sqlite3_value_text(argv[0]); + + if (sqlite3_value_type(argv[1]) != SQLITE_BLOB) { + sqlite3_result_error(context, "2nd parameter: should be a time blob", -1); + return; + } + if (sqlite3_value_bytes(argv[1]) != TIMEX_BLOB_SIZE) { + sqlite3_result_error(context, "2nd parameter: invalid time blob size", -1); + return; + } + Time t = time_blob(sqlite3_value_blob(argv[1])); + + get_field(context, t, field); +} + +// time_unix(sec[, nsec]) +static void fn_unix(sqlite3_context* context, int argc, sqlite3_value** argv) { + assert(argc == 1 || argc == 2); + for (int i = 0; i < argc; i++) { + if (sqlite3_value_type(argv[i]) != SQLITE_INTEGER) { + sqlite3_result_error(context, "all parameters should be integers", -1); + return; + } + } + + int64_t sec = sqlite3_value_int64(argv[0]); + int64_t nsec = 0; + if (argc == 2) { + nsec = sqlite3_value_int64(argv[1]); + } + + Time t = time_unix(sec, nsec); + result_blob(context, t); +} + +// time_milli(msec) +// time_micro(usec) +// time_nano(nsec) +static void fn_unix_n(sqlite3_context* context, int argc, sqlite3_value** argv) { + assert(argc == 1); + if (sqlite3_value_type(argv[0]) != SQLITE_INTEGER) { + sqlite3_result_error(context, "parameter should be an integer", -1); + return; + } + int64_t n = sqlite3_value_int64(argv[0]); + Time (*convert)(int64_t n) = (Time(*)(int64_t))sqlite3_user_data(context); + Time t = convert(n); + result_blob(context, t); +} + +// time_to_unix(t) +// time_to_milli(t) +// time_to_micro(t) +// time_to_nano(t) +static void fn_convert(sqlite3_context* context, int argc, sqlite3_value** argv) { + assert(argc == 1); + if (sqlite3_value_type(argv[0]) != SQLITE_BLOB) { + sqlite3_result_error(context, "parameter should be a time blob", -1); + return; + } + if (sqlite3_value_bytes(argv[0]) != TIMEX_BLOB_SIZE) { + sqlite3_result_error(context, "invalid time blob size", -1); + return; + } + int64_t (*convert)(Time t) = (int64_t(*)(Time t))sqlite3_user_data(context); + Time t = time_blob(sqlite3_value_blob(argv[0])); + sqlite3_result_int64(context, convert(t)); +} + +// time_after(t, u) +// time_before(t, u) +// time_compare(t, u) +// time_equal(t, u) +static void fn_compare(sqlite3_context* context, int argc, sqlite3_value** argv) { + assert(argc == 2); + if (sqlite3_value_type(argv[0]) != SQLITE_BLOB) { + sqlite3_result_error(context, "1st parameter: should be a time blob", -1); + return; + } + if (sqlite3_value_bytes(argv[0]) != TIMEX_BLOB_SIZE) { + sqlite3_result_error(context, "1st parameter: invalid time blob size", -1); + return; + } + Time t = time_blob(sqlite3_value_blob(argv[0])); + + if (sqlite3_value_type(argv[1]) != SQLITE_BLOB) { + sqlite3_result_error(context, "2nd parameter: should be a time blob", -1); + return; + } + if (sqlite3_value_bytes(argv[1]) != TIMEX_BLOB_SIZE) { + sqlite3_result_error(context, "2nd parameter: invalid time blob size", -1); + return; + } + Time u = time_blob(sqlite3_value_blob(argv[1])); + + int (*compare)(Time t, Time u) = (int (*)(Time, Time))sqlite3_user_data(context); + sqlite3_result_int(context, compare(t, u)); +} + +// time_add(t, d) +static void fn_add(sqlite3_context* context, int argc, sqlite3_value** argv) { + assert(argc == 2); + if (sqlite3_value_type(argv[0]) != SQLITE_BLOB) { + sqlite3_result_error(context, "1st parameter: should be a time blob", -1); + return; + } + if (sqlite3_value_bytes(argv[0]) != TIMEX_BLOB_SIZE) { + sqlite3_result_error(context, "1st parameter: invalid time blob size", -1); + return; + } + Time t = time_blob(sqlite3_value_blob(argv[0])); + + if (sqlite3_value_type(argv[1]) != SQLITE_INTEGER) { + sqlite3_result_error(context, "2nd parameter: should be an integer", -1); + return; + } + Duration d = sqlite3_value_int64(argv[1]); + + Time r = time_add(t, d); + result_blob(context, r); +} + +// time_sub(t, u) +static void fn_sub(sqlite3_context* context, int argc, sqlite3_value** argv) { + assert(argc == 2); + if (sqlite3_value_type(argv[0]) != SQLITE_BLOB) { + sqlite3_result_error(context, "1st parameter: should be a time blob", -1); + return; + } + if (sqlite3_value_bytes(argv[0]) != TIMEX_BLOB_SIZE) { + sqlite3_result_error(context, "1st parameter: invalid time blob size", -1); + return; + } + Time t = time_blob(sqlite3_value_blob(argv[0])); + + if (sqlite3_value_type(argv[1]) != SQLITE_BLOB) { + sqlite3_result_error(context, "2nd parameter: should be a time blob", -1); + return; + } + if (sqlite3_value_bytes(argv[1]) != TIMEX_BLOB_SIZE) { + sqlite3_result_error(context, "2nd parameter: invalid time blob size", -1); + return; + } + Time u = time_blob(sqlite3_value_blob(argv[1])); + + Duration d = time_sub(t, u); + sqlite3_result_int64(context, d); +} + +// time_since(t) +static void fn_since(sqlite3_context* context, int argc, sqlite3_value** argv) { + assert(argc == 1); + if (sqlite3_value_type(argv[0]) != SQLITE_BLOB) { + sqlite3_result_error(context, "parameter should be a time blob", -1); + return; + } + if (sqlite3_value_bytes(argv[0]) != TIMEX_BLOB_SIZE) { + sqlite3_result_error(context, "invalid time blob size", -1); + return; + } + Time t = time_blob(sqlite3_value_blob(argv[0])); + + Duration d = time_since(t); + sqlite3_result_int64(context, d); +} + +// time_until(t) +static void fn_until(sqlite3_context* context, int argc, sqlite3_value** argv) { + assert(argc == 1); + if (sqlite3_value_type(argv[0]) != SQLITE_BLOB) { + sqlite3_result_error(context, "parameter should be a time blob", -1); + return; + } + if (sqlite3_value_bytes(argv[0]) != TIMEX_BLOB_SIZE) { + sqlite3_result_error(context, "invalid time blob size", -1); + return; + } + Time t = time_blob(sqlite3_value_blob(argv[0])); + + Duration d = time_until(t); + sqlite3_result_int64(context, d); +} + +// time_add_date(t, years[, months[, days]]) +static void fn_add_date(sqlite3_context* context, int argc, sqlite3_value** argv) { + assert(argc == 2 || argc == 3 || argc == 4); + if (sqlite3_value_type(argv[0]) != SQLITE_BLOB) { + sqlite3_result_error(context, "1st parameter: should be a time blob", -1); + return; + } + if (sqlite3_value_bytes(argv[0]) != TIMEX_BLOB_SIZE) { + sqlite3_result_error(context, "1st parameter: invalid time blob size", -1); + return; + } + Time t = time_blob(sqlite3_value_blob(argv[0])); + + if (sqlite3_value_type(argv[1]) != SQLITE_INTEGER) { + sqlite3_result_error(context, "2nd parameter: should be an integer", -1); + return; + } + int years = sqlite3_value_int(argv[1]); + + int months = 0; + if (argc >= 3) { + if (sqlite3_value_type(argv[2]) != SQLITE_INTEGER) { + sqlite3_result_error(context, "3rd parameter: should be an integer", -1); + return; + } + months = sqlite3_value_int(argv[2]); + } + + int days = 0; + if (argc == 4) { + if (sqlite3_value_type(argv[3]) != SQLITE_INTEGER) { + sqlite3_result_error(context, "4th parameter: should be an integer", -1); + return; + } + days = sqlite3_value_int(argv[3]); + } + + Time r = time_add_date(t, years, months, days); + result_blob(context, r); +} + +// trunc_field truncates t according to a given field +static void trunc_field(sqlite3_context* context, Time t, const char* field) { + // millennium, century, decade + if (strcmp(field, "millennium") == 0) { + int year = time_get_year(t); + int millennium = year / 1000 * 1000; + Time r = time_date(millennium, January, 1, 0, 0, 0, 0, TIMEX_UTC); + result_blob(context, r); + return; + } + if (strcmp(field, "century") == 0) { + int year = time_get_year(t); + int century = year / 100 * 100; + Time r = time_date(century, January, 1, 0, 0, 0, 0, TIMEX_UTC); + result_blob(context, r); + return; + } + if (strcmp(field, "decade") == 0) { + int year = time_get_year(t); + int decade = year / 10 * 10; + Time r = time_date(decade, January, 1, 0, 0, 0, 0, TIMEX_UTC); + result_blob(context, r); + return; + } + + // year, quarter, month, week, day + if (strcmp(field, "year") == 0) { + Time r = time_date(time_get_year(t), January, 1, 0, 0, 0, 0, TIMEX_UTC); + result_blob(context, r); + return; + } + if (strcmp(field, "quarter") == 0) { + int quarter = (time_get_month(t) - 1) / 3; + Time r = time_date(time_get_year(t), quarter * 3 + 1, 1, 0, 0, 0, 0, TIMEX_UTC); + result_blob(context, r); + return; + } + if (strcmp(field, "month") == 0) { + Time r = time_date(time_get_year(t), time_get_month(t), 1, 0, 0, 0, 0, TIMEX_UTC); + result_blob(context, r); + return; + } + if (strcmp(field, "week") == 0) { + int year, week; + time_get_isoweek(t, &year, &week); + Time r = time_date(year, January, 1, 0, 0, 0, 0, 0); + r = time_add_date(r, 0, 0, (week - 1) * 7); + result_blob(context, r); + return; + } + if (strcmp(field, "day") == 0) { + Time r = + time_date(time_get_year(t), time_get_month(t), time_get_day(t), 0, 0, 0, 0, TIMEX_UTC); + result_blob(context, r); + return; + } + + // hour, minute, second, millisecond, microsecond + if (strcmp(field, "hour") == 0) { + Time r = time_truncate(t, Hour); + result_blob(context, r); + return; + } + if (strcmp(field, "minute") == 0) { + Time r = time_truncate(t, Minute); + result_blob(context, r); + return; + } + if (strcmp(field, "second") == 0) { + Time r = time_truncate(t, Second); + result_blob(context, r); + return; + } + if (strncmp(field, "milli", 5) == 0) { + int64_t nsec = (t.nsec / 1000000) * 1000000; + Time r = time_unix(time_to_unix(t), nsec); + result_blob(context, r); + return; + } + if (strncmp(field, "micro", 5) == 0) { + int64_t nsec = (t.nsec / 1000) * 1000; + Time r = time_unix(time_to_unix(t), nsec); + result_blob(context, r); + return; + } + + sqlite3_result_error(context, "unknown field", -1); +} + +// time_trunc(t, field) +// time_trunc(t, d) +static void fn_trunc(sqlite3_context* context, int argc, sqlite3_value** argv) { + assert(argc == 2); + + // first parameter is a time blob + if (sqlite3_value_type(argv[0]) != SQLITE_BLOB) { + sqlite3_result_error(context, "1st parameter: should be a time blob", -1); + return; + } + if (sqlite3_value_bytes(argv[0]) != TIMEX_BLOB_SIZE) { + sqlite3_result_error(context, "1st parameter: invalid time blob size", -1); + return; + } + Time t = time_blob(sqlite3_value_blob(argv[0])); + + // second parameter can be a custom duration + if (sqlite3_value_type(argv[1]) == SQLITE_INTEGER) { + // truncate to custom duration + Duration d = sqlite3_value_int64(argv[1]); + Time r = time_truncate(t, d); + result_blob(context, r); + return; + } + + // or a field name + if (sqlite3_value_type(argv[1]) != SQLITE_TEXT) { + sqlite3_result_error(context, "2nd parameter: should be a field name", -1); + return; + } + const char* field = (const char*)sqlite3_value_text(argv[1]); + + // truncate to field + trunc_field(context, t, field); +} + +// date_trunc(field, t) +// Postgres-compatible. +static void date_trunc(sqlite3_context* context, int argc, sqlite3_value** argv) { + assert(argc == 2); + + // first parameter is a field name + if (sqlite3_value_type(argv[0]) != SQLITE_TEXT) { + sqlite3_result_error(context, "1st parameter: should be a field name", -1); + return; + } + const char* field = (const char*)sqlite3_value_text(argv[0]); + + // second parameter is a time blob + if (sqlite3_value_type(argv[1]) != SQLITE_BLOB) { + sqlite3_result_error(context, "2nd parameter: should be a time blob", -1); + return; + } + if (sqlite3_value_bytes(argv[1]) != TIMEX_BLOB_SIZE) { + sqlite3_result_error(context, "2nd parameter: invalid time blob size", -1); + return; + } + Time t = time_blob(sqlite3_value_blob(argv[1])); + + trunc_field(context, t, field); +} + +// time_round(t, d) +static void fn_round(sqlite3_context* context, int argc, sqlite3_value** argv) { + assert(argc == 2); + + if (sqlite3_value_type(argv[0]) != SQLITE_BLOB) { + sqlite3_result_error(context, "1st parameter: should be a time blob", -1); + return; + } + if (sqlite3_value_bytes(argv[0]) != TIMEX_BLOB_SIZE) { + sqlite3_result_error(context, "1st parameter: invalid time blob size", -1); + return; + } + Time t = time_blob(sqlite3_value_blob(argv[0])); + + if (sqlite3_value_type(argv[1]) != SQLITE_INTEGER) { + sqlite3_result_error(context, "2nd parameter: should be an integer", -1); + return; + } + Duration d = sqlite3_value_int64(argv[1]); + + Time r = time_round(t, d); + result_blob(context, r); +} + +// time_fmt_iso(t[, offset_sec]) +// time_fmt_datetime(t[, offset_sec]) +// time_fmt_date(t[, offset_sec]) +// time_fmt_time(t[, offset_sec]) +static void fn_format(sqlite3_context* context, int argc, sqlite3_value** argv) { + assert(argc == 1 || argc == 2); + if (sqlite3_value_type(argv[0]) != SQLITE_BLOB) { + sqlite3_result_error(context, "1st parameter: should be a time blob", -1); + return; + } + if (sqlite3_value_bytes(argv[0]) != TIMEX_BLOB_SIZE) { + sqlite3_result_error(context, "1st parameter: invalid time blob size", -1); + return; + } + Time t = time_blob(sqlite3_value_blob(argv[0])); + + int offset_sec = 0; + if (argc == 2) { + if (sqlite3_value_type(argv[1]) != SQLITE_INTEGER) { + sqlite3_result_error(context, "2nd parameter: should be an integer", -1); + return; + } + offset_sec = sqlite3_value_int(argv[1]); + } + + char buf[36]; + size_t (*format)(char* buf, size_t size, Time t, int offset_sec) = + (size_t(*)(char*, size_t, Time, int))sqlite3_user_data(context); + format(buf, sizeof(buf), t, offset_sec); + sqlite3_result_text(context, buf, -1, SQLITE_TRANSIENT); +} + +// time_parse(v) +static void fn_parse(sqlite3_context* context, int argc, sqlite3_value** argv) { + assert(argc == 1); + const char* val = (const char*)sqlite3_value_text(argv[0]); + Time t = time_parse(val); + result_blob(context, t); +} + +// dur_h(), dur_m(), dur_s(), dur_ms(), dur_us(), dur_ns() +static void fn_dur_const(sqlite3_context* context, int argc, sqlite3_value** argv) { + assert(argc == 0); + int64_t d = (intptr_t)sqlite3_user_data(context); + sqlite3_result_int64(context, d); +} + +int time_init(sqlite3* db) { + static const int flags = SQLITE_UTF8 | SQLITE_INNOCUOUS | SQLITE_DETERMINISTIC; + static const int flags_nd = SQLITE_UTF8 | SQLITE_INNOCUOUS; + + // constructors + sqlite3_create_function(db, "time_now", 0, flags_nd, 0, fn_now, 0, 0); + sqlite3_create_function(db, "time_date", 3, flags, 0, fn_date, 0, 0); + sqlite3_create_function(db, "time_date", 6, flags, 0, fn_date, 0, 0); + sqlite3_create_function(db, "time_date", 7, flags, 0, fn_date, 0, 0); + sqlite3_create_function(db, "time_date", 8, flags, 0, fn_date, 0, 0); + + // time parts + sqlite3_create_function(db, "time_get_year", 1, flags, time_get_year, fn_extract, 0, 0); + sqlite3_create_function(db, "time_get_month", 1, flags, time_get_month, fn_extract, 0, 0); + sqlite3_create_function(db, "time_get_day", 1, flags, time_get_day, fn_extract, 0, 0); + sqlite3_create_function(db, "time_get_hour", 1, flags, time_get_hour, fn_extract, 0, 0); + sqlite3_create_function(db, "time_get_minute", 1, flags, time_get_minute, fn_extract, 0, 0); + sqlite3_create_function(db, "time_get_second", 1, flags, time_get_second, fn_extract, 0, 0); + sqlite3_create_function(db, "time_get_nano", 1, flags, time_get_nano, fn_extract, 0, 0); + sqlite3_create_function(db, "time_get_weekday", 1, flags, time_get_weekday, fn_extract, 0, 0); + sqlite3_create_function(db, "time_get_yearday", 1, flags, time_get_yearday, fn_extract, 0, 0); + sqlite3_create_function(db, "time_get_isoyear", 1, flags, 0, fn_get_isoyear, 0, 0); + sqlite3_create_function(db, "time_get_isoweek", 1, flags, 0, fn_get_isoweek, 0, 0); + sqlite3_create_function(db, "time_get", 2, flags, 0, fn_get, 0, 0); + + // unix time + sqlite3_create_function(db, "time_unix", 1, flags, 0, fn_unix, 0, 0); + sqlite3_create_function(db, "time_unix", 2, flags, 0, fn_unix, 0, 0); + sqlite3_create_function(db, "time_milli", 1, flags, time_milli, fn_unix_n, 0, 0); + sqlite3_create_function(db, "time_micro", 1, flags, time_micro, fn_unix_n, 0, 0); + sqlite3_create_function(db, "time_nano", 1, flags, time_nano, fn_unix_n, 0, 0); + sqlite3_create_function(db, "time_to_unix", 1, flags, time_to_unix, fn_convert, 0, 0); + sqlite3_create_function(db, "time_to_milli", 1, flags, time_to_milli, fn_convert, 0, 0); + sqlite3_create_function(db, "time_to_micro", 1, flags, time_to_micro, fn_convert, 0, 0); + sqlite3_create_function(db, "time_to_nano", 1, flags, time_to_nano, fn_convert, 0, 0); + + // comparison + sqlite3_create_function(db, "time_after", 2, flags, time_after, fn_compare, 0, 0); + sqlite3_create_function(db, "time_before", 2, flags, time_before, fn_compare, 0, 0); + sqlite3_create_function(db, "time_compare", 2, flags, time_compare, fn_compare, 0, 0); + sqlite3_create_function(db, "time_equal", 2, flags, time_equal, fn_compare, 0, 0); + + // arithmetic + sqlite3_create_function(db, "time_add", 2, flags, 0, fn_add, 0, 0); + sqlite3_create_function(db, "time_sub", 2, flags, 0, fn_sub, 0, 0); + sqlite3_create_function(db, "time_since", 1, flags_nd, 0, fn_since, 0, 0); + sqlite3_create_function(db, "time_until", 1, flags_nd, 0, fn_until, 0, 0); + sqlite3_create_function(db, "time_add_date", 2, flags, 0, fn_add_date, 0, 0); + sqlite3_create_function(db, "time_add_date", 3, flags, 0, fn_add_date, 0, 0); + sqlite3_create_function(db, "time_add_date", 4, flags, 0, fn_add_date, 0, 0); + + // rounding + sqlite3_create_function(db, "time_trunc", 2, flags, 0, fn_trunc, 0, 0); + sqlite3_create_function(db, "time_round", 2, flags, 0, fn_round, 0, 0); + + // formatting + sqlite3_create_function(db, "time_fmt_iso", 1, flags, time_fmt_iso, fn_format, 0, 0); + sqlite3_create_function(db, "time_fmt_iso", 2, flags, time_fmt_iso, fn_format, 0, 0); + sqlite3_create_function(db, "time_fmt_datetime", 1, flags, time_fmt_datetime, fn_format, 0, 0); + sqlite3_create_function(db, "time_fmt_datetime", 2, flags, time_fmt_datetime, fn_format, 0, 0); + sqlite3_create_function(db, "time_fmt_date", 1, flags, time_fmt_date, fn_format, 0, 0); + sqlite3_create_function(db, "time_fmt_date", 2, flags, time_fmt_date, fn_format, 0, 0); + sqlite3_create_function(db, "time_fmt_time", 1, flags, time_fmt_time, fn_format, 0, 0); + sqlite3_create_function(db, "time_fmt_time", 2, flags, time_fmt_time, fn_format, 0, 0); + sqlite3_create_function(db, "time_parse", 1, flags, 0, fn_parse, 0, 0); + + // duration constants + sqlite3_create_function(db, "dur_h", 0, flags, (void*)Hour, fn_dur_const, 0, 0); + sqlite3_create_function(db, "dur_m", 0, flags, (void*)Minute, fn_dur_const, 0, 0); + sqlite3_create_function(db, "dur_s", 0, flags, (void*)Second, fn_dur_const, 0, 0); + sqlite3_create_function(db, "dur_ms", 0, flags, (void*)Millisecond, fn_dur_const, 0, 0); + sqlite3_create_function(db, "dur_us", 0, flags, (void*)Microsecond, fn_dur_const, 0, 0); + sqlite3_create_function(db, "dur_ns", 0, flags, (void*)Nanosecond, fn_dur_const, 0, 0); + + // postgres compatibility layer + sqlite3_create_function(db, "age", 2, flags, 0, fn_sub, 0, 0); + sqlite3_create_function(db, "date_add", 2, flags, 0, fn_add, 0, 0); + sqlite3_create_function(db, "date_part", 2, flags, 0, date_part, 0, 0); + sqlite3_create_function(db, "date_trunc", 2, flags, 0, date_trunc, 0, 0); + sqlite3_create_function(db, "make_date", 3, flags, 0, fn_date, 0, 0); + sqlite3_create_function(db, "make_timestamp", 6, flags, 0, fn_date, 0, 0); + sqlite3_create_function(db, "now", 0, flags_nd, 0, fn_now, 0, 0); + sqlite3_create_function(db, "to_timestamp", 1, flags, 0, fn_unix, 0, 0); + + return SQLITE_OK; +} diff --git a/src/time/extension.h b/src/time/extension.h new file mode 100644 index 00000000..53de529f --- /dev/null +++ b/src/time/extension.h @@ -0,0 +1,13 @@ +// Copyright (c) 2024 Anton Zhiyanov, MIT License +// https://github.com/nalgeon/sqlean + +// SQLite extension for working with time. + +#ifndef TIME_EXTENSION_H +#define TIME_EXTENSION_H + +#include "sqlite3ext.h" + +int time_init(sqlite3* db); + +#endif /* TIME_EXTENSION_H */ diff --git a/src/time/time.c b/src/time/time.c new file mode 100644 index 00000000..c008ae75 --- /dev/null +++ b/src/time/time.c @@ -0,0 +1,876 @@ +// Copyright (c) 2024 Anton Zhiyanov, MIT License +// https://github.com/nalgeon/sqlean + +// Based on Go's time package, BSD 3-Clause License +// https://github.com/golang/go + +// Time functions and methods. + +#include +#include +#include +#include +#include +#include "time/timex.h" + +#pragma region Private + +static const int64_t seconds_per_minute = 60; +static const int64_t seconds_per_hour = 60 * seconds_per_minute; +static const int64_t seconds_per_day = 24 * seconds_per_hour; +static const int64_t seconds_per_week = 7 * seconds_per_day; +static const int64_t days_per_400_years = 365 * 400 + 97; +static const int64_t days_per_100_years = 365 * 100 + 24; +static const int64_t days_per_4_years = 365 * 4 + 1; + +// The unsigned zero year for internal calculations. +// Must be 1 mod 400, and times before it will not compute correctly, +// but otherwise can be changed at will. +static const int64_t absolute_zero_year = -292277022399LL; + +// Offsets to convert between internal and absolute or Unix times. +// = (absoluteZeroYear - internalYear) * 365.2425 * secondsPerDay +static const int64_t absolute_to_internal = -9223371966579724800LL; +static const int64_t internal_to_absolute = -absolute_to_internal; + +static const int64_t unix_to_internal = + (1969 * 365 + 1969 / 4 - 1969 / 100 + 1969 / 400) * seconds_per_day; +static const int64_t internal_to_unix = -unix_to_internal; + +// days_before[m] counts the number of days in a non-leap year +// before month m begins. There is an entry for m=12, counting +// the number of days before January of next year (365). +static const int days_before[] = { + 0, + 31, + 31 + 28, + 31 + 28 + 31, + 31 + 28 + 31 + 30, + 31 + 28 + 31 + 30 + 31, + 31 + 28 + 31 + 30 + 31 + 30, + 31 + 28 + 31 + 30 + 31 + 30 + 31, + 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31, + 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30, + 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31, + 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30, + 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30 + 31, +}; + +// norm returns nhi, nlo such that +// +// hi * base + lo == nhi * base + nlo +// 0 <= nlo < base +static void norm(int hi, int lo, int base, int* nhi, int* nlo) { + if (lo < 0) { + int n = (-lo - 1) / base + 1; + hi -= n; + lo += n * base; + } + if (lo >= base) { + int n = lo / base; + hi += n; + lo -= n * base; + } + *nhi = hi; + *nlo = lo; +} + +// days_since_epoch takes a year and returns the number of days from +// the absolute epoch to the start of that year. +// This is basically (year - zeroYear) * 365, but accounting for leap days. +static uint64_t days_since_epoch(int year) { + uint64_t y = year - absolute_zero_year; + + // Add in days from 400-year cycles. + uint64_t n = y / 400; + y -= 400 * n; + uint64_t d = days_per_400_years * n; + + // Add in 100-year cycles. + n = y / 100; + y -= 100 * n; + d += days_per_100_years * n; + + // Add in 4-year cycles. + n = y / 4; + y -= 4 * n; + d += days_per_4_years * n; + + // Add in non-leap years. + n = y; + d += 365 * n; + + return d; +} + +// is_leap reports whether the year is a leap year. +static bool is_leap(int year) { + return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); +} + +static int64_t unix_sec(Time t) { + return t.sec + internal_to_unix; +} + +static Time unix_time(int64_t sec, int32_t nsec) { + return (Time){sec + unix_to_internal, nsec}; +} + +// abs_time returns the time t as an absolute time, adjusted by the zone offset. +// It is called when computing a presentation property like Month or Hour. +static uint64_t abs_time(Time t) { + return t.sec + internal_to_absolute; +} + +// abs_weekday is like Weekday but operates on an absolute time. +static enum Weekday abs_weekday(uint64_t abs) { + // January 1 of the absolute year, like January 1 of 2001, was a Monday. + uint64_t sec = (abs + Monday * seconds_per_day) % seconds_per_week; + return sec / seconds_per_day; +} + +static void abs_date(uint64_t abs, int* year, int* yday) { + // Split into time and day. + uint64_t d = abs / seconds_per_day; + + // Account for 400 year cycles. + uint64_t n = d / days_per_400_years; + uint64_t y = 400 * n; + d -= days_per_400_years * n; + + // Cut off 100-year cycles. + // The last cycle has one extra leap year, so on the last day + // of that year, day / days_per_100_years will be 4 instead of 3. + // Cut it back down to 3 by subtracting n>>2. + n = d / days_per_100_years; + n -= n >> 2; + y += 100 * n; + d -= days_per_100_years * n; + + // Cut off 4-year cycles. + // The last cycle has a missing leap year, which does not + // affect the computation. + n = d / days_per_4_years; + y += 4 * n; + d -= days_per_4_years * n; + + // Cut off years within a 4-year cycle. + // The last year is a leap year, so on the last day of that year, + // day / 365 will be 4 instead of 3. Cut it back down to 3 + // by subtracting n>>2. + n = d / 365; + n -= n >> 2; + y += n; + d -= 365 * n; + + *year = y + absolute_zero_year; + *yday = d; +} + +static void abs_date_full(uint64_t abs, int* year, enum Month* month, int* day, int* yday) { + abs_date(abs, year, yday); + + *day = *yday; + if (is_leap(*year)) { + // Leap year + if (*day > 31 + 29 - 1) { + // After leap day; pretend it wasn't there. + *day -= 1; + } + if (*day == 31 + 29 - 1) { + // Leap day. + *month = February; + *day = 29; + return; + } + } + + // Estimate month on assumption that every month has 31 days. + // The estimate may be too low by at most one month, so adjust. + *month = *day / 31; + int end = days_before[(int)(*month) + 1]; + int begin; + if (*day >= end) { + *month += 1; + begin = end; + } else { + begin = days_before[(int)(*month)]; + } + + *month += 1; // because January is 1 + *day = *day - begin + 1; +} + +void abs_clock(uint64_t abs, int* hour, int* min, int* sec) { + *sec = abs % seconds_per_day; + *hour = *sec / seconds_per_hour; + *sec -= *hour * seconds_per_hour; + *min = *sec / seconds_per_minute; + *sec -= *min * seconds_per_minute; +} + +// less_than_half reports whether x+x < y but avoids overflow, +// assuming x and y are both positive (Duration is signed). +static bool less_than_half(Duration x, Duration y) { + return (uint64_t)x + (uint64_t)x < (uint64_t)y; +} + +// div divides t by d and returns the remainder. +// Only supports d which is a multiple of 1 second. +static Duration div(Time t, Duration d) { + if (d % Second != 0) { + return 0; + } + + bool neg = false; + int64_t sec = t.sec; + int64_t nsec = t.nsec; + if (sec < 0) { + // Operate on absolute value. + neg = true; + sec = -sec; + nsec = -nsec; + if (nsec < 0) { + nsec += 1e9; + sec--; // sec >= 1 before the -- so safe + } + } + + // d is a multiple of 1 second. + int64_t d1 = d / Second; + Duration r = (sec % d1) * Second + nsec; + + if (neg && r != 0) { + r = d - r; + } + return r; +} + +#pragma endregion + +#pragma region Constructors + +// time_now returns the current time in UTC. +Time time_now(void) { + struct timespec ts; + timespec_get(&ts, TIME_UTC); + return unix_time(ts.tv_sec, ts.tv_nsec); +} + +// time_date returns the Time corresponding to +// yyyy-mm-dd hh:mm:ss + nsec nanoseconds +// +// The month, day, hour, min, sec, and nsec values may be outside +// their usual ranges and will be normalized during the conversion. +// For example, October 32 converts to November 1. +// +// The time is converted to UTC using offset_sec in seconds east of UTC. +Time time_date(int year, + enum Month month, + int day, + int hour, + int min, + int sec, + int nsec, + int offset_sec) { + // Normalize month, overflowing into year. + int m = month - 1; + norm(year, m, 12, &year, &m); + month = m + 1; + + // Normalize nsec, sec, min, hour, overflowing into day. + norm(sec, nsec, 1000000000, &sec, &nsec); + norm(min, sec, 60, &min, &sec); + norm(hour, min, 60, &hour, &min); + norm(day, hour, 24, &day, &hour); + + // Compute days since the absolute epoch. + uint64_t d = days_since_epoch(year); + + // Add in days before this month. + d += days_before[month - 1]; + if (is_leap(year) && month >= March) { + d++; // February 29 + } + + // Add in days before today. + d += day - 1; + + // Add in time elapsed today. + uint64_t abs = d * seconds_per_day; + abs += hour * seconds_per_hour + min * seconds_per_minute + sec; + + // Convert to UTC. + abs -= offset_sec; + + return (Time){abs + absolute_to_internal, nsec}; +} + +#pragma endregion + +#pragma region Time parts + +// time_get_date returns the year, month, and day in which t occurs. +void time_get_date(Time t, int* year, enum Month* month, int* day) { + uint64_t abs = abs_time(t); + int yday; + abs_date_full(abs, year, month, day, &yday); +} + +// time_get_year returns the year in which t occurs. +int time_get_year(Time t) { + uint64_t abs = abs_time(t); + int year, yday; + abs_date(abs, &year, &yday); + return year; +} + +// time_get_month returns the month of the year specified by t. +enum Month time_get_month(Time t) { + uint64_t abs = abs_time(t); + int year, day, yday; + enum Month month; + abs_date_full(abs, &year, &month, &day, &yday); + return month; +} + +// time_get_day returns the day of the month specified by t. +int time_get_day(Time t) { + uint64_t abs = abs_time(t); + int year, day, yday; + enum Month month; + abs_date_full(abs, &year, &month, &day, &yday); + return day; +} + +// time_get_clock returns the hour, minute, and second within the day specified by t. +void time_get_clock(Time t, int* hour, int* min, int* sec) { + uint64_t abs = abs_time(t); + abs_clock(abs, hour, min, sec); +} + +// time_get_hour returns the hour within the day specified by t, in the range [0, 23]. +int time_get_hour(Time t) { + uint64_t abs = abs_time(t); + return (abs % seconds_per_day) / seconds_per_hour; +} + +// time_get_minute returns the minute offset within the hour specified by t, in the range [0, 59]. +int time_get_minute(Time t) { + uint64_t abs = abs_time(t); + return (abs % seconds_per_hour) / seconds_per_minute; +} + +// time_get_second returns the second offset within the minute specified by t, in the range [0, 59]. +int time_get_second(Time t) { + uint64_t abs = abs_time(t); + return abs % seconds_per_minute; +} + +// time_get_nano returns the nanosecond offset within the second specified by t, +// in the range [0, 999999999]. +int time_get_nano(Time t) { + return t.nsec; +} + +// time_get_weekday returns the day of the week specified by t. +enum Weekday time_get_weekday(Time t) { + uint64_t abs = abs_time(t); + return abs_weekday(abs); +} + +// time_get_yearday returns the day of the year specified by t, in the range [1,365] for non-leap +// years, and [1,366] in leap years. +int time_get_yearday(Time t) { + uint64_t abs = abs_time(t); + int year, yday; + abs_date(abs, &year, &yday); + return yday + 1; +} + +// time_get_isoweek returns the ISO 8601 year and week number in which t occurs. +// Week ranges from 1 to 53. Jan 01 to Jan 03 of year n might belong to +// week 52 or 53 of year n-1, and Dec 29 to Dec 31 might belong to week 1 of year n+1. +void time_get_isoweek(Time t, int* year, int* week) { + // According to the rule that the first calendar week of a calendar year is + // the week including the first Thursday of that year, and that the last one is + // the week immediately preceding the first calendar week of the next calendar year. + // See https://www.iso.org/obp/ui#iso:std:iso:8601:-1:ed-1:v1:en:term:3.1.1.23 for details. + + // weeks start with Monday + // Monday Tuesday Wednesday Thursday Friday Saturday Sunday + // 1 2 3 4 5 6 7 + // +3 +2 +1 0 -1 -2 -3 + // the offset to Thursday + uint64_t abs = abs_time(t); + int d = (Thursday - abs_weekday(abs)); + // handle Sunday + if (d == 4) { + d = -3; + } + // find the Thursday of the calendar week + int yday; + abs += d * seconds_per_day; + abs_date(abs, year, &yday); + *week = yday / 7 + 1; +} + +#pragma endregion + +#pragma region Unix time + +// time_unix returns the Time corresponding to the given Unix time, +// sec seconds and nsec nanoseconds since January 1, 1970 UTC. +// It is valid to pass nsec outside the range [0, 999999999]. +// Not all sec values have a corresponding time value. One such +// value is 1<<63-1 (the largest int64 value). +Time time_unix(int64_t sec, int64_t nsec) { + if (nsec < 0 || nsec >= 1000000000) { + int64_t n = nsec / 1000000000; + sec += n; + nsec -= n * 1000000000; + if (nsec < 0) { + nsec += 1000000000; + sec--; + } + } + return unix_time(sec, nsec); +} + +// time_milli returns the Time corresponding to the given Unix time, +// msec milliseconds since January 1, 1970 UTC. +Time time_milli(int64_t msec) { + return time_unix(msec / 1000, (msec % 1000) * 1000000); +} + +// time_micro returns the Time corresponding to the given Unix time, +// usec microseconds since January 1, 1970 UTC. +Time time_micro(int64_t usec) { + return time_unix(usec / 1000000, (usec % 1000000) * 1000); +} + +// time_nano returns the Time corresponding to the given Unix time, +// nsec nanoseconds since January 1, 1970 UTC. +Time time_nano(int64_t nsec) { + return time_unix(0, nsec); +} + +// time_to_unix returns t as a Unix time, the number of seconds elapsed +// since January 1, 1970 UTC. +// Unix-like operating systems often record time as a 32-bit +// count of seconds, but since the method here returns a 64-bit +// value it is valid for billions of years into the past or future. +int64_t time_to_unix(Time t) { + return unix_sec(t); +} + +// time_to_milli returns t as a Unix time, the number of milliseconds elapsed since +// January 1, 1970 UTC. The result is undefined if the Unix time in +// milliseconds cannot be represented by an int64 (a date more than 292 million +// years before or after 1970). +int64_t time_to_milli(Time t) { + return unix_sec(t) * 1000 + t.nsec / 1000000; +} + +// time_to_micro returns t as a Unix time, the number of microseconds elapsed since +// January 1, 1970 UTC. The result is undefined if the Unix time in +// microseconds cannot be represented by an int64 (a date before year -290307 or +// after year 294246). +int64_t time_to_micro(Time t) { + return unix_sec(t) * 1000000 + t.nsec / 1000; +} + +// time_to_nano returns t as a Unix time, the number of nanoseconds elapsed +// since January 1, 1970 UTC. The result is undefined if the Unix time +// in nanoseconds cannot be represented by an int64 (a date before the year +// 1678 or after 2262). Note that this means the result of calling UnixNano +// on the zero Time is undefined. +int64_t time_to_nano(Time t) { + return unix_sec(t) * 1000000000 + t.nsec; +} + +#pragma endregion + +#pragma region Calendar time + +// time_tm returns the Time corresponding to the given calendar time at the given timezone offset. +Time time_tm(struct tm tm, int offset_sec) { + int year = tm.tm_year + 1900; + int month = tm.tm_mon + 1; + int day = tm.tm_mday; + int hour = tm.tm_hour; + int min = tm.tm_min; + int sec = tm.tm_sec; + return time_date(year, month, day, hour, min, sec, 0, offset_sec); +} + +// time_to_tm returns t in the given timezone offset as a calendar time. +struct tm time_to_tm(Time t, int offset_sec) { + Time loc_t = time_add(t, offset_sec * Second); + int year, day, hour, min, sec; + enum Month month; + time_get_date(loc_t, &year, &month, &day); + time_get_clock(loc_t, &hour, &min, &sec); + struct tm tm = { + .tm_year = year - 1900, + .tm_mon = month - 1, + .tm_mday = day, + .tm_hour = hour, + .tm_min = min, + .tm_sec = sec, + .tm_isdst = -1, + }; + return tm; +} + +#pragma endregion + +#pragma region Comparison + +// time_after reports whether the time instant t is after u. +bool time_after(Time t, Time u) { + return t.sec > u.sec || (t.sec == u.sec && t.nsec > u.nsec); +} + +// time_before reports whether the time instant t is before u. +bool time_before(Time t, Time u) { + return t.sec < u.sec || (t.sec == u.sec && t.nsec < u.nsec); +} + +// time_compare compares the time instant t with u. If t is before u, it returns -1; +// if t is after u, it returns +1; if they're the same, it returns 0. +int time_compare(Time t, Time u) { + if (time_before(t, u)) { + return -1; + } + if (time_after(t, u)) { + return +1; + } + return 0; +} + +// time_equal reports whether t and u represent the same time instant. +bool time_equal(Time t, Time u) { + return t.sec == u.sec && t.nsec == u.nsec; +} + +// time_is_zero reports whether t represents the zero time instant, +// January 1, year 1, 00:00:00 UTC. +bool time_is_zero(Time t) { + return t.sec == 0 && t.nsec == 0; +} + +#pragma endregion + +#pragma region Arithmetic + +// time_add returns the time t+d. +Time time_add(Time t, Duration d) { + int64_t dsec = d / Second; + int64_t nsec = t.nsec + d % 1000000000; + if (nsec >= 1e9) { + dsec++; + nsec -= 1e9; + } else if (nsec < 0) { + dsec--; + nsec += 1e9; + } + return (Time){t.sec + dsec, nsec}; +} + +// time_sub returns the duration t-u. If the result exceeds the maximum (or minimum) +// value that can be stored in a Duration, the maximum (or minimum) duration +// will be returned. +Duration time_sub(Time t, Time u) { + int64_t d = (t.sec - u.sec) * Second + (t.nsec - u.nsec); + if (time_equal(time_add(u, d), t)) { + return d; // d is correct + } + if (time_before(t, u)) { + return MIN_DURATION; // t - u is negative out of range + } + return MAX_DURATION; // t - u is positive out of range +} + +// time_since returns the time elapsed since t. +// It is shorthand for time_sub(time_now(), t). +Duration time_since(Time t) { + return time_sub(time_now(), t); +} + +// time_until returns the duration until t. +// It is shorthand for time_sub(t, time_now()). +Duration time_until(Time t) { + return time_sub(t, time_now()); +} + +// time_add_date returns the time corresponding to adding the +// given number of years, months, and days to t. +// For example, time_add_date(-1, 2, 3) applied to January 1, 2011 +// returns March 4, 2010. +// +// time_add_date normalizes its result in the same way that Date does, +// so, for example, adding one month to October 31 yields +// December 1, the normalized form for November 31. +Time time_add_date(Time t, int years, int months, int days) { + int year, day; + enum Month month; + time_get_date(t, &year, &month, &day); + int hour, min, sec; + time_get_clock(t, &hour, &min, &sec); + return time_date(year + years, month + months, day + days, hour, min, sec, t.nsec, TIMEX_UTC); +} + +#pragma endregion + +#pragma region Rounding + +// time_truncate returns the result of rounding t down to a multiple of d (since the zero time). +// Only supports d which is a multiple of 1 second. If d <= 0, returns t unchanged. +Time time_truncate(Time t, Duration d) { + if (d <= 0) { + return t; + } + Duration r = div(t, d); + return time_add(t, -r); +} + +// time_round returns the result of rounding t to the nearest multiple of d (since the zero time). +// The rounding behavior for halfway values is to round up. +// If d <= 0, returns t unchanged. +Time time_round(Time t, Duration d) { + if (d <= 0) { + return t; + } + Duration r = div(t, d); + if (less_than_half(r, d)) { + return time_add(t, -r); + } + return time_add(t, d - r); +} + +#pragma endregion + +#pragma region Formatting + +// time_fmt_iso returns an ISO 8601 time string for the given time value. +// Converts the time value to the given timezone offset before formatting. +// Chooses the most compact representation: +// - 2006-01-02T15:04:05.999999999+07:00 +// - 2006-01-02T15:04:05.999999999Z +// - 2006-01-02T15:04:05+07:00 +// - 2006-01-02T15:04:05Z +size_t time_fmt_iso(char* buf, size_t size, Time t, int offset_sec) { + int year, day, hour, min, sec; + enum Month month; + const char* layout; + size_t n = 0; + + if (offset_sec == 0) { + time_get_date(t, &year, &month, &day); + time_get_clock(t, &hour, &min, &sec); + if (t.nsec == 0) { + layout = "%04d-%02d-%02dT%02d:%02d:%02dZ"; + n = snprintf(buf, size, layout, year, month, day, hour, min, sec); + } else { + layout = "%04d-%02d-%02dT%02d:%02d:%02d.%09dZ"; + n = snprintf(buf, size, layout, year, month, day, hour, min, sec, t.nsec); + } + } else { + Time loc_t = time_add(t, offset_sec * Second); + time_get_date(loc_t, &year, &month, &day); + time_get_clock(loc_t, &hour, &min, &sec); + int ofhour = offset_sec / 3600; + int ofmin = (offset_sec % 3600) / 60; + if (ofmin < 0) { + ofmin = -ofmin; + } + if (loc_t.nsec == 0) { + layout = "%04d-%02d-%02dT%02d:%02d:%02d%+03d:%02d"; + n = snprintf(buf, size, layout, year, month, day, hour, min, sec, ofhour, ofmin); + } else { + layout = "%04d-%02d-%02dT%02d:%02d:%02d.%09d%+03d:%02d"; + n = snprintf(buf, size, layout, year, month, day, hour, min, sec, loc_t.nsec, ofhour, + ofmin); + } + } + return n; +} + +// time_fmt_datetime returns a datetime string +// (2006-01-02 15:04:05) for the given time value. +// Converts the time value to the given timezone offset before formatting. +size_t time_fmt_datetime(char* buf, size_t size, Time t, int offset_sec) { + int year, day, hour, min, sec; + enum Month month; + if (offset_sec == 0) { + time_get_date(t, &year, &month, &day); + time_get_clock(t, &hour, &min, &sec); + } else { + Time loc_t = time_add(t, offset_sec * Second); + time_get_date(loc_t, &year, &month, &day); + time_get_clock(loc_t, &hour, &min, &sec); + } + return snprintf(buf, size, "%04d-%02d-%02d %02d:%02d:%02d", year, month, day, hour, min, sec); +} + +// time_fmt_date returns a date string +// (2006-01-02) for the given time value. +// Converts the time value to the given timezone offset before formatting. +size_t time_fmt_date(char* buf, size_t size, Time t, int offset_sec) { + int year, day; + enum Month month; + if (offset_sec == 0) { + time_get_date(t, &year, &month, &day); + } else { + Time loc_t = time_add(t, offset_sec * Second); + time_get_date(loc_t, &year, &month, &day); + } + return snprintf(buf, size, "%04d-%02d-%02d", year, month, day); +} + +// time_fmt_time returns a time string +// (15:04:05) for the given time value. +// Converts the time value to the given timezone offset before formatting. +size_t time_fmt_time(char* buf, size_t size, Time t, int offset_sec) { + int hour, min, sec; + if (offset_sec == 0) { + time_get_clock(t, &hour, &min, &sec); + } else { + Time loc_t = time_add(t, offset_sec * Second); + time_get_clock(loc_t, &hour, &min, &sec); + } + return snprintf(buf, size, "%02d:%02d:%02d", hour, min, sec); +} + +// time_parse parses a formatted string and returns the time value it represents. +// Supports a limited set of layouts: +// - "2006-01-02T15:04:05.999999999+07:00" (ISO 8601 with nanoseconds and timezone) +// - "2006-01-02T15:04:05.999999999Z" (ISO 8601 with nanoseconds, UTC) +// - "2006-01-02T15:04:05+07:00" (ISO 8601 with timezone) +// - "2006-01-02T15:04:05Z" (ISO 8601, UTC) +// - "2006-01-02 15:04:05" (date and time, UTC) +// - "2006-01-02" (date only, UTC) +// - "15:04:05" (time only, UTC) +Time time_parse(const char* value) { + Time zero = {0, 0}; + size_t len = strlen(value); + if (len < 8 || len > 35) { + return zero; + } + + int year = 1, day = 1, hour = 0, min = 0, sec = 0, nsec = 0, offset_sec = TIMEX_UTC; + enum Month month = 1; + char tz[7] = ""; + + if (len == 35) { + // "2006-01-02T15:04:05.999999999+07:00" + int n = sscanf(value, "%d-%d-%dT%d:%d:%d.%d%6s", &year, &month, &day, &hour, &min, &sec, + &nsec, tz); + if (n != 8) { + return zero; + } + } + + if (len == 30) { + // "2006-01-02T15:04:05.999999999Z" + int n = + sscanf(value, "%d-%d-%dT%d:%d:%d.%dZ", &year, &month, &day, &hour, &min, &sec, &nsec); + if (n != 7) { + return zero; + } + } + + if (len == 25) { + // "2006-01-02T15:04:05+07:00" + int n = sscanf(value, "%d-%d-%dT%d:%d:%d%6s", &year, &month, &day, &hour, &min, &sec, tz); + if (n != 7) { + return zero; + } + } + + if (len == 19 || len == 20) { + // "2006-01-02T15:04:05Z" + // "2006-01-02 15:04:05" + int n = sscanf(value, "%d-%d-%d%*c%d:%d:%d", &year, &month, &day, &hour, &min, &sec); + if (n != 6) { + return zero; + } + } + + if (len == 10) { + // "2006-01-02" + int n = sscanf(value, "%d-%d-%d", &year, &month, &day); + if (n != 3) { + return zero; + } + } + + if (len == 8) { + // "15:04:05" + int n = sscanf(value, "%d:%d:%d", &hour, &min, &sec); + if (n != 3) { + return zero; + } + } + + if (tz[0] != '\0') { + // Parse timezone offset. + // + 0 7 : 0 0 + // ⁰ ¹ ² ³ ⁴ ⁵ + // tz[0] is the sign. + int sign = (tz[0] == '-') ? -1 : 1; + // tz[1] and tz[2] are hours. + offset_sec = ((tz[1] - '0') * 10 + (tz[2] - '0')) * 3600 * sign; + // tz[4] and tz[5] are minutes. + offset_sec += ((tz[4] - '0') * 10 + (tz[5] - '0')) * 60 * sign; + } + + return time_date(year, month, day, hour, min, sec, nsec, offset_sec); +} + +#pragma endregion + +#pragma region Marshaling + +// time_blob returns the time instant represented by the binary data. +// The blob must have been created by time_to_blob and be at least 13 bytes long. +Time time_blob(const uint8_t* buf) { + const uint8_t version = buf[0]; + if (version != 1) { + return (Time){0, 0}; + } + + int64_t sec = (int64_t)buf[8] | (int64_t)buf[7] << 8 | (int64_t)buf[6] << 16 | + (int64_t)buf[5] << 24 | (int64_t)buf[4] << 32 | (int64_t)buf[3] << 40 | + (int64_t)buf[2] << 48 | (int64_t)buf[1] << 56; + + int32_t nsec = + (int32_t)buf[12] | (int32_t)buf[11] << 8 | (int32_t)buf[10] << 16 | (int32_t)buf[9] << 24; + + return (Time){sec, nsec}; +} + +// time_to_blob returns the binary representation of the time instant t. +// The result is a byte slice with the following layout: +// 0: version (currently 1) +// 1-8: seconds +// 9-12: nanoseconds +void time_to_blob(Time t, uint8_t* buf) { + const uint8_t version = 1; + buf[0] = version; + buf[1] = t.sec >> 56; // bytes 1-8: seconds + buf[2] = t.sec >> 48; + buf[3] = t.sec >> 40; + buf[4] = t.sec >> 32; + buf[5] = t.sec >> 24; + buf[6] = t.sec >> 16; + buf[7] = t.sec >> 8; + buf[8] = t.sec; + buf[9] = t.nsec >> 24; // bytes 9-12: nanoseconds + buf[10] = t.nsec >> 16; + buf[11] = t.nsec >> 8; + buf[12] = t.nsec; +} + +#pragma endregion diff --git a/src/time/timex.h b/src/time/timex.h new file mode 100644 index 00000000..4047ff48 --- /dev/null +++ b/src/time/timex.h @@ -0,0 +1,270 @@ +// Copyright (c) 2024 Anton Zhiyanov, MIT License +// https://github.com/nalgeon/sqlean + +// Based on Go's time package, BSD 3-Clause License +// https://github.com/golang/go + +// Package timex provides functionality for working with time. +// The calendrical calculations always assume a Gregorian calendar, with no leap seconds. + +#ifndef TIMEX_H +#define TIMEX_H + +#include +#include +#include + +// Month is a month of the year. +enum Month { + January = 1, + February, + March, + April, + May, + June, + July, + August, + September, + October, + November, + December, +}; + +// Weekday is a day of the week (Sunday = 0, ...). +enum Weekday { + Sunday = 0, + Monday, + Tuesday, + Wednesday, + Thursday, + Friday, + Saturday, +}; + +// Time represents an instant in time with nanosecond precision. +// The zero value is January 1, year 1, 00:00:00.000000000 UTC. +typedef struct { + int64_t sec; // seconds since zero time + int32_t nsec; // nanoseconds within the second [0, 999999999] +} Time; + +#define TIMEX_BLOB_SIZE 13 +#define TIMEX_UTC 0 + +// Duration represents the elapsed time between two instants +// as an int64 nanosecond count. The representation limits the +// largest representable duration to approximately 290 years. +typedef int64_t Duration; + +// --- Time --- + +// Constructors. + +// time_now returns the current time in UTC. +Time time_now(void); + +// time_date returns the Time corresponding to +// yyyy-mm-dd hh:mm:ss + nsec nanoseconds +// with the given timezone offset in seconds. +Time time_date(int year, + enum Month month, + int day, + int hour, + int min, + int sec, + int nsec, + int offset_sec); + +// Time parts. + +// time_get_date returns the year, month, and day in which t occurs. +void time_get_date(Time t, int* year, enum Month* month, int* day); + +// time_get_year returns the year in which t occurs. +int time_get_year(Time t); + +// time_get_month returns the month of the year specified by t. +enum Month time_get_month(Time t); + +// time_get_day returns the day of the month specified by t. +int time_get_day(Time t); + +// time_get_clock returns the hour, minute, and second within the day specified by t. +void time_get_clock(Time t, int* hour, int* min, int* sec); + +// time_get_hour returns the hour within the day specified by t. +int time_get_hour(Time t); + +// time_get_minute returns the minute offset within the hour specified by t. +int time_get_minute(Time t); + +// time_get_second returns the second offset within the minute specified by t. +int time_get_second(Time t); + +// time_get_nano returns the nanosecond offset within the second specified by t. +int time_get_nano(Time t); + +// time_get_weekday returns the day of the week specified by t. +enum Weekday time_get_weekday(Time t); + +// time_get_yearday returns the day of the year specified by t. +int time_get_yearday(Time t); + +// time_get_isoweek returns the ISO 8601 year and week number in which t occurs. +void time_get_isoweek(Time t, int* year, int* week); + +// Unix time. + +// time_unix returns the Time corresponding to the given Unix time, +// sec seconds and nsec nanoseconds since January 1, 1970 UTC. +Time time_unix(int64_t sec, int64_t nsec); + +// time_milli returns the Time corresponding to the given Unix time, +// msec milliseconds since January 1, 1970 UTC. +Time time_milli(int64_t msec); + +// time_micro returns the local Time corresponding to the given Unix time, +// usec microseconds since January 1, 1970 UTC. +Time time_micro(int64_t usec); + +// time_nano returns the Time corresponding to the given Unix time, +// nsec nanoseconds since January 1, 1970 UTC. +Time time_nano(int64_t nsec); + +// time_to_unix returns t as a Unix time, the number of seconds elapsed +// since January 1, 1970 UTC. +int64_t time_to_unix(Time t); + +// time_to_milli returns t as a Unix time, the number of milliseconds elapsed since +// January 1, 1970 UTC. +int64_t time_to_milli(Time t); + +// time_to_micro returns t as a Unix time, the number of microseconds elapsed since +// January 1, 1970 UTC. +int64_t time_to_micro(Time t); + +// time_to_nano returns t as a Unix time, the number of nanoseconds elapsed +// since January 1, 1970 UTC. +int64_t time_to_nano(Time t); + +// Calendar time. + +// time_tm returns the Time corresponding to the given calendar time at the given timezone offset. +Time time_tm(struct tm tm, int offset_sec); + +// time_to_tm returns t in the given timezone offset as a calendar time. +struct tm time_to_tm(Time t, int offset_sec); + +// Comparison. + +// time_after reports whether the time instant t is after u. +bool time_after(Time t, Time u); + +// time_before reports whether the time instant t is before u. +bool time_before(Time t, Time u); + +// time_compare compares the time instant t with u. +int time_compare(Time t, Time u); + +// time_equal reports whether t and u represent the same time instant. +bool time_equal(Time t, Time u); + +// time_is_zero reports whether t represents the zero time instant, +// January 1, year 1, 00:00:00 UTC. +bool time_is_zero(Time t); + +// Arithmetic. + +// time_add returns the time t+d. +Time time_add(Time t, Duration d); + +// time_sub returns the duration t-u. +Duration time_sub(Time t, Time u); + +// time_since returns the time elapsed since t. +Duration time_since(Time t); + +// time_until returns the duration until t. +Duration time_until(Time t); + +// time_add_date returns the time corresponding to adding the +// given number of years, months, and days to t. +Time time_add_date(Time t, int years, int months, int days); + +// Rounding. + +// time_truncate returns the result of rounding t down to a multiple of d. +Time time_truncate(Time t, Duration d); + +// time_round returns the result of rounding t to the nearest multiple of d. +Time time_round(Time t, Duration d); + +// Formatting. + +// time_fmt_iso returns an ISO 8601 time string for the given time value. +size_t time_fmt_iso(char* buf, size_t size, Time t, int offset_sec); + +// time_fmt_datetime returns a datetime string for the given time value. +size_t time_fmt_datetime(char* buf, size_t size, Time t, int offset_sec); + +// time_fmt_date returns a date string for the given time value. +size_t time_fmt_date(char* buf, size_t size, Time t, int offset_sec); + +// time_fmt_time returns a time string for the given time value. +size_t time_fmt_time(char* buf, size_t size, Time t, int offset_sec); + +// time_parse parses a formatted string and returns the time value it represents. +Time time_parse(const char* value); + +// Marshaling. + +// time_blob returns the time instant represented by the binary data. +Time time_blob(const uint8_t* buf); + +// time_to_blob returns the binary representation of the time instant t. +void time_to_blob(Time t, uint8_t* buf); + +// --- Duration --- + +// Min/Max durations. +#define MIN_DURATION INT64_MIN +#define MAX_DURATION INT64_MAX + +// Common durations. There is no definition for units of Day or larger +// to avoid confusion across daylight savings time zone transitions. +extern const Duration Nanosecond; +extern const Duration Microsecond; +extern const Duration Millisecond; +extern const Duration Second; +extern const Duration Minute; +extern const Duration Hour; + +// Conversion. + +// dur_to_micro returns the duration as an integer microsecond count. +int64_t dur_to_micro(Duration d); + +// dur_to_milli returns the duration as an integer millisecond count. +int64_t dur_to_milli(Duration d); + +// dur_to_seconds returns the duration as a floating point number of seconds. +double dur_to_seconds(Duration d); + +// dur_to_minutes returns the duration as a floating point number of minutes. +double dur_to_minutes(Duration d); + +// dur_to_hours returns the duration as a floating point number of hours. +double dur_to_hours(Duration d); + +// Rounding. + +// dur_truncate returns the result of rounding d toward zero to a multiple of m. +Duration dur_truncate(Duration d, Duration m); + +// dur_round returns the result of rounding d to the nearest multiple of m. +Duration dur_round(Duration d, Duration m); + +// dur_abs returns the absolute value of d. +Duration dur_abs(Duration d); + +#endif /* TIMEX_H */ diff --git a/test/time.sql b/test/time.sql new file mode 100644 index 00000000..4a772bd4 --- /dev/null +++ b/test/time.sql @@ -0,0 +1,284 @@ +-- Copyright (c) 2024 Anton Zhiyanov, MIT License +-- https://github.com/nalgeon/sqlean + +.load dist/time + +-- 2011-11-18 00:00:00 = 1321574400 sec +-- 2011-11-18 15:56:35 = 1321631795 sec +-- 2011-11-18 15:56:35.666777888 = 1321631795666777888 nsec + +-- time_date +select '01_01', time_to_unix(time_date(2011, 11, 18)) = 1321574400; +select '01_02', time_to_unix(time_date(2011, 11, 18, 15, 56, 35)) = 1321631795; +select '01_03', time_to_unix(time_date(2011, 11, 18, 15, 56, 35, 666777888)) = 1321631795; +select '01_04', time_to_nano(time_date(2011, 11, 18, 15, 56, 35, 666777888)) = 1321631795666777888; +select '01_05', time_to_unix(time_date(2011, 11, 18, 16, 56, 35, 0, 3600)) = 1321631795; +select '01_06', time_to_unix(time_date(2011, 11, 18, 14, 56, 35, 0, -3600)) = 1321631795; + +-- time_get_x +-- 2011-11-18 15:56:35.666777888 +select '11_01', time_get_year(time_unix(1321631795, 666777888)) = 2011; +select '11_02', time_get_month(time_unix(1321631795, 666777888)) = 11; +select '11_03', time_get_day(time_unix(1321631795, 666777888)) = 18; +select '11_04', time_get_hour(time_unix(1321631795, 666777888)) = 15; +select '11_05', time_get_minute(time_unix(1321631795, 666777888)) = 56; +select '11_06', time_get_second(time_unix(1321631795, 666777888)) = 35; +select '11_07', time_get_nano(time_unix(1321631795, 666777888)) = 666777888; +select '11_08', time_get_weekday(time_unix(1321631795, 666777888)) = 5; +select '11_09', time_get_yearday(time_unix(1321631795, 666777888)) = 322; +select '11_10', time_get_isoyear(time_unix(1321631795, 666777888)) = 2011; +select '11_11', time_get_isoweek(time_unix(1321631795, 666777888)) = 46; + +-- time_get +-- 2011-11-18 15:56:35.666777888 +select '12_01', time_get(time_unix(1321631795, 666777888), 'millennium') = 2; +select '12_02', time_get(time_unix(1321631795, 666777888), 'century') = 20; +select '12_03', time_get(time_unix(1321631795, 666777888), 'decade') = 201; +select '12_04', time_get(time_unix(1321631795, 666777888), 'year') = 2011; +select '12_05', time_get(time_unix(1321631795, 666777888), 'quarter') = 4; +select '12_06', time_get(time_unix(1321631795, 666777888), 'month') = 11; +select '12_07', time_get(time_unix(1321631795, 666777888), 'day') = 18; +select '12_08', time_get(time_unix(1321631795, 666777888), 'hour') = 15; +select '12_09', time_get(time_unix(1321631795, 666777888), 'minute') = 56; +select '12_10', time_get(time_unix(1321631795, 666777888), 'second') = 35.666777888; +select '12_11', time_get(time_unix(1321631795, 666777888), 'milli') = 666; +select '12_12', time_get(time_unix(1321631795, 666777888), 'micro') = 666777; +select '12_13', time_get(time_unix(1321631795, 666777888), 'nano') = 666777888; +select '12_14', time_get(time_unix(1321631795, 666777888), 'isoyear') = 2011; +select '12_15', time_get(time_unix(1321631795, 666777888), 'isoweek') = 46; +select '12_16', time_get(time_unix(1321631795, 666777888), 'isodow') = 5; +select '12_17', time_get(time_unix(1321631795, 666777888), 'yearday') = 322; +select '12_18', time_get(time_unix(1321631795, 666777888), 'weekday') = 5; +select '12_19', time_get(time_unix(1321631795, 666777888), 'epoch') = 1321631795.666777888; + +-- time_unix +select '21_01', time_unix(1321574400) = time_date(2011, 11, 18); +select '21_02', time_unix(1321631795) = time_date(2011, 11, 18, 15, 56, 35); +select '21_03', time_unix(1321631795, 666777888) = time_date(2011, 11, 18, 15, 56, 35, 666777888); +select '21_04', time_unix(0, 1321631795666777888) = time_date(2011, 11, 18, 15, 56, 35, 666777888); + +-- time_milli +select '22_01', time_milli(1321574400000) = time_date(2011, 11, 18); +select '22_02', time_milli(1321631795000) = time_date(2011, 11, 18, 15, 56, 35); +select '22_03', time_milli(1321631795666) = time_date(2011, 11, 18, 15, 56, 35, 666000000); + +-- time_micro +select '23_01', time_micro(1321574400000000) = time_date(2011, 11, 18); +select '23_02', time_micro(1321631795000000) = time_date(2011, 11, 18, 15, 56, 35); +select '23_03', time_micro(1321631795666777) = time_date(2011, 11, 18, 15, 56, 35, 666777000); + +-- time_nano +select '24_01', time_nano(1321574400000000000) = time_date(2011, 11, 18); +select '24_02', time_nano(1321631795000000000) = time_date(2011, 11, 18, 15, 56, 35); +select '24_03', time_nano(1321631795666777888) = time_date(2011, 11, 18, 15, 56, 35, 666777888); + +-- to unix time +-- 2011-11-18 15:56:35.666777888 +select '25_01', time_to_unix(time_unix(1321631795, 666777888)) = 1321631795; +select '25_02', time_to_milli(time_unix(1321631795, 666777888)) = 1321631795666; +select '25_03', time_to_micro(time_unix(1321631795, 666777888)) = 1321631795666777; +select '25_04', time_to_nano(time_unix(1321631795, 666777888)) = 1321631795666777888; + +-- time_after +select '31_01', time_after(time_date(2011, 11, 19), time_date(2011, 11, 18)) = 1; +select '31_02', time_after(time_date(2011, 11, 18, 15, 56, 35), time_date(2011, 11, 18)) = 1; +select '31_03', time_after(time_date(2011, 11, 18, 15, 56, 35, 666777888), time_date(2011, 11, 18, 15, 56, 35)) = 1; + +-- time_before +select '32_01', time_before(time_date(2011, 11, 18), time_date(2011, 11, 19)) = 1; +select '32_02', time_before(time_date(2011, 11, 18), time_date(2011, 11, 18, 15, 56, 35)) = 1; +select '32_03', time_before(time_date(2011, 11, 18, 15, 56, 35), time_date(2011, 11, 18, 15, 56, 35, 666777888)) = 1; + +-- time_compare +select '33_01', time_compare(time_date(2011, 11, 18), time_date(2011, 11, 18)) = 0; +select '33_02', time_compare(time_date(2011, 11, 18), time_date(2011, 11, 19)) = -1; +select '33_03', time_compare(time_date(2011, 11, 19), time_date(2011, 11, 18)) = 1; +select '33_04', time_compare(time_date(2011, 11, 18, 15, 56, 35), time_date(2011, 11, 18, 15, 56, 35)) = 0; +select '33_05', time_compare(time_date(2011, 11, 18), time_date(2011, 11, 18, 15, 56, 35)) = -1; +select '33_06', time_compare(time_date(2011, 11, 18, 15, 56, 35), time_date(2011, 11, 18)) = 1; +select '33_07', time_compare(time_date(2011, 11, 18, 15, 56, 35, 666777888), time_date(2011, 11, 18, 15, 56, 35, 666777888)) = 0; +select '33_08', time_compare(time_date(2011, 11, 18, 15, 56, 35), time_date(2011, 11, 18, 15, 56, 35, 666777888)) = -1; +select '33_09', time_compare(time_date(2011, 11, 18, 15, 56, 35, 666777888), time_date(2011, 11, 18, 15, 56, 35)) = 1; + +-- time_equal +select '34_01', time_date(2011, 11, 18) = time_date(2011, 11, 18); +select '34_02', time_date(2011, 11, 18) <> time_date(2011, 11, 19); +select '34_03', time_equal(time_date(2011, 11, 18), time_date(2011, 11, 18)) = 1; +select '34_04', time_equal(time_date(2011, 11, 18), time_date(2011, 11, 19)) = 0; +select '34_05', time_date(2011, 11, 18, 15, 56, 35) = time_date(2011, 11, 18, 15, 56, 35); +select '34_06', time_date(2011, 11, 18, 15, 56, 35) <> time_date(2011, 11, 18); +select '34_07', time_equal(time_date(2011, 11, 18, 15, 56, 35), time_date(2011, 11, 18, 15, 56, 35)) = 1; +select '34_08', time_equal(time_date(2011, 11, 18, 15, 56, 35), time_date(2011, 11, 18)) = 0; +select '34_09', time_date(2011, 11, 18, 15, 56, 35, 666777888) = time_date(2011, 11, 18, 15, 56, 35, 666777888); +select '34_10', time_date(2011, 11, 18, 15, 56, 35) <> time_date(2011, 11, 18, 15, 56, 35, 666777888); +select '34_11', time_equal(time_date(2011, 11, 18, 15, 56, 35, 666777888), time_date(2011, 11, 18, 15, 56, 35, 666777888)) = 1; +select '34_12', time_equal(time_date(2011, 11, 18, 15, 56, 35), time_date(2011, 11, 18, 15, 56, 35, 666777888)) = 0; + +-- time_add +select '41_01', time_add(time_date(2011, 11, 18), 24*dur_h()) = time_date(2011, 11, 19); +select '41_02', time_add(time_date(2011, 11, 18, 15, 56, 35), 3*dur_h()) = time_date(2011, 11, 18, 18, 56, 35); +select '41_03', time_add(time_date(2011, 11, 18, 15, 56, 35), 60*dur_m()) = time_date(2011, 11, 18, 16, 56, 35); +select '41_04', time_add(time_date(2011, 11, 18, 15, 56, 35), 5*dur_m()) = time_date(2011, 11, 18, 16, 1, 35); +select '41_05', time_add(time_date(2011, 11, 18, 15, 56, 35), 60*dur_s()) = time_date(2011, 11, 18, 15, 57, 35); +select '41_06', time_add(time_date(2011, 11, 18, 15, 56, 35), 5*dur_s()) = time_date(2011, 11, 18, 15, 56, 40); +select '41_07', time_add(time_unix(1321631795, 0), 5*dur_s()) = time_unix(1321631795, 5000000000); +select '41_08', time_add(time_unix(1321631795, 0), 5*dur_ms()) = time_unix(1321631795, 5000000); +select '41_09', time_add(time_unix(1321631795, 0), 5*dur_us()) = time_unix(1321631795, 5000); +select '41_10', time_add(time_unix(1321631795, 0), 5*dur_ns()) = time_unix(1321631795, 5); +select '41_11', time_add(time_unix(1321631795, 0), 5) = time_unix(1321631795, 5); + +-- time_sub +select '42_01', time_sub(time_date(2011, 11, 19), time_date(2011, 11, 18)) = 24*dur_h(); +select '42_02', time_sub(time_date(2011, 11, 18, 18, 56, 35), time_date(2011, 11, 18, 15, 56, 35)) = 3*dur_h(); +select '42_03', time_sub(time_date(2011, 11, 18, 16, 56, 35), time_date(2011, 11, 18, 15, 56, 35)) = 60*dur_m(); +select '42_04', time_sub(time_date(2011, 11, 18, 16, 1, 35), time_date(2011, 11, 18, 15, 56, 35)) = 5*dur_m(); +select '42_05', time_sub(time_date(2011, 11, 18, 15, 57, 35), time_date(2011, 11, 18, 15, 56, 35)) = 60*dur_s(); +select '42_06', time_sub(time_date(2011, 11, 18, 15, 56, 40), time_date(2011, 11, 18, 15, 56, 35)) = 5*dur_s(); +select '42_07', time_sub(time_unix(1321631795, 5000000000), time_unix(1321631795, 0)) = 5*dur_s(); +select '42_08', time_sub(time_unix(1321631795, 5000000), time_unix(1321631795, 0)) = 5*dur_ms(); +select '42_09', time_sub(time_unix(1321631795, 5000), time_unix(1321631795, 0)) = 5*dur_us(); +select '42_10', time_sub(time_unix(1321631795, 5), time_unix(1321631795, 0)) = 5*dur_ns(); +select '42_11', time_sub(time_unix(1321631795, 5), time_unix(1321631795, 0)) = 5; + +-- time_since, time_until +select '43_01', time_since(time_add(time_now(), -3*dur_h()-dur_s())) / dur_h() = 3; +select '43_02', time_until(time_add(time_now(), 3*dur_h()+dur_s())) / dur_h() = 3; + +-- time_add_date: years +select '44_01', time_add_date(time_date(2011, 11, 18), 0) = time_date(2011, 11, 18); +select '44_02', time_add_date(time_date(2011, 11, 18), 1) = time_date(2012, 11, 18); +select '44_03', time_add_date(time_date(2011, 11, 18), 5) = time_date(2016, 11, 18); +select '44_04', time_add_date(time_date(2011, 11, 18), -1) = time_date(2010, 11, 18); +select '44_05', time_add_date(time_date(2011, 11, 18), -5) = time_date(2006, 11, 18); + +-- time_add_date: years, months +select '44_11', time_add_date(time_date(2011, 11, 18), 0, 0) = time_date(2011, 11, 18); +select '44_12', time_add_date(time_date(2011, 11, 18), 0, 1) = time_date(2011, 12, 18); +select '44_13', time_add_date(time_date(2011, 11, 18), 0, 5) = time_date(2012, 4, 18); +select '44_14', time_add_date(time_date(2011, 11, 18), 0, 18) = time_date(2013, 5, 18); +select '44_15', time_add_date(time_date(2011, 11, 18), 0, -1) = time_date(2011, 10, 18); +select '44_16', time_add_date(time_date(2011, 11, 18), 0, -5) = time_date(2011, 6, 18); +select '44_17', time_add_date(time_date(2011, 11, 18), 0, -18) = time_date(2010, 5, 18); +select '44_18', time_add_date(time_date(2011, 11, 18), 3, 5) = time_date(2015, 4, 18); +select '44_19', time_add_date(time_date(2011, 11, 18), 3, -5) = time_date(2014, 6, 18); + +-- time_add_date: years, months, days +select '44_21', time_add_date(time_date(2011, 11, 18), 0, 0, 0) = time_date(2011, 11, 18); +select '44_22', time_add_date(time_date(2011, 11, 18), 0, 0, 1) = time_date(2011, 11, 19); +select '44_23', time_add_date(time_date(2011, 11, 18), 0, 0, 5) = time_date(2011, 11, 23); +select '44_24', time_add_date(time_date(2011, 11, 18), 0, 0, 30) = time_date(2011, 12, 18); +select '44_25', time_add_date(time_date(2011, 11, 18), 0, 0, 500) = time_date(2013, 4, 1); +select '44_26', time_add_date(time_date(2011, 11, 18), 0, 0, -1) = time_date(2011, 11, 17); +select '44_27', time_add_date(time_date(2011, 11, 18), 0, 0, -5) = time_date(2011, 11, 13); +select '44_28', time_add_date(time_date(2011, 11, 18), 0, 0, -30) = time_date(2011, 10, 19); +select '44_29', time_add_date(time_date(2011, 11, 18), 0, 0, -500) = time_date(2010, 7, 6); +select '44_30', time_add_date(time_date(2011, 11, 18), 0, 5, 10) = time_date(2012, 4, 28); +select '44_31', time_add_date(time_date(2011, 11, 18), 0, 5, -10) = time_date(2012, 4, 8); +select '44_32', time_add_date(time_date(2011, 11, 18), 3, 5, 10) = time_date(2015, 4, 28); +select '44_33', time_add_date(time_date(2011, 11, 18), -3, -5, -10) = time_date(2008, 6, 8); +select '44_34', time_add_date(time_date(2011, 11, 18), 3, 18, 500) = time_date(2017, 9, 30); + +-- time_trunc +-- 2011-11-18 15:56:35.666777888 +select '51_01', time_trunc(time_unix(1321631795, 666777888), 'millennium') = time_date(2000, 1, 1); +select '51_02', time_trunc(time_unix(1321631795, 666777888), 'century') = time_date(2000, 1, 1); +select '51_03', time_trunc(time_unix(1321631795, 666777888), 'decade') = time_date(2010, 1, 1); +select '51_04', time_trunc(time_unix(1321631795, 666777888), 'year') = time_date(2011, 1, 1); +select '51_05', time_trunc(time_unix(1321631795, 666777888), 'quarter') = time_date(2011, 10, 1); +select '51_06', time_trunc(time_unix(1321631795, 666777888), 'month') = time_date(2011, 11, 1); +select '51_07', time_trunc(time_unix(1321631795, 666777888), 'week') = time_date(2011, 11, 12); +select '51_08', time_trunc(time_unix(1321631795, 666777888), 'day') = time_date(2011, 11, 18); +select '51_09', time_trunc(time_unix(1321631795, 666777888), 'hour') = time_date(2011, 11, 18, 15, 0, 0); +select '51_10', time_trunc(time_unix(1321631795, 666777888), 'minute') = time_date(2011, 11, 18, 15, 56, 0); +select '51_11', time_trunc(time_unix(1321631795, 666777888), 'second') = time_date(2011, 11, 18, 15, 56, 35); +select '51_12', time_trunc(time_unix(1321631795, 666777888), 'milli') = time_date(2011, 11, 18, 15, 56, 35, 666000000); +select '51_13', time_trunc(time_unix(1321631795, 666777888), 'micro') = time_date(2011, 11, 18, 15, 56, 35, 666777000); + +-- truncate to custom duration +-- 2011-11-18 15:56:35.666777888 +select '52_01', time_trunc(time_unix(1321631795, 666777888), dur_s()) = time_date(2011, 11, 18, 15, 56, 35); +select '52_02', time_trunc(time_unix(1321631795, 666777888), 30*dur_s()) = time_date(2011, 11, 18, 15, 56, 30); +select '52_03', time_trunc(time_unix(1321631795, 666777888), dur_m()) = time_date(2011, 11, 18, 15, 56, 0); +select '52_04', time_trunc(time_unix(1321631795, 666777888), 30*dur_m()) = time_date(2011, 11, 18, 15, 30, 0); +select '52_05', time_trunc(time_unix(1321631795, 666777888), dur_h()) = time_date(2011, 11, 18, 15, 0, 0); +select '52_06', time_trunc(time_unix(1321631795, 666777888), 12*dur_h()) = time_date(2011, 11, 18, 12, 0, 0); + +-- time_round +-- 2011-11-18 15:56:35.666777888 +select '53_01', time_round(time_unix(1321631795, 666777888), dur_s()) = time_date(2011, 11, 18, 15, 56, 36); +select '53_02', time_round(time_unix(1321631795, 666777888), 30*dur_s()) = time_date(2011, 11, 18, 15, 56, 30); +select '53_03', time_round(time_unix(1321631795, 666777888), dur_m()) = time_date(2011, 11, 18, 15, 57, 0); +select '53_04', time_round(time_unix(1321631795, 666777888), 30*dur_m()) = time_date(2011, 11, 18, 16, 00, 0); +select '53_05', time_round(time_unix(1321631795, 666777888), dur_h()) = time_date(2011, 11, 18, 16, 0, 0); +select '53_06', time_round(time_unix(1321631795, 666777888), 12*dur_h()) = time_date(2011, 11, 18, 12, 0, 0); + +-- time_fmt_iso +-- 2011-11-18 15:56:35.666777888 +select '61_01', time_fmt_iso(time_unix(1321631795, 666777888)) = '2011-11-18T15:56:35.666777888Z'; +select '61_02', time_fmt_iso(time_unix(1321631795, 666777888), 0) = '2011-11-18T15:56:35.666777888Z'; +select '61_03', time_fmt_iso(time_unix(1321631795, 666777888), 3*3600+30*60) = '2011-11-18T19:26:35.666777888+03:30'; +select '61_04', time_fmt_iso(time_unix(1321631795, 666777888), -3*3600-30*60) = '2011-11-18T12:26:35.666777888-03:30'; +select '61_05', time_fmt_iso(time_unix(1321631795, 0)) = '2011-11-18T15:56:35Z'; +select '61_06', time_fmt_iso(time_unix(1321631795, 0), 0) = '2011-11-18T15:56:35Z'; +select '61_07', time_fmt_iso(time_unix(1321631795, 0), 3*3600+30*60) = '2011-11-18T19:26:35+03:30'; +select '61_08', time_fmt_iso(time_unix(1321631795, 0), -3*3600-30*60) = '2011-11-18T12:26:35-03:30'; + +-- time_fmt_datetime +-- 2011-11-18 15:56:35.666777888 +select '62_01', time_fmt_datetime(time_unix(1321631795, 666777888)) = '2011-11-18 15:56:35'; +select '62_02', time_fmt_datetime(time_unix(1321631795, 666777888), 0) = '2011-11-18 15:56:35'; +select '62_03', time_fmt_datetime(time_unix(1321631795, 666777888), 3*3600+30*60) = '2011-11-18 19:26:35'; +select '62_04', time_fmt_datetime(time_unix(1321631795, 666777888), -3*3600-30*60) = '2011-11-18 12:26:35'; +select '62_05', time_fmt_datetime(time_unix(1321631795, 0)) = '2011-11-18 15:56:35'; +select '62_06', time_fmt_datetime(time_unix(1321631795, 0), 0) = '2011-11-18 15:56:35'; +select '62_07', time_fmt_datetime(time_unix(1321631795, 0), 3*3600+30*60) = '2011-11-18 19:26:35'; +select '62_08', time_fmt_datetime(time_unix(1321631795, 0), -3*3600-30*60) = '2011-11-18 12:26:35'; + +-- time_fmt_date +-- 2011-11-18 15:56:35.666777888 +select '63_01', time_fmt_date(time_unix(1321631795, 666777888)) = '2011-11-18'; +select '63_02', time_fmt_date(time_unix(1321631795, 0)) = '2011-11-18'; +select '62_04', time_fmt_date(time_unix(1321631795, 0), 12*3600) = '2011-11-19'; +select '62_05', time_fmt_date(time_unix(1321631795, 0), -12*3600) = '2011-11-18'; + +-- time_fmt_time +-- 2011-11-18 15:56:35.666777888 +select '64_01', time_fmt_time(time_unix(1321631795, 666777888)) = '15:56:35'; +select '64_02', time_fmt_time(time_unix(1321631795, 0)) = '15:56:35'; +select '64_03', time_fmt_time(time_unix(1321631795, 0), 3*3600+30*60) = '19:26:35'; +select '64_04', time_fmt_time(time_unix(1321631795, 0), -3*3600-30*60) = '12:26:35'; + +-- time_parse +-- 2011-11-18 15:56:35.666777888 +select '65_01', time_parse('2011-11-18T15:56:35.666777888Z') = time_unix(1321631795, 666777888); +select '65_02', time_parse('2011-11-18T19:26:35.666777888+03:30') = time_unix(1321631795, 666777888); +select '65_03', time_parse('2011-11-18T12:26:35.666777888-03:30') = time_unix(1321631795, 666777888); +select '65_04', time_parse('2011-11-18T15:56:35Z') = time_unix(1321631795, 0); +select '65_05', time_parse('2011-11-18T19:26:35+03:30') = time_unix(1321631795, 0); +select '65_06', time_parse('2011-11-18T12:26:35-03:30') = time_unix(1321631795, 0); +select '65_07', time_parse('2011-11-18 15:56:35') = time_unix(1321631795, 0); +select '65_08', time_parse('2011-11-18') = time_date(2011, 11, 18); +select '65_09', time_parse('15:56:35') = time_date(1, 1, 1, 15, 56, 35); + +-- duration constants +select '71_01', dur_ns() = 1; +select '71_02', dur_us() = 1000*dur_ns(); +select '71_03', dur_ms() = 1000*dur_us(); +select '71_04', dur_s() = 1000*dur_ms(); +select '71_05', dur_m() = 60*dur_s(); +select '71_06', dur_h() = 60*dur_m(); + +-- storing time as blob +create table data ( + id integer primary key, + t blob +); +insert into data values (1, time_unix(1321631790)); +insert into data values (2, time_unix(1321631795)); +insert into data values (3, time_unix(1321631795, 666777888)); +select '81_01', t = time_date(2011, 11, 18, 15, 56, 30) from data where id = 1; +select '81_02', t = time_date(2011, 11, 18, 15, 56, 35) from data where id = 2; +select '81_03', t = time_date(2011, 11, 18, 15, 56, 35, 666777888) from data where id = 3; +select '81_04', length(t) = 13 from data where id = 1; +select '81_05', max(t) = time_date(2011, 11, 18, 15, 56, 35, 666777888) from data; +select '81_06', min(t) = time_date(2011, 11, 18, 15, 56, 30) from data; diff --git a/test/time/duration.test.c b/test/time/duration.test.c new file mode 100644 index 00000000..ff315596 --- /dev/null +++ b/test/time/duration.test.c @@ -0,0 +1,249 @@ +// Copyright (c) 2024 Anton Zhiyanov, MIT License +// https://github.com/nalgeon/sqlean + +// Duration tests. + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "sqlite3ext.h" +SQLITE_EXTENSION_INIT1 + +#include "time/timex.h" + +typedef struct { + int64_t nsec; + int64_t usec; + int64_t msec; + double sec; + double min; + double hour; +} ParsedDuration; + +typedef struct { + int64_t nsec; + ParsedDuration golden; +} ToTest; + +static ToTest to_tests[] = { + {0, {.nsec = 0, .usec = 0, .msec = 0, .sec = 0.000000, .min = 0.000000, .hour = 0.000000}}, + {1, {.nsec = 1, .usec = 0, .msec = 0, .sec = 0.000000, .min = 0.000000, .hour = 0.000000}}, + {1100, + {.nsec = 1100, .usec = 1, .msec = 0, .sec = 0.000001, .min = 0.000000, .hour = 0.000000}}, + {2200000, + {.nsec = 2200000, + .usec = 2200, + .msec = 2, + .sec = 0.002200, + .min = 0.000037, + .hour = 0.000001}}, + {3300000000, + {.nsec = 3300000000, + .usec = 3300000, + .msec = 3300, + .sec = 3.300000, + .min = 0.055000, + .hour = 0.000917}}, + {245000000000, + {.nsec = 245000000000, + .usec = 245000000, + .msec = 245000, + .sec = 245.000000, + .min = 4.083333, + .hour = 0.068056}}, + {245001000000, + {.nsec = 245001000000, + .usec = 245001000, + .msec = 245001, + .sec = 245.001000, + .min = 4.083350, + .hour = 0.068056}}, + {18367001000000, + {.nsec = 18367001000000, + .usec = 18367001000, + .msec = 18367001, + .sec = 18367.001000, + .min = 306.116683, + .hour = 5.101945}}, + {480000000001, + {.nsec = 480000000001, + .usec = 480000000, + .msec = 480000, + .sec = 480.000000, + .min = 8.000000, + .hour = 0.133333}}, + {9223372036854775807, + {.nsec = 9223372036854775807, + .usec = 9223372036854775, + .msec = 9223372036854, + .sec = 9223372036.854776, + .min = 153722867.280913, + .hour = 2562047.788015}}, + {-9223372036854775807, + {.nsec = -9223372036854775807, + .usec = -9223372036854775, + .msec = -9223372036854, + .sec = -9223372036.854776, + .min = -153722867.280913, + .hour = -2562047.788015}}, +}; + +static void test_to_x(void) { + printf("test_to_x..."); + for (size_t i = 0; i < sizeof(to_tests) / sizeof(to_tests[0]); i++) { + ToTest test = to_tests[i]; + Duration d = test.nsec; + // printf("%lld\n", d); + assert(dur_to_micro(d) == test.golden.usec); + assert(dur_to_milli(d) == test.golden.msec); + assert(round(dur_to_seconds(d)) == round(test.golden.sec)); + assert(round(dur_to_minutes(d)) == round(test.golden.min)); + assert(round(dur_to_hours(d)) == round(test.golden.hour)); + } + printf("OK\n"); +} + +typedef struct { + Duration d; + double want; +} ToMinuteTest; + +static ToMinuteTest to_minute_tests[] = { + {-60000000000, -1}, {-1, -1 / 60e9}, {1, 1 / 60e9}, {60000000000, 1}, {3000, 5e-8}, +}; + +static void test_to_minutes(void) { + printf("test_to_minutes..."); + for (size_t i = 0; i < sizeof(to_minute_tests) / sizeof(to_minute_tests[0]); i++) { + ToMinuteTest test = to_minute_tests[i]; + double got = dur_to_minutes(test.d); + // printf("want %f, got %f\n", test.want, got); + assert(got == test.want); + } + printf("OK\n"); +} + +typedef struct { + Duration d; + double want; +} ToHourTest; + +static ToHourTest to_hour_tests[] = { + {-3600000000000, -1}, {-1, -1 / 3600e9}, {1, 1 / 3600e9}, {3600000000000, 1}, {36, 1e-11}, +}; + +static void test_to_hours(void) { + printf("test_to_hours..."); + for (size_t i = 0; i < sizeof(to_hour_tests) / sizeof(to_hour_tests[0]); i++) { + ToHourTest test = to_hour_tests[i]; + double got = dur_to_hours(test.d); + // printf("want %f, got %f\n", test.want, got); + assert(got == test.want); + } + printf("OK\n"); +} + +typedef struct { + Duration d, m, want; +} RoundTest; + +static RoundTest truncate_tests[] = { + {0, 1e9, 0}, // 0 / 1s = 0 + {60 * 1e9, -7 * 1e9, 60 * 1e9}, // 1m / -7s = 1m + {60 * 1e9, 0, 60 * 1e9}, // 1m / 0 = 1m + {60 * 1e9, 1, 60 * 1e9}, // 1m / 1 = 1m + {60 * 1e9 + 10 * 1e9, 10 * 1e9, 60 * 1e9 + 10 * 1e9}, // 1m + 10s / 10s = 1m + 10s + {2 * 60 * 1e9 + 10 * 1e9, 60 * 1e9, 2 * 60 * 1e9}, // 2m + 10s / 1m = 2m + {10 * 60 * 1e9 + 10 * 1e9, 3 * 60 * 1e9, 9 * 60 * 1e9}, // 10m + 10s / 3m = 9m + {60 * 1e9 + 10 * 1e9, 60 * 1e9 + 10 * 1e9 + 1, 0}, // 1m + 10s / 1m + 10s + 1 = 0 + {60 * 1e9 + 10 * 1e9, 3600 * 1e9, 0}, // 1m + 10s / 1h = 0 + {-60 * 1e9, 1e9, -60 * 1e9}, // -1m / 1s = -1m + {-10 * 60 * 1e9, 3 * 60 * 1e9, -9 * 60 * 1e9}, // -10m / 3m = -9m + {-10 * 60 * 1e9, 3600 * 1e9, 0}, // -10m / 1h = 0 +}; + +static void test_truncate(void) { + printf("test_truncate..."); + for (size_t i = 0; i < sizeof(truncate_tests) / sizeof(truncate_tests[0]); i++) { + RoundTest test = truncate_tests[i]; + Duration got = dur_truncate(test.d, test.m); + // printf("want %lld, got %lld\n", test.want, got); + assert(got == test.want); + } + printf("OK\n"); +} + +static RoundTest round_tests[] = { + {0, 1e9, 0}, // 0 / 1s = 0 + {60 * 1e9, -11 * 1e9, 60 * 1e9}, // 1m / -11s = 1m + {60 * 1e9, 0, 60 * 1e9}, // 1m / 0 = 1m + {60 * 1e9, 1, 60 * 1e9}, // 1m / 1 = 1m + {2 * 60 * 1e9, 60 * 1e9, 2 * 60 * 1e9}, // 2m / 1m = 2m + {2 * 60 * 1e9 + 10 * 1e9, 60 * 1e9, 2 * 60 * 1e9}, // 2m + 10s / 1m = 2m + {2 * 60 * 1e9 + 30 * 1e9, 60 * 1e9, 3 * 60 * 1e9}, // 2m + 30s / 1m = 3m + {2 * 60 * 1e9 + 50 * 1e9, 60 * 1e9, 3 * 60 * 1e9}, // 2m + 50s / 1m = 3m + {-60 * 1e9, 1, -60 * 1e9}, // -1m / 1 = -1m + {-2 * 60 * 1e9, 60 * 1e9, -2 * 60 * 1e9}, // -2m / 1m = -2m + {-2 * 60 * 1e9 - 10 * 1e9, 60 * 1e9, -2 * 60 * 1e9}, // -2m - 10s / 1m = -2m + {-2 * 60 * 1e9 - 30 * 1e9, 60 * 1e9, -3 * 60 * 1e9}, // -2m - 30s / 1m = -3m, + {-2 * 60 * 1e9 - 50 * 1e9, 60 * 1e9, -3 * 60 * 1e9}, // -2m - 50s / 1m = -3m, + {8e18, 3e18, 9e18}, + {9e18, 5e18, 9223372036854775807}, + {-8e18, 3e18, -9e18}, + {-9e18, 5e18, -(1LL << 63)}, + {(3LL << 61) - 1, 3LL << 61, 3LL << 61}, +}; + +static void test_round(void) { + printf("test_round..."); + for (size_t i = 0; i < sizeof(round_tests) / sizeof(round_tests[0]); i++) { + RoundTest test = round_tests[i]; + Duration got = dur_round(test.d, test.m); + // printf("want %lld, got %lld\n", test.want, got); + assert(got == test.want); + } + printf("OK\n"); +} + +typedef struct { + Duration d, want; +} AbsTest; + +static AbsTest abs_tests[] = { + {0, 0}, + {1, 1}, + {-1, 1}, + {1 * 60 * 1e9, 1 * 60 * 1e9}, + {-1 * 60 * 1e9, 1 * 60 * 1e9}, + {MIN_DURATION, MAX_DURATION}, + {MIN_DURATION + 1, MAX_DURATION}, + {MIN_DURATION + 2, MAX_DURATION - 1}, + {MAX_DURATION, MAX_DURATION}, + {MAX_DURATION - 1, MAX_DURATION - 1}, +}; + +static void test_abs(void) { + printf("test_abs..."); + for (size_t i = 0; i < sizeof(abs_tests) / sizeof(abs_tests[0]); i++) { + AbsTest test = abs_tests[i]; + Duration got = dur_abs(test.d); + // printf("want %lld, got %lld\n", test.want, got); + assert(got == test.want); + } + printf("OK\n"); +} + +int main(void) { + test_to_x(); + test_to_minutes(); + test_to_hours(); + test_truncate(); + test_round(); + test_abs(); +} diff --git a/test/time/time.test.c b/test/time/time.test.c new file mode 100644 index 00000000..ac3ddf03 --- /dev/null +++ b/test/time/time.test.c @@ -0,0 +1,929 @@ +// Copyright (c) 2024 Anton Zhiyanov, MIT License +// https://github.com/nalgeon/sqlean + +// Time tests. + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "sqlite3ext.h" +SQLITE_EXTENSION_INIT1 + +#include "time/timex.h" + +typedef struct { + int year; + enum Month month; + int day; + int hour; + int min; + int sec; + int nsec; + enum Weekday weekday; +} ParsedTime; + +typedef struct { + int64_t sec; + int64_t nsec; + ParsedTime golden; +} TimeTest; + +#pragma region Constructors. + +typedef struct { + int year, month, day, hour, min, sec, nsec, offset_sec; + int64_t unix; +} DateTest; + +static DateTest date_tests[] = { + {2011, 11, 6, 8, 0, 0, 0, TIMEX_UTC, 1320566400}, // 8:00:00 UTC + {2011, 11, 6, 8, 59, 59, 0, TIMEX_UTC, 1320569999}, // 8:59:59 UTC + {2011, 11, 6, 10, 0, 0, 0, TIMEX_UTC, 1320573600}, // 10:00:00 UTC + + {2011, 3, 13, 9, 0, 0, 0, TIMEX_UTC, 1300006800}, // 9:00:00 UTC + {2011, 3, 13, 9, 59, 59, 0, TIMEX_UTC, 1300010399}, // 9:59:59 UTC + {2011, 3, 13, 10, 0, 0, 0, TIMEX_UTC, 1300010400}, // 10:00:00 UTC + {2011, 3, 13, 9, 30, 0, 0, TIMEX_UTC, 1300008600}, // 9:30:00 UTC + {2012, 12, 24, 8, 0, 0, 0, TIMEX_UTC, 1356336000}, // Leap year + + // Many names for 2011-11-18 15:56:35.0 UTC + {2011, 11, 18, 15, 56, 35, 0, TIMEX_UTC, 1321631795}, // Nov 18 15:56:35 + {2011, 11, 19, -9, 56, 35, 0, TIMEX_UTC, 1321631795}, // Nov 19 -9:56:35 + {2011, 11, 17, 39, 56, 35, 0, TIMEX_UTC, 1321631795}, // Nov 17 39:56:35 + {2011, 11, 18, 14, 116, 35, 0, TIMEX_UTC, 1321631795}, // Nov 18 14:116:35 + {2011, 10, 49, 15, 56, 35, 0, TIMEX_UTC, 1321631795}, // Oct 49 15:56:35 + {2011, 11, 18, 15, 55, 95, 0, TIMEX_UTC, 1321631795}, // Nov 18 15:55:95 + {2011, 11, 18, 15, 56, 34, 1e9, TIMEX_UTC, 1321631795}, // Nov 18 15:56:34 + 10⁹ns + {2011, 12, -12, 15, 56, 35, 0, TIMEX_UTC, 1321631795}, // Dec -12 15:56:35 + {2012, 1, -43, 15, 56, 35, 0, TIMEX_UTC, 1321631795}, // 2012 Jan -43 15:56:35 + {2012, January - 2, 18, 15, 56, 35, 0, TIMEX_UTC, 1321631795}, // 2012 (Jan-2) 18 15:56:35 + {2010, December + 11, 18, 15, 56, 35, 0, TIMEX_UTC, 1321631795}, // 2010 (Dec+11) 18 15:56:35 + {1970, 1, 15297, 15, 56, 35, 0, TIMEX_UTC, 1321631795}, // large number of days + + {2011, 11, 18, 10, 56, 35, 0, -5 * 3600, 1321631795}, // UTC-5 + {2011, 11, 18, 3, 56, 35, 0, -12 * 3600, 1321631795}, // UTC-12 + {2011, 11, 18, 16, 56, 35, 0, 1 * 3600, 1321631795}, // UTC+1 + {2011, 11, 19, 3, 56, 35, 0, 12 * 3600, 1321631795}, // UTC+12 + + {1970, 1, -25508, 8, 0, 0, 0, TIMEX_UTC, -2203948800}, // negative Unix time +}; + +static void test_date(void) { + printf("test_date..."); + for (size_t i = 0; i < sizeof(date_tests) / sizeof(date_tests[0]); i++) { + DateTest test = date_tests[i]; + Time t = time_date(test.year, test.month, test.day, test.hour, test.min, test.sec, + test.nsec, test.offset_sec); + assert(time_to_unix(t) == test.unix); + } + printf("OK\n"); +} + +#pragma endregion + +#pragma region Time parts. + +static TimeTest unix_tests[] = { + {0, 0, {1970, January, 1, 0, 0, 0, 0, Thursday}}, + {1221681866, 0, {2008, September, 17, 20, 4, 26, 0, Wednesday}}, + {-1221681866, 0, {1931, April, 16, 3, 55, 34, 0, Thursday}}, + {-11644473600, 0, {1601, January, 1, 0, 0, 0, 0, Monday}}, + {599529660, 0, {1988, December, 31, 0, 1, 0, 0, Saturday}}, + {978220860, 0, {2000, December, 31, 0, 1, 0, 0, Sunday}}, + {0, 1e8, {1970, January, 1, 0, 0, 0, 1e8, Thursday}}, + {1221681866, 2e8, {2008, September, 17, 20, 4, 26, 2e8, Wednesday}}, +}; + +static void test_get_part(void) { + printf("test_get_part..."); + for (size_t i = 0; i < sizeof(unix_tests) / sizeof(unix_tests[0]); i++) { + TimeTest test = unix_tests[i]; + Time t = time_unix(test.sec, test.nsec); + assert(time_get_year(t) == test.golden.year); + assert(time_get_month(t) == test.golden.month); + assert(time_get_day(t) == test.golden.day); + assert(time_get_hour(t) == test.golden.hour); + assert(time_get_minute(t) == test.golden.min); + assert(time_get_second(t) == test.golden.sec); + assert(time_get_nano(t) == test.golden.nsec); + assert(time_get_weekday(t) == test.golden.weekday); + } + for (size_t i = 0; i < 8; i++) { + DateTest test = date_tests[i]; + Time t = time_unix(test.unix, 0); + assert(time_get_year(t) == test.year); + assert(time_get_month(t) == test.month); + assert(time_get_day(t) == test.day); + assert(time_get_hour(t) == test.hour); + assert(time_get_minute(t) == test.min); + assert(time_get_second(t) == test.sec); + assert(time_get_nano(t) == test.nsec); + } + printf("OK\n"); +} + +typedef struct { + int year; + int month; + int day; + int yex; // expected year + int wex; // expected week +} ISOWeekTest; + +static ISOWeekTest isoweek_tests[] = { + {2014, 1, 1, 2014, 1}, {2014, 1, 5, 2014, 1}, {2014, 1, 6, 2014, 2}, {2015, 1, 1, 2015, 1}, + {2016, 1, 1, 2015, 53}, {2017, 1, 1, 2016, 52}, {2018, 1, 1, 2018, 1}, {2019, 1, 1, 2019, 1}, + {2020, 1, 1, 2020, 1}, {2021, 1, 1, 2020, 53}, {2022, 1, 1, 2021, 52}, {2023, 1, 1, 2022, 52}, + {2024, 1, 1, 2024, 1}, {2025, 1, 1, 2025, 1}, {2026, 1, 1, 2026, 1}, {2027, 1, 1, 2026, 53}, + {2028, 1, 1, 2027, 52}, {2029, 1, 1, 2029, 1}, {2030, 1, 1, 2030, 1}, {2031, 1, 1, 2031, 1}, + {2032, 1, 1, 2032, 1}, {2033, 1, 1, 2032, 53}, {2034, 1, 1, 2033, 52}, {2035, 1, 1, 2035, 1}, + {2036, 1, 1, 2036, 1}, {2037, 1, 1, 2037, 1}, {2038, 1, 1, 2037, 53}, {2039, 1, 1, 2038, 52}, + {2040, 1, 1, 2039, 52}, +}; + +static void test_get_isoweek(void) { + printf("test_get_isoweek..."); + for (size_t i = 0; i < sizeof(isoweek_tests) / sizeof(isoweek_tests[0]); i++) { + ISOWeekTest test = isoweek_tests[i]; + Time t = time_date(test.year, test.month, test.day, 0, 0, 0, 0, TIMEX_UTC); + int year, week; + time_get_isoweek(t, &year, &week); + // printf("%d %d %d, want %d %d, got %d %d\n", test.year, test.month, test.day, test.yex, + // test.wex, year, week); + assert(year == test.yex && week == test.wex); + } + printf("OK\n"); +} + +typedef struct { + int year, month, day; + int yday; +} YearDayTest; + +// Test YearDay in several different scenarios and corner cases +static YearDayTest yearday_tests[] = { + // Non-leap-year tests + {2007, 1, 1, 1}, + {2007, 1, 15, 15}, + {2007, 2, 1, 32}, + {2007, 2, 15, 46}, + {2007, 3, 1, 60}, + {2007, 3, 15, 74}, + {2007, 4, 1, 91}, + {2007, 12, 31, 365}, + + // Leap-year tests + {2008, 1, 1, 1}, + {2008, 1, 15, 15}, + {2008, 2, 1, 32}, + {2008, 2, 15, 46}, + {2008, 3, 1, 61}, + {2008, 3, 15, 75}, + {2008, 4, 1, 92}, + {2008, 12, 31, 366}, + + // Looks like leap-year (but isn't) tests + {1900, 1, 1, 1}, + {1900, 1, 15, 15}, + {1900, 2, 1, 32}, + {1900, 2, 15, 46}, + {1900, 3, 1, 60}, + {1900, 3, 15, 74}, + {1900, 4, 1, 91}, + {1900, 12, 31, 365}, + + // Year one tests (non-leap) + {1, 1, 1, 1}, + {1, 1, 15, 15}, + {1, 2, 1, 32}, + {1, 2, 15, 46}, + {1, 3, 1, 60}, + {1, 3, 15, 74}, + {1, 4, 1, 91}, + {1, 12, 31, 365}, + + // Year minus one tests (non-leap) + {-1, 1, 1, 1}, + {-1, 1, 15, 15}, + {-1, 2, 1, 32}, + {-1, 2, 15, 46}, + {-1, 3, 1, 60}, + {-1, 3, 15, 74}, + {-1, 4, 1, 91}, + {-1, 12, 31, 365}, + + // 400 BC tests (leap-year) + {-400, 1, 1, 1}, + {-400, 1, 15, 15}, + {-400, 2, 1, 32}, + {-400, 2, 15, 46}, + {-400, 3, 1, 61}, + {-400, 3, 15, 75}, + {-400, 4, 1, 92}, + {-400, 12, 31, 366}, + + // Special Cases + + // Gregorian calendar change (no effect) + {1582, 10, 4, 277}, + {1582, 10, 15, 288}, +}; + +static void test_get_yearday(void) { + printf("test_get_yearday..."); + for (size_t i = 0; i < sizeof(yearday_tests) / sizeof(yearday_tests[0]); i++) { + YearDayTest test = yearday_tests[i]; + Time t = time_date(test.year, test.month, test.day, 0, 0, 0, 0, TIMEX_UTC); + int yday = time_get_yearday(t); + // printf("%d %d %d, want %d, got %d\n", test.year, test.month, test.day, test.yday, yday); + assert(yday == test.yday); + } + printf("OK\n"); +} + +#pragma endregion + +#pragma region Unix time. + +static bool same(Time t, ParsedTime u) { + int year, day; + enum Month month; + time_get_date(t, &year, &month, &day); + int hour, min, sec; + time_get_clock(t, &hour, &min, &sec); + enum Weekday weekday = time_get_weekday(t); + return year == u.year && month == u.month && day == u.day && hour == u.hour && min == u.min && + sec == u.sec && t.nsec == u.nsec && weekday == u.weekday; +} + +static void test_unix(void) { + printf("test_unix..."); + for (size_t i = 0; i < sizeof(unix_tests) / sizeof(unix_tests[0]); i++) { + TimeTest test = unix_tests[i]; + Time t = time_unix(test.sec, test.nsec); + assert(same(t, test.golden)); + } + printf("OK\n"); +} + +static void test_milli(void) { + printf("test_milli..."); + for (size_t i = 0; i < sizeof(unix_tests) / sizeof(unix_tests[0]); i++) { + TimeTest test = unix_tests[i]; + int64_t msec = test.sec * 1000 + test.nsec / 1000000; + Time t = time_milli(msec); + assert(same(t, test.golden)); + } + printf("OK\n"); +} + +static void test_micro(void) { + printf("test_micro..."); + for (size_t i = 0; i < sizeof(unix_tests) / sizeof(unix_tests[0]); i++) { + TimeTest test = unix_tests[i]; + int64_t usec = test.sec * 1000000 + test.nsec / 1000; + Time t = time_micro(usec); + assert(same(t, test.golden)); + } + printf("OK\n"); +} + +static void test_to_unix(void) { + printf("test_to_unix..."); + for (size_t i = 0; i < sizeof(unix_tests) / sizeof(unix_tests[0]); i++) { + TimeTest test = unix_tests[i]; + Time t = time_unix(test.sec, test.nsec); + assert(time_to_unix(t) == test.sec); + } + printf("OK\n"); +} + +static void test_to_milli(void) { + printf("test_to_milli..."); + for (size_t i = 0; i < sizeof(unix_tests) / sizeof(unix_tests[0]); i++) { + TimeTest test = unix_tests[i]; + Time t = time_unix(test.sec, test.nsec); + int64_t msec = test.sec * 1000 + test.nsec / 1000000; + assert(time_to_milli(t) == msec); + } + printf("OK\n"); +} + +static void test_to_micro(void) { + printf("test_to_micro..."); + for (size_t i = 0; i < sizeof(unix_tests) / sizeof(unix_tests[0]); i++) { + TimeTest test = unix_tests[i]; + Time t = time_unix(test.sec, test.nsec); + int64_t usec = test.sec * 1000000 + test.nsec / 1000; + assert(time_to_micro(t) == usec); + } + printf("OK\n"); +} + +static void test_to_nano(void) { + printf("test_to_nano..."); + for (size_t i = 0; i < sizeof(unix_tests) / sizeof(unix_tests[0]); i++) { + TimeTest test = unix_tests[i]; + Time t = time_unix(test.sec, test.nsec); + assert(time_to_nano(t) == test.sec * 1000000000 + test.nsec); + } + printf("OK\n"); +} + +#pragma endregion + +#pragma region Calendar time. + +typedef struct { + int year, month, day, hour, min, sec, offset_sec; + int tm_year, tm_mon, tm_mday, tm_hour, tm_min, tm_sec; +} CalendarTest; + +static CalendarTest calendar_tests[] = { + {2011, 11, 18, 15, 56, 35, TIMEX_UTC, 111, 10, 18, 15, 56, 35}, + {1901, 11, 18, 15, 56, 35, TIMEX_UTC, 1, 10, 18, 15, 56, 35}, + {1900, 11, 18, 15, 56, 35, TIMEX_UTC, 0, 10, 18, 15, 56, 35}, + {1800, 11, 18, 15, 56, 35, TIMEX_UTC, -100, 10, 18, 15, 56, 35}, + {1, 1, 1, 0, 0, 0, TIMEX_UTC, -1899, 0, 1, 0, 0, 0}, + + {2011, 11, 18, 15, 56, 35, -5 * 3600, 111, 10, 18, 10, 56, 35}, + {2011, 11, 18, 15, 56, 35, 1 * 3600, 111, 10, 18, 16, 56, 35}, + {2011, 11, 18, 15, 56, 35, 12 * 3600, 111, 10, 19, 3, 56, 35}, + {2011, 11, 18, 15, 56, 35, -12 * 3600, 111, 10, 18, 3, 56, 35}, +}; + +static void test_tm(void) { + printf("test_tm..."); + for (size_t i = 0; i < sizeof(calendar_tests) / sizeof(calendar_tests[0]); i++) { + CalendarTest test = calendar_tests[i]; + Time want = + time_date(test.year, test.month, test.day, test.hour, test.min, test.sec, 0, TIMEX_UTC); + struct tm tm = {.tm_year = test.tm_year, + .tm_mon = test.tm_mon, + .tm_mday = test.tm_mday, + .tm_hour = test.tm_hour, + .tm_min = test.tm_min, + .tm_sec = test.tm_sec}; + Time got = time_tm(tm, test.offset_sec); + // printf("%d-%d-%d %d:%d:%d +%d, want %lld, got %lld\n", test.tm_year, test.tm_mon, + // test.tm_mday, test.tm_hour, test.tm_min, test.tm_sec, test.offset_sec / 3600, + // time_to_unix(want), time_to_unix(got)); + assert(time_equal(want, got)); + } + printf("OK\n"); +} + +static void test_to_tm(void) { + printf("test_to_tm..."); + for (size_t i = 0; i < sizeof(calendar_tests) / sizeof(calendar_tests[0]); i++) { + CalendarTest test = calendar_tests[i]; + Time t = + time_date(test.year, test.month, test.day, test.hour, test.min, test.sec, 0, TIMEX_UTC); + struct tm tm = time_to_tm(t, test.offset_sec); + assert(tm.tm_year == test.tm_year); + assert(tm.tm_mon == test.tm_mon); + assert(tm.tm_mday == test.tm_mday); + assert(tm.tm_hour == test.tm_hour); + assert(tm.tm_min == test.tm_min); + assert(tm.tm_sec == test.tm_sec); + } + printf("OK\n"); +} + +#pragma endregion + +#pragma region Comparisons. + +typedef struct { + ParsedTime t1, t2; + int cmp; +} CompareTest; + +static CompareTest compare_tests[] = { + { + {2011, 11, 18, 15, 56, 35, 1321631795}, + {2011, 11, 18, 15, 56, 35, 1321631795}, + 0, + }, + { + {2011, 11, 18, 15, 56, 35, 0}, + {2011, 11, 18, 15, 56, 35, 1321631795}, + -1, + }, + { + {2011, 11, 18, 15, 56, 35, 1321631795}, + {2011, 11, 18, 15, 56, 35, 0}, + 1, + }, + { + {2011, 11, 18, 15, 56, 25, 1321631795}, + {2011, 11, 18, 15, 56, 35, 1321631795}, + -1, + }, + { + {2011, 11, 18, 15, 56, 45, 1321631795}, + {2011, 11, 18, 15, 56, 35, 1321631795}, + 1, + }, + { + {2011, 11, 18, 15, 55, 35, 1321631795}, + {2011, 11, 18, 15, 56, 35, 1321631795}, + -1, + }, + { + {2011, 11, 18, 15, 57, 35, 1321631795}, + {2011, 11, 18, 15, 56, 35, 1321631795}, + 1, + }, + { + {2011, 11, 18, 14, 56, 35, 1321631795}, + {2011, 11, 18, 15, 56, 35, 1321631795}, + -1, + }, + { + {2011, 11, 18, 16, 56, 35, 1321631795}, + {2011, 11, 18, 15, 56, 35, 1321631795}, + 1, + }, + { + {2011, 11, 17, 15, 56, 35, 1321631795}, + {2011, 11, 18, 15, 56, 35, 1321631795}, + -1, + }, + { + {2011, 11, 19, 15, 56, 35, 1321631795}, + {2011, 11, 18, 15, 56, 35, 1321631795}, + 1, + }, + { + {2011, 10, 18, 15, 56, 35, 1321631795}, + {2011, 11, 18, 15, 56, 35, 1321631795}, + -1, + }, + { + {2011, 12, 18, 15, 56, 35, 1321631795}, + {2011, 11, 18, 15, 56, 35, 1321631795}, + 1, + }, + { + {2010, 11, 18, 15, 56, 35, 1321631795}, + {2011, 11, 18, 15, 56, 35, 1321631795}, + -1, + }, + { + {2012, 11, 18, 15, 56, 35, 1321631795}, + {2011, 11, 18, 15, 56, 35, 1321631795}, + 1, + }, +}; + +static void test_compare(void) { + printf("test_compare..."); + for (size_t i = 0; i < sizeof(compare_tests) / sizeof(compare_tests[0]); i++) { + CompareTest test = compare_tests[i]; + Time t1 = time_date(test.t1.year, test.t1.month, test.t1.day, test.t1.hour, test.t1.min, + test.t1.sec, test.t1.nsec, TIMEX_UTC); + Time t2 = time_date(test.t2.year, test.t2.month, test.t2.day, test.t2.hour, test.t2.min, + test.t2.sec, test.t2.nsec, TIMEX_UTC); + int cmp = time_compare(t1, t2); + // printf("{%lld %lld} vs {%lld %lld}: want %d, got %d\n", t1.sec, t1.nsec, t2.sec, t2.nsec, + // test.cmp, cmp); + assert(cmp == test.cmp); + if (cmp == 0) { + assert(time_equal(t1, t2)); + assert(!time_before(t1, t2)); + assert(!time_after(t1, t2)); + } else if (cmp < 0) { + assert(!time_equal(t1, t2)); + assert(time_before(t1, t2)); + assert(!time_after(t1, t2)); + } else { + assert(!time_equal(t1, t2)); + assert(!time_before(t1, t2)); + assert(time_after(t1, t2)); + } + } + printf("OK\n"); +} + +static void test_is_zero(void) { + printf("test_is_zero..."); + Time t1 = time_date(2011, 11, 18, 15, 56, 35, 1321631795, TIMEX_UTC); + assert(!time_is_zero(t1)); + Time t2 = time_date(1, 1, 1, 0, 0, 0, 0, TIMEX_UTC); + assert(time_is_zero(t2)); + Time zero = {0, 0}; + assert(time_is_zero(zero)); + printf("OK\n"); +} + +#pragma endregion + +#pragma region Arithmetic. + +typedef struct { + Time t; + Duration d; + Time r; +} AddTest; + +static AddTest add_tests[] = { + {{0, 0}, 0, {0, 0}}, + // 2009-11-23 00:00:00 +1ns + {{1258916400, 0}, 1, {1258916400, 1}}, + // 2009-11-23 + 24h = 2009-11-24 + {{1258916400, 0}, 24 * 3600e9, {1259002800, 0}}, + // 2009-11-24 - 24h = 2009-11-23 + {{1259002800, 0}, -24 * 3600e9, {1258916400, 0}}, + // -2009-11-23 +24h = -2009-11-24 + {{-1259002800, 0}, 24 * 3600e9, {-1258916400, 0}}, + // 2000-01-01 + (290*365*24h + 71*24h) = 2290-01-01 + {{946684800, 0}, (290 * 365 + 71) * (24 * 3600e9), {10098259200, 0}}, + // 2290-01-01 - 290*365*24h + 71*24h = 2000-01-01 + {{10098259200, 0}, -(290 * 365 + 71) * (24 * 3600e9), {946684800, 0}}, + // 2019-08-16 02:29:30.268436582 + 9223372036795099414 = 2311-11-26 02:16:47.63535996 + {{1565922570, 268436582}, 9223372036795099414, {10789294607, 63535996}}, +}; + +static void test_add(void) { + printf("test_add..."); + for (size_t i = 0; i < sizeof(add_tests) / sizeof(add_tests[0]); i++) { + AddTest test = add_tests[i]; + Time t = time_add(test.t, test.d); + // printf("want %lld, got %lld\n", test.r.sec, t.sec); + assert(time_equal(t, test.r)); + } + printf("OK\n"); +} + +static void test_add_to_exact_second(void) { + // Add an amount to the current time to round it up to the next exact second. + // This test checks that the nsec field still lies within the range [0, 999999999]. + printf("test_add_to_exact_second..."); + Time t1 = time_now(); + Time t2 = time_add(t1, Second - t1.nsec); + int sec = (t1.sec + 1) % 60; + assert(time_get_second(t2) == sec && t2.nsec == 0); + printf("OK\n"); +} + +typedef struct { + Time t; + Time u; + Duration d; +} SubTest; + +static SubTest sub_tests[] = { + {{0, 0}, {0, 0}, 0}, + // 2009-11-23 00:00:00 -1ns + {{1258916400, 1}, {1258916400, 0}, 1}, + // 2009-11-23 + 24h = 2009-11-24 + {{1258916400, 0}, {1259002800, 0}, -24 * 3600e9}, + // 2009-11-24 - 24h = 2009-11-23 + {{1259002800, 0}, {1258916400, 0}, 24 * 3600e9}, + // -2009-11-24 -24h = -2009-11-23 + {{-1258916400, 0}, {-1259002800, 0}, 24 * 3600e9}, + // 0001-01-01 - min = 2109-11-23 + {{-9223372036, -854775808}, {4414590000, 0}, MIN_DURATION}, + // 2109-11-23 - max = 0001-01-01 + {{4414590000, 0}, {-9223372036, -854775808}, MAX_DURATION}, + // 0001-01-01 - max = -2109-11-23 + {{-9223372036, -854775808}, {-128692627200, 0}, MAX_DURATION}, + // 2290-01-01 - (290*365*24h + 71*24h) = 2000-01-01 + {{10098259200, 0}, {946684800, 0}, (290 * 365 + 71) * (24 * 3600e9)}, + // 2300-01-01 - max = 2000-01-01 + {{10413792000, 0}, {946684800, 0}, MAX_DURATION}, + // 2000-01-01 + 290*365*24h + 71*24h = 2290-01-01 + {{946684800, 0}, {10098259200, 0}, (-290 * 365 - 71) * (24 * 3600e9)}, + // 2000-01-01 - min = 2300-01-01 + {{946684800, 0}, {10413792000, 0}, MIN_DURATION}, + // 2311-11-26 02:16:47.63535996 - 9223372036795099414 = 2019-08-16 02:29:30.268436582 + {{10789294607, 63535996}, {1565922570, 268436582}, 9223372036795099414}, +}; + +static void test_sub(void) { + printf("test_sub..."); + for (size_t i = 0; i < sizeof(sub_tests) / sizeof(sub_tests[0]); i++) { + SubTest test = sub_tests[i]; + Duration d = time_sub(test.t, test.u); + // printf("want %lld, got %lld\n", test.d, d); + assert(d == test.d); + } + printf("OK\n"); +} + +typedef struct AddDateTest { + int years, months, days; + int y, m, d; +} AddDateTest; + +// Several ways of getting from +// Fri Nov 18 7:56:35 PST 2011 +// to +// Thu Mar 19 7:56:35 PST 2016 +static AddDateTest add_date_tests[] = { + {4, 4, 1, 2016, 3, 19}, + {3, 16, 1, 2016, 3, 19}, + {3, 15, 30, 2016, 3, 19}, + {5, -6, -18 - 30 - 12, 2016, 3, 19}, +}; + +static void test_add_date(void) { + printf("test_add_date..."); + Time t0 = time_date(2011, November, 18, 7, 56, 35, 0, TIMEX_UTC); + Time t1 = time_date(2016, March, 19, 7, 56, 35, 0, TIMEX_UTC); + for (size_t i = 0; i < sizeof(add_date_tests) / sizeof(add_date_tests[0]); i++) { + AddDateTest test = add_date_tests[i]; + Time t = time_add_date(t0, test.years, test.months, test.days); + assert(time_equal(t, t1)); + } + printf("OK\n"); +} + +#pragma endregion + +#pragma region Rounding. + +typedef struct { + ParsedTime t; + Duration d; + ParsedTime want; +} RoundTest; + +static RoundTest truncate_tests[] = { + // 1 second + {{2011, 11, 18, 15, 56, 35, 777888999}, 1e9, {2011, 11, 18, 15, 56, 35, 0}}, + // 10 seconds + {{2011, 11, 18, 15, 56, 35, 0}, 10e9, {2011, 11, 18, 15, 56, 30, 0}}, + // 30 seconds + {{2011, 11, 18, 15, 56, 35, 0}, 30e9, {2011, 11, 18, 15, 56, 30, 0}}, + // 1 minute + {{2011, 11, 18, 15, 56, 35, 0}, 60e9, {2011, 11, 18, 15, 56, 0, 0}}, + // 5 minutes + {{2011, 11, 18, 15, 56, 35, 0}, 5 * 60e9, {2011, 11, 18, 15, 55, 0, 0}}, + // 30 minutes + {{2011, 11, 18, 15, 56, 35, 0}, 30 * 60e9, {2011, 11, 18, 15, 30, 0, 0}}, + // 1 hour + {{2011, 11, 18, 15, 56, 35, 0}, 3600e9, {2011, 11, 18, 15, 0, 0, 0}}, + // 6 hours + {{2011, 11, 18, 15, 56, 35, 0}, 6 * 3600e9, {2011, 11, 18, 12, 0, 0, 0}}, + // 1 day (= 24 hours) + {{2011, 11, 18, 15, 56, 35, 0}, 86400e9, {2011, 11, 18, 0, 0, 0, 0}}, +}; + +static void test_truncate(void) { + printf("test_truncate..."); + for (size_t i = 0; i < sizeof(truncate_tests) / sizeof(truncate_tests[0]); i++) { + RoundTest test = truncate_tests[i]; + Time t = time_date(test.t.year, test.t.month, test.t.day, test.t.hour, test.t.min, + test.t.sec, test.t.nsec, TIMEX_UTC); + Time got = time_truncate(t, test.d); + Time want = time_date(test.want.year, test.want.month, test.want.day, test.want.hour, + test.want.min, test.want.sec, test.want.nsec, TIMEX_UTC); + // printf("want: {%lld %lld}, got: {%lld %lld}\n", want.sec, want.nsec, got.sec, got.nsec); + assert(time_equal(got, want)); + } + printf("OK\n"); +} + +static RoundTest round_tests[] = { + // 1 second + {{2011, 11, 18, 15, 56, 35, 777888999}, 1e9, {2011, 11, 18, 15, 56, 36, 0}}, + // 10 seconds + {{2011, 11, 18, 15, 56, 35, 0}, 10e9, {2011, 11, 18, 15, 56, 40, 0}}, + // 30 seconds + {{2011, 11, 18, 15, 56, 35, 0}, 30e9, {2011, 11, 18, 15, 56, 30, 0}}, + // 1 minute + {{2011, 11, 18, 15, 56, 35, 0}, 60e9, {2011, 11, 18, 15, 57, 0, 0}}, + // 5 minutes + {{2011, 11, 18, 15, 56, 35, 0}, 5 * 60e9, {2011, 11, 18, 15, 55, 0, 0}}, + // 30 minutes + {{2011, 11, 18, 15, 56, 35, 0}, 30 * 60e9, {2011, 11, 18, 16, 0, 0, 0}}, + // 1 hour + {{2011, 11, 18, 15, 56, 35, 0}, 3600e9, {2011, 11, 18, 16, 0, 0, 0}}, + // 6 hours + {{2011, 11, 18, 15, 56, 35, 0}, 6 * 3600e9, {2011, 11, 18, 18, 0, 0, 0}}, + // 1 day (= 24 hours) + {{2011, 11, 18, 15, 56, 35, 0}, 86400e9, {2011, 11, 19, 0, 0, 0, 0}}, +}; + +static void test_round(void) { + printf("test_round..."); + for (size_t i = 0; i < sizeof(round_tests) / sizeof(round_tests[0]); i++) { + RoundTest test = round_tests[i]; + Time t = time_date(test.t.year, test.t.month, test.t.day, test.t.hour, test.t.min, + test.t.sec, test.t.nsec, TIMEX_UTC); + Time got = time_round(t, test.d); + Time want = time_date(test.want.year, test.want.month, test.want.day, test.want.hour, + test.want.min, test.want.sec, test.want.nsec, TIMEX_UTC); + // printf("want: {%lld %lld}, got: {%lld %lld}\n", want.sec, want.nsec, got.sec, got.nsec); + assert(time_equal(got, want)); + } + printf("OK\n"); +} + +#pragma endregion + +#pragma region Formatting. + +typedef struct { + int year, month, day, hour, min, sec, nsec; + const char* want; + int offset_sec; +} FormatTest; + +FormatTest fmt_iso_tests[] = { + {2011, 11, 18, 15, 56, 35, 0, "2011-11-18T15:56:35Z", TIMEX_UTC}, + {2011, 11, 18, 15, 56, 35, 666777888, "2011-11-18T15:56:35.666777888Z", TIMEX_UTC}, + {2011, 11, 18, 15, 56, 35, 0, "2011-11-18T20:56:35+05:00", 5 * 3600}, + {2011, 11, 18, 15, 56, 35, 0, "2011-11-18T21:26:35+05:30", 5 * 3600 + 30 * 60}, + {2011, 11, 18, 15, 56, 35, 0, "2011-11-18T10:56:35-05:00", -5 * 3600}, + {2011, 11, 18, 15, 56, 35, 0, "2011-11-18T10:26:35-05:30", -5 * 3600 - 30 * 60}, + {2011, 11, 18, 15, 56, 35, 666777888, "2011-11-18T20:56:35.666777888+05:00", 5 * 3600}, + {2011, 11, 18, 15, 56, 35, 666777888, "2011-11-18T10:56:35.666777888-05:00", -5 * 3600}, +}; + +static void test_fmt_iso(void) { + printf("test_fmt_iso..."); + for (size_t i = 0; i < sizeof(fmt_iso_tests) / sizeof(fmt_iso_tests[0]); i++) { + FormatTest test = fmt_iso_tests[i]; + Time t = time_date(test.year, test.month, test.day, test.hour, test.min, test.sec, + test.nsec, TIMEX_UTC); + char got[64]; + time_fmt_iso(got, sizeof(got), t, test.offset_sec); + // printf("want: %s, got: %s\n", test.want, got); + assert(strcmp(got, test.want) == 0); + } + printf("OK\n"); +} + +FormatTest fmt_dt_tests[] = { + {2011, 11, 18, 15, 56, 35, 0, "2011-11-18 15:56:35", TIMEX_UTC}, + {2011, 11, 18, 15, 56, 35, 666777888, "2011-11-18 15:56:35", TIMEX_UTC}, + {2011, 11, 18, 15, 56, 35, 0, "2011-11-18 20:56:35", 5 * 3600}, + {2011, 11, 18, 15, 56, 35, 0, "2011-11-18 21:26:35", 5 * 3600 + 30 * 60}, + {2011, 11, 18, 15, 56, 35, 0, "2011-11-18 10:56:35", -5 * 3600}, + {2011, 11, 18, 15, 56, 35, 0, "2011-11-18 10:26:35", -5 * 3600 - 30 * 60}, + {2011, 11, 18, 15, 56, 35, 666777888, "2011-11-18 20:56:35", 5 * 3600}, + {2011, 11, 18, 15, 56, 35, 666777888, "2011-11-18 10:56:35", -5 * 3600}, +}; + +static void test_fmt_datetime(void) { + printf("test_fmt_datetime..."); + for (size_t i = 0; i < sizeof(fmt_dt_tests) / sizeof(fmt_dt_tests[0]); i++) { + FormatTest test = fmt_dt_tests[i]; + Time t = time_date(test.year, test.month, test.day, test.hour, test.min, test.sec, + test.nsec, TIMEX_UTC); + char got[64]; + time_fmt_datetime(got, sizeof(got), t, test.offset_sec); + // printf("want: %s, got: %s\n", test.want, got); + assert(strcmp(got, test.want) == 0); + } + printf("OK\n"); +} + +FormatTest fmt_date_tests[] = { + {2011, 11, 18, 15, 56, 35, 0, "2011-11-18", TIMEX_UTC}, + {2011, 11, 18, 15, 56, 35, 0, "2011-11-18", 5 * 3600}, + {2011, 11, 18, 15, 56, 35, 0, "2011-11-19", 12 * 3600}, + {2011, 11, 18, 15, 56, 35, 0, "2011-11-18", -5 * 3600}, + {2011, 11, 18, 15, 56, 35, 0, "2011-11-17", -20 * 3600}, +}; + +static void test_fmt_date(void) { + printf("test_fmt_date..."); + for (size_t i = 0; i < sizeof(fmt_date_tests) / sizeof(fmt_date_tests[0]); i++) { + FormatTest test = fmt_date_tests[i]; + Time t = time_date(test.year, test.month, test.day, test.hour, test.min, test.sec, + test.nsec, TIMEX_UTC); + char got[64]; + time_fmt_date(got, sizeof(got), t, test.offset_sec); + // printf("want: %s, got: %s\n", test.want, got); + assert(strcmp(got, test.want) == 0); + } + printf("OK\n"); +} + +FormatTest fmt_time_tests[] = { + {2011, 11, 18, 15, 56, 35, 0, "15:56:35", TIMEX_UTC}, + {2011, 11, 18, 15, 56, 35, 0, "20:56:35", 5 * 3600}, + {2011, 11, 18, 15, 56, 35, 0, "03:56:35", 12 * 3600}, + {2011, 11, 18, 15, 56, 35, 0, "10:56:35", -5 * 3600}, + {2011, 11, 18, 15, 56, 35, 0, "19:56:35", -20 * 3600}, +}; + +static void test_fmt_time(void) { + printf("test_fmt_time..."); + for (size_t i = 0; i < sizeof(fmt_time_tests) / sizeof(fmt_time_tests[0]); i++) { + FormatTest test = fmt_time_tests[i]; + Time t = time_date(test.year, test.month, test.day, test.hour, test.min, test.sec, + test.nsec, TIMEX_UTC); + char got[64]; + time_fmt_time(got, sizeof(got), t, test.offset_sec); + // printf("want: %s, got: %s\n", test.want, got); + assert(strcmp(got, test.want) == 0); + } + printf("OK\n"); +} + +FormatTest parse_tests[] = { + {2011, 11, 18, 15, 56, 35, 0, "2011-11-18T15:56:35Z", TIMEX_UTC}, + {2011, 11, 18, 15, 56, 35, 666777888, "2011-11-18T15:56:35.666777888Z", TIMEX_UTC}, + {2011, 11, 18, 15, 56, 35, 0, "2011-11-18T20:56:35+05:00", 5 * 3600}, + {2011, 11, 18, 15, 56, 35, 0, "2011-11-18T21:26:35+05:30", 5 * 3600 + 30 * 60}, + {2011, 11, 18, 15, 56, 35, 0, "2011-11-18T10:56:35-05:00", -5 * 3600}, + {2011, 11, 18, 15, 56, 35, 0, "2011-11-18T10:26:35-05:30", -5 * 3600 - 30 * 60}, + {2011, 11, 18, 15, 56, 35, 666777888, "2011-11-18T20:56:35.666777888+05:00", 5 * 3600}, + {2011, 11, 18, 15, 56, 35, 666777888, "2011-11-18T10:56:35.666777888-05:00", -5 * 3600}, + {2011, 11, 18, 15, 56, 35, 0, "2011-11-18 15:56:35", TIMEX_UTC}, + {2011, 11, 18, 0, 0, 0, 0, "2011-11-18", TIMEX_UTC}, + {1, 1, 1, 15, 56, 35, 0, "15:56:35", TIMEX_UTC}, + {1, 1, 1, 0, 0, 0, 0, "2011-11-18 10:56", TIMEX_UTC}, +}; + +static void test_parse(void) { + printf("test_parse..."); + for (size_t i = 0; i < sizeof(parse_tests) / sizeof(parse_tests[0]); i++) { + FormatTest test = parse_tests[i]; + Time want = time_date(test.year, test.month, test.day, test.hour, test.min, test.sec, + test.nsec, TIMEX_UTC); + Time got = time_parse(test.want); + // printf("want: {%lld %d}, got: {%lld %d}\n", time_to_unix(want), want.nsec, + // time_to_unix(got), got.nsec); + assert(time_equal(got, want)); + } + printf("OK\n"); +} + +#pragma endregion + +#pragma region Marshaling. + +static void test_marshal_blob(void) { + printf("test_marshal_blob..."); + uint8_t buf[15]; + for (size_t i = 0; i < sizeof(unix_tests) / sizeof(unix_tests[0]); i++) { + TimeTest test = unix_tests[i]; + Time want = time_unix(test.sec, test.nsec); + time_to_blob(want, buf); + Time got = time_blob(buf); + // printf("want: {%lld %lld}, got: {%lld %lld}\n", want.sec, want.nsec, got.sec, got.nsec); + assert(time_equal(got, want)); + } + printf("OK\n"); +} + +#pragma endregion + +int main(void) { + // Constructors. + test_date(); + + // Time parts. + test_get_part(); + test_get_isoweek(); + test_get_yearday(); + + // Unix time. + test_unix(); + test_milli(); + test_micro(); + test_to_unix(); + test_to_milli(); + test_to_micro(); + test_to_nano(); + + // Calendar time. + test_tm(); + test_to_tm(); + + // Comparisons. + test_compare(); + test_is_zero(); + + // Arithmetic. + test_add(); + test_add_to_exact_second(); + test_sub(); + test_add_date(); + + // Rounding. + test_truncate(); + test_round(); + + // Formatting. + test_fmt_iso(); + test_fmt_datetime(); + test_fmt_date(); + test_fmt_time(); + test_parse(); + + // Marshaling. + test_marshal_blob(); +}