Coder Social home page Coder Social logo

proposal-intl-relative-time's Introduction

Intl.RelativeTimeFormat API Specification [draft]

Overview

Motivation

Due to common use, relative time–formatted values exist in a majority of websites and are available for the majority of frameworks (e.g., React, via react-intl and react-globalize; Ember, via ember-intl). Popular localization libraries like Moment.js, Format.js, Globalize, and others have implemented a formatting process for relative time values as well.

It is highly probable that the majority of current relative time formatting implementations require a large portion of CLDR raw or compiled data to format relative time values. Bringing this into the platform will improve performance of the web and developer productivity as they no longer have to bring extra weight to format relative time values.

Usage examples

The following example shows how to create a relative time formatter using the English language.

Units : "year", "quarter", "month", "week", "day", "hour", "minute" and "second".

// Create a relative time formatter in your locale
// with default values explicitly passed in.
const rtf = new Intl.RelativeTimeFormat("en", {
    localeMatcher: "best fit", // other values: "lookup"
    numeric: "always", // other values: "auto"
    style: "long", // other values: "short" or "narrow"
});


// Format relative time using negative value (-1).
rtf.format(-1, "day");
// > "1 day ago"

// Format relative time using positive  value (1).
rtf.format(1, "day");
// > "in 1 day"

Note: If numeric:auto option is passed, it will produce the string yesterday or tomorrow instead of 1 day ago or in 1 day, this allows to not always have to use numeric values in the output.

// Create a relative time formatter in your locale
// with numeric: "auto" option value passed in.
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });

// Format relative time using negative value (-1).
rtf.format(-1, "day");
// > "yesterday"

// Format relative time using positive day unit (1).
rtf.format(1, "day");
// > "tomorrow"

Implementation Status

Stage 4

Implementation Progress

Backpointers

Polyfills

There're several polyfills available which are listed in the comparison table below. The functionality of all polyfills is the same in terms of the API: they only differ in their implementation details like the way the polyfill is imported or the way locales are loaded or whether the implementation passes the Official ECMAScript Conformance Test for the complete coverage of all possible edge cases.

Polyfill intl-relative-time-format @formatjs/intl-relativetimeformat relative-time-format
Requirements Requirements: Intl.NumberFormat, Intl.PluralRules, Intl.getCanonicalLocales, Object.is, WeakMap and others Requirements: Intl.NumberFormat, Intl.PluralRules, Object.is No requirements
Core bundle size (gzipped) 3.2 kB 2.7 kB 3.0 kB
Passes the Official ECMAScript Conformance Test ✔️ Yes ✔️ Yes No

Authors

  • Caridy Patiño (@caridy)
  • Eric Ferraiuolo (@ericf)
  • Zibi Braniecki (@zbraniecki)
  • Rafael Xavier (@rxaviers)
  • Daniel Ehrenberg (@littledan)

Reviewers

TBD

Proposal

Intl.RelativeTimeFormat is a low level API to facilitate libraries and frameworks to format relative time in a localized fashion by providing internationalized messages for date and time fields, using customary word or phrase when available.

Spec

You can view the spec text or rendered as HTML.

Technical Design

This proposal is based on the ICU Relative Date Time Formatter and on the Unicode CLDR Calendar Fields Relative values:

It is also based on the LDML spec, C.11 Language Plural Rules:

Prior Art

Java
  • ICU: com.ibm.icu.impl.RelativeDateFormat
  • org.ocpsoft.prettytime.PrettyTime
Ruby
include ActionView::Helpers::DateHelper
def index
  @friendly_date = time_ago_in_words(Date.today - 1)
end

Naming

For consistency with Intl.NumberFormat and Intl.DateTimeFormat, we have chosen a similar form for this new feature. The creation of an Intl.RelativeTimeFormat instance is an expensive operation that requires resolution of locale data, and most likely, libraries will attempt to cache those instances, just like they do for Intl.NumberFormat and Intl.DateTimeFormat.

We have also chosen style as the primary form of switching between different formatting forms for consistency with Intl.NumberFormat and Intl.DateTimeFormat.

Since this new feature does format a provided value just like instances of Intl.NumberFormat, and Intl.DateTimeFormat, we have chosen the same form by providing a format(value) method of the instance, which returns a formatted string value.

Take number instead of date object for input

Relative time is used to display date distances, therefore the natural form of input should intuitively be a date object. Although, in this API we chose to take a number instead due to the following reasons:

  1. Basically, taking a number as input for the format method instead of a date object significantly simplifies the scope of this proposal while it still fully addresses the main objective which is to provide i18n building blocks to address this problem realm.
  2. Taking a date object means we should implement the comparison logic (relative time is about date distance between target and source dates). The source date is usually now, but not always. We would have to address modifying that. See #4.
  3. Taking a date object also means we should allow for different calendar calculations, which implies Date should support it. See #6 and #13.
  4. Taking a date object suggests we should be able to implement a bestFit algorithm, which has its own API challenges with respect to standardizing an approach that works for all cases. See #7, #14, and #15. We'd probably need to provide a flag for users to fill, with no default setting, to choose between options for calendar calculation.

Take number as input rather than exposing the underlying database

An idea has been floated, in the context of "the extensible web", of just exposing the engine's copy of the CLDR database rather than a higher-level interface would be better. In the case of this specification, there is already a JS object model ready to go--the locale database is represented internally in the spec as a JavaScript object.

However, we opted not to go that route for a couple reasons:

  • As described above, the API is already fairly low-level, taking numbers rather than dates.
  • Although there are clearly use cases for different policies about rounding dates into units, we haven't come across a use case for seeing the underlying data.
  • This new API is analogous to previous APIs, which should be useful for people learning the system.
  • CLDR changes schema over time; if the data model improves, implementations can transparently upgrade users to better results with the same API. However, if we freeze things to the current logic, the old data model would need to be emulated.

Difference between this and UnitFormat

The fundamental difference between RelativeTimeFormat and UnitFormat is that RelativeTimeFormat displays a relative unit (e.g., 5 days ago or in 5 days) while UnitFormat displays an absolute unit (e.g., -5 meters or 5 meters). Note that RelativeTimeFormat uses different internationalized messages based on the value sign direction, while UnitFormat uses the same internationalized message for all values.

Countdowns, e.g., 15 days, 0 hours, 27 minutes, and 52 seconds

A countdown for example is a mix of UnitFormat and ListFormat, and is not a RelativeTimeFormat.

NumberFormat options (e.g., useGrouping, maximumFractionDigits)

RelativeTimeFormat messages may include number parts (e.g., the 1,000 in 1,000 days ago), which are formatted using the NumberFormat default options.

In this design, we didn't find any use case that could justify allowing to change/override these NumberFormat default options. Therefore, RelativeTimeFormat doesn't include any NumberFormat option.

API

Intl.RelativeTimeFormat([locales[, options]])

The Intl.RelativeTimeFormat object is a constructor for objects that enable language-sensitive relative time formatting.

locales

Optional. A string with a BCP 47 language tag, or an array of such strings. For the general form and interpretation of the locales argument, see the Intl page.

options

Optional. An object with some or all of the following properties:

localeMatcher

The locale matching algorithm to use. Possible values are "lookup" and "best fit"; the default is "best fit". For information about this option, see the Intl page.

numeric

The format of output message. Possible values are "always" (default, e.g., 1 day ago), or "auto" (e.g., yesterday). "auto" allows to not always have to use numeric values in the output.

style

The length of the internationalized message. Possible values are: "long" (default, e.g., in 1 month); "short" (e.g., in 1 mo.), or "narrow" (e.g., in 1 mo.). The narrow style could be similar to the short style for some locales.

Example

// Create a relative time formatter in your locale.
let rtf = new Intl.RelativeTimeFormat("en", {
    localeMatcher: "best fit", // other values: "lookup"
    numeric: "always", // other values: "auto"
    style: "long", // other values: "short" or "narrow"
});

Intl.RelativeTimeFormat.prototype.format(value, unit)

The Intl.RelativeTimeFormat.prototype.format method formats a value and unit according to the locale and formatting options of this Intl.RelativeTimeFormat object.

While this method automatically provides the correct plural forms, the grammatical form is otherwise as neutral as possible. It is the caller's responsibility to handle cut-off logic such as deciding between displaying "in 7 days" or "in 1 week". This API does not support relative dates involving compound units. e.g "in 5 days and 4 hours".

value

Numeric value to use in the internationalized relative time message.

unit

Unit to use in the relative time internationalized message. Possible values are: "year", "quarter", "month", "week", "day", "hour", "minute", "second". Plural forms are also permitted.

Example

const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });

// Format relative time using the day unit.
rtf.format(-1, "day");
// > "yesterday"

rtf.format(2.15, "day");
// > "in 2.15 days"

rtf.format(100, "day");
// > "in 100 days"

rtf.format(0, "day");
// > "today"

rtf.format(-0, "day");
// > "today"

Additionally, by combining the class option style and unit, you can achieve any of the following results:

last year
this year
next year
in 1 year
in 2 years
1 year ago
2 years ago
yr.
last yr.
this yr.
next yr.
in 1 yr.
in 2 yr.
1 yr. ago
2 yr. ago
last quarter
this quarter
next quarter
in 1 quarter
in 2 quarters
1 quarter ago
2 quarters ago
last qtr.
this qtr.
next qtr.
in 1 qtr.
in 2 qtrs.
1 qtr. ago
2 qtrs. ago
last month
this month
next month
in 1 month
in 2 months
1 month ago
2 months ago
last mo.
this mo.
next mo.
in 1 mo.
in 2 mo.
1 mo. ago
2 mo. ago
last week
this week
next week
in 1 week
in 2 weeks
1 week ago
2 weeks ago
last wk.
this wk.
next wk.
in 1 wk.
in 2 wk.
1 wk. ago
2 wk. ago
in 1 day
in 2 days
1 day ago
2 days ago
yesterday
today
tomorrow
in 1 hour
in 2 hours
1 hour ago
2 hours ago
in 1 hr.
in 2 hr.
1 hr. ago
2 hr. ago
in 1 minute
in 2 minutes
1 minute ago
2 minutes ago
in 1 min.
in 2 min.
1 min. ago
2 min. ago
in 1 second
in 2 seconds
1 second ago
2 seconds ago
in 1 sec.
in 1 sec.
1 sec. ago
2 sec. ago
now

Intl.RelativeTimeFormat.prototype.formatToParts(value, unit)

The Intl.RelativeTimeFormat.prototype.formatToParts method is a version of the format method which it returns an array of objects which represent "parts" of the object, separating the formatted number into its consituent parts and separating it from other surrounding text. These objects have two properties: type a NumberFormat formatToParts type, and value, which is the String which is the component of the output. If a "part" came from NumberFormat, it will have a unit property which indicates the unit being formatted; literals which are part of the larger frame will not have this property.

Example

const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });

// Format relative time using the day unit.
rtf.formatToParts(-1, "day");
// > [{ type: "literal", value: "yesterday"}]

rtf.formatToParts(100, "day");
// > [{ type: "literal", value: "in " }, { type: "integer", value: "100", unit: "day" }, { type: "literal", value: " days" }]

Development

Render Spec

npm install
npm run build
open index.html

proposal-intl-relative-time's People

Contributors

anba avatar caiolima avatar caridy avatar catamphetamine avatar frankyftang avatar keithamus avatar koddsson avatar littledan avatar longlho avatar mathiasbynens avatar ms2ger avatar okuryu avatar romulocintra avatar rxaviers avatar zbraniecki avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

proposal-intl-relative-time's Issues

Remove quarters from `best fit`

In the current spec language quarters will never be selected by the GetBestMatchUnit because they overlap months.

I suggest that we remove it from the best fit scenario, and leave it only for manual selection with {unit: 'quarter'}.

Textual names like "yesterday"

The ICU interface for getting at these times exposes strings like "yesterday" through a separate UDAT_ABSOLUTE_DAY unit. On the other hand, if you make an ordinary UDAT_RELATIVE_DAYS query for -1 days, you'll get "1 day ago". A couple questions about this:

  • The current spec text says, first get the PluralRules, then select the template based on the PluralRules and direction, then fill it in. However, this would not allow getting something like "anteayer" (the day before yesterday) in Spanish because Spanish has no dual category; further, in English, it seems like a natural template to select would be "{0} day ago", rather than "yesterday". Do we want to use the ABSOLUTE version when available, falling back to the RELATIVE one only when appropriate?
  • Are there any applications that would want to render as "1 day ago", or would really everybody want to render as "yesterday"? If so, maybe we should introduce separate "text" vs "numeric" modes, where "text" mode would use ABSOLUTE where applicable and fall back to RELATIVE if needed, and "numeric" would just start with RELATIVE. But something I don't understand is, does anyone want "numeric"?

Naming for type: numeric/text

In #9, a new option was added to RelativeTimeFormat: setting type to numeric or text. Some hesitations about naming were expressed in #9 (comment) ; @caridy has also expressed concern offline. Let's think harder about the right name here and brainstorm alternatives.

[Future] Add `type` option for numeric vs. text formatting

ICU provides two functions for formatting relative date: text and numeric - the difference is that in text mode it attempts to use human readable terms like yesterday and last week instead of numeric 1 day ago and 1 week ago.

I'd like to suggest adding this in form of type option:

let rtf = new Intl.RelativeTimeFormat('en-US', {
  type: 'text'
});
rtf.format(new Date() - 1000 * 60 * 60 * 24); // 'yesterday'


let rtf2 = new Intl.RelativeTimeFormat('en-US', {
  type: 'numeric'
});
rtf2.format(new Date() - 1000 * 60 * 60 * 24); // '1 day ago'

I'd suggest the text value to be the default.

bestFit API choices

We decided to not include any bestFit algorithm in the first iteration of the RelativeTimeFormat API and instead we expect the client libraries like momentjs and others to add them on top.

I'm working currently on such higher-level wrapper and I encountered an interesting API design dilemma that I'd like to get a recommendation for.

When we designed the core formatters for the Intl API, we pushed all resolving pieces into the object constructors. That has two nice benefits - it does the computations and data retrieving at object creation, making the core operation (usually format) as lightweight as possible, and gives us resolvedOptions method that allows users to retrieve the values the object options were resolved to.

Now, in RelativeTimeFormat, we introduce a new concept - an option that is resolved at core operation calltime, rather than construction time. The format(value, unit) differs from all previous Intl APIs in the unit component.
Since it's a dummy operation which does not compute anything, that works, but both for higher level APIs and for the future bestFit, we should plan ahead how we're going to facilitate an API that has to select the unit when core operation is called, and how to provide an ability for the user to retrieve not only the result string, but also the unit it resolved to.

To present it as JS, I see two ways to do it:

let rtf = new Intl.RelativeTimeFormat('en-US', {style: 'short'});
let result = rtf.format(5, 'bestFit');
result === {'value': 'in 5 sec', unit: 'second'};

or:

let rtf = new Intl.RelativeTimeFormat('en-US', {style: 'short'});
let value = rtf.format(5, 'bestFit'); // 'in 5 sec'
let unit = rtf.resolveBestFitUnit(5); // 'second'

There of course may be others that I didn't think of :)

@rxaviers , @caridy , @littledan , @maggiepint, @icambron - can you share your preference?

[future] Add time formatting option

CLDR and ICU provide as part of the RelativeDateTimeFormatter API an option to format time together with the date to produce strings like tomorrow at 3 pm or today at 3:45 pm.

Since we pass Date object as parameter already, all we really have to do is enable formatting of that.

My suggestion is to add hour, minute, second and hourCycle as options for RelativeTimeFormat.

Example:

let rtf = new Intl.DateTimeFormat('en-US', {
  hour: 'numeric'
});

rtf.format(new Date() + 1000 * 60 * 60 * 24); // 'tomorrow at 3 pm'
let rtf = new Intl.DateTimeFormat('en-US', {
  hour: 'numeric',
  minute: 'numeric'
});

rtf.format(new Date() + 1000 * 60 * 60 * 24); // 'tomorrow at 3:45 pm'

by default those options would be undefined and no date+time formatting would happen.

I suggest that we keep this idea for second revision of the API, but wanted to propose it already to put it in scope of any API conversations.

Add IntervalFormat

https://github.com/tc39/ecma402/blob/master/README.md does not have 'Interval format'. So, I guess it's not on the table, yet.

This is a place holder for IntervalFormat. I'll add more details later.

For now, briefly what this API would do is to format an interval with the minimum redundancy given two points in time and a "pattern" (out of a fixed list of patterns).

For instance, given 2016-04-05 and 2016-04-07 and "YMD", the formatted result can be "April 5 - 7, 2016". For 2017-05-12 and 2017-07-04, the result can be "May 12 - July 4, 2017".

Numeric of 0 "day"?

What should be the "numeric" output for (0, "day")? 0 days ago or in 0 days?

Would we use the below?

rtf.format(0, "day");
// > "in 0 days"

rtf.format(-0, "day");
// > "0 days ago"

We need to update the spec to reflect this special case.

[future] capitalization context

Relative time format may be useful within localization contexts but in order for that to work, we'd need to follow ICU and expose ability to capitalize the formatted string.

ICU exposes UDisplayContext [0] which in this case would take one of the three values standalone, sentence-start or sentence-middle. Example:

let rtf = new Intl.RelativeTimeFormat('en-US', {
  context: 'sentence-start'
});
rtf.format(new Date() - 1000 * 60 * 60 * 24); // 'Yesterday'
let rtf = new Intl.RelativeTimeFormat('en-US', {
  context: 'sentence-middle'
});
rtf.format(new Date() - 1000 * 60 * 60 * 24); // 'yesterday'

I don't think we should do this for the first revision, but wanted to file an issue already to put it in peoples radar when talking about API.

[0] http://www.icu-project.org/apiref/icu4c/udisplaycontext_8h.html#ac80aa1aceff6c7ad2e9f983a19d8d868

