High-precision date/time in SQLite

While SQLite provides a certain number of date functions, I wanted something more. So I've created sqlean-time — a high-precision date/time extension with a structured API and a rich set of functions.

Note. Adding extensions to SQLite is a breeze. Download a file, run one database command — and you are good to go.

ConceptsCreating valuesExtracting fieldsUnix timeTime comparisonTime arithmeticRoundingFormattingAcknowledgementsInstallation and usage

Concepts

This extension works with two types of values: Time and Duration.

  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 offset.

  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. For the current time:

select time_fmt_iso(time_now());
-- 2024-08-06T21:22:15.431295000Z
2024-08-06T21:22:15.431295000Z

And for a specific date/time:

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
2011-11-18T00:00:00Z
2011-11-18T15:56:35Z
2011-11-18T15:56:35.666777888Z
2011-11-18T12:56:35Z
2011-11-18T12:56:35.666777888Z

Extracting time fields

There are a number of functions for extracting different date/time fields:

select 'year    = ' || time_get_year(time_now());
select 'month   = ' || time_get_month(time_now());
select 'day     = ' || time_get_day(time_now());

select 'hour    = ' || time_get_hour(time_now());
select 'minute  = ' || time_get_minute(time_now());
select 'second  = ' || time_get_second(time_now());
select 'nanosec = ' || time_get_nano(time_now());

select 'weekday = ' || time_get_weekday(time_now());
select 'yearday = ' || time_get_yearday(time_now());
select 'isoyear = ' || time_get_isoyear(time_now());
select 'isoweek = ' || time_get_isoweek(time_now());
year    = 2024
month   = 8
day     = 8
hour    = 11
minute  = 2
second  = 29
nanosec = 646015917
weekday = 4
yearday = 221
isoyear = 2024
isoweek = 32

There is also a generic time_get function for extracting time fields by name:

select 'millennium = ' || time_get(time_now(), 'millennium');
select 'century = ' || time_get(time_now(), 'century');
select 'decade  = ' || time_get(time_now(), 'decade');

select 'year    = ' || time_get(time_now(), 'year');
select 'quarter = ' || time_get(time_now(), 'quarter');
select 'month   = ' || time_get(time_now(), 'month');
select 'day     = ' || time_get(time_now(), 'day');

select 'hour    = ' || time_get(time_now(), 'hour');
select 'minute  = ' || time_get(time_now(), 'minute');
select 'second  = ' || time_get(time_now(), 'second');

select 'milli   = ' || time_get(time_now(), 'milli');
select 'micro   = ' || time_get(time_now(), 'micro');
select 'nano    = ' || time_get(time_now(), 'nano');

select 'isoyear = ' || time_get(time_now(), 'isoyear');
select 'isweek  = ' || time_get(time_now(), 'isoweek');
select 'isodow  = ' || time_get(time_now(), 'isodow');

select 'yearday = ' || time_get(time_now(), 'yearday');
select 'weekday = ' || time_get(time_now(), 'weekday');

select 'epoch   = ' || time_get(time_now(), 'epoch');
millennium = 2
century = 20
decade  = 202
year    = 2024
quarter = 3
month   = 8
day     = 8
hour    = 11
minute  = 7
second  = 8.800600319
milli   = 800
micro   = 800733
nano    = 800750131
isoyear = 2024
isweek  = 32
isodow  = 4
yearday = 221
weekday = 4
epoch   = 1723115228.80122

Unix time

There are functions for creating time values from Unix time (time since the Unix epoch — January 1, 1970 UTC):

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

select time_fmt_iso(time_milli(1321631795666));
-- 2011-11-18T15:56:35.666000000Z

select time_fmt_iso(time_micro(1321631795666777));
-- 2011-11-18T15:56:35.666777000Z

select time_fmt_iso(time_nano(1321631795666777888));
-- 2011-11-18T15:56:35.666777888Z
2011-11-18T15:56:35Z
2011-11-18T15:56:35.666777888Z
2011-11-18T15:56:35.666000000Z
2011-11-18T15:56:35.666777000Z
2011-11-18T15:56:35.666777888Z

And functions for converting time values back to Unix time:

select time_to_unix(time_now());
-- 1722979335

select time_to_milli(time_now());
-- 1722979335431

select time_to_micro(time_now());
-- 1722979335431295

select time_to_nano(time_now());
-- 1722979335431295000
1723119379
1723119379000
1723119379000192
1723119379000199404

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. time_to_milli can represent dates 292 million years before or after 1970, time_to_micro — years from -290307 to 294246, and time_to_nano — years from 1678 to 2262.

Time comparison

time_after, time_before and time_compare compare time values:

select time_after(time_now(), time_date(2011, 11, 18));
-- 1

select time_before(time_now(), time_date(2011, 11, 18));
-- 0

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
1
0
1
-1
0

time_equal reports whether two time values represent the same time instant:

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
0
1

Time arithmetic

time_add adds duration to a time value (use negative duration to subtract):

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
2024-08-09T12:28:16.686654780Z
2024-08-08T13:28:16.686695178Z
2024-08-08T12:33:46.686707964Z

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.

Do not use time_add to add days, months or years. Use time_add_date instead.

time_add_date adds a given number of years, months, and days to a time value (use negative values to subtract):

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
2016-11-18
2015-04-18
2015-04-08

time_sub returns the duration between two time values (in nanoseconds):

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
86400000000000
3600000000000
5000000

time_since returns the time elapsed since a given time (in nanoseconds):

select time_since(time_now());
-- 5000
5000

time_until returns the duration until a given time (in nanoseconds):

select time_until(time_date(2024, 9, 1));
-- 2144479530297000
2144479530297000

Rounding

time_trunc truncates a time value to the precision specified by a given field:

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;
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

Alternatively, time_trunc rounds a time value down to a multiple of a given duration:

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;
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

time_round rounds a time value to the nearest multiple of a given duration:

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;
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

time_fmt_iso returns an ISO 8601 time string for a given time value (optionally converts the time value to a given timezone offset before formatting):

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
2011-11-18T18:56:35.666777888+03:00
2011-11-18T15:56:35.666777888Z
2011-11-18T18:56:35+03:00
2011-11-18T15:56:35Z

time_fmt_datetime, time_fmt_date and time_fmt_time return a datetime/date/time string for a given time value:

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

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

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
2011-11-18 18:56:35
2011-11-18 15:56:35
2011-11-18 00:00:00
2011-11-19
2011-11-18
2011-11-18
18:56:35
15:56:35
00:00:00

time_parse parses a formatted string and returns the time value it represents:

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);
1
1
1
1
1
1
1
1
1

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

Duration constants

There are functions that return common durations in nanoseconds:

select dur_ns();
-- 1
select dur_us();
-- 1000
select dur_ms();
-- 1000000
select dur_s();
-- 1000000000
select dur_m();
-- 60000000000
select dur_h();
-- 3600000000000
1
1000
1000000
1000000000
60000000000
3600000000000

Acknowledgements

While this extension is implemented in C, its design and implementation is largely based on Go's stdlib time package (BSD 3-Clause License), which I think is awesome (except for the formatting part).

Installation and usage

  1. Download the latest release

  2. Use with SQLite command-line interface:

sqlite> .load ./time
sqlite> select time_now();

See also:

──

P.S. Interactive examples in this post are powered by codapi — an open source tool I'm building. Use it to embed live code snippets into your product docs, online course or blog.

★ Subscribe to keep up with new posts.