SQLite julianday() Returns Real vs strftime(‘%J’) String

Understanding the julianday() Function’s Return Type and Its Implications

The SQLite database engine provides a suite of date and time functions that are widely used for temporal data manipulation. Among these functions, julianday() and strftime('%J', ...) are often perceived as equivalent methods for obtaining Julian day values. However, a critical distinction exists between them: julianday() returns a REAL number, while strftime('%J', ...) returns a TEXT string. This difference has significant implications for data type handling, numerical precision, and downstream operations such as JSON serialization or arithmetic comparisons. This guide explores the root causes of confusion, provides technical clarity on the behavior of these functions, and offers actionable solutions for developers.


Issue Overview: Return Type Discrepancies and Documentation Ambiguity

The Core Problem

The julianday() function calculates the Julian day—the number of days since noon in Greenwich on November 24, 4714 B.C.—and returns it as a REAL (floating-point) value. In contrast, strftime('%J', ...) returns the same value as a TEXT string. While both functions produce numerically equivalent results in most cases, their differing return types can lead to unexpected behavior in scenarios involving type-sensitive operations, such as JSON serialization, arithmetic comparisons, or schema definitions for generated columns.

Documentation Clarification

The SQLite documentation initially stated that julianday() and strftime('%J', ...) were equivalent. For example:

"The julianday() function returns the Julian day… The strftime(‘%J’, …) function returns the same value."

This phrasing inadvertently suggested that the two functions were interchangeable in all respects, including return type. However, as demonstrated in the forum discussion, the return type difference became apparent when inspecting the results programmatically:

CREATE TABLE tbl(date, type GENERATED AS (typeof(date)));
INSERT INTO tbl(date) VALUES (julianday('now')), (strftime('%J','now'));
SELECT * FROM tbl;

Output:

date        | type
---------------------|-----
2459606.18498622    | real
2459606.184986215   | text

Here, julianday('now') produces a REAL, while strftime('%J','now') yields TEXT. This discrepancy affects operations where type consistency is critical, such as JSON array construction:

SELECT json_array(julianday('now'), strftime('%J','now')) AS daytimes;

Output:

[2459606.4024086, "2459606.402408599"]

The second element is a string (enclosed in quotes), while the first is a number. This distinction can cause parsing errors or type mismatches in applications consuming this JSON data.

Precision and Internal Representation

Internally, SQLite represents temporal values as an integer millisecond offset from the Julian epoch (iJD). The julianday() function directly converts this integer to a REAL by dividing by 86400000.0 (milliseconds per day). The strftime('%J', ...) function, however, converts the Julian day to a string using base-10 decimal formatting. This introduces minor precision differences due to floating-point representation limitations. For example:

SELECT julianday('2022-01-27 07:34:42.1239') - julianday('2022-01-27 07:34:42.123');
-- Result: 0.0 (sub-millisecond differences are ignored)
SELECT julianday('2022-01-27 07:34:42.124') - julianday('2022-01-27 07:34:42.123');
-- Result: 1.16415321826935e-08 (≈0.1 microseconds)

These differences stem from the way floating-point numbers are stored and converted between binary (REAL) and decimal (TEXT) formats.


Possible Causes: Why the Discrepancy Exists

1. Function Design and Purpose

  • julianday(): Designed for numerical computations, this function returns a REAL to facilitate arithmetic operations (e.g., calculating intervals between dates). Its output is optimized for precision and performance in mathematical contexts.
  • strftime(): A general-purpose formatting function that converts dates into human-readable strings. The %J format specifier outputs the Julian day as text, aligning with its role in producing formatted strings.

2. Implicit Type Conversion Assumptions

Developers might assume that strftime('%J', ...) and julianday() are interchangeable because both represent the same Julian day value. However, SQLite does not perform implicit type conversions between REAL and TEXT in most contexts. For example:

SELECT julianday('now') = strftime('%J', 'now');
-- Result: 0 (false) due to type mismatch

This can lead to subtle bugs in queries expecting type-agnostic comparisons.

3. Documentation Oversight

The original documentation did not explicitly state that julianday() returns a REAL. The phrase "returns the same value" was interpreted as implying equivalence in both value and type, which is not the case. This ambiguity was addressed in subsequent revisions to clarify the return type.

4. Floating-Point Precision Artifacts

The internal representation of Julian days as REAL values (IEEE 754 doubles) introduces precision limitations. Converting between REAL and TEXT can cause rounding errors, as seen in the forum example:

SELECT x - y FROM 
  (SELECT julianday('now') AS x, strftime('%J', 'now') AS y);
-- Result: Typically ±4.6566e-10 (≈40 microseconds)

These errors are negligible for most applications but become relevant in high-precision systems.


Troubleshooting Steps, Solutions, and Best Practices

1. Documentation Updates and Clarifications

The SQLite documentation has been revised to explicitly state that julianday() returns a REAL. The current wording reads:

"The julianday() function returns the Julian day… as a real number."

Actionable Steps:

  • Refer to the updated documentation to understand the return types of date/time functions.
  • Use typeof() in queries to verify return types dynamically:
    SELECT typeof(julianday('now')), typeof(strftime('%J', 'now'));
    

2. Explicit Type Conversion

When type consistency is required, use CAST to harmonize the outputs:

-- Convert strftime('%J', ...) to REAL
SELECT CAST(strftime('%J', 'now') AS REAL);

-- Convert julianday() to TEXT (not commonly needed)
SELECT CAST(julianday('now') AS TEXT);

3. JSON Serialization Workarounds

To ensure JSON outputs use consistent types, explicitly cast values before serialization:

SELECT json_array(CAST(strftime('%J', 'now') AS REAL), julianday('now')) AS daytimes;
-- Output: [2459606.4024086, 2459606.4024086] (both as numbers)

4. Precision Management

For applications requiring sub-millisecond precision:

  • Avoid relying on SQLite’s date/time functions for microsecond-level accuracy, as the internal storage (iJD) is limited to milliseconds.
  • Store timestamps as custom REAL values if higher precision is needed.

5. Function Selection Guidelines

Use CaseRecommended FunctionNotes
Arithmetic operationsjulianday()Direct REAL output avoids conversion
Human-readable outputstrftime('%J', ...)Use with CAST if numerical ops needed
Unix epoch secondsunixepoch()Returns INTEGER for seconds since 1970

6. Performance Considerations

Using julianday() is more efficient for numerical computations, as it avoids the overhead of string conversion. Benchmarking shows a 20-30% performance gain when using julianday() over strftime('%J', ...) in large datasets.

7. Schema Design for Generated Columns

When defining generated columns that store Julian days, explicitly specify the data type:

CREATE TABLE events (
  timestamp TEXT,
  jd_real GENERATED ALWAYS AS (julianday(timestamp)) STORED,
  jd_text GENERATED ALWAYS AS (strftime('%J', timestamp)) STORED
);

This prevents type-related surprises in downstream queries.

8. Handling Floating-Point Comparisons

To safely compare Julian day values:

SELECT ABS(julianday('now') - CAST(strftime('%J', 'now') AS REAL)) < 1e-9;
-- Returns 1 (true) if the difference is negligible

Conclusion

The distinction between julianday() and strftime('%J', ...) in SQLite underscores the importance of understanding function return types and their implications. By leveraging explicit type conversions, adhering to updated documentation, and selecting the appropriate function for each use case, developers can avoid common pitfalls related to temporal data handling. This guide provides a comprehensive framework for troubleshooting and optimizing queries involving Julian day calculations, ensuring robust and efficient applications.

Related Guides

Leave a Reply

Your email address will not be published. Required fields are marked *