[[LocaleData]][Locale] is expected to have a "fields" field, but it doesn't

InitializeRelativeTimeFormat has

Let localeData be %RelativeTimeFormat%.[[LocaleData]].
Let dataLocaleData be Get(localeData, dataLocale).
Let fields be Get(dataLocaleData, "fields").
Assert: fields is an object (see 1.3.3).
Set relativeTimeFormat.[[Fields]] to fields.

and relativeTimeFormat.[[Fields]] is later used, expecting that it has "second", "minute", "hour", … properties. However, http://tc39.github.io/proposal-intl-relative-time/#sec-Intl.RelativeTimeFormat-internal-slots claims

[[LocaleData]][Locale] has ordinary data properties "second", "minute", "hour", "day", "week", "month", "quarter" and "year".

without the intermediary fields object.

Does the current proposal make sense outside of the gregorian calendar?

Update: this can be solved by using a calendar option as Zibi suggests below.


Problem

The current proposal takes a number of ms as argument and calculates the correspondent second, minute, ..., year for formatting, being a positive in the future and a negative in past, which seems very sane.

It happens that some calendards have different months and years lengths [1]. Thankfully, the equation (used here for deducing time units) should always work for seconds, minutes, hours, and days, but it might fail for deducing months and years. For example, in the Chinese Calendar there are 12 or 13 months; in the Islamic Calendar, there are 354 or 355 days year.

In a general sense, e.g., suppose a developer is writing a game where he needs to display the relative time of Martian months, the current calculation doesn't make sense. Note that in this example case, it doesn't make sense even for day.

General thoughts

Above we have a good and a bad news. The bad news is that the problem lies in the calendar calculation and that's usually a tough problem. The good news is that it doesn't have anything to do with internationalization/localization (i18n).

The i18n problem is very simple, it's to display the localized message, e.g., 10 seconds ago where numeric value is -10 and unit is second. Additionally to this problem is whether or not the formatted text is integer or decimal, because it could change the plural form.

The calendar calculation problem is whether or not a bunch of ms corresponds to a bunch of months.

The current relative time proposal tries to solve both problems. It would be easier if it sticks with the i18n problem only.

Any thoughts?

1:

Option to retrieve words as opposed to numbers

In English, most style guides agree that integers in the range [1, 9] should be written as words (e.g. "in five minutes" as opposed to "in 5 minutes"). The same concept also exists in other languages; in German, this range is commonly said to be [1, 12].

In order to achieve this, are users supposed to formatToParts and use a library of their choice to write out the words based on custom rule sets? Is it maybe worth incorporating this behavior into the Intl API?

bestFit doesn't work with "past" values

1.3.4 Plural Form Functions, FormatRelativeTime states:

  1. Let units be ComputeTimeUnits(ms).
    8a. Let unit be GetBestMatchUnit(units).

Currently, ComputerTimeUnits returns units that may have negative values, and GetBestMatchUnit only checks if the unit value is smaller than a constant.

That makes the algorithm not work for "past" values.

units to consider when selecting the unit

Currently, the spec is considering "all" units possible when trying to determine the correct one - seconds, minutes, hours, days, weeks, months, quarters, years.

We still have more units to consider like "last Tuesday", and I can see users wanting to skip "quarters" or calculate only up to days, so - seconds, minutes, hours, normally, but nothing above days (so "4500 days ago").

It seems like a simple solution would be to allow people to pass the list of units to consider:

let x = new Intl.RelativeTimeFormat(locales, {
  unit: 'best fit',
  units: ['second', 'minute', 'hour', 'day']
});

Then, if you want to add weekdays or quarters, you'd just list it as one of the units.

Does it sound realistic?

Write an explainer for the design

There's been a lot written about RelativeTimeFormat, but it's scattered in a few different places. We should collect everything in a single README. The explainer should get at the main points of motivation for this design, including:

  • Why this is the right level of abstraction
  • Why did we make the main API choices that we made
  • Why is this in terms of numbers, rather than dates/duration

Should formatToParts break down the inner NumberFormat?

When discussing the RelativeTimeFormat formatToParts method in the April 18th ICU-TC call, we agreed that the underlying ICU API would break up the inner number format into parts (in addition to exposing the entire number range). The current specification draft exposes just the range of the whole number. Would it be useful to have this level of detail in JavaScript as well? For example, this could come up in examples like, "in 1,234 days" or "1.5 seconds ago". Currently, the substrings "1234" or "1.5" are represented as a single part.

Calendar option for non-gregorian calendars

Add calendar option, which follows the selected language and can be overridden manually, to handle non-Gregorian calendars relative times, e.g., "13 months ago" for Chinese calendar.

This needs to be properly defined...

For context, see #6.

Ambiguous word value

Hi,

In the interest of full disclosure, this is my first time reviewing spec text - so I may just not understand a convention. However, I find this wording ambiguous:

When the Intl.RelativeTimeFormat.prototype.format is called with an optional argument value, the following steps are taken:

Let relativeTimeFormat be *this* value.

What does this refer to? If I understand correctly it should mean that the object relativeTimeFormat is the same as the this value in the function - which would make sense because the next line validates that relativeTimeFormat is an instance of a relative time format object. It's just confusing because the word value is already defined as a different (number) argument.

If anything, I might change it to Let relativeTimeFormat be *this*.

Reconsider style names

Names are currently "long", "short" and "narrow". Why not medium in the middle? Where do they come from? This was raised at the July 2017 TC39 meeting. (cc @msaboff)

Polyfill implementation

Hello.
I've been developing an implementation of a similar library based on CLDR some time ago.
https://github.com/catamphetamine/javascript-time-ago
That would be good if browsers finally incorporate this feature, still I guess I could transform my library into a polyfill compatible with your spec (once it's approved by the committee) for older browsers (iOS, Android).

I also found that things like yesterday and last year don't really work and I replaced them manually with 1 day ago and 1 year ago.
That's something you may find reasonable.

bestFit skips weeks

The current draft in 1.3.4 'GetBestMatchUnit' specifies:

  1. If units.[[day]] is less than 26, return "day".

That in result means that 2 weeks will be displayed as "14 days".

I believe it would be useful to switch it to:

  1. If units.[[day]] is less than 7, return "day".

Anchored relative time

After reading this bugzilla thread I have a worry. Just a thought from the peanut gallery: it's not obvious to me that a Date + duration is the best input to get "tomorrow" out of. Wouldn't it be clearer to take two Dates?

(Edit: rewriting this paragraph) In the current proposal, how exactly is the math going to work? The easiest way for me to ask this is to point to Luxon's documentation: https://moment.github.io/luxon/docs/manual/math.html#calendar-math-vs-time-math. E.g. is 24 hours different than 1 day? How about 1 year vs 6 months 365 days? In what order do multiple units get added?

Luxon spends a lot of code and documentation sorting out how all that works and I know from supporting Moment that these issues come up all the time. It's not just that you'd have to do the math; it's that you'd need to commit to a very specific way of doing it, which sounds like something a built-in API would rather not do. Worse, if I wanted to do the math differently, I wouldn't be able to use the API, because I can't pass it the date I want to end up on.

What would be more clear and flexible would be telling me what one date is called relative to some other date, i.e. it's my job to do the math and hand the API both endpoints. I recognize this requires a very different API from what the non-anchored version does, which is essentially formatting durations. But it really does seem different. Of course, you have to do some math in any case, but the right answer to that math is way less ambiguous.

Compound relative time

One item we do not have covered for the relativetime is compound units.

For typical UnitFormat+ListFormat it's easy - "5 hours, 35 minutes" is:

let lf = new Intl.ListFormat(locales, { type: "unit" });
let uf = new Intl.UnitFormat(locales);
lf.format([uf.format(5, "hour"), uf.format(35, "minute")]); // "5 hours, 35 minutes"

but that doesn't translate to RelativeTimeFormat:

let lf = new Intl.ListFormat(locales, { type: "unit" });
let rtf = new Intl.RelativeTimeFormat(locales);
lf.format([rtf.format(5, "hour"), rtf.format(35, "minute")]); // "in 5 hours, in 35 minutes"

[future] Add days of the week

This is a pretty basic feature that's missing. Should we add it into the [[Units]] field, as @rxaviers suggested? I don't think it should overlap as CLDR stores them in the same place.

"an optional argument value"

When the Intl.RelativeTimeFormat.prototype.format is called with an optional argument value, the following steps are taken:

When the Intl.RelativeTimeFormat.prototype.formatToParts is called with an optional argument value, the following steps are taken:

Both of these methods have two required arguments, rather than a single optional one.

Lower case unit names

Seems like at the moment the spec does not lowercase the unit names, so code like this will throw:

let rtf = new Intl.RelativeTimeFormat();
rtf.format(1, "Second");

Do we want it to throw or do we want to lowercase it?

Review @@toStringTag value

1.4.2 Intl.RelativeTimeFormat.prototype[ @@toStringTag ]

The initial value of the @@toStringTag property is the string value "Object".

Why? What is even the point of having the property if you're going to make it say "Object"?

"rt" and "pr" extension keys don't exist

Current version of the spec specifies two relevant extension keys:

 The value of the [[RelevantExtensionKeys]] internal slot is ["rt", "pr"].
Note 1 Unicode Technical Standard 35 describes three locale extension keys that are relevant to relative time formatting, "rt" for relative time, and "pr" language plural rules to choose the proper relative time pattern. 

yet TR35 does not have either of those:

http://www.unicode.org/reports/tr35/#Locale_Extension_Key_and_Type_Data

Should we remove them?

Current bestFit returns wrong textual output

The "bestFit" unit is currently specified as following:

  • If units.[[Second]] is less than 60, return "Second".
  • If units.[[Minute]] is less than 60, return "Minute".
  • If units.[[Hour]] is less than 24, return "Hour".
  • If units.[[Day]] is less than 7, return "Day".
  • If units.[[Week]] is less than 4, return "Week".
  • If units.[[Month]] is less than 12, return "Month".
  • Return "Year".

For numeric type output, this works nicely, i.e., the following table shows the output for past tense, but we obviously have the equivalent for the future tense.

Distance from now Numeric output
0 seconds ago 0 seconds ago
<2 second 1 second ago
<1 minute 59 seconds ago
<2 minutes 1 minute ago
<1 hour 59 minutes ago
<2 hours 1 hour ago
<1 day 23 hours ago
<2 days 1 day ago
<1 week 6 days ago
<2 weeks 1 week ago
<1 month 3 weeks ago
<2 months 1 month ago
<1 year 11 months ago
else 1 year ago

For the textual output, we have the following.

Distance from now Verbose output
0 seconds ago now
<2 second 1 second ago
<1 minute 59 seconds ago
<2 minutes 1 minute ago
<1 hour 59 minutes ago
<2 hours 1 hour ago
<1 day 23 hours ago
<2 days yesterday
<1 week 6 days ago
<2 weeks last week
<1 month 3 weeks ago
<2 months last month
<1 year 11 months ago
else last year

Problem

The verbose outputs can be misleading depending on the context. For example:

Subject date Now Verbose output Confusion
The day before yesterday 10 pm (i.e., 36 hours ago) Today 10 am "yesterday" It should be "2 days ago"!
The Thu before the past Thu (i.e., 11 days ago) Mon "last week" It should be "2 weeks ago"! (additional details)
The day 20 of the month before the past month. (i.e., ~40 days ago) Day 10 "last month" It should be "2 months ago"
Jul of the year before the past year. (i.e., 18 months ago) Jan "last year" It should be "2 years ago"

Diagnose

For the numeric output, the continuous distance basted on seconds (or milliseconds) works fine. Although for the textual output, the continuous distance can lead to incorrect results, instead it should be used a discrete count based on the subject unit. For example, see the 4 tables below that corresponds to the 4 items above respectively.

Subject date Now Distance (continuous) Distance (discrete, based on unit) Current bestFit Output Improved bestFit output
Today 00:00 am Today 10 am 10 hours ago (-0.416 days) 0 days "10 hours ago" "10 hours ago"
Yesterday 11:59 pm Today 10 am 10 hours + 1 minute ago (-0.416 days) -1 day "10 hours ago" "10 hours ago" / "yesterday"⁉️
Yesterday 00:00 am Today 10 am 34 hours ago (-1.416 days) -1 day "yesterday" "yesterday"
The day before yesterday 11:59pm Today 10 am 34 hours ago (-1.416 days) -2 day "yesterday" Wrong‼️ "2 days ago"

The last row of the table above shows a case where "yesterday" is wrong. Note it happens when the continuous distance !== discrete distance in days. The second row shows a case where both "10 hours ago" or "yesterday" could be used without prejudice. Note when the continuous distance === discrete distance in days, there's no doubt.

The similar occurs to the following units.

Subject date Now Distance (continuous) Distance (discrete, based on unit) Current bestFit Output Improved bestFit output
Sun (note first day of week a localized info) Mon 1 day ago (-0.14 weeks) 0 weeks "yesterday" "yesterday"
Past Sat Mon 2 days ago (-0.29 weeks) -1 week "2 days ago" "2 days ago" / "last week"⁉️
Past Sun Mon 8 days ago (-1.14 weeks) -1 week "last week" "last week"
The Sat before past Sat Mon 9 days ago (-1.25 weeks) -2 weeks "last week" Wrong‼️ "2 weeks ago"
Subject date Now Distance (continuous) Distance (discrete, based on unit) Current bestFit Output Improved bestFit output
The first day of this month Day 10 10 days ago (-0.33 months) 0 months "10 days ago" "10 days ago"
The last day of the previous month Day 10 11 days ago (-0.34 months) -1 month "11 days ago" "11 days ago" / "last month"⁉️
Day 10 of the past month Day 10 ~30 days ago (-1 month) -1 month "last month" "last month"
The day 20 of the month before the past month Day 10 40 days ago (-1.33 months) -2 months "last month" Wrong‼️ "2 months ago"
Subject date Now Distance (continuous) Distance (discrete, based on unit) Current bestFit Output Improved bestFit output
Jan Feb 1 month ago (-0.08 years) 0 years "last month" "last month"
Past Dec Feb 2 months ago (-0.17 years) -1 year "2 months ago" "2 months ago" / "last year"⁉️
Past Jan Feb 12 months ago (-1 year) -1 year "last year" "last year"
Aug of the year before the past year Feb 18 months ago (-1.5 years) -2 years "last year" Wrong‼️ "2 years ago"

For the numeric output, the continuous distance basted on seconds (or milliseconds) works fine. Although for the textual output, the continuous distance can lead to incorrect results, instead it should be used a discrete count based on the subject unit. For example, see the 4 tables below that corresponds to the 4 items above respectively.

Subject date Now Distance (continuous) Distance (discrete, based on unit) Current bestFit Output Improved bestFit output
Today 00:00 am Today 10 am 10 hours ago (-0.416 days) 0 days "10 hours ago" "10 hours ago"
Yesterday 11:59 pm Today 10 am 10 hours + 1 minute ago (-0.416 days) -1 day "10 hours ago" "10 hours ago" / "yesterday"⁉️
Yesterday 00:00 am Today 10 am 34 hours ago (-1.416 days) -1 day "yesterday" "yesterday"
The day before yesterday 11:59pm Today 10 am 34 hours ago (-1.416 days) -2 day "yesterday" Wrong‼️ "2 days ago"

The last row of the table above shows a case where "yesterday" is wrong. Note it happens when the continuous distance !== discrete distance in days. The second row shows a case where both "10 hours ago" or "yesterday" could be used without prejudice. Note when the continuous distance === discrete distance in days, there's no doubt.

The similar occurs to the following units.

Subject date Now Distance (continuous) Distance (discrete, based on unit) Current bestFit Output Improved bestFit output
Sun (note first day of week a localized info) Mon 1 day ago (-0.14 weeks) 0 weeks "yesterday" "yesterday"
Past Sat Mon 2 days ago (-0.29 weeks) -1 week "2 days ago" "2 days ago" / "last week"⁉️
Past Sun Mon 8 days ago (-1.14 weeks) -1 week "last week" "last week"
The Sat before past Sat Mon 9 days ago (-1.25 weeks) -2 weeks "last week" Wrong‼️ "2 weeks ago"
Subject date Now Distance (continuous) Distance (discrete, based on unit) Current bestFit Output Improved bestFit output
The first day of this month Day 10 10 days ago (-0.33 months) 0 months "10 days ago" "10 days ago"
The last day of the previous month Day 10 11 days ago (-0.34 months) -1 month "11 days ago" "11 days ago" / "last month"⁉️
Day 10 of the past month Day 10 ~30 days ago (-1 month) -1 month "last month" "last month"
The day 20 of the month before the past month Day 10 40 days ago (-1.33 months) -2 months "last month" Wrong‼️ "2 months ago"
Subject date Now Distance (continuous) Distance (discrete, based on unit) Current bestFit Output Improved bestFit output
Jan Feb 1 month ago (-0.08 years) 0 years "last month" "last month"
Past Dec Feb 2 months ago (-0.17 years) -1 year "2 months ago" "2 months ago" / "last year"⁉️
Past Jan Feb 12 months ago (-1 year) -1 year "last year" "last year"
Aug of the year before the past year Feb 18 months ago (-1.5 years) -2 years "last year" Wrong‼️ "2 years ago"

Conclusion

For textual output, we need an additional handling for the bestFit that takes into account what I'm calling the discrece distance in the respective unit.

For the case where the output is wrong, the fix is a clear requirement. For the cases where both are ok, it is subject to additional analysis. Ideally the API should handle this nicely. I mean, if now is "10 AM", the output "11 hours ago" should be ok instead of "yesterday". Although, I would definitely prefer seeing "yesterday" than "19 hours ago". There should be a threshold based on the discrete distance for the switch.

FWIW, https://github.com/rxaviers/relative-time/ handles discrete distance.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.