Skip to main content

JavaScript: Formatting ISO 8601 Complete Date-Time to a Readable One

Greetings!

The complete ISO 8601 date-time format is:

Breaking that down:

  • YYYY

    Four-digit year.

  • -MM

    Two-digit month (01–12).

  • -DD

    Two-digit day (01–31).

  • T

    Literal separator between date and time.

  • HH

    Two-digit hour (00–23, 24-hour clock).

  • :mm

    Two-digit minute (00–59).

  • :ss

    Two-digit second (00–59).

  • .sss

    three-digit millisecond.

  • Z

    Indicates UTC ("Zulu time").

    If we're using a specific offset instead, it'd be +HH:mm or -HH:mm in place of Z.

    "Zulu":

    UTC used to be referred to by the letter Z in navigation and military contexts, meaning "zero offset" from Greenwich. So Z for zero-offset time, spoken aloud as "Zulu" per the phonetic alphabet. Hence, "Zulu time" as an old military / aviation term for UTC, which is where the Z suffix in ISO 8601 timestamps traces back to.

    "Zulu" here has nothing to do with the Philippines, or even the Zulu people of Southern Africa, really.

    They could always use "zero", but nay, that was too mathematical. —

    We don't do that sort of jargon! Can't have anyone thinking too hard, best just borrow an entire nation's name instead of typing two syllables of maths.

    ðŸĪ” Yes, yes.

For example:

Now, the formatting method.

We can do either:

  1. the JS string .split() and such,
  2. or directly employ .toLocaleDateString().

Let's simply use toLocaleDateString to achieve that. "That" is the readable date / date-time format, I mean. Option number two. Since option number one would seem like we were taking a long scenic detour.

Right. toLocaleDateString, the syntax goes as such:

Let's break it down:

  • date

    That's a variable. We may name it other than that.

    The data type is Date instance. As in the output of new Date().

  • "BCP 47"

    BCP 47 is used as a locale identifier for toLocaleDateString.

    BCP stands for Best Current Practice, a persistent label used by the IETF (Internet Engineering Task Force) for a specific set of technical recommendations and guidelines.

    Some of them:

    1. en — English (generic)
    2. en-GB — English (UK)
    3. en-US — English (US)
    4. en-AU — English (Australia)
    5. en-CA — English (Canada)
    6. en-IE — English (Ireland)
    7. en-IN — English (India)
    8. en-NZ — English (New Zealand)
    9. en-ZA — English (South Africa)
    10. fr / fr-FR / fr-CA — French
    11. de / de-DE / de-AT / de-CH — German
    12. it — Italian
    13. es / es-ES / es-MX — Spanish
    14. pt / pt-PT / pt-BR — Portuguese
    15. nl / nl-NL / nl-BE — Dutch
    16. ja — Japanese
    17. zh / zh-CN / zh-TW / zh-HK — Chinese
    18. ko — Korean
    19. ru — Russian
    20. ar — Arabic
    21. hi — Hindi
    22. pl — Polish
    23. sv — Swedish
    24. da — Danish
    25. nb / no — Norwegian
    26. fi — Finnish
    27. tr — Turkish
    28. el — Greek
    29. he — Hebrew

    The 47 in BCP 47 is simply the sequential document number in the IETF's "Best Current Practice" series, NOTHING to do with a count of language tags at all.

    This is the IANA Language Subtag Registry. THOUSANDS of entries!

  • options

    It is an object to adjust the output of toLocaleDateString.

    Complete list:

    The weekday is the name of the day of the week. For example, Sunday, Monday, Tuesday, etc.

    While day is the numeric date of a particular month. 1, 2, 3, etc.

    The "long" value will output the complete word for that particular option. For instance, Tuesday (weekday) and September (month).

    The "short" value will output the locale's own commonly abbreviated form for a particular option. For instance, using en-GB language tag — Tuesday = Tue (weekday), September = Sept (month), and 2026 = 26 (year).

    The "narrow" value will output the compact form for a particular option. For instance, Tuesday = T (weekday), and September = S (month). Though worth noting the clash there too: May, March, and several others could just as easily be M, and we'd get more than one month starting with the same letter. J for January, June, and July, for example. So it is COMPACT but NOT unique. Just how it goes.

    The "numeric" value will output the numeric form for that particular option. For instance, 1, 2, 3, etc.

    The "2-digit" value will output the 0-padded numeric form for that particular option. For instance, 01, 02, 03, etc.

    The timeZoneName:

    • "long" — e.g. Greenwich Mean Time
    • "short" — e.g. GMT
    • "shortOffset" — e.g. GMT+1
    • "longOffset" — e.g. GMT+01:00
    • "shortGeneric" — e.g. UK Time
    • "longGeneric" — e.g. United Kingdom Time

    The timeZone:

    "Asia/Tokyo" / "Australia/Sydney" / "Europe/Paris" is IANA time zone identifier (tz identifier or TZ identifier).

    IANA = Internet Assigned Numbers Authority.

    The list on Wikipedia.

    Or we can use the latest database release from IANA.

    The others:

    • hour12

      true | false, forces 12-hour or 24-hour clock display.

    • hourCycle

      "h11" | "h12" | "h23" | "h24", finer control over hour cycling than hour12.

    • era

      "long" | "short" | "narrow", shows things like Anno Domini, AD, or A. Only works for certain calendar / locale combinations, often needs year set alongside it.

    • dayPeriod

      "long" | "short" | "narrow", shows AM/PM-style labels (e.g. "in the morning" for "long").

    • calendar

      Sets which calendar system to use ➡️ "gregory" | "buddhist" | "islamic" | "japanese", and so on.

    • numberingSystem

      For example, "latn" | "arab" | "hans", controls which digit characters get used.

      Indeed, latn — the shortened ISO 15924 script code for Latin, not the full word.

    • dateStyle

      "full" | "long" | "medium" | "short", a shorthand that auto-formats date parts. CANNOT be mixed with individual options like year/month/day.

    • timeStyle

      Same idea with dateStyle but for time parts. CANNOT mix with hour/minute/second either.

    Specifically for dateStyle and timeStyle. Worth flagging: they are mutually exclusive with the individual field options (weekday, year, month, day, hour, minute, second) — we use ONE approach or the other, NOT both together.

Right. That's the introduction. Moving on to the JS snippets.


Simple Format

We may wrap the method in a function as such:

I'm using en-GB language tag. You can swap it to your own locale.

That function above only outputs the readable date string: "day month year".

Usage:

Snapshot from my console:

Simple format output

Or we can use the birthdate of Kublai Khan converted to a complete ISO date-time string — using new Date() and .toISOString().

His birthdate is 23 September 1215 in Gregorian calendar. But we flip it first so it adheres ISO 8601 format: YYYY-MM-DD — to avoid the funny business JS has.

And so: 1215-09-23. Thus:

So let's use 1215-09-23T00:01:15.000Z.

Birthdate of Kublai Khan with simple date output

Well, Kublai Khan's ghost would certainly:

What's Gregorian? Plonker?


Extended Format

Let's use more options. Something like so:

The usage is similar to the simple version but with the extended output.

Snapshot:

Birthdate of Kublai Khan with extended output

About that GMT-0:01:15 offset or the 00:01:15.000Z part. As you can see, it has the minute-and-second precision.

In general — every location recorded in the IANA tz (timezone) database carries its own pre-standardisation LMT (Local Mean Time) offset, derived precisely from that location's longitude, producing odd minute-and-second precision rather than clean hours. Each place then transitions to a standardised, rounder offset at its own historical cutover date, whenever that particular region formally adopted standard time — dates that vary from place to place, not a single global year.

Britain's railway companies began adopting a standardised time in the 1840s, informally converging on it over that decade, though the change was not made law nationwide until the Statutes (Definition of Time) Act in 1880.

Kublai Khan's birthyear — 1215 — is prior to that standardisation year — 1880...ish — thus any date calculation involving London's timezone for that year falls back to the pre-standardisation LMT offset recorded in the tz database, rather than the clean, modern UTC+0.

Note: the tz database itself openly states that most of its pre-1970 timestamps — including LMT offsets like London's — may be wrong or misleading, being best-effort reconstructions rather than verified historical fact.

⬆️ Is it confusing? I hope not. But it is confusing. Right. Moving on.


A Note for timeZoneName and timeZone

In summary:

  • timeZoneName is set to "long" / "short" / "shortOffset" / etc.

    It controls how the zone's name gets displayed.

    Without an explicit timeZone set alongside it, that zone follows the OS's (or server's) own default.

  • timeZone is set to an IANA identifier / tz identifier (e.g. "Europe/Paris").

    It overrides the default entirely, forcing the date to follow whatever zone we have specified instead.

🙋 What will happen if I use both timeZoneName and timeZone?

Didn't I just show you that?

🙋 AGAIN! Please.

Very well. Then they work together.

timeZone picks which zone the date's calculated in, and timeZoneName decides how that zone's label gets displayed alongside it.

🙍 Ah.

Quite.


Great Leaders

Let's derail a bit since I brought Kublai Khan into this. To name a few others like Caesar, Alexander, Genghis, Gajah Mada, Napoleon.

I look at it as a force of nature. It's triggered or it naturally happens because of large populations, competing states, and the right conditions for one ambitious bloke to seize the moment. So first, the number of population.

It wouldn't happen if there were only three people existed.

Tim: Oi, Geoff. Get the woods.

Geoff: You're not the leader!

Sasha: Oi! Geoff, get some meat too!

Geoff: Oh, you melons!

⬆️ We can't be a "great conqueror" with a headcount of three, we're just the annoying one bossing Geoff about firewood.

ðŸĪ”

Here's the thing — one afternoon of being bossed about firewood is just mild annoyance, but give it a decade of Geoff quietly becoming the only one who actually knows where the good hunting grounds are, how to track, how to build a proper shelter, while Tim and Sasha never learnt a thing beyond barking orders... that's when the power genuinely inverts.

Knowledge and skill, compounded over time, is precisely how the "underling" becomes the "indispensable one" — though it takes one more ingredient, ambition, to turn that into the one giving orders instead of taking them.

And the cycle carries on.

Geoff: Oi, Tim. Get the water!

And I just contradict myself.

Meet Geoff, the Conqueror of the Three-Person Woodland Commune.


Thanks for visiting! ðŸŧ

Monkey Raptor uses cookies for analytics, advertisements, and functionality. Privacy Policy