Repository: iamkun/dayjs (JavaScript)

Issue:
How to get precise second(s) in `relativeTime` plugin?
**Describe the bug**
https://github.com/iamkun/dayjs/blob/dev/src/plugin/relativeTime/index.js#L8
All `seconds` displays `a few seconds`

**Expected behavior**
I want to display `1 second, 2 seconds...`

**Information**
 - Day.js Version: 1.8.22


Hints:
You can customize these string using:

https://day.js.org/docs/en/display/from-now#list-of-breakdown-range
But why they're different with other units by default?

And I tried

```js
dayjs.updateLocale('en', {
  relativeTime: {
    ...dayjs.Ls.en.relativeTime,
    s: 'a second',
    ss: '%d seconds',
  },
});
```

It seems `ss` is not working at all.

And when it comes to 44 seconds, it changes to `1 minute` suddenly. Is that possible to change the threads to real world? We don't need the fuzzification.
Maybe we can add another plugin `relativeTimeStrict` like [`formatDistanceStrict`](https://date-fns.org/v2.10.0/docs/formatDistanceStrict) from `date-fns`.

How do you think?

<img width="537" alt="Screen Shot 2020-03-10 at 3 53 55 PM" src="https://user-images.githubusercontent.com/17680888/76292935-1ce7a080-62eb-11ea-8fc5-d9e2d1685041.png">

We do not support `ss`

What we really need is a duration plugin?
Yes, we can do `s: '%d seconds'`, but it's incorrect for `1 seconds`.

What did you mean with `a duration plugin`?
I see.

You can pass a function then:

https://day.js.org/docs/en/customization/relative-time#additional-token-processing

@iamkun How about add a `strict` option for `relativeTime` plugin?

```ts
// based on https://github.com/iamkun/dayjs/blob/dev/src/plugin/relativeTime/index.js
/// <reference path="./relative-time.shim.d.ts" />
import { ConfigType, Dayjs, PluginFunc, UnitType } from 'dayjs';

export const MS = 'millisecond';
export const S = 'second';
export const MIN = 'minute';
export const H = 'hour';
export const D = 'day';
export const W = 'week';
export const M = 'month';
export const Q = 'quarter';
export const Y = 'year';

export interface RelativeTimeThread {
  l: string;
  r?: number;
  d?: UnitType;
}

export interface RelativeTimeOptions {
  strict?: boolean;
  threads?: RelativeTimeThread[];
}

export const relativeTime: PluginFunc = (
  options: RelativeTimeOptions,
  c,
  d,
  // eslint-disable-next-line sonarjs/cognitive-complexity
) => {
  const proto = c.prototype;
  d.Ls.en.relativeTime = {
    future: 'in %s',
    past: '%s ago',
    s: 'a few seconds',
    m: 'a minute',
    mm: '%d minutes',
    h: 'an hour',
    hh: '%d hours',
    d: 'a day',
    dd: '%d days',
    M: 'a month',
    MM: '%d months',
    y: 'a year',
    yy: '%d years',
  };

  if (options?.strict) {
    Object.assign(d.Ls.en.relativeTime, {
      s: '%d second',
      ss: '%d seconds',
      m: '1 minute',
      h: '1 hour',
      d: '1 day',
      M: '1 month',
      y: '1 year',
    });
  }

  const fromTo = (
    input: ConfigType,
    withoutSuffix: boolean,
    instance: Dayjs,
    isFrom = false,
  ) => {
    // @ts-ignore
    const loc = instance.$locale().relativeTime;
    const T: RelativeTimeThread[] =
      options?.threads || options?.strict
        ? [
            { l: 's', r: 1 },
            { l: 'ss', r: 59, d: S },
            { l: 'm', r: 1 },
            { l: 'mm', r: 59, d: MIN },
            { l: 'h', r: 1 },
            { l: 'hh', r: 23, d: H },
            { l: 'd', r: 1 },
            { l: 'dd', r: 30, d: D },
            { l: 'M', r: 1 },
            { l: 'MM', r: 11, d: M },
            { l: 'y' },
            { l: 'yy', d: Y },
          ]
        : [
            { l: 's', r: 44, d: S },
            { l: 'm', r: 89 },
            { l: 'mm', r: 44, d: MIN },
            { l: 'h', r: 89 },
            { l: 'hh', r: 21, d: H },
            { l: 'd', r: 35 },
            { l: 'dd', r: 25, d: D },
            { l: 'M', r: 45 },
            { l: 'MM', r: 10, d: M },
            { l: 'y', r: 17 },
            { l: 'yy', d: Y },
          ];

    if (!loc.ss) {
      loc.ss = loc.s;
    }

    const Tl = T.length;
    let result;
    let out;
    let isFuture;

    for (let i = 0; i < Tl; i += 1) {
      let t = T[i];
      if (t.d) {
        result = isFrom
          ? d(input).diff(instance, t.d, true)
          : instance.diff(input, t.d, true);
      }
      const abs = Math[options?.strict ? 'floor' : 'round'](Math.abs(result));
      isFuture = result > 0;
      if (abs <= t.r || !t.r) {
        if (abs <= 1 && i > 0) t = T[i - 1]; // 1 minutes -> a minute
        const format = loc[t.l];
        if (typeof format === 'string') {
          out = format.replace('%d', String(abs));
        } else {
          out = format(abs, withoutSuffix, t.l, isFuture);
        }
        break;
      }
    }
    if (withoutSuffix) return out;
    return (isFuture ? loc.future : loc.past).replace('%s', out);
  };
  proto.to = function(input, withoutSuffix) {
    return fromTo(input, withoutSuffix, this, true);
  };
  proto.from = function(input, withoutSuffix) {
    return fromTo(input, withoutSuffix, this);
  };

  // @ts-ignore
  const makeNow = (thisDay: Dayjs) => ('$u' in thisDay ? d.utc() : d());

  proto.toNow = function(withoutSuffix) {
    return this.to(makeNow(this), withoutSuffix);
  };
  proto.fromNow = function(withoutSuffix) {
    return this.from(makeNow(this), withoutSuffix);
  };
};
```

I can raise a PR if you'd like to have this feature.

Looks sweet. 😁 

Let me think for a while.
@iamkun Any news for this?
Why not 😬 thanks

Patch:
diff --git a/src/plugin/relativeTime/index.js b/src/plugin/relativeTime/index.js
--- a/src/plugin/relativeTime/index.js
+++ b/src/plugin/relativeTime/index.js
@@ -1,6 +1,7 @@
 import * as C from '../../constant'
 
 export default (o, c, d) => {
+  o = o || {}
   const proto = c.prototype
   d.en.relativeTime = {
     future: 'in %s',
@@ -19,7 +20,7 @@ export default (o, c, d) => {
   }
   const fromTo = (input, withoutSuffix, instance, isFrom) => {
     const loc = instance.$locale().relativeTime
-    const T = [
+    const T = o.thresholds || [
       { l: 's', r: 44, d: C.S },
       { l: 'm', r: 89 },
       { l: 'mm', r: 44, d: C.MIN },
@@ -44,10 +45,10 @@ export default (o, c, d) => {
           ? d(input).diff(instance, t.d, true)
           : instance.diff(input, t.d, true)
       }
-      const abs = Math.round(Math.abs(result))
+      const abs = (o.rounding || Math.round)(Math.abs(result))
       isFuture = result > 0
       if (abs <= t.r || !t.r) {
-        if (abs === 1 && i > 0) t = T[i - 1] // 1 minutes -> a minute
+        if (abs <= 1 && i > 0) t = T[i - 1] // 1 minutes -> a minute, 0 seconds -> 0 second
         const format = loc[t.l]
         if (typeof format === 'string') {
           out = format.replace('%d', abs)
diff --git a/types/index.d.ts b/types/index.d.ts
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -96,9 +96,9 @@ declare namespace dayjs {
     locale(preset: string | ILocale, object?: Partial<ILocale>): Dayjs
   }
 
-  export type PluginFunc = (option: any, c: typeof Dayjs, d: typeof dayjs) => void
+  export type PluginFunc<T = unknown> = (option: T, c: typeof Dayjs, d: typeof dayjs) => void
 
-  export function extend(plugin: PluginFunc, option?: any): Dayjs
+  export function extend<T = unknown>(plugin: PluginFunc<T>, option?: T): Dayjs
 
   export function locale(preset: string | ILocale, object?: Partial<ILocale>, isLocal?: boolean): string
 
diff --git a/types/plugin/relativeTime.d.ts b/types/plugin/relativeTime.d.ts
--- a/types/plugin/relativeTime.d.ts
+++ b/types/plugin/relativeTime.d.ts
@@ -1,6 +1,17 @@
 import { PluginFunc, ConfigType } from 'dayjs'
 
-declare const plugin: PluginFunc
+declare interface RelativeTimeThreshold {
+  l: string
+  r?: number
+  d?: string
+}
+
+declare interface RelativeTimeOptions {
+  rounding?: (num: number) => number
+  thresholds?: RelativeTimeThreshold[]
+}
+
+declare const plugin: PluginFunc<RelativeTimeOptions>
 export = plugin
 
 declare module 'dayjs' {


Test patch:
diff --git a/test/plugin/relativeTime.test.js b/test/plugin/relativeTime.test.js
--- a/test/plugin/relativeTime.test.js
+++ b/test/plugin/relativeTime.test.js
@@ -1,6 +1,7 @@
 import MockDate from 'mockdate'
 import moment from 'moment'
 import dayjs from '../../src'
+import * as C from '../../src/constant'
 import relativeTime from '../../src/plugin/relativeTime'
 import utc from '../../src/plugin/utc'
 import '../../src/locale/ru'
@@ -115,3 +116,24 @@ it('Time from now with UTC', () => {
 
   expect(dutc.fromNow()).toBe(mutc.fromNow())
 })
+
+it('Custom thresholds and rounding support', () => {
+  expect(dayjs().subtract(45, 'm').fromNow()).toBe('an hour ago')
+  dayjs.extend(relativeTime, {
+    rounding: Math.floor,
+    thresholds: [
+      { l: 's', r: 1 },
+      { l: 'm', r: 1 },
+      { l: 'mm', r: 59, d: C.MIN },
+      { l: 'h', r: 1 },
+      { l: 'hh', r: 23, d: C.H },
+      { l: 'd', r: 1 },
+      { l: 'dd', r: 29, d: C.D },
+      { l: 'M', r: 1 },
+      { l: 'MM', r: 11, d: C.M },
+      { l: 'y' },
+      { l: 'yy', d: C.Y }
+    ]
+  })
+  expect(dayjs().subtract(45, 'm').fromNow()).toBe('45 minutes ago')
+})