Data Formats (JSON, YAML, TOML, XML, CSV) Module 10 – Across formats: time, conversion, streaming, evolution, binary
Dates and times across data formats
Write timestamps every parser reads alike with RFC 3339, see what each format returns for a date, attach time zones on purpose and read DD/MM/YYYY safely.
What you will learn
- Write timestamps that every parser reads the same way, using RFC 3339
- Explain which formats have native date types and what each parser returns
- Detect local times without offsets and attach the right zone deliberately
- Convert Indian DD/MM/YYYY text to ISO 8601 dates without ambiguity
Before you start
What you’ll learn · Before you start
What you will learn
- Write timestamps that every parser reads the same way, using RFC 3339
- Explain which formats have native date types and what each parser returns
- Detect local times without offsets and attach the right zone deliberately
- Convert Indian DD/MM/YYYY text to ISO 8601 dates without ambiguity
Before you start
On this page
A clinic in Pune books an appointment for 10:30. Read by a program on a computer in India the time is right, but
read by the same program on a server set to UTC, or by Ruby’s YAML parser anywhere, it turns into 16:00 in Pune,
five and a half hours late, and nothing reports an error.
Dates and times break between formats because most of them have no date type at all, and the ones that do disagree on what a time without an offset means. You will check timestamps against RFC 3339, see what JSON, YAML, TOML, XML and CSV parsers return for the same appointment, attach time zones on purpose, and convert day-first Indian dates without guessing.
An RFC 3339 timestamp needs a date, a time and an offset
2026-10-04T10:30:00+05:30 and 2026-10-04T05:00:00Z name the same instant: half past ten in India is five o’clock
UTC. Both follow RFC 3339: The IETF's profile of ISO 8601 for timestamps on the internet: a full date, the letter T, a time with seconds, an optional fraction and an offset, Z or a sign with hours and minutes. JSON Schema's date-time format refers to its grammar., the internet’s profile of ISO 8601. Its grammar, in section 5.6,
writes a full date, the letter T, a time with seconds, an optional fraction and a
UTC offset: How far a local clock is ahead of or behind UTC, written Z or as a sign with hours and minutes, such as +05:30 for India. An offset fixes one instant but says nothing about the rules of the place., which is Z or a sign with hours and minutes. Step through the six parts:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The date. A timestamp starts with the full date: a year of four digits, a month and a day of two digits each, joined by hyphens. ISO 8601 also allows week dates and days of the year; RFC 3339 does not.
- The separator. An upper-case T joins the date to the time, and the grammar also accepts a lower-case t. A space is a choice an application may make for readability, outside the grammar, so a strict validator refuses it.
- The time. Hours from 00 to 23, then minutes, then seconds, which may not be left out. The seconds may read 60 at the end of a month that has a leap second, a value both Python and JavaScript refuse.
- The fraction. A full stop and any number of digits. The grammar sets no limit, but Python keeps 6 digits and the Date of JavaScript keeps 3, so a timestamp written with 9 comes back shorter.
- The offset. Z, or a sign with hours and minutes, as +05:30 for India. Without it the text is only the reading of some clock, not one instant, and each reader guesses whose clock it was.
- The zone's name. RFC 9557 adds an optional suffix in brackets with the name of the time zone. The offset still fixes the instant; the name says which rules apply when you add a day or a month in that place.
Two rules sit beside the grammar. Section 5.7 sets the ranges: a day the month really has, hours to 23, and seconds
up to 60 for a leap second: A second added to UTC as second 60 of the last minute of a month, to keep UTC close to the Earth's rotation. RFC 3339 allows it; Python's datetime and JavaScript's Date both refuse it.. And a note allows a lower-case t and z, and lets an
application put a space between the date and the time for readability. A space is the application’s choice, not the
grammar: JSON Schema’s date-time format refers to the grammar, so a strict validator refuses it. I-JSON, RFC 7493
section 4.3, narrows the choice for JSON: upper-case letters, a zone that is stated rather than assumed, and seconds
even when they are zero.
RFC 9557 adds two things. A suffix in brackets names the time zone, as in 2026-10-04T10:30:00+05:30[Asia/Kolkata],
and a ! just inside the bracket, [!Asia/Kolkata], makes it critical, so a reader that cannot use it must refuse the
string. It also updates the meaning of Z: it now says the time in UTC is known and the local offset is not, the
meaning RFC 3339 had given to -00:00, while +00:00 still says that UTC is the preferred reference point.
Is a parser a validator? This program checks twelve timestamps against the grammar and its ranges, against I-JSON’s
advice, and with Python’s datetime.fromisoformat:
"""Twelve timestamps checked against the RFC 3339 grammar, against I-JSON's stricter advice, and by Python's parser."""
import calendar
import re
from datetime import datetime, timezone
# RFC 3339 section 5.6: date-time = full-date "T" full-time; "T" and "Z" may also be lower case.
RFC3339 = re.compile(
r"(\d{4})-(\d{2})-(\d{2})[Tt](\d{2}):(\d{2}):(\d{2})(\.\d+)?([Zz]|[+-](\d{2}):(\d{2}))"
)
def rfc3339(text):
"""True when text follows RFC 3339's grammar and its value ranges (section 5.7)."""
m = RFC3339.fullmatch(text)
if not m:
return False
year, month, day, hour, minute, second = (int(g) for g in m.groups()[:6])
if not 1 <= month <= 12 or not 1 <= day <= calendar.monthrange(year, month)[1]:
return False
if hour > 23 or minute > 59 or second > 60: # 60 only for a leap second
return False
if m.group(9) and (int(m.group(9)) > 23 or int(m.group(10)) > 59):
return False
return True
def i_json(text):
"""RFC 7493 section 4.3 adds: upper-case letters, and seconds even when they are zero."""
return rfc3339(text) and text == text.upper()
def python_reads(text):
try:
value = datetime.fromisoformat(text)
except ValueError as e:
return "ValueError: " + str(e).split(":")[0]
if value.tzinfo is None:
return "naive " + value.isoformat()
return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
STAMPS = [
"2026-10-04T10:30:00+05:30",
"2026-10-04T05:00:00Z",
"2026-10-04t05:00:00z",
"2026-10-04T10:30:00.123456789+05:30",
"2026-10-04 10:30:00+05:30",
"2026-10-04T10:30:00",
"2026-10-04T10:30+05:30",
"20261004T103000+0530",
"2026-10-04T10:30:00+05:30[Asia/Kolkata]",
"2026-02-30T10:00:00Z",
"2026-10-04T24:00:00Z",
"2016-12-31T23:59:60Z",
]
print(f"{'text':<41}{'RFC 3339':<10}{'I-JSON':<8}Python 3.14 fromisoformat")
for stamp in STAMPS:
print(f"{stamp:<41}{'yes' if rfc3339(stamp) else 'no':<10}{'yes' if i_json(stamp) else 'no':<8}{python_reads(stamp)}") Output
text RFC 3339 I-JSON Python 3.14 fromisoformat 2026-10-04T10:30:00+05:30 yes yes 2026-10-04T05:00:00Z 2026-10-04T05:00:00Z yes yes 2026-10-04T05:00:00Z 2026-10-04t05:00:00z yes no ValueError: Invalid isoformat string 2026-10-04T10:30:00.123456789+05:30 yes yes 2026-10-04T05:00:00.123456Z 2026-10-04 10:30:00+05:30 no no 2026-10-04T05:00:00Z 2026-10-04T10:30:00 no no naive 2026-10-04T10:30:00 2026-10-04T10:30+05:30 no no 2026-10-04T05:00:00Z 20261004T103000+0530 no no 2026-10-04T05:00:00Z 2026-10-04T10:30:00+05:30[Asia/Kolkata] no no ValueError: Invalid isoformat string 2026-02-30T10:00:00Z no no ValueError: day 30 must be in range 1..28 for month 2 in year 2026 2026-10-04T24:00:00Z no no 2026-10-05T00:00:00Z 2016-12-31T23:59:60Z yes yes ValueError: second must be in 0..59, not 60
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 rfc3339_check.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Python and the grammar disagree on 7 of the 12. fromisoformat accepts the space, the missing seconds, the compact
20261004T103000+0530 and the hour 24, none of which RFC 3339 allows, and it refuses the lower-case z and the leap
second, which RFC 3339 allows. The lower-case t was not the problem: its documentation lets any single character
stand between the date and the time. It also returns a naive datetime, one with no zone, for the time without an
offset instead of an error, and keeps 6 of the 9 fraction digits.
Validating with a parser
Wrong: accepting a timestamp because datetime.fromisoformat or Date.parse read it. Both accept forms RFC 3339
does not, and both read a missing offset as some clock. Right: check the text against the RFC 3339 grammar first,
as rfc3339() does, and refuse anything without an offset before you parse it.
Checkpoint
Check: RFC 3339 timestamps
Each format returns a date type or plain text
Here is one appointment written in five formats, each with a date, a time with its offset, and a time without one:
JSON · formats/appointment.json
{"day": "2026-10-04", "at": "2026-10-04T10:30:00+05:30", "local": "2026-10-04T10:30:00"} TOML · formats/appointment.toml
# One clinic appointment in Pune, written three ways
day = 2026-10-04
at = 2026-10-04T10:30:00+05:30
local = 2026-10-04T10:30:00 YAML · formats/appointment.yaml
# The same appointment in YAML: the three values are written plain, without quotes
day: 2026-10-04
at: 2026-10-04T10:30:00+05:30
local: 2026-10-04 10:30:00 XML · formats/appointment.xml
<appointment day="2026-10-04">
<at>2026-10-04T10:30:00+05:30</at>
<local>2026-10-04T10:30:00</local>
</appointment> CSV · formats/appointment.csv
day,at,local
2026-10-04,2026-10-04T10:30:00+05:30,2026-10-04T10:30:00 Python’s standard library reads four of them. This program prints what each parser hands back, then applies XML Schema’s rule for ordering a time without an offset against one with an offset:
"""One appointment written in JSON, TOML, CSV and XML: what Python's standard parsers hand back for its three values."""
import csv
import json
import tomllib
import xml.etree.ElementTree as ET
from datetime import datetime, timedelta, timezone
with open("appointment.json", encoding="utf-8") as f:
from_json = json.load(f)
with open("appointment.toml", "rb") as f:
from_toml = tomllib.load(f)
with open("appointment.csv", encoding="utf-8", newline="") as f:
from_csv = next(csv.DictReader(f))
root = ET.parse("appointment.xml").getroot()
from_xml = {"day": root.get("day"), "at": root.findtext("at"), "local": root.findtext("local")}
print(f"{'format':<7}{'key':<7}{'Python type':<13}value")
for name, record in [("JSON", from_json), ("TOML", from_toml), ("CSV", from_csv), ("XML", from_xml)]:
for key in ("day", "at", "local"):
value = record[key]
zone = getattr(value, "tzinfo", "")
shown = value.isoformat() if hasattr(value, "isoformat") else repr(value)
extra = "" if zone == "" else f" tzinfo={zone}"
print(f"{name:<7}{key:<7}{type(value).__name__:<13}{shown}{extra}")
# XML Schema 1.1 orders a dateTime without an offset against one with an offset by trying the extreme offsets,
# +14:00 and -14:00, on the value that lacks one. If the two tries disagree, the values are incomparable.
def xsd_order(no_offset, with_offset):
tries = set()
for hours in (14, -14):
imputed = no_offset.replace(tzinfo=timezone(timedelta(hours=hours)))
tries.add("less" if imputed < with_offset else "greater" if imputed > with_offset else "equal")
return tries.pop() if len(tries) == 1 else "incomparable"
local = datetime.fromisoformat("2026-10-04T10:30:00")
for other in ["2026-10-04T05:00:00Z", "2026-10-05T05:00:00Z", "2026-10-02T05:00:00Z"]:
print(f"xs:dateTime 2026-10-04T10:30:00 vs {other}: {xsd_order(local, datetime.fromisoformat(other))}") Output
format key Python type value JSON day str '2026-10-04' JSON at str '2026-10-04T10:30:00+05:30' JSON local str '2026-10-04T10:30:00' TOML day date 2026-10-04 TOML at datetime 2026-10-04T10:30:00+05:30 tzinfo=UTC+05:30 TOML local datetime 2026-10-04T10:30:00 tzinfo=None CSV day str '2026-10-04' CSV at str '2026-10-04T10:30:00+05:30' CSV local str '2026-10-04T10:30:00' XML day str '2026-10-04' XML at str '2026-10-04T10:30:00+05:30' XML local str '2026-10-04T10:30:00' xs:dateTime 2026-10-04T10:30:00 vs 2026-10-04T05:00:00Z: incomparable xs:dateTime 2026-10-04T10:30:00 vs 2026-10-05T05:00:00Z: less xs:dateTime 2026-10-04T10:30:00 vs 2026-10-02T05:00:00Z: greater
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 read_dates.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
JSON has no date type in its data model, so all three values arrive as strings, and so do the CSV fields and the XML
text. TOML is the only one of the four with types of its own, as the TOML module showed: a local date, an
offset date-time: A TOML date and time with its offset from UTC, such as 10:00 at +05:30. It names one instant, the same on every computer, and Python's tomllib returns it as an aware datetime. More in “TOML 1.1, dates, integers and parsers” that comes back as an aware datetime: A Python datetime whose tzinfo is set, so it names one instant. A naive datetime has no tzinfo, and Python refuses to compare the two kinds for order. More in “TOML 1.1, dates, integers and parsers”,
and a local date-time: A TOML date and time with no offset. It is a reading of a wall clock, not an instant: computers in two time zones turn it into two different instants. More in “TOML 1.1, dates, integers and parsers” that comes back naive, with tzinfo=None.
XML text becomes a date only when a schema types it. XML Schema’s xs:dateTime makes the offset optional, and it
orders a value without one by trying the two extreme offsets, +14:00 and -14:00: when the two tries disagree, the
values are incomparable. The last three lines show it: incomparable against the instant on the same day, less and
greater against the instants a day or more away. XPath instead compares such a value with the processor’s implicit
time zone, so the answer depends on how the processor was set up.
YAML depends on the schema. The 1.2 core schema: The schema that YAML 1.2.2 recommends as the default. Plain scalars may be null, booleans written true or false in three spellings, integers in base 10, 0o octal or 0x hex, or floats; anything else is a string. More in “YAML tags and schemas: failsafe, JSON, core” has no timestamp tag, so a parser that follows it returns all three values as strings. Ruby’s Psych reads YAML 1.1’s timestamp type instead:
# The appointment in YAML, read by Psych (Ruby's YAML parser, which follows YAML 1.1) on a computer in UTC and on
# one in India. Ruby reads the time zone again when ENV["TZ"] changes, so one program can play both computers.
require "yaml"
require "date"
text = File.read("appointment.yaml")
begin
YAML.safe_load(text)
rescue Psych::DisallowedClass => e
puts "safe_load alone: #{e.message}"
end
["UTC", "Asia/Kolkata"].each do |zone|
ENV["TZ"] = zone
puts "TZ=#{zone}"
YAML.safe_load(text, permitted_classes: [Date, Time]).each do |key, value|
puts format(" %-6s %-5s %s", key, value.class, value.is_a?(Time) ? value.strftime("%Y-%m-%d %H:%M:%S %:z") : value.to_s)
end
end Output
safe_load alone: Tried to load unspecified class: Date TZ=UTC day Date 2026-10-04 at Time 2026-10-04 10:30:00 +05:30 local Time 2026-10-04 10:30:00 +00:00 TZ=Asia/Kolkata day Date 2026-10-04 at Time 2026-10-04 10:30:00 +05:30 local Time 2026-10-04 16:00:00 +05:30
Recorded with ruby 4.0.7 (revision 229531a6cf) +PRISM [arm64-darwin23] on macOS 26 arm64. To run it yourself: ruby read_dates.rb
Psych refused to load the date at all until the program named Date and Time in permitted_classes. Then it built
a Date and two Time values, and it read the time without an offset as UTC, which is what YAML 1.1’s timestamp type
says to assume. On a computer in India that Time prints as 16:00 +05:30: the late appointment of the opening.
| Format | A date | A time with an offset | A time without one |
|---|---|---|---|
| JSON, CSV | text | text | text |
| XML | text, or xs:date with a schema |
text, or xs:dateTime |
text, or xs:dateTime without its zone |
| TOML | local date | aware datetime | naive datetime |
| YAML 1.2 core schema | text | text | text |
| YAML 1.1, as Psych reads it | Date |
Time with its offset |
Time read as UTC |
Key idea
Only TOML always types its dates, and YAML 1.1 types them when the loader allows it; JSON, CSV, XML without a schema and YAML’s 1.2 core schema hand back text, so the reader decides what the text means.
Checkpoint
Check: What each format returns for a date
A local reading needs a zone, attached on purpose
A time without an offset is the reading of a clock, not an instant. JavaScript’s Date turns it into an instant with
the clock of the computer it runs on. This program plays a computer set to UTC and one set to India:
// What JavaScript's Date makes of eight timestamps on a computer set to UTC and on one set to India (Asia/Kolkata).
// Node.js reads the time zone again when process.env.TZ changes, so one program can play both computers.
const stamps = [
"2026-10-04T10:30:00+05:30", // offset given: one instant everywhere
"2026-10-04T10:30:00", // date and time, no offset: read as local time
"2026-10-04", // date only: read as UTC midnight
"2026-10-04 10:30:00", // not ECMAScript's format: the engine guesses, as local time
"2026-10-04t05:00:00z", // lower case, allowed by RFC 3339
"04/10/2026", // not ECMAScript's format: read month first
"2026-02-30T10:00:00Z", // no such day: V8 rolls it into March
"2016-12-31T23:59:60Z", // a real leap second
];
for (const zone of ["UTC", "Asia/Kolkata"]) {
process.env.TZ = zone;
console.log(`TZ=${zone}`);
for (const text of stamps) {
const ms = Date.parse(text);
console.log(` ${text.padEnd(26)}${Number.isNaN(ms) ? "NaN (refused)" : new Date(ms).toISOString()}`);
}
}
// The same instant, shown to an Indian reader: formatting needs a zone, and en-IN writes the day first.
const instant = new Date("2026-10-04T05:00:00Z");
console.log(new Intl.DateTimeFormat("en-IN", { timeZone: "Asia/Kolkata" }).format(instant));
console.log(new Intl.DateTimeFormat("en-IN", { timeZone: "Asia/Kolkata", dateStyle: "long", timeStyle: "short" }).format(instant));
// Temporal has types for dates without times, zoned date-times and RFC 9557 strings; check before using it.
console.log(`Temporal in Node.js ${process.versions.node}: ${typeof globalThis.Temporal === "undefined" ? "not available" : "available"}`); Output
TZ=UTC 2026-10-04T10:30:00+05:30 2026-10-04T05:00:00.000Z 2026-10-04T10:30:00 2026-10-04T10:30:00.000Z 2026-10-04 2026-10-04T00:00:00.000Z 2026-10-04 10:30:00 2026-10-04T10:30:00.000Z 2026-10-04t05:00:00z 2026-10-04T05:00:00.000Z 04/10/2026 2026-04-10T00:00:00.000Z 2026-02-30T10:00:00Z 2026-03-02T10:00:00.000Z 2016-12-31T23:59:60Z NaN (refused) TZ=Asia/Kolkata 2026-10-04T10:30:00+05:30 2026-10-04T05:00:00.000Z 2026-10-04T10:30:00 2026-10-04T05:00:00.000Z 2026-10-04 2026-10-04T00:00:00.000Z 2026-10-04 10:30:00 2026-10-04T05:00:00.000Z 2026-10-04t05:00:00z 2026-10-04T05:00:00.000Z 04/10/2026 2026-04-09T18:30:00.000Z 2026-02-30T10:00:00Z 2026-03-02T10:00:00.000Z 2016-12-31T23:59:60Z NaN (refused) 4/10/2026 4 October 2026 at 10:30 am Temporal in Node.js 24.21.0: not available
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node date_parse.mjs
The ECMAScript rule is there in the first three lines of each block. With an offset, both computers agree. A date and
time without one is read as local time, so the two computers store instants five and a half hours apart. A date
alone is read as UTC midnight. The other forms are left to the engine: V8 read 04/10/2026 month first, rolled
2026-02-30 into March, and refused the leap second. Intl.DateTimeFormat with en-IN writes the day first, so the
same instant appears as 4/10/2026. Temporal, a newer date API with types for dates, zoned date-times and RFC 9557
strings, is not in Node.js 24.21.0, so the program checks before it uses it.
Here is the same slip between two systems, with an order placed late in the evening:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- An order at 22:00. A customer in Pune pays at 22:00 on a Sunday. The app writes the time as its clock shows it and leaves out the offset, +05:30.
- Read as UTC. The report server runs in UTC, and its parser reads a time without an offset as local time, which on this server is UTC. The order is now stored five and a half hours later than it happened.
- The wrong day. The daily report shows times in India. 22:00 UTC is 03:30 on Monday in Pune, so the order is counted in the next day's total, and nothing reported an error.
- The offset kept. With the offset written, the same order names one instant on every computer: 16:30 UTC, which is 22:00 on Sunday in Pune, whatever the clock of the reader.
An offset fixes one instant, but it says nothing about the rules of a place. An
IANA time zone: A named region such as Asia/Kolkata or Europe/London, whose history of UTC offsets and daylight-saving changes the IANA Time Zone Database records. Python's zoneinfo and JavaScript's Intl read it. such as Asia/Kolkata or Europe/London carries the history of a
region’s offsets and its daylight-saving changes, and Python’s zoneinfo reads it:
"""Attaching a time zone to a local date and time on purpose, with the IANA time zone database through zoneinfo."""
from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo
UTC = timezone.utc
kolkata = ZoneInfo("Asia/Kolkata")
london = ZoneInfo("Europe/London")
# A naive datetime is a wall-clock reading. replace(tzinfo=...) says where the wall clock hangs.
reading = datetime.fromisoformat("2026-10-04T10:30:00")
for zone in (kolkata, london):
aware = reading.replace(tzinfo=zone)
print(f"10:30 on that day in {zone.key:<13} = {aware.isoformat()} = {aware.astimezone(UTC):%H:%M} UTC")
# A fixed offset is right only while the zone keeps it. London changes its offset twice a year; India does not.
winter_offset = timezone(timedelta(0))
summer_day = datetime.fromisoformat("2026-07-08T10:30:00")
print("London in July with a winter offset:", summer_day.replace(tzinfo=winter_offset).astimezone(UTC).strftime("%H:%M UTC"))
print("London in July with its zone: ", summer_day.replace(tzinfo=london).astimezone(UTC).strftime("%H:%M UTC"))
# Clocks in London jumped from 01:00 to 02:00: 01:30 never happened on that day.
gap = datetime.fromisoformat("2026-03-29T01:30:00").replace(tzinfo=london)
back = gap.astimezone(UTC).astimezone(london)
print(f"01:30 in the spring gap: offset {gap.utcoffset()}, and back from UTC it reads {back:%H:%M}")
# Clocks went back from 02:00 to 01:00: 01:30 happened twice. fold picks the first (0) or the second (1).
for fold in (0, 1):
twice = datetime.fromisoformat("2025-10-26T01:30:00").replace(tzinfo=london, fold=fold)
print(f"01:30 in the autumn overlap, fold={fold}: {twice.isoformat()} = {twice.astimezone(UTC):%H:%M} UTC") Output
10:30 on that day in Asia/Kolkata = 2026-10-04T10:30:00+05:30 = 05:00 UTC 10:30 on that day in Europe/London = 2026-10-04T10:30:00+01:00 = 09:30 UTC London in July with a winter offset: 10:30 UTC London in July with its zone: 09:30 UTC 01:30 in the spring gap: offset 0:00:00, and back from UTC it reads 02:30 01:30 in the autumn overlap, fold=0: 2025-10-26T01:30:00+01:00 = 00:30 UTC 01:30 in the autumn overlap, fold=1: 2025-10-26T01:30:00+00:00 = 01:30 UTC
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 attach_zone.py
India keeps +05:30 all year, so a fixed offset is exact there. London does not: a winter offset applied to a July
time put it an hour late, 10:30 UTC instead of 09:30 UTC. On the night the clocks go forward, 01:30 does not
exist, and Python read it with the offset from before the change. On the night they go back, 01:30 happens twice,
and the fold: The attribute of a Python datetime that chooses between the two moments a local time names when clocks go back: 0 for the first, 1 for the second, as PEP 495 defines it. attribute of PEP 495 picks the first or the second.
Which form to send depends on the field. Answer three questions about a time field of your own:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Written as | Names one instant | Keeps the local time | Survives a rule change | People can read it |
|---|---|---|---|---|
| RFC 3339, local offset Holds: the local time and its offset | Good 10:30+05:30 is 05:00 UTC on every computer | Good the reader still sees 10:30, as the clock in Pune showed it | Mixed an offset chosen today is wrong for a future time if the zone changes its rules first | Good readable, and the offset says where it was written |
| RFC 3339 in UTC Holds: UTC, marked Z | Good Z: one instant, and every value sorts as text | Poor the local 10:30 is gone; showing it again needs the zone, which is not kept | Mixed right for events that happened; a future local time drifts if the rules change | Mixed readable, but in UTC rather than the local clock |
| Local time and zone name Holds: the wall time and a name such as [Europe/London] | Mixed an instant only once the zone database turns the wall time into one | Good the time the person chose, 10:30, exactly as chosen | Good the instant is worked out with the rules in force at the time | Good readable, and the zone name says which rules apply |
| Unix seconds Holds: a count of seconds | Good one instant as a whole number, such as 1791090000 | Poor no local time and no zone | Mixed as for UTC: fine for the past, drifts for a booked local time | Poor a person needs a converter to read 1791090000 |
| Local time alone Holds: the wall time, nothing else | Poor every reader guesses the zone, as the report server did | Good the wall time is there | Poor nothing says which zone, so no rule can be applied | Good readable, and silently wrong elsewhere |
How the answers decide
- “Is the field a moment that has happened, or a local time booked for later?” It has happened: rules out Local time alone. A past event is one instant; without an offset every reader guesses which one, as the report server did.
- “Is the field a moment that has happened, or a local time booked for later?” A local time booked for later: rules out Local time alone. Without the zone, nobody can work out the instant of a booked local time later.
- “Is the field a moment that has happened, or a local time booked for later?” A local time booked for later: adds a caveat to RFC 3339, local offset, RFC 3339 in UTC and Unix seconds. If the zone changes its rules before the booking, a stored instant no longer matches the local time the person chose.
- “Must readers see the local time the person saw or chose?” Yes: rules out RFC 3339 in UTC and Unix seconds. Both drop the local time, and showing it again needs the zone, which they do not keep.
- “Do people read the raw value, in logs or exports?” Yes: adds a caveat to Unix seconds. A count of seconds is exact, but nobody reads it without a converter.
A moment that has happened travels as RFC 3339 with its offset, or in UTC. A local time booked for later, such as a
clinic slot next year, travels as the local time with the zone’s name, because a government may change the zone’s
rules before then, and the person still expects their 10:30.
Checkpoint
Check: Offsets, zones and local readings
Day-first dates need a parser that is told the order
04/10/2026 is the fourth of October in India and the tenth of April in the United States, and en-IN formatting
writes the day first, as the JavaScript program printed. A parser that guesses reads it month first, as V8 did. The safe
reader is told the order, in a format string such as %d/%m/%Y or a pattern that names each part, and it refuses
every other form.
Dates in an e-invoice acknowledgement
The official sample response of India’s e-invoice system holds two date forms. AckDt is 2020-08-05 22:55:00,
with a space instead of T and no offset, so the zone, India’s +05:30, is known only because the portal is in
India. The signed invoice inside it carries "Dt": "05/08/2020", day first. Neither is RFC 3339, and a reader that
guesses gets both wrong.
Follow the two fields through a reader that never guesses:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- AckDt. AckDt is written year, month, day, a space and a time with seconds, and no offset. Its pattern names each part, so a value in any other form is refused instead of guessed.
- A real day. strptime builds the date and refuses a day the month does not have. In JavaScript, Date.UTC would roll such a day into the next month, so the code compares the parts it got back.
- The zone, on purpose. The portal's clock is in India, which keeps +05:30 all year, so the code attaches that offset. Nothing in the text says so: the zone comes from what the sender documents.
- Written out. The value is written with its offset, 22:55+05:30, and in UTC, 17:25Z. Every reader now gets the same instant.
- The invoice's Dt. The invoice date is day first, two digits each and a year of four. The format string says so, %d/%m/%Y, and a date needs no zone, so it is written as an ISO date.
- Refused. A text that matches no pattern, or names a day that does not exist, goes back with the field's name, so the sender can correct the record.
Here are the fields of the sample response that this program reads, with the invoice’s DocDtls taken from its signed
part:
{
"AckNo": 112010000011474,
"AckDt": "2020-08-05 22:55:00",
"Status": "ACT",
"DocDtls": { "Typ": "INV", "No": "CTDN23456", "Dt": "05/08/2020" }
} The program reads them, writes both date fields in standard forms, then shows the two slips:
"""Two date fields of an e-invoice acknowledgement, AckDt and the invoice's Dt, written as RFC 3339 on purpose."""
import json
from datetime import datetime, timedelta, timezone
IST = timezone(timedelta(hours=5, minutes=30)) # India keeps one offset all year, so a fixed offset is exact
with open("ack.json", encoding="utf-8") as f:
ack = json.load(f)
# AckDt has a space instead of T and no offset. The portal's clock is in India, so the zone is attached by us.
acked = datetime.strptime(ack["AckDt"], "%Y-%m-%d %H:%M:%S").replace(tzinfo=IST)
print("AckDt as RFC 3339:", acked.isoformat())
print("AckDt in UTC: ", acked.astimezone(timezone.utc).isoformat().replace("+00:00", "Z"))
# The invoice date is day first. Say so in the format; never let a parser guess.
invoice_day = datetime.strptime(ack["DocDtls"]["Dt"], "%d/%m/%Y").date()
print("Dt as an ISO date:", invoice_day.isoformat())
# The two slips this avoids, run on a computer whose clock is set to UTC:
naive = datetime.fromisoformat(ack["AckDt"])
print("Slip 1, zone left to the computer:", naive.astimezone().isoformat())
print("Slip 2, month first:", datetime.strptime(ack["DocDtls"]["Dt"], "%m/%d/%Y").date().isoformat())
for text in ["25/08/2020", "05/08/2020"]:
try:
datetime.strptime(text, "%m/%d/%Y")
print(f" month first accepts {text} without a word")
except ValueError:
print(f" month first refuses {text}: only days above 12 give the slip away") Output
AckDt as RFC 3339: 2020-08-05T22:55:00+05:30 AckDt in UTC: 2020-08-05T17:25:00Z Dt as an ISO date: 2020-08-05 Slip 1, zone left to the computer: 2020-08-05T22:55:00+00:00 Slip 2, month first: 2020-05-08 month first refuses 25/08/2020: only days above 12 give the slip away month first accepts 05/08/2020 without a word
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 normalise_gst.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
AckDt becomes 2020-08-05T22:55:00+05:30, which is 2020-08-05T17:25:00Z, and Dt becomes 2020-08-05. Left to the
computer, the zone came out as UTC, five and a half hours late; read month first, the invoice date became the eighth
of May. The month-first reading refused 25/08/2020, because no month 25 exists, but accepted 05/08/2020 without a
word: tests with days up to 12 pass and production data breaks. JavaScript has no format strings for parsing, so its
version names every part in a pattern and checks that the day exists, because Date.UTC would roll 31/02 into
March:
// The same two fields in JavaScript, which has no strptime: each form is matched by a pattern that names its parts.
import { readFileSync } from "node:fs";
const ack = JSON.parse(readFileSync("ack.json", "utf8"));
const IST_MINUTES = 330; // +05:30
// "YYYY-MM-DD HH:MM:SS", a wall-clock reading in India.
function istToUtc(text) {
const m = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/.exec(text);
if (!m) throw new Error(`not YYYY-MM-DD HH:MM:SS: ${text}`);
const [y, mo, d, h, mi, s] = m.slice(1).map(Number);
return new Date(Date.UTC(y, mo - 1, d, h, mi, s) - IST_MINUTES * 60_000);
}
// "DD/MM/YYYY": refused unless the day really exists (Date.UTC would roll 31/02 into March).
function dayFirst(text) {
const m = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(text);
if (!m) throw new Error(`not DD/MM/YYYY: ${text}`);
const [d, mo, y] = m.slice(1).map(Number);
const date = new Date(Date.UTC(y, mo - 1, d));
if (date.getUTCMonth() !== mo - 1 || date.getUTCDate() !== d) throw new Error(`no such day: ${text}`);
return date.toISOString().slice(0, 10);
}
const acked = istToUtc(ack.AckDt);
console.log("AckDt in UTC: ", acked.toISOString().replace(".000Z", "Z"));
console.log("AckDt as RFC 3339:", new Date(acked.getTime() + IST_MINUTES * 60_000).toISOString().slice(0, 19) + "+05:30");
console.log("Dt as an ISO date:", dayFirst(ack.DocDtls.Dt));
// The two slips, on a computer whose clock is set to UTC.
console.log("Slip 1, zone left to the computer:", new Date(ack.AckDt).toISOString());
console.log("Slip 2, month first:", new Date(ack.DocDtls.Dt).toISOString().slice(0, 10));
for (const text of ["31/02/2020", "5/8/2020"]) {
try {
dayFirst(text);
} catch (e) {
console.log(" refused:", e.message);
}
} Output
AckDt in UTC: 2020-08-05T17:25:00Z AckDt as RFC 3339: 2020-08-05T22:55:00+05:30 Dt as an ISO date: 2020-08-05 Slip 1, zone left to the computer: 2020-08-05T22:55:00.000Z Slip 2, month first: 2020-05-08 refused: no such day: 31/02/2020 refused: not DD/MM/YYYY: 5/8/2020
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node normalise_gst.mjs
Testing a date parser with small days
Wrong: a test file whose dates all fall on days 1 to 12, so a month-first parser passes every test. Right: include a
day above 12 in every date test, such as 25/08/2020, and a day that does not exist, such as 31/02/2020, which must
be refused.
Checkpoint
Check: Day-first dates and implied zones
Practise: normalise four kinds of timestamp
The exercise asks for one function that reads four forms of a moment, a timestamp with its own offset, a local reading, a day-first date and Unix time: The number of seconds since midnight UTC at the start of 1970, not counting leap seconds, such as 1791090000. JavaScript's Date counts milliseconds from the same moment, so its numbers are 1,000 times larger., and writes each as RFC 3339 in UTC. Its sample tests include the boundaries, the days that do not exist and the forms that must be refused:
Exercise · Medium · Python, JavaScript
Normalise four kinds of timestamp to UTC
A merchant's export mixes four ways of writing a moment. Write to_utc(text, offset) in Python (toUtc(text, offset) in JavaScript) that returns the moment as an RFC 3339 string in UTC, YYYY-MM-DDTHH:MM:SSZ, and refuses everything else with an error (ValueError in Python, an Error in JavaScript).
The four forms of text:
- A timestamp with its own offset:
YYYY-MM-DDTHH:MM:SSfollowed byZor±HH:MM. The offset in the text wins. - A local reading without an offset: the same, with a
Tor a space between the date and the time. It is a reading of a clock atoffset. - A day-first date,
DD/MM/YYYY: midnight atoffseton that day. - Unix seconds: one to eleven digits, the seconds since
1970-01-01T00:00:00Z. Eleven digits reach the year 5138, well inside the four-digit years of RFC 3339; a longer number is refused.
offset is Z or ±HH:MM and defaults to +05:30. A day that does not exist, an hour above 23, an offset such as +5:30 and any other form, such as a date without a time, are refused.
The sample tests show each form, the boundaries and what must be refused.
Python · Starter code · stamps.py
from datetime import datetime, timezone
def to_utc(text, offset="+05:30"):
"""The moment text names, as YYYY-MM-DDTHH:MM:SSZ. This first try reads what fromisoformat reads, month first
for slashes, and takes every reading without an offset as UTC: the tests show where that goes wrong."""
if text.isdigit():
moment = datetime.fromtimestamp(int(text), timezone.utc)
elif "/" in text:
moment = datetime.strptime(text, "%m/%d/%Y")
else:
moment = datetime.fromisoformat(text)
if moment.tzinfo is None:
moment = moment.replace(tzinfo=timezone.utc)
return moment.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") The sample tests · test_stamps.py
from stamps import to_utc
def refused(text, offset="+05:30"):
try:
to_utc(text, offset)
except ValueError:
return True
return False
def test_own_offset_wins():
"""a timestamp with its own offset names one moment, whatever offset is passed"""
assert to_utc("2026-10-04T10:30:00+05:30") == "2026-10-04T05:00:00Z"
assert to_utc("2026-10-04T05:00:00Z", "-04:00") == "2026-10-04T05:00:00Z"
assert to_utc("2026-10-03T19:00:00-10:00") == "2026-10-04T05:00:00Z"
def test_local_readings():
"""a reading without an offset is a clock at offset, with T or a space"""
assert to_utc("2020-08-05 22:55:00") == "2020-08-05T17:25:00Z"
assert to_utc("2026-10-04T10:30:00", "-04:00") == "2026-10-04T14:30:00Z"
assert to_utc("2026-12-31 23:59:59", "Z") == "2026-12-31T23:59:59Z"
assert to_utc("2026-01-01T03:00:00", "+05:30") == "2025-12-31T21:30:00Z"
def test_day_first_dates():
"""DD/MM/YYYY is midnight at offset, day first"""
assert to_utc("05/08/2020") == "2020-08-04T18:30:00Z"
assert to_utc("31/12/2025", "Z") == "2025-12-31T00:00:00Z"
assert to_utc("29/02/2024", "+01:00") == "2024-02-28T23:00:00Z"
def test_unix_seconds():
"""digits are seconds since the epoch, in UTC"""
assert to_utc("0") == "1970-01-01T00:00:00Z"
assert to_utc("1791090000", "-04:00") == "2026-10-04T05:00:00Z"
def test_refuses_what_does_not_exist():
"""days, months and hours that do not exist are refused"""
for text in ["30/02/2026", "29/02/2026", "01/13/2026", "2026-02-29 10:00:00", "2026-04-31T10:00:00Z", "2026-10-04T24:00:00Z"]:
assert refused(text), text
def test_refuses_other_forms():
"""a date alone, other separators, short offsets and empty text are refused"""
for text in ["2026-10-04", "04-10-2026", "5/8/2020", "2026-10-04T10:30:00+5:30", "1e9", "999999999999", ""]:
assert refused(text), text
assert refused("2026-10-04 10:30:00", "+5:30")
assert refused("2026-10-04 10:30:00", "IST")
assert refused("2026-10-04 10:30:00", "+24:00")
assert refused("2026-10-04T10:30:00+05:60") JavaScript · Starter code · stamps.mjs
/** The moment text names, as YYYY-MM-DDTHH:MM:SSZ. This first try lets Date guess every form: the tests show where
* that goes wrong. */
export function toUtc(text, offset = "+05:30") {
const ms = /^\d+$/.test(text) ? Number(text) * 1000 : Date.parse(text);
return new Date(ms).toISOString().slice(0, 19) + "Z";
} The sample tests · stamps.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { toUtc } from './stamps.mjs';
const refused = (text, offset = '+05:30') => {
try {
toUtc(text, offset);
} catch {
return true;
}
return false;
};
test('a timestamp with its own offset names one moment, whatever offset is passed', () => {
assert.equal(toUtc('2026-10-04T10:30:00+05:30'), '2026-10-04T05:00:00Z');
assert.equal(toUtc('2026-10-04T05:00:00Z', '-04:00'), '2026-10-04T05:00:00Z');
assert.equal(toUtc('2026-10-03T19:00:00-10:00'), '2026-10-04T05:00:00Z');
});
test('a reading without an offset is a clock at offset, with T or a space', () => {
assert.equal(toUtc('2020-08-05 22:55:00'), '2020-08-05T17:25:00Z');
assert.equal(toUtc('2026-10-04T10:30:00', '-04:00'), '2026-10-04T14:30:00Z');
assert.equal(toUtc('2026-12-31 23:59:59', 'Z'), '2026-12-31T23:59:59Z');
assert.equal(toUtc('2026-01-01T03:00:00', '+05:30'), '2025-12-31T21:30:00Z');
});
test('DD/MM/YYYY is midnight at offset, day first', () => {
assert.equal(toUtc('05/08/2020'), '2020-08-04T18:30:00Z');
assert.equal(toUtc('31/12/2025', 'Z'), '2025-12-31T00:00:00Z');
assert.equal(toUtc('29/02/2024', '+01:00'), '2024-02-28T23:00:00Z');
});
test('digits are seconds since the epoch, in UTC', () => {
assert.equal(toUtc('0'), '1970-01-01T00:00:00Z');
assert.equal(toUtc('1791090000', '-04:00'), '2026-10-04T05:00:00Z');
});
test('days, months and hours that do not exist are refused', () => {
for (const text of ['30/02/2026', '29/02/2026', '01/13/2026', '2026-02-29 10:00:00', '2026-04-31T10:00:00Z', '2026-10-04T24:00:00Z']) {
assert.ok(refused(text), text);
}
});
test('a date alone, other separators, short offsets and empty text are refused', () => {
for (const text of ['2026-10-04', '04-10-2026', '5/8/2020', '2026-10-04T10:30:00+5:30', '1e9', '999999999999', '']) assert.ok(refused(text), text);
assert.ok(refused('2026-10-04 10:30:00', '+5:30'));
assert.ok(refused('2026-10-04 10:30:00', 'IST'));
assert.ok(refused('2026-10-04 10:30:00', '+24:00'));
assert.ok(refused('2026-10-04T10:30:00+05:60'));
}); A hint
Turn every offset into minutes first (+05:30 is 330, -04:00 is −240, Z is 0). A local reading at offset m is the UTC moment m minutes earlier, so subtract the offset; never add it. Let the date library check that the day exists, and match each form with a pattern anchored at both ends, so that 2026-10-04 alone matches none of them.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide, QuickJS): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then test the four ideas, from the parts of a timestamp to day-first dates. Each answer explains itself, and the session returns to the idea you miss:
Practice that adapts to you
Practise this lesson
12 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to 5 years): how should an API send a timestamp in JSON, and why?
As an RFC 3339 string with seconds and an explicit offset, such as 2026-10-04T10:30:00+05:30 or the same instant in
UTC, 2026-10-04T05:00:00Z. JSON has no date type, so the string is the whole contract, and RFC 3339 is the profile
of ISO 8601 that the IETF and JSON Schema’s date-time format use. The offset is what matters most: without it the
text is only the reading of a clock, and each reader attaches its own zone, so the same text becomes different
instants on different computers. I-JSON, RFC 7493, adds upper-case letters and seconds even when they are zero. For a
local time booked for the future, send the local time and the zone’s name as well, because the zone’s offset may
change before then.
Two design questions follow, about a feed whose times have no offset and about appointments in several countries. Make your own first decision before you read the first step of each:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A partner's JSON feed sends order times without an offset. Your daily totals look right, but orders placed late in the evening sometimes land on the next day. What happened, and how do you fix the feed and the rows already stored?
-
Senior to staff
Design how a booking service stores and returns appointment times for clinics in India, the United Kingdom and the United States, including bookings made months ahead. What do you store, and what does the API return?
Flashcards · 7 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What does an RFC 3339 timestamp contain?
A full date, the letter T, a time with seconds, and an offset: Z or a sign with hours and minutes. I-JSON adds upper-case letters and seconds even when they are zero.
-
What does RFC 9557 add to an RFC 3339 timestamp?
A time zone name in brackets after the offset, such as [Asia/Kolkata]; a ! inside the bracket makes it critical, so a reader that cannot use it must refuse.
-
Which formats give back typed dates?
Only TOML always does, and Psych does for YAML 1.1 timestamps when allowed. JSON, CSV, XML without a schema and YAML's 1.2 core schema give back text.
-
How does Psych read a YAML timestamp that has no offset?
As UTC.
-
Why is a time without an offset dangerous in data?
It is only a clock reading: each reader attaches its own zone, so one text becomes different instants on different computers.
-
How should a future appointment be stored?
As its local time with the zone's name, because the zone's offset may change before the day; attach the zone with zoneinfo for places whose offset changes.
-
Why do tests of day-first date parsing need days above 12?
Only those dates fail under a month-first reading. A date whose day is 12 or less parses either way, wrongly but without an error.
Key takeaways
- An RFC 3339 timestamp is a full date,
T, a time with seconds and an offset; I-JSON asks for upper case and seconds, RFC 9557 adds a zone name in brackets, and a parser that reads a string is not proof that the string is valid. - JSON, CSV, XML without a schema and YAML’s 1.2 core schema return dates as text; TOML types them, and Psych reads YAML 1.1 timestamps, taking a time without an offset as UTC.
- A time without an offset is a clock reading that each reader turns into a different instant; attach the zone on
purpose, with
zoneinfofor places whose offset changes, and send future local times with the zone’s name. - Day-first dates such as
05/08/2020need a parser that is told the order and checks that the day exists, and tests need days above 12, because a month-first reading fails only for them.
Terms in this lesson
- fold
- The attribute of a Python datetime that chooses between the two moments a local time names when clocks go back: 0 for the first, 1 for the second, as PEP 495 defines it.
- IANA time zone
- A named region such as Asia/Kolkata or Europe/London, whose history of UTC offsets and daylight-saving changes the IANA Time Zone Database records. Python's zoneinfo and JavaScript's Intl read it.
- leap second
- A second added to UTC as second 60 of the last minute of a month, to keep UTC close to the Earth's rotation. RFC 3339 allows it; Python's datetime and JavaScript's Date both refuse it.
- RFC 3339
- The IETF's profile of ISO 8601 for timestamps on the internet: a full date, the letter T, a time with seconds, an optional fraction and an offset, Z or a sign with hours and minutes. JSON Schema's date-time format refers to its grammar.
- Unix time
- The number of seconds since midnight UTC at the start of 1970, not counting leap seconds, such as 1791090000. JavaScript's Date counts milliseconds from the same moment, so its numbers are 1,000 times larger.
- UTC offset
- How far a local clock is ahead of or behind UTC, written Z or as a sign with hours and minutes, such as +05:30 for India. An offset fixes one instant but says nothing about the rules of the place.
References
- RFC 3339: Date and Time on the Internet: Timestamps (IETF)
- RFC 9557: Date and Time on the Internet: Timestamps with Additional Information (IETF)
- RFC 7493: The I-JSON Message Format (IETF)
- JSON Schema Validation 2020-12: vocabularies for semantic content with format (JSON Schema)
- YAML Ain't Markup Language (YAML) version 1.2.2 (YAML Language Development Team)
- Timestamp Language-Independent Type for YAML Version 1.1 (YAML Language Development Team)
- TOML v1.1.0 (TOML)
- W3C XML Schema Definition Language (XSD) 1.1 Part 2: Datatypes (W3C)
- XML Path Language (XPath) 3.1: dynamic context (W3C)
- datetime: basic date and time types (Python Software Foundation)
- zoneinfo: IANA time zone support (Python Software Foundation)
- PEP 495: Local Time Disambiguation (Python Software Foundation)
- Date.parse() (MDN Web Docs) (Mozilla)
- Temporal documentation (Ecma TC39)
- Psych: Ruby's YAML library (safe_load) (Ruby)
- Time Zone Database (IANA)
- e-Invoice sample JSON (response with signed invoice) (GST e-Invoice System, Government of India)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress