ISO 8601 vs RFC 3339 timestamps
The types
| Type | Form | Pattern | DuckDB transform | Sample |
|---|---|---|---|---|
| ISO 8601 | T and Z | ^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,6})?Z$ | strptime({col}, '%Y-%m-%dT%H:%M:%SZ') | 2024-01-15T10:30:00Z |
| ISO 8601 with Timezone Offset | T and offset | ^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}[+-](?:\d{2}(?::?\d{2})?|\d:\d{2})$ | CAST(regexp_replace(upper({col}), '([+-])([0-9]):([0-9]{2})$', '\10\2:\3') AS TIMESTAMPTZ) | 2024-01-15T10:30:00+05:00 |
| ISO Datetime, second precision (no timezone) | T, no zone | ^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$ | strptime({col}, '%Y-%m-%dT%H:%M:%S') | 2013-06-04T01:02:03 |
| ISO with Space Separator and Z | Space and Z | ^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}Z$ | strptime({col}, '%Y-%m-%d %H:%M:%SZ') | 2024-01-15 14:30:00Z |
| RFC 3339 (Space Separator) | Space and offset | ^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)? ?(?:[Zz]|[+-]\d{2}:?\d{2})$ | CAST(regexp_replace(upper({col}), ' ([+-]|Z)', '\1') AS TIMESTAMPTZ) | 2024-01-15 10:30:00+00:00 |
What the standards say
ISO 8601 joins a date and a time of day into one value with the letter T.[1] ISO 8601:2004 allowed the T to be left out by mutual agreement, but ISO 8601-1:2019 removed that provision.[1] ISO 8601 itself does not allow a space between the date and time parts.[1] RFC 3339 defines a date and time format for use in Internet protocols that is a profile of the ISO 8601 standard.[2] RFC 3339's grammar joins the full date and the full time with the letter T.[3] A note in RFC 3339 says applications may choose, for the sake of readability, to separate the full date and full time with a space character instead.[3]
ISO 8601 says that if no UTC relation information is given with a time representation, the time is assumed to be in local time.[1] RFC 3339's date-time grammar requires a time offset, either Z or a numeric offset, after the time.[3] RFC 3339 defines Z as a suffix that, applied to a time, denotes a UTC offset of 00:00.[3] Wikipedia notes that RFC 3339 deviates from ISO 8601 by allowing a zero offset to be written as -00:00.[1]
RFC 3339 notes that the T and Z characters may alternatively be written in lower case as t and z.[3] RFC 3339 says applications that generate its format should use upper case letters.[3] RFC 3339 only allows hours from 00 to 23, although ISO 8601 permits the hour to be 24.[2]
FineType's registry labels the space-separated form datetime.timestamp.rfc_3339.[4] FineType's registry labels the T-separated form iso_8601_offset.[4]
How finetype tells them apart
From finetype 0.6.66's source.
finetype's datetime format reader reads a value with a T between the date and the time, and whole seconds, as iso_8601 when a Z follows the seconds, as iso_8601_offset when a four-digit offset such as +05:30 follows them, and as iso_seconds when nothing does.[5][6] It reads a value with a space between the date and the time, and whole seconds, as iso_space_zulu when a Z follows the seconds, and as rfc_3339 when a four-digit offset such as +05:30 does; a two-digit offset such as +03 reads as pg_short_offset instead.[7][8] The reader takes the offset with or without a colon, as +05:30 or +0530, and for rfc_3339 it also takes a space before the offset.[9][10] finetype's source says each format is anchored at both ends with exact field widths, so a value matches at most one format.[11] The reader picks a format only when at least 95% of the column's sampled non-null values match it.[12][13]
Because these forms carry separators, finetype does not ask its model to agree before applying the reader's choice, as it does for a bare integer.[14][15] finetype applies that reading only when at least 90% of the column's non-empty sampled values pass the chosen type's own validation rule.[16][17]
finetype's own tests pin that a space-separated value ending in Z reads as iso_space_zulu, not rfc_3339.[18] The same tests pin that the reader reads none of 2024-01-01t10:00:00+05:30, 2024-01-01 10:00:00z, 2024-01-01T10:00:00+05 or 2024-01-01T10:00:00+5:30, although the registry's published rules accept them.[19][20][21] When no single format matches at least 95% of a column of these timestamps, the reader returns nothing and this step leaves the column's type as it was.[22][23]
Read the names as a description of the characters a value holds, not as a verdict on which standard its writer followed. (Meridian's view)
What finetype's evaluation shows
The evaluation report in finetype's repository at release 0.6.66 measured release 0.6.60 on 2026-09-23.[24] The report says its tables are generated by a separate tool that has drifted from the SQL harness it claims to mirror, and that a figure taken from it for publication must be its SQL headline.[25] Until finetype publishes a measurement for each type, this page gives no figure for how often it mistakes one form for another. (Meridian's view)
Who writes each form
T and Z
MDN documents the output of JavaScript's Date.prototype.toISOString() as always 24 or 27 characters long, in the form YYYY-MM-DDTHH:mm:ss.sssZ or ±YYYYYY-MM-DDTHH:mm:ss.sssZ.[26] GitHub's REST API documentation says all timestamps return in UTC time, ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ.[27]
T and offset
The OpenAQ air-quality API returns each datetime with both a UTC value and a local value, the local one written with an offset such as 2019-07-11T14:00:00-06:00.[28]
T, no zone
Java's LocalDateTime class is a date-time without a time-zone in the ISO-8601 calendar system, such as 2007-12-03T10:15:30.[29] The HTML Standard's normalised local date and time string separates the date from the time with a capital T.[30]
Space and Z
Elixir's DateTime.to_string renders a UTC datetime in this shape, with 2000-02-29 23:00:07Z as its documented example.[31]
Space and offset
PostgreSQL accepts the T separator on input, but on output it uses a space rather than T.[32] Python's documentation shows isoformat(' ') producing 2002-12-25 00:00:00-06:39.[33]
Sources
- [1]ISO 8601secondary source
- [2]Date and Time on the Internet: Timestampsstandard
- [3]RFC 3339: Date and Time on the Internet: Timestampsstandard
- [4]finetype/labels/definitions_datetime.yaml at main · meridian-online/finetype · GitHubFineType
- [5]finetype 0.6.66, datetime_format.rsFineType
- [6]finetype 0.6.66, datetime_format.rsFineType
- [7]finetype 0.6.66, datetime_format.rsFineType
- [8]finetype 0.6.66, datetime_format.rsFineType
- [9]finetype 0.6.66, datetime_format.rsFineType
- [10]finetype 0.6.66, datetime_format.rsFineType
- [11]finetype 0.6.66, datetime_format.rsFineType
- [12]finetype 0.6.66, datetime_format.rsFineType
- [13]finetype 0.6.66, datetime_format.rsFineType
- [14]finetype 0.6.66, datetime_format.rsFineType
- [15]finetype 0.6.66, column/mod.rsFineType
- [16]finetype 0.6.66, column/mod.rsFineType
- [17]finetype 0.6.66, column/mod.rsFineType
- [18]finetype 0.6.66, datetime_format.rsFineType
- [19]finetype 0.6.66, datetime_format.rsFineType
- [20]finetype 0.6.66, definitions_datetime.yamlFineType
- [21]finetype 0.6.66, definitions_datetime.yamlFineType
- [22]finetype 0.6.66, datetime_format.rsFineType
- [23]finetype 0.6.66, column/mod.rsFineType
- [24]finetype evaluation report (release 0.6.60)FineType
- [25]finetype evaluation report (release 0.6.60)FineType
- [26]Date.prototype.toISOString() - JavaScript | MDNdocumentation
- [27]Getting started with the REST APIorganisation
- [28]Dates, times and timezonesorganisation
- [29]LocalDateTime (Java SE 21 & JDK 21)documentation
- [30]HTML Standardstandard
- [31]DateTime (Elixir v1.20.4)documentation
- [32]PostgreSQL: Documentation: 18: 8.5. Date/Time Typesdocumentation
- [33]datetime — Basic date and time typesdocumentation