formatjs/formatjs

Best approach to express duration?

LukasUrban opened this issue · 18 comments

Lets say I want to return time taken by walking from point A to point B in minutes

1 minute
5 minutes

Is simple pluralization best practice, or is there any better approach?

react-intl supports relative time, more information here: http://formatjs.io/react/#formatted-relative

Currently the relative time formatting always evaluates the date against Date.now(). I'm curious to hear more about the data structure you have and the values you have for A and B as it will help us determine how we might add support for specifying a reference date other than "now" to compare to.

Current structure is based on start and stop (target) locations. Duration property is integer representing time between these two points in minutes.

route = {
    start:{station:'Kačerov', type:'metro metro-c'}, 
    stop:{station:'Tempo', type:'bus'}, 
    duration:7
}

Link to the case: http://fluid.cz/clients/libere/kontakt/react/production.html#/pobocky/praha_4 ("JAK SE K NÁM DOSTANETE" section)

Is there a unit property to accompany the duration value?

Nope, I just want to hardcode it to show minutes

Okay, so in this case we don't currently have a convenient way to access the term "minute(s)" in all the locales, even though we have the data and it backs our relative time formatting feature.

If you only need to support a few languages for this, then I would recommend treating as a translated message and using the <FormattedMessage> component:

<FormattedMessage
  message="{duration, plural, one{# minute} other{# minutes}}"
  duration={route.duration} />

This will give you the pluralization of minute/minutes based on the duration value. If you're supporting more than one language, extract this message out of your React components into a messages collection that's passed into the root React component (using the Intl Mixin).

And if unit property would be present?

@ericf Have you had to deal with displaying hour/minute duration between two datetimes?

Something like 1h 30m or 1 hour and 30 minutes.

I'm also trying to solve this. Trying to display the duration of two dates. My implementation is really ugly at the moment something like this would be nice:

<FormattedRelative value={endDate} valueFrom={startDate} units="years" precision="2" />

1 year and 2 months ago

My current implementation:

    renderDuration() {
        var startDate = this.props.experience.get('start_date');
        var endDate = this.props.experience.get('end_date') || moment();
        var duration = moment.duration(endDate.diff(startDate));
        var self = this;
        var renderAnd = function() {
            if (duration.years() < 1) { return false; }
            return (
                <span>
                    &nbsp;
                    <FormattedMessage message={self.getIntlMessage('and')} />
                    &nbsp;
                </span>
            );
        };        

        return (
            <span>
                <FormattedMessage
                    message={this.getIntlMessage('duration_years')}
                    years={duration.years()} />
                {renderAnd()}
                <FormattedMessage
                    message={this.getIntlMessage('duration_months')}
                    months={duration.months()} />
            </span>
        );
    }
{
            "and":"'and",
            "duration_years": "{years, plural, =0 {} =1 {1 year} other {# years}}",
            "duration_months": "{months, plural, =0 {} =1 {1 month} other {# months}}"
}

PR #94 adds a now prop to the <FormattedRelative> component. While this is not exactly what this thread is about, it is related.

Hi everyone,
I'm trying to understand what i should include and what is the right component signature to turn an integer into a HH:mm:ss representation.

I.e.:

42 --> 42
120 --> 2:00
3695 --> 1:00:59

ericf commented

@nicolabortignon This is not currently supported and there's no built-in duration/unit formatting API in ECMAScript. There is some discussion around creating Intl.DurationFormat: tc39/ecma402#32

Once this feature is officially proposed, work could begin on a polyfill, and I'd be happy to support adding formatDuration() and <FormattedDuration> to React Intl.

Today, as stated above in this thread, relative time formatting is the closest feature to durations; but it doesn't sound like the thing you're looking for.

ericf commented

Intl.DurationFormat is the right solution. Closing this…

Here's the correct link for Intl.DurationFormat: tc39/ecma402#47

I know this is quite old, but I stumbled upon it recently... I've created this https://github.com/en-japan-air/react-intl-formatted-duration it'll help until the ECMAScript API is implemented

I had the same issue today, get minutes as props and need to render minutes and hours.

export interface FormattedDurationProps {
    minutes: number;
}
export const FormattedDuration = (props: FormattedDurationProps) => {
    const durationHours = Math.floor(props.minutes / 60);
    const durationMinutes = props.minutes % 60;

    const renderHours = (hours: number) => (
        <FormattedMessage
            id="shift.durationHours"
            defaultMessage="{value,plural,=0{}one{1 Hour}other{# Hours}}"
            values={{ value: hours }}
        />
    );

    const renderMinutes = (minutes: number) => (
        <FormattedMessage
            id="shift.durationMinutes"
            defaultMessage="{value,plural,=0{}one{1 Minute}other{# Minutes}}"
            values={{ value: minutes }}
        />
    );

    if (durationHours && !durationMinutes) {
        return renderHours(durationHours);
    }

    if (!durationHours && durationMinutes) {
        return renderMinutes(durationMinutes);
    }

    return (
        <Fragment>
            {renderHours(durationHours)}
            <span>&nbsp;</span>
            {renderMinutes(durationMinutes)}
        </Fragment>
    );
};

Leave it here just in case anyone need simple FormattedDuration component

If you're still looking for a workaround, I created this gist for you. There's no magic there! just a wrapper around Intl. Hope this helps.

import React from 'react';
import { useIntl, defineMessages } from 'react-intl';
import PropTypes from 'prop-types';

const FormattedDuration = ({ value, children, unitDisplay, ...props }) => {
  const durationHours = Math.floor(value / 60);
  const durationMinutes = value % 60;
  const Intl = useIntl();

  const renderHours = (hours) =>
    Intl.formatNumber(hours, {
      style: 'unit',
      unit: 'hour',
      unitDisplay,
    });

  const renderMinutes = (minutes) =>
    Intl.formatNumber(minutes, {
      style: 'unit',
      unit: 'minute',
      unitDisplay,
    });

  if (durationHours <= 0 && durationMinutes <= 0) {
    return children(renderMinutes(durationMinutes));
  }
  if (durationHours <= 0 && durationMinutes > 0) {
    return children(renderMinutes(durationMinutes));
  }
  if (durationHours > 0 && durationMinutes <= 0) {
    return children(renderHours(durationHours));
  }

  const messageAnd = defineMessages({
    and: {
      id: 'and',
      defaultMessage: 'and',
      description: 'Used between durations',
    },
  });

  return children(
    `${renderHours(durationHours)} ${Intl.formatMessage(
      messageAnd.and
    )} ${renderMinutes(durationMinutes)}`
  );
};

FormattedDuration.propTypes = {
  value: PropTypes.number.isRequired,
  unitDisplay: PropTypes.oneOf(['long', 'narrow', 'short']),
};

FormattedDuration.defaultProps = {
  unitDisplay: 'long',
};

export default FormattedDuration;

I wouldn't recommend doing that @meness. Manual string concat is never i18n-safe. You can do:

const fractionUnits = [
	intl.formatNumber(1, { style: 'unit', unit: 'hour'}),
	intl.formatNumber(1, { style: 'unit', unit: 'minute'}),
]
intl.formatList(fractionUnits, {type: 'unit'})