JavaScript: Humanizing durations

Posted . Visible to the public.

Modern JavaScript includes Intl.NumberFormat Show archive.org snapshot to format numbers in different formats and locales.
In this card, we describe a wrapper for it that humanizes a given number of seconds in the "next best" unit, like seconds, minutes, etc.

Example usage

>> new Duration(42).humanized()
=> '42 Sekunden'
>> new Duration(123456).humanized()
=> '1 Tag'
>> new Duration(123456).humanized('es')
=> '1 día'

Code

Here is the code as an ECMAScript module.

Note that we default to German in this case. You can easily switch that to your application language, document.documentElement.lang, or just use the user's system locale.

export default class Duration {
  constructor(seconds) {
    this.seconds = seconds
    this.minutes = this.seconds / 60
    this.hours = this.minutes / 60
    this.days = this.hours / 24
    this.years = this.days / 365
  }

  humanized(locale = 'de') {
    const humanizer = new Duration.Humanizer(locale)

    if (this.minutes < 1) {
      return humanizer.humanize(this.seconds, 'second')
    } else if (this.hours < 1) {
      return humanizer.humanize(this.minutes, 'minute')
    } else if (this.days < 1) {
      return humanizer.humanize(this.hours, 'hour')
    } else if (this.years < 1) {
      return humanizer.humanize(this.days, 'day')
    } else {
      return humanizer.humanize(this.years, 'year')
    }
  }

  static Humanizer = class {
    constructor(locale) {
      this.locale = locale
    }

    humanize(number, unit) {
      const formatOptions = {
        style: 'unit',
        unit,
        unitDisplay: 'long',
      }
      return new Intl.NumberFormat(this.locale, formatOptions).format(Math.round(number))
    }
  }
}

Yes, not every year has 365 days, but the above is usually good enough when formatting durations.

If ESLint or similar complains about the static Humanizer field, you need to set your ecmaVersion to at least 13. Static class fields are supported in all relevant browsers Show archive.org snapshot .

Jasmine spec

This Jasmine spec goes along with the code above:

import Duration from './duration'

describe('Duration', () => {

  describe('humanized', () => {
    it('humanizes in German by default, but accepts other locales as well', () => {
      const duration = new Duration(23)
      expect(duration.humanized()).toBe('23 Sekunden')
      expect(duration.humanized('en')).toBe('23 seconds')
      expect(duration.humanized('es')).toBe('23 segundos')
      expect(duration.humanized('ja')).toBe('23 秒')
    })

    it('returns formatted seconds if less than a minute', () => {
      const duration = new Duration(45)
      expect(duration.humanized()).toBe('45 Sekunden')
    })

    it('returns one formatted minute for roughly 1 minute', () => {
      const duration = new Duration(62)
      expect(duration.humanized()).toBe('1 Minute')
    })

    it('returns formatted minutes if less than an hour', () => {
      const duration = new Duration(1800)
      expect(duration.humanized()).toBe('30 Minuten')
    })

    it('returns one formatted hour for roughly 1 hour', () => {
      const duration = new Duration(3602)
      expect(duration.humanized()).toBe('1 Stunde')
    })

    it('returns formatted hours if less than a day', () => {
      const duration = new Duration(2 * 3600)
      expect(duration.humanized()).toBe('2 Stunden')
    })

    it('returns one formatted day for roughly 1 day', () => {
      const duration = new Duration(24 * 3600)
      expect(duration.humanized()).toBe('1 Tag')
    })

    it('returns formatted days if less than a year', () => {
      const duration = new Duration(10 * 24 * 3600)
      expect(duration.humanized()).toBe('10 Tage')
    })

    it('returns one formatted year for roughly 1 year', () => {
      const duration = new Duration(400 * 24 * 3600)
      expect(duration.humanized()).toBe('1 Jahr')
    })

    it('returns formatted years if more than a year', () => {
      const duration = new Duration(2 * 365 * 24 * 3600)
      expect(duration.humanized()).toBe('2 Jahre')
    })
  })

})

More modern API

There are several other objects for similar use cases, like Intl.RelativeTimeFormat Show archive.org snapshot if you want to format as e.g. "42 seconds ago" or Intl.DateTimeFormat Show archive.org snapshot to humanize dates natively.

Also note that you can use Intl.NumberFormat Show archive.org snapshot to format e.g. currencies or "only" format numbers with a specific number of significant digits, and a locale's decimal separators and similiar. It's really useful.

Arne Hartherz
Last edit
Arne Hartherz
License
Source code in this card is licensed under the MIT License.
Posted by Arne Hartherz to makandra dev (2024-08-21 13:02)