How to parse ISO 8601 duration strings in JavaScript

February 02, 2022
Written by
Reviewed by

If you've encountered a duration string that looks like P1DT12H36M and been confused about what to do with it, then you're in the right place. Much like parsing phone numbers, while you can write a regular expression to parse a duration, I don't recommend it.

What is an ISO 8601 duration?

ISO 8601 is a set of standardized date and time formats in an attempt to tame every programmer's favorite challenge. Durations represent the amount of time between two dates or times.

This format displays a duration in the following format: PnYnMnDTnHnMnS where n is the number for the corresponding interval:

  • Y = years
  • M = months
  • W = weeks
  • D = days
  • T = delineator between dates and times, necessary to disambiguate between months and minutes
  • H = hours
  • M = minutes
  • S = seconds

You can leave certain intervals off  if they don't apply, but you must include the T before any time intervals (P<date>T<time>). Valid examples include:

  • P3Y6M4DT12H30M5S (3 years, 6 months, 4 days, 12 hours, 30 minutes, and 5 seconds)
  • P3DT12H (3 days and 12 hours)
  • P1M (1 month)
  • PT1M (1 minute)
  • PT0S (0)
  • P0.5Y (1/2 a year)
  • PT1M3.025S (1 minute and 3025 milliseconds)

You can use decimals in the last and smallest value like in the 0.5Y or 3.025S examples above. You can also ignore "carry over points" like 24 hours or 60 seconds so PT36H and PT1D12H are both valid. Like any date or time problem there can be gotchas, so "keep in mind that 'PT36H' is not the same as 'P1DT12H' when switching from or to Daylight saving time." [source]

Does JavaScript have native support for durations?

There is no API to parse durations in JavaScript's Date API. However, durations are part of the Temporal proposal. Learn more about Temporal on the Igalia blog.

I've included 3 ways to parse durations in JavaScript in this blog post, they all have their pros and cons:

  1. Temporal
  2. tinyduration
  3. moment.js

How to parse a duration with Temporal

This is not recommended for production use yet, but the interface is really great and I'm hopeful it will become the standard solution soon. Head over to https://tc39.es/proposal-temporal/docs/duration.html and open up the developer tools console (Chrome instructions) in your browser. The polyfill is automatically included in the DevTools for experimentation so you don't have to install anything.

Parse an ISO 8601 duration string with the following code:

const d = Temporal.Duration.from("P4DT12H30M5S");

From the Duration object in the d variable, you can access elements or calculate totals:

d.milliseconds; // 0, read only not provided
d.total("millisecond"); // 390605000

If the duration contains a year, month, or week, you'll need to include a "relative" date. Skipping the relativeTo parameter will result in the error Uncaught RangeError: a starting point is required for balancing calendar units (learn more).

Fix the error by adding a relative date:

d.total({ relativeTo: Temporal.Now.plainDateISO(), unit: "millisecond"});

You can even add durations together:

const d1 = Temporal.Duration.from("PT1H");
const d2 = Temporal.Duration.from("PT30M");
d1.add(d2); // PT1H30M

Or round to the nearest unit of time:

Temporal.Duration.from("PT75H").round("day"); // "P3D"

Learn more about what's possible in the Temporal documentation.

How to parse a duration with tinyduration

I found tinyduration in a Stack Overflow post. From my testing it effectively does the job and seems maintained.

Install the library (you'll need Node.js and npm or Yarn):

  • NPM: npm install --save tinyduration
  • Yarn: yarn add tinyduration

Open the Node.js REPL from your terminal (type node) and type in or paste the following code:

const td = require("tinyduration")

td.parse("P3Y6M4DT12H30M5S")
// { years: 3, months: 6, days: 4, hours: 12, minutes: 30, seconds: 5 }

The parse method returns a simple object containing the relevant duration details, and you can also serialize objects back to ISO 8601 string durations. Unfortunately, you'll have to do additional work or use another library to convert that to something aggregate like milliseconds, but depending on your needs this might be a good option.

How to parse a duration with moment.js

Unfortunately moment.js is a large library with known issues (including one specific to durations). However, this library provides a lot of useful tools that the native Date API does not. It is also more powerful than options like tinyduration because it offers things like converting durations to milliseconds or friendlier strings so it's an option until Temporal is production ready.

Install the library (detailed installation instructions):

  • NPM: npm install --save moment
  • Yarn: yarn add moment

Open the Node.js REPL from your terminal (type node) and type in or paste the following code:

const moment = require("moment");

const d = moment.duration("P3Y6M4DT12H30M5S");
console.log(d.months()); // 6
console.log(d.hours()); // 12
console.log(d.asMilliseconds()); // 110809805000
console.log(d.humanize()); // 4 years

Learn more about working with durations using in the Moment.js documentation.

Where are ISO 8601 durations used?

I first discovered this duration format in recipe metadata. According to the Schema.org Recipe standard, things like cook time should be represented as a duration. The schema will also infer total time from prep time and cook time if it's not explicitly provided.

picture of soup with developer tools open that shows cookTime and prepTime with ISO 8601 duration formats

Bonus soup recipe

Twilio also uses durations in our new SIM Swap package in the Lookup API. Since data varies by country and may not include a specific swapped date, we also include a swapped period that's represented by a duration. A sample response to a SIM Swap Lookup would look like this:

{
   "calling_country_code":"44",
   "country_code":"GB",
   "phone_number":"+447772000001",
   "national_format":"07772 000001",
   "valid":true,
   "sim_swap":{
      "last_sim_swap":{
         "last_sim_swap_date":"2022-01-27T10:18:50Z",
         "swapped_period":"PT15H33M44S",
         "swapped_in_period":true
      },
      "carrier_name":"Vodafone UK",
      "mobile_country_code":"276",
      "mobile_network_code":"02",
      "error_code":null
   },
   "url":"https://lookups.twilio.com/v2/PhoneNumbers/+447772000001"
}

To check if the SIM swap occurred in the last day you could do something like this with moment.js:

let max = moment.duration({ day: 1 });
let swappedPeriod = moment.duration("PT15H33M44S");

let risky = swappedPeriod.asMilliseconds() < max.asMilliseconds()  // true

To format durations in human-readable format you might like the Humanize Duration library. It can easily turn 110809805000 milliseconds into '3 years, 6 months, 4 days, 3 hours, 30 minutes, 5 seconds'. Learn how to use the project in its documentation.

Have you encountered ISO 8601 durations in the wild? Let me know where. Find me on Twitter @kelleyrobinson.