@design-edito/tools 0.5.2 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agnostic/html/deep-select/index.js +5 -3
- package/agnostic/html/hyper-json/smart-tags/coalesced/index.d.ts +8 -8
- package/agnostic/html/hyper-json/smart-tags/coalesced/index.js +8 -8
- package/agnostic/html/hyper-json/smart-tags/isolated/index.d.ts +2 -2
- package/agnostic/html/hyper-json/smart-tags/isolated/index.js +2 -2
- package/agnostic/html/index.d.ts +1 -1
- package/agnostic/html/index.js +1 -1
- package/agnostic/misc/index.d.ts +3 -3
- package/agnostic/misc/index.js +3 -3
- package/agnostic/numbers/index.d.ts +1 -1
- package/agnostic/numbers/index.js +1 -1
- package/agnostic/optim/index.d.ts +1 -1
- package/agnostic/optim/index.js +1 -1
- package/agnostic/strings/index.d.ts +1 -1
- package/agnostic/strings/index.js +1 -1
- package/agnostic/time/dates/format-date/index.d.ts +4 -2
- package/agnostic/time/dates/format-date/index.js +43 -94
- package/agnostic/time/dates/format-date/index.test.js +154 -0
- package/agnostic/time/dates/format-date/parts.d.ts +28 -0
- package/agnostic/time/dates/format-date/parts.js +136 -0
- package/agnostic/time/dates/format-date/types.d.ts +84 -0
- package/agnostic/time/dates/format-date/types.js +1 -0
- package/agnostic/time/duration/format-duration/index.d.ts +42 -0
- package/agnostic/time/duration/format-duration/index.js +94 -0
- package/agnostic/time/duration/format-duration/index.test.d.ts +1 -0
- package/agnostic/time/duration/format-duration/index.test.js +148 -0
- package/agnostic/time/duration/format-duration/parts.d.ts +32 -0
- package/agnostic/time/duration/format-duration/parts.js +107 -0
- package/agnostic/time/duration/format-duration/types.d.ts +93 -0
- package/agnostic/time/duration/format-duration/types.js +1 -0
- package/agnostic/time/duration/index.d.ts +6 -1
- package/agnostic/time/duration/index.js +2 -2
- package/agnostic/time/duration/index.test.d.ts +1 -0
- package/agnostic/time/duration/index.test.js +168 -0
- package/agnostic/time/index.d.ts +1 -1
- package/agnostic/time/index.js +1 -1
- package/components/BeforeAfter/index.controlled.d.ts +25 -25
- package/components/BeforeAfter/index.controlled.js +47 -53
- package/components/BeforeAfter/index.d.ts +20 -23
- package/components/BeforeAfter/index.js +48 -40
- package/components/Button/index.js +1 -1
- package/components/Clippable/index.d.ts +17 -26
- package/components/Clippable/index.js +21 -23
- package/components/Disclaimer/index.d.ts +29 -22
- package/components/Disclaimer/index.js +29 -24
- package/components/Drawer/index.d.ts +45 -25
- package/components/Drawer/index.js +61 -52
- package/components/EventListener/index.d.ts +4 -5
- package/components/Gallery/index.d.ts +14 -16
- package/components/Gallery/index.js +25 -30
- package/components/Iframe/index.js +2 -2
- package/components/Image/index.d.ts +5 -12
- package/components/Image/index.js +11 -32
- package/components/Input/index.d.ts +23 -19
- package/components/Input/index.js +30 -20
- package/components/JsonEditor/index.d.ts +177 -9
- package/components/JsonEditor/index.js +133 -77
- package/components/JsonEditor/types.d.ts +11 -0
- package/components/JsonEditor/types.js +1 -0
- package/components/JsonEditor/utils.d.ts +8 -0
- package/components/JsonEditor/utils.js +23 -0
- package/components/ListLoader/index.controlled.d.ts +5 -5
- package/components/ListLoader/index.controlled.js +4 -4
- package/components/ListLoader/index.d.ts +11 -11
- package/components/ListLoader/index.js +12 -12
- package/components/Overlayer/index.js +4 -4
- package/components/Paginator/index.d.ts +7 -12
- package/components/Paginator/index.js +7 -11
- package/components/ResizeObserver/index.d.ts +14 -3
- package/components/ResizeObserver/index.js +49 -24
- package/components/Scrllgngn/index.d.ts +17 -13
- package/components/Scrllgngn/index.js +25 -84
- package/components/Scrllgngn/utils.d.ts +52 -0
- package/components/Scrllgngn/utils.js +102 -0
- package/components/ScrollListener/index.d.ts +43 -28
- package/components/ScrollListener/index.js +59 -79
- package/components/ScrollListener/utils.d.ts +37 -16
- package/components/ScrollListener/utils.js +141 -28
- package/components/Select/index.d.ts +24 -20
- package/components/Select/index.js +30 -20
- package/components/Sequencer/index.controlled.d.ts +16 -26
- package/components/Sequencer/index.controlled.js +15 -12
- package/components/Sequencer/index.d.ts +25 -30
- package/components/Sequencer/index.js +20 -34
- package/components/Subtitles/index.d.ts +29 -5
- package/components/Subtitles/index.js +90 -12
- package/components/Textarea/index.d.ts +26 -20
- package/components/Textarea/index.js +40 -21
- package/components/Theatre/index.d.ts +35 -39
- package/components/Theatre/index.js +46 -58
- package/components/UIModule/index.d.ts +25 -29
- package/components/UIModule/index.js +81 -110
- package/components/Video/index.controlled.d.ts +51 -51
- package/components/Video/index.controlled.js +75 -72
- package/components/Video/index.d.ts +12 -14
- package/components/Video/index.js +41 -86
- package/components/Video/utils.d.ts +35 -3
- package/components/Video/utils.js +37 -14
- package/components/index.d.ts +1 -1
- package/components/index.js +1 -1
- package/components/utils/index.d.ts +22 -0
- package/components/utils/index.js +31 -0
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/node/@aws-s3/storage/file/index.d.ts +1 -1
- package/node/@aws-s3/storage/file/index.js +1 -1
- package/node/@google-cloud/storage/index.d.ts +1 -1
- package/node/@google-cloud/storage/index.js +1 -1
- package/node/images/index.d.ts +1 -1
- package/node/images/index.js +1 -1
- package/node/images/transform/operations/index.d.ts +1 -1
- package/node/images/transform/operations/index.js +1 -1
- package/node/index.d.ts +1 -1
- package/node/index.js +1 -1
- package/package.json +20 -5
- package/TODO.md +0 -269
- package/components/BeforeAfter/utils.d.ts +0 -4
- package/components/BeforeAfter/utils.js +0 -9
- package/components/Input/index.controlled.d.ts +0 -42
- package/components/Input/index.controlled.js +0 -54
- package/components/Select/index.controlled.d.ts +0 -43
- package/components/Select/index.controlled.js +0 -54
- package/components/Subtitles/types.d.ts +0 -24
- package/components/Subtitles/utils.d.ts +0 -32
- package/components/Subtitles/utils.js +0 -107
- package/components/Textarea/index.controlled.d.ts +0 -50
- package/components/Textarea/index.controlled.js +0 -75
- /package/{components/Subtitles/types.js → agnostic/time/dates/format-date/index.test.d.ts} +0 -0
- /package/components/ResizeObserver/{style.module.css → styles.module.css} +0 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The localised weekday names, indexed the way `Date.prototype.getDay` numbers
|
|
3
|
+
* them. January 1st 2023 was a Sunday, so the week is walked from there.
|
|
4
|
+
*/
|
|
5
|
+
function weekdayNames(locale, weekday) {
|
|
6
|
+
const formatter = new Intl.DateTimeFormat(locale, { weekday });
|
|
7
|
+
return [...Array(7)].map((_, i) => formatter.format(new Date(2023, 0, i + 1)));
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The localised month names, indexed the way `Date.prototype.getMonth` numbers
|
|
11
|
+
* them — January at 0.
|
|
12
|
+
*/
|
|
13
|
+
function monthNames(locale, month) {
|
|
14
|
+
const formatter = new Intl.DateTimeFormat(locale, { month });
|
|
15
|
+
return [...Array(12)].map((_, i) => formatter.format(new Date(2023, i, 1)));
|
|
16
|
+
}
|
|
17
|
+
/** The ordinal suffix a locale appends to a day number, empty when it has none. */
|
|
18
|
+
function dayOrdinalSuffix(day, locale) {
|
|
19
|
+
if (locale.startsWith('fr'))
|
|
20
|
+
return day === 1 ? 'er' : '';
|
|
21
|
+
if (!locale.startsWith('en'))
|
|
22
|
+
return '';
|
|
23
|
+
const mod10 = day % 10;
|
|
24
|
+
const mod100 = day % 100;
|
|
25
|
+
if (mod10 === 1 && mod100 !== 11)
|
|
26
|
+
return 'st';
|
|
27
|
+
if (mod10 === 2 && mod100 !== 12)
|
|
28
|
+
return 'nd';
|
|
29
|
+
if (mod10 === 3 && mod100 !== 13)
|
|
30
|
+
return 'rd';
|
|
31
|
+
return 'th';
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Breaks a `Date` down into every fragment `formatDate` knows how to render,
|
|
35
|
+
* localised and ready to concatenate.
|
|
36
|
+
*
|
|
37
|
+
* Reach for this over `formatDate` when the fragments go somewhere a template
|
|
38
|
+
* string can't — separate DOM nodes, a `<time>` element's parts, an object
|
|
39
|
+
* handed to a view layer.
|
|
40
|
+
*
|
|
41
|
+
* @param date - The `Date` to break down.
|
|
42
|
+
* @param locale - Optional locale code, falling back to `'en'` when the runtime
|
|
43
|
+
* doesn't support it.
|
|
44
|
+
* @returns The parts, each one reachable under its unit token and under its
|
|
45
|
+
* readable name. Quantities come out as numbers — `formatDate` is what pads
|
|
46
|
+
* them.
|
|
47
|
+
* @see {@link DateParts}
|
|
48
|
+
*
|
|
49
|
+
* @remarks
|
|
50
|
+
* Every fragment is read off the `Date` in the runtime's local timezone, since a
|
|
51
|
+
* `Date` carries no timezone of its own.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* const parts = getDateParts(new Date(2026, 0, 1, 15, 5), 'fr')
|
|
55
|
+
* parts.MMMM // => 'janvier'
|
|
56
|
+
* parts.fullMonthName // => 'janvier', the same value
|
|
57
|
+
* parts.D // => 1, the number — '01' is formatDate's business
|
|
58
|
+
*/
|
|
59
|
+
export function getDateParts(date, locale = 'en') {
|
|
60
|
+
const safeLocale = Intl.DateTimeFormat.supportedLocalesOf(locale).length === 0
|
|
61
|
+
? 'en'
|
|
62
|
+
: locale;
|
|
63
|
+
const day = date.getDate();
|
|
64
|
+
const dayOfWeek = date.getDay();
|
|
65
|
+
const month = date.getMonth();
|
|
66
|
+
const hours = date.getHours();
|
|
67
|
+
const isPM = hours >= 12;
|
|
68
|
+
const tokens = {
|
|
69
|
+
'D': day,
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
71
|
+
'd': weekdayNames(safeLocale, 'short')[dayOfWeek],
|
|
72
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
73
|
+
'dd': weekdayNames(safeLocale, 'long')[dayOfWeek],
|
|
74
|
+
'M': month + 1,
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
76
|
+
'MMM': monthNames(safeLocale, 'short')[month],
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
78
|
+
'MMMM': monthNames(safeLocale, 'long')[month],
|
|
79
|
+
'YYYY': date.getFullYear(),
|
|
80
|
+
'H': hours,
|
|
81
|
+
'h': hours % 12 === 0 ? 12 : hours % 12,
|
|
82
|
+
'm': date.getMinutes(),
|
|
83
|
+
's': date.getSeconds(),
|
|
84
|
+
'ms': date.getMilliseconds(),
|
|
85
|
+
'A': isPM ? 'PM' : 'AM',
|
|
86
|
+
'a': isPM ? 'pm' : 'am',
|
|
87
|
+
'th': dayOrdinalSuffix(day, safeLocale)
|
|
88
|
+
};
|
|
89
|
+
return {
|
|
90
|
+
...tokens,
|
|
91
|
+
dayOfMonth: tokens.D,
|
|
92
|
+
shortWeekdayName: tokens.d,
|
|
93
|
+
fullWeekdayName: tokens.dd,
|
|
94
|
+
monthNumber: tokens.M,
|
|
95
|
+
shortMonthName: tokens.MMM,
|
|
96
|
+
fullMonthName: tokens.MMMM,
|
|
97
|
+
year: tokens.YYYY,
|
|
98
|
+
hours24: tokens.H,
|
|
99
|
+
hours12: tokens.h,
|
|
100
|
+
minutes: tokens.m,
|
|
101
|
+
seconds: tokens.s,
|
|
102
|
+
milliseconds: tokens.ms,
|
|
103
|
+
upperCaseMeridiem: tokens.A,
|
|
104
|
+
lowerCaseMeridiem: tokens.a,
|
|
105
|
+
dayOrdinalSuffix: tokens.th
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// [WIP] timezone tokens were removed since JavaScript's Date object doesn't hold
|
|
109
|
+
// timezone data — those templates would only reflect the current client's
|
|
110
|
+
// timezone, no matter what the input Date obj was
|
|
111
|
+
// 'ZZ': () => {
|
|
112
|
+
// const timezoneOffset = date.getTimezoneOffset()
|
|
113
|
+
// const offsetHours = Math.floor(Math.abs(timezoneOffset) / 60)
|
|
114
|
+
// const offsetMinutes = Math.abs(timezoneOffset) % 60
|
|
115
|
+
// const sign = timezoneOffset > 0 ? '-' : '+'
|
|
116
|
+
// return `${sign}${String(offsetHours).padStart(2, '0')}:${String(offsetMinutes).padStart(2, '0')}`
|
|
117
|
+
// },
|
|
118
|
+
// 'Z': () => {
|
|
119
|
+
// const timezoneOffset = date.getTimezoneOffset()
|
|
120
|
+
// if (timezoneOffset === 0) return 'Z'
|
|
121
|
+
// const offsetHours = Math.floor(Math.abs(timezoneOffset) / 60)
|
|
122
|
+
// const offsetMinutes = Math.abs(timezoneOffset) % 60
|
|
123
|
+
// const sign = timezoneOffset > 0 ? '-' : '+'
|
|
124
|
+
// return `${sign}${String(offsetHours).padStart(2, '0')}:${String(offsetMinutes).padStart(2, '0')}`
|
|
125
|
+
// },
|
|
126
|
+
// 'z': () => {
|
|
127
|
+
// return Intl.DateTimeFormat(locale, { timeZoneName: 'short' })
|
|
128
|
+
// .formatToParts(date)
|
|
129
|
+
// .find(part => part.type === 'timeZoneName')?.value || ''
|
|
130
|
+
// },
|
|
131
|
+
// 'zz': () => {
|
|
132
|
+
// return Intl.DateTimeFormat(locale, { timeZoneName: 'long' })
|
|
133
|
+
// .formatToParts(date)
|
|
134
|
+
// .find(part => part.type === 'timeZoneName')?.value || ''
|
|
135
|
+
// },
|
|
136
|
+
// 'tz': () => Intl.DateTimeFormat().resolvedOptions().timeZone || ''
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** Every token `formatDate` substitutes in a template. */
|
|
2
|
+
export type DateToken = 'D' | 'DD' | 'd' | 'dd' | 'M' | 'MM' | 'MMM' | 'MMMM' | 'YY' | 'YYYY' | 'H' | 'HH' | 'h' | 'hh' | 'm' | 'mm' | 's' | 'ss' | 'ms' | 'A' | 'a' | 'th';
|
|
3
|
+
/**
|
|
4
|
+
* A date's parts, keyed by unit token.
|
|
5
|
+
*
|
|
6
|
+
* Quantities are **numbers** — a day of month is `1`, not `'01'`. Padding and
|
|
7
|
+
* truncation are rendering concerns and belong to `formatDate`, which is why the
|
|
8
|
+
* `DD`, `MM`, `YY`, `HH`, `hh`, `mm` and `ss` tokens have no entry here: they
|
|
9
|
+
* render the same quantity as their unpadded twin. The parts that are localised
|
|
10
|
+
* text rather than a quantity stay strings.
|
|
11
|
+
*/
|
|
12
|
+
export type DateTokenParts = {
|
|
13
|
+
/** Day of month, 1–31. */
|
|
14
|
+
D: number;
|
|
15
|
+
/** Short weekday name, localised. */
|
|
16
|
+
d: string;
|
|
17
|
+
/** Full weekday name, localised. */
|
|
18
|
+
dd: string;
|
|
19
|
+
/** Month number, 1–12 — January is `1`, not `0`. */
|
|
20
|
+
M: number;
|
|
21
|
+
/** Short month name, localised. */
|
|
22
|
+
MMM: string;
|
|
23
|
+
/** Full month name, localised. */
|
|
24
|
+
MMMM: string;
|
|
25
|
+
/** Full year. */
|
|
26
|
+
YYYY: number;
|
|
27
|
+
/** Hours on a 24-hour clock, 0–23. */
|
|
28
|
+
H: number;
|
|
29
|
+
/** Hours on a 12-hour clock, 1–12. Midnight and noon are both `12`. */
|
|
30
|
+
h: number;
|
|
31
|
+
/** Minutes, 0–59. */
|
|
32
|
+
m: number;
|
|
33
|
+
/** Seconds, 0–59. */
|
|
34
|
+
s: number;
|
|
35
|
+
/** Milliseconds, 0–999. */
|
|
36
|
+
ms: number;
|
|
37
|
+
/** Upper-case meridiem, `'AM'` or `'PM'`. */
|
|
38
|
+
A: string;
|
|
39
|
+
/** Lower-case meridiem, `'am'` or `'pm'`. */
|
|
40
|
+
a: string;
|
|
41
|
+
/**
|
|
42
|
+
* Day ordinal suffix — `'st'`, `'nd'`, `'rd'` or `'th'` in English, `'er'` for
|
|
43
|
+
* the French 1st. Empty for every other locale, and for French days 2 onwards.
|
|
44
|
+
*/
|
|
45
|
+
th: string;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* The same parts as {@link DateTokenParts}, under names that read on their own —
|
|
49
|
+
* one per token, same value.
|
|
50
|
+
*/
|
|
51
|
+
export type NamedDateParts = {
|
|
52
|
+
/** {@link DateTokenParts.D} */
|
|
53
|
+
dayOfMonth: number;
|
|
54
|
+
/** {@link DateTokenParts.d} */
|
|
55
|
+
shortWeekdayName: string;
|
|
56
|
+
/** {@link DateTokenParts.dd} */
|
|
57
|
+
fullWeekdayName: string;
|
|
58
|
+
/** {@link DateTokenParts.M} */
|
|
59
|
+
monthNumber: number;
|
|
60
|
+
/** {@link DateTokenParts.MMM} */
|
|
61
|
+
shortMonthName: string;
|
|
62
|
+
/** {@link DateTokenParts.MMMM} */
|
|
63
|
+
fullMonthName: string;
|
|
64
|
+
/** {@link DateTokenParts.YYYY} */
|
|
65
|
+
year: number;
|
|
66
|
+
/** {@link DateTokenParts.H} */
|
|
67
|
+
hours24: number;
|
|
68
|
+
/** {@link DateTokenParts.h} */
|
|
69
|
+
hours12: number;
|
|
70
|
+
/** {@link DateTokenParts.m} */
|
|
71
|
+
minutes: number;
|
|
72
|
+
/** {@link DateTokenParts.s} */
|
|
73
|
+
seconds: number;
|
|
74
|
+
/** {@link DateTokenParts.ms} */
|
|
75
|
+
milliseconds: number;
|
|
76
|
+
/** {@link DateTokenParts.A} */
|
|
77
|
+
upperCaseMeridiem: string;
|
|
78
|
+
/** {@link DateTokenParts.a} */
|
|
79
|
+
lowerCaseMeridiem: string;
|
|
80
|
+
/** {@link DateTokenParts.th} */
|
|
81
|
+
dayOrdinalSuffix: string;
|
|
82
|
+
};
|
|
83
|
+
/** Every part of a date, reachable both by unit token and by name. */
|
|
84
|
+
export type DateParts = DateTokenParts & NamedDateParts;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Duration } from '../index.js';
|
|
2
|
+
import type { FormatDurationOptions } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Formats a duration into a string according to a custom template.
|
|
5
|
+
*
|
|
6
|
+
* The breakdown follows the template: the units it mentions are the units the
|
|
7
|
+
* duration is split across, each taking its whole part and handing the rest
|
|
8
|
+
* down. `'{{h}}:{{mm}}:{{ss}}'` therefore reads a duration as hours, minutes and
|
|
9
|
+
* seconds, while `'{{s}}s'` puts the whole duration on seconds.
|
|
10
|
+
*
|
|
11
|
+
* Supported tokens (placeholders must be wrapped in `{{...}}`). The bare form is
|
|
12
|
+
* the plain number, the doubled form the same number padded to two digits:
|
|
13
|
+
*
|
|
14
|
+
* - `Y` / `YY` : Years
|
|
15
|
+
* - `M` / `MM` : Months
|
|
16
|
+
* - `w` / `ww` : Weeks
|
|
17
|
+
* - `d` / `dd` : Days
|
|
18
|
+
* - `h` / `hh` : Hours
|
|
19
|
+
* - `m` / `mm` : Minutes
|
|
20
|
+
* - `s` / `ss` : Seconds
|
|
21
|
+
* - `ms` : Milliseconds, padded to three digits
|
|
22
|
+
*
|
|
23
|
+
* @param duration - The duration to format, as a {@link Duration} or a number of
|
|
24
|
+
* milliseconds.
|
|
25
|
+
* @param format - The template string containing tokens.
|
|
26
|
+
* @param options - How to handle the remainder and the month/year approximation.
|
|
27
|
+
* @returns Formatted duration string. An unknown token is left untouched, braces
|
|
28
|
+
* included.
|
|
29
|
+
* @see {@link getDurationParts} to reach the same parts as plain values.
|
|
30
|
+
*
|
|
31
|
+
* @remarks
|
|
32
|
+
* A negative duration yields negative parts, so every token renders a signed
|
|
33
|
+
* number — `'{{m}}:{{ss}}'` gives `-1:-30`, not `-1:30`. This is deliberate: the
|
|
34
|
+
* output stays faithful to the parts, and keeping a duration positive is the
|
|
35
|
+
* caller's business. Format the magnitude and write the sign yourself when a
|
|
36
|
+
* single leading minus is what you need.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* formatDuration(seconds(3725), '{{h}}:{{mm}}:{{ss}}')
|
|
40
|
+
* // => "1:02:05"
|
|
41
|
+
*/
|
|
42
|
+
export declare function formatDuration(duration: Duration | number, format: string, options?: FormatDurationOptions): string;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { getDurationParts } from './parts.js';
|
|
2
|
+
// Alternation is ordered, and the first branch that fits wins — so the longest
|
|
3
|
+
// tokens come first. `m` ahead of `ms` or `mm` would swallow them.
|
|
4
|
+
const tokenRegexp = /\{\{(YY|MM|ww|dd|hh|mm|ss|ms|Y|M|w|d|h|m|s)\}\}/gv;
|
|
5
|
+
/** Which tokens read which unit, longest unit first. */
|
|
6
|
+
const unitTokens = [
|
|
7
|
+
['Y', ['Y', 'YY']],
|
|
8
|
+
['M', ['M', 'MM']],
|
|
9
|
+
['w', ['w', 'ww']],
|
|
10
|
+
['d', ['d', 'dd']],
|
|
11
|
+
['h', ['h', 'hh']],
|
|
12
|
+
['m', ['m', 'mm']],
|
|
13
|
+
['s', ['s', 'ss']],
|
|
14
|
+
['ms', ['ms']]
|
|
15
|
+
];
|
|
16
|
+
function pad(value, length) {
|
|
17
|
+
return `${value}`.padStart(length, '0');
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* How each token renders the parts it reads. This is where padding lives —
|
|
21
|
+
* {@link getDurationParts} hands over plain quantities, and a doubled token is
|
|
22
|
+
* the same quantity written on two digits.
|
|
23
|
+
*/
|
|
24
|
+
const renderers = {
|
|
25
|
+
'Y': parts => `${parts.Y}`,
|
|
26
|
+
'YY': parts => pad(parts.Y, 2),
|
|
27
|
+
'M': parts => `${parts.M}`,
|
|
28
|
+
'MM': parts => pad(parts.M, 2),
|
|
29
|
+
'w': parts => `${parts.w}`,
|
|
30
|
+
'ww': parts => pad(parts.w, 2),
|
|
31
|
+
'd': parts => `${parts.d}`,
|
|
32
|
+
'dd': parts => pad(parts.d, 2),
|
|
33
|
+
'h': parts => `${parts.h}`,
|
|
34
|
+
'hh': parts => pad(parts.h, 2),
|
|
35
|
+
'm': parts => `${parts.m}`,
|
|
36
|
+
'mm': parts => pad(parts.m, 2),
|
|
37
|
+
's': parts => `${parts.s}`,
|
|
38
|
+
'ss': parts => pad(parts.s, 2),
|
|
39
|
+
'ms': parts => pad(parts.ms, 3)
|
|
40
|
+
};
|
|
41
|
+
/** The units a template asks for, read off the tokens it actually uses. */
|
|
42
|
+
function unitsInFormat(format) {
|
|
43
|
+
return unitTokens
|
|
44
|
+
.filter(([, tokens]) => tokens.some(token => format.includes(`{{${token}}}`)))
|
|
45
|
+
.map(([unit]) => unit);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Formats a duration into a string according to a custom template.
|
|
49
|
+
*
|
|
50
|
+
* The breakdown follows the template: the units it mentions are the units the
|
|
51
|
+
* duration is split across, each taking its whole part and handing the rest
|
|
52
|
+
* down. `'{{h}}:{{mm}}:{{ss}}'` therefore reads a duration as hours, minutes and
|
|
53
|
+
* seconds, while `'{{s}}s'` puts the whole duration on seconds.
|
|
54
|
+
*
|
|
55
|
+
* Supported tokens (placeholders must be wrapped in `{{...}}`). The bare form is
|
|
56
|
+
* the plain number, the doubled form the same number padded to two digits:
|
|
57
|
+
*
|
|
58
|
+
* - `Y` / `YY` : Years
|
|
59
|
+
* - `M` / `MM` : Months
|
|
60
|
+
* - `w` / `ww` : Weeks
|
|
61
|
+
* - `d` / `dd` : Days
|
|
62
|
+
* - `h` / `hh` : Hours
|
|
63
|
+
* - `m` / `mm` : Minutes
|
|
64
|
+
* - `s` / `ss` : Seconds
|
|
65
|
+
* - `ms` : Milliseconds, padded to three digits
|
|
66
|
+
*
|
|
67
|
+
* @param duration - The duration to format, as a {@link Duration} or a number of
|
|
68
|
+
* milliseconds.
|
|
69
|
+
* @param format - The template string containing tokens.
|
|
70
|
+
* @param options - How to handle the remainder and the month/year approximation.
|
|
71
|
+
* @returns Formatted duration string. An unknown token is left untouched, braces
|
|
72
|
+
* included.
|
|
73
|
+
* @see {@link getDurationParts} to reach the same parts as plain values.
|
|
74
|
+
*
|
|
75
|
+
* @remarks
|
|
76
|
+
* A negative duration yields negative parts, so every token renders a signed
|
|
77
|
+
* number — `'{{m}}:{{ss}}'` gives `-1:-30`, not `-1:30`. This is deliberate: the
|
|
78
|
+
* output stays faithful to the parts, and keeping a duration positive is the
|
|
79
|
+
* caller's business. Format the magnitude and write the sign yourself when a
|
|
80
|
+
* single leading minus is what you need.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* formatDuration(seconds(3725), '{{h}}:{{mm}}:{{ss}}')
|
|
84
|
+
* // => "1:02:05"
|
|
85
|
+
*/
|
|
86
|
+
export function formatDuration(duration, format, options = {}) {
|
|
87
|
+
const { floorSmallestUnit = true, useApproximateMonthAndYear = true } = options;
|
|
88
|
+
const parts = getDurationParts(duration, {
|
|
89
|
+
units: unitsInFormat(format),
|
|
90
|
+
floorSmallestUnit,
|
|
91
|
+
useApproximateMonthAndYear
|
|
92
|
+
});
|
|
93
|
+
return format.replace(tokenRegexp, (_match, token) => renderers[token](parts));
|
|
94
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { days, minutes, seconds } from '../index.js';
|
|
3
|
+
import { formatDuration } from './index.js';
|
|
4
|
+
import { getDurationParts } from './parts.js';
|
|
5
|
+
const DAY = 86400000;
|
|
6
|
+
describe('formatDuration', () => {
|
|
7
|
+
describe('template', () => {
|
|
8
|
+
it('splits the duration across the units the template mentions', () => {
|
|
9
|
+
expect(formatDuration(seconds(3725), '{{h}}:{{mm}}:{{ss}}')).toBe('1:02:05');
|
|
10
|
+
});
|
|
11
|
+
it('puts the whole duration on the only unit a template asks for', () => {
|
|
12
|
+
expect(formatDuration(seconds(3725), '{{s}}s')).toBe('3725s');
|
|
13
|
+
expect(formatDuration(days(400), '{{d}} days')).toBe('400 days');
|
|
14
|
+
});
|
|
15
|
+
it('accepts a plain number of milliseconds in place of a Duration', () => {
|
|
16
|
+
expect(formatDuration(3725000, '{{h}}:{{mm}}:{{ss}}')).toBe('1:02:05');
|
|
17
|
+
});
|
|
18
|
+
it('pads a doubled token to two digits and leaves its bare twin alone', () => {
|
|
19
|
+
expect(formatDuration(minutes(5), '{{mm}}:{{ss}}')).toBe('05:00');
|
|
20
|
+
expect(formatDuration(minutes(5), '{{m}}:{{s}}')).toBe('5:0');
|
|
21
|
+
});
|
|
22
|
+
it('pads milliseconds to three digits', () => {
|
|
23
|
+
expect(formatDuration(1042, '{{s}}.{{ms}}')).toBe('1.042');
|
|
24
|
+
});
|
|
25
|
+
it('substitutes every occurrence of a repeated token', () => {
|
|
26
|
+
expect(formatDuration(seconds(5), '{{s}}/{{s}}')).toBe('5/5');
|
|
27
|
+
});
|
|
28
|
+
it('leaves an unknown token untouched, braces included', () => {
|
|
29
|
+
expect(formatDuration(seconds(5), '{{ZZ}} {{s}}')).toBe('{{ZZ}} 5');
|
|
30
|
+
});
|
|
31
|
+
it('does not substitute a bare letter outside a placeholder', () => {
|
|
32
|
+
expect(formatDuration(seconds(5), 'd h m s {{s}}')).toBe('d h m s 5');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
describe('remainder', () => {
|
|
36
|
+
it('drops the fraction of the smallest unit by default', () => {
|
|
37
|
+
expect(formatDuration(119600, '{{m}}:{{s}}')).toBe('1:59');
|
|
38
|
+
});
|
|
39
|
+
it('keeps the fraction when floorSmallestUnit is off', () => {
|
|
40
|
+
expect(formatDuration(119600, '{{m}}:{{s}}', { floorSmallestUnit: false })).toBe('1:59.6');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
describe('month and year approximation', () => {
|
|
44
|
+
it('counts 365 days as a full year by default', () => {
|
|
45
|
+
expect(formatDuration(days(365), '{{Y}}')).toBe('1');
|
|
46
|
+
});
|
|
47
|
+
it('falls one year short of the Gregorian mean year when the approximation is off', () => {
|
|
48
|
+
expect(formatDuration(days(365), '{{Y}}', { useApproximateMonthAndYear: false })).toBe('0');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe('negative durations', () => {
|
|
52
|
+
it('signs every part rather than the string as a whole', () => {
|
|
53
|
+
expect(formatDuration(seconds(-90), '{{m}}:{{ss}}')).toBe('-1:-30');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe('getDurationParts', () => {
|
|
58
|
+
describe('breakdown', () => {
|
|
59
|
+
it('cascades the remainder from each unit down to the next', () => {
|
|
60
|
+
expect(getDurationParts(days(400), { units: ['Y', 'd'] })).toMatchObject({
|
|
61
|
+
Y: 1,
|
|
62
|
+
M: 0,
|
|
63
|
+
w: 0,
|
|
64
|
+
d: 35,
|
|
65
|
+
h: 0,
|
|
66
|
+
m: 0,
|
|
67
|
+
s: 0,
|
|
68
|
+
ms: 0
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
it('orders the units itself, whatever order they are given in', () => {
|
|
72
|
+
expect(getDurationParts(3725000, { units: ['s', 'h', 'm'] })).toMatchObject({ h: 1, m: 2, s: 5 });
|
|
73
|
+
});
|
|
74
|
+
it('ignores a unit listed twice', () => {
|
|
75
|
+
expect(getDurationParts(3725000, { units: ['h', 'm', 's', 'h'] })).toMatchObject({ h: 1, m: 2, s: 5 });
|
|
76
|
+
});
|
|
77
|
+
it('reaches every part under its readable name too', () => {
|
|
78
|
+
const parts = getDurationParts(3725000, { units: ['h', 'm', 's'] });
|
|
79
|
+
expect(parts).toMatchObject({
|
|
80
|
+
hours: parts.h,
|
|
81
|
+
minutes: parts.m,
|
|
82
|
+
seconds: parts.s,
|
|
83
|
+
milliseconds: parts.ms,
|
|
84
|
+
days: parts.d,
|
|
85
|
+
weeks: parts.w,
|
|
86
|
+
months: parts.M,
|
|
87
|
+
years: parts.Y
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe('units left out', () => {
|
|
92
|
+
it('puts the whole duration, fraction included, on the largest unit it reaches', () => {
|
|
93
|
+
const parts = getDurationParts(days(400));
|
|
94
|
+
expect(parts.Y).toBeCloseTo(1.0959, 4);
|
|
95
|
+
expect(parts.d).toBe(0);
|
|
96
|
+
});
|
|
97
|
+
it('picks the unit off the duration, not off the largest one that exists', () => {
|
|
98
|
+
expect(getDurationParts(90000).m).toBe(1.5);
|
|
99
|
+
expect(getDurationParts(500).ms).toBe(500);
|
|
100
|
+
});
|
|
101
|
+
it('falls back to milliseconds for a duration shorter than one', () => {
|
|
102
|
+
expect(getDurationParts(0.5)).toMatchObject({ ms: 0.5, s: 0 });
|
|
103
|
+
});
|
|
104
|
+
it('leaves every part at zero for a zero duration', () => {
|
|
105
|
+
expect(getDurationParts(0)).toMatchObject({
|
|
106
|
+
Y: 0, M: 0, w: 0, d: 0, h: 0, m: 0, s: 0, ms: 0
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
describe('floorSmallestUnit', () => {
|
|
111
|
+
it('leaves the remainder on the smallest unit by default', () => {
|
|
112
|
+
expect(getDurationParts(119600, { units: ['m', 's'] })).toMatchObject({ m: 1, s: 59.6 });
|
|
113
|
+
});
|
|
114
|
+
it('truncates the whole duration, so the missing fraction leaves every part', () => {
|
|
115
|
+
expect(getDurationParts(119600, { units: ['m', 's'], floorSmallestUnit: true })).toMatchObject({ m: 1, s: 59 });
|
|
116
|
+
});
|
|
117
|
+
it('rounds off the floating-point dust the cascade leaves behind', () => {
|
|
118
|
+
// 30 days on ['M', 'd'] divides exactly, but only in decimal.
|
|
119
|
+
expect(getDurationParts(30 * DAY, { units: ['M', 'd'], floorSmallestUnit: true })).toMatchObject({ M: 1, d: 0 });
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
describe('month and year approximation', () => {
|
|
123
|
+
it('uses 30-day months and 365-day years by default', () => {
|
|
124
|
+
expect(getDurationParts(365 * DAY, { units: ['Y', 'd'] })).toMatchObject({ Y: 1, d: 0 });
|
|
125
|
+
expect(getDurationParts(30 * DAY, { units: ['M', 'd'] })).toMatchObject({ M: 1, d: 0 });
|
|
126
|
+
});
|
|
127
|
+
it('uses the Gregorian mean year when the approximation is off', () => {
|
|
128
|
+
expect(getDurationParts(365 * DAY, {
|
|
129
|
+
units: ['Y', 'd'],
|
|
130
|
+
useApproximateMonthAndYear: false
|
|
131
|
+
})).toMatchObject({ Y: 0, d: 365 });
|
|
132
|
+
expect(getDurationParts(30 * DAY, {
|
|
133
|
+
units: ['M'],
|
|
134
|
+
useApproximateMonthAndYear: false
|
|
135
|
+
}).M).toBeCloseTo(0.9856, 4);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe('negative durations', () => {
|
|
139
|
+
it('breaks the magnitude down, then negates every part', () => {
|
|
140
|
+
expect(getDurationParts(seconds(-90), { units: ['m', 's'] })).toMatchObject({ m: -1, s: -30 });
|
|
141
|
+
});
|
|
142
|
+
it('leaves an empty part at zero rather than at negative zero', () => {
|
|
143
|
+
const parts = getDurationParts(-30000, { units: ['m', 's'] });
|
|
144
|
+
expect(Object.is(parts.m, 0)).toBe(true);
|
|
145
|
+
expect(parts.s).toBe(-30);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Duration } from '../index.js';
|
|
2
|
+
import type { DurationParts, GetDurationPartsOptions } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Breaks a duration down into its parts, cascading the remainder from each unit
|
|
5
|
+
* to the next.
|
|
6
|
+
*
|
|
7
|
+
* With several units, each one takes its whole part and hands the rest down, so
|
|
8
|
+
* `['M', 'd', 's']` reads as *X months and Y days and Z.mmm seconds*. With a
|
|
9
|
+
* single unit — including the one picked when `units` is omitted — that unit
|
|
10
|
+
* carries the whole duration, fraction included.
|
|
11
|
+
*
|
|
12
|
+
* @param duration - The duration to break down, as a {@link Duration} or a
|
|
13
|
+
* number of milliseconds.
|
|
14
|
+
* @param options - Which units to use, and how to handle the remainder.
|
|
15
|
+
* @returns The parts, each one reachable under its unit token and under its
|
|
16
|
+
* readable name. Units left out of the breakdown are `0`.
|
|
17
|
+
* @see {@link DurationParts}
|
|
18
|
+
*
|
|
19
|
+
* @remarks
|
|
20
|
+
* A negative duration is broken down by magnitude, then every part is negated.
|
|
21
|
+
* `floorSmallestUnit` truncates the duration *before* the cascade, so the whole
|
|
22
|
+
* breakdown describes the truncated duration rather than the original one —
|
|
23
|
+
* `1m 59.6s` gives `1m 59s`, and the missing `.6s` is gone from every part.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* getDurationParts(days(400), { units: ['Y', 'd'] })
|
|
27
|
+
* // => { Y: 1, years: 1, d: 35, days: 35, … every other unit at 0 }
|
|
28
|
+
*
|
|
29
|
+
* getDurationParts(days(400))
|
|
30
|
+
* // => { Y: 1.0958…, years: 1.0958…, … } — the largest unit it reaches
|
|
31
|
+
*/
|
|
32
|
+
export declare function getDurationParts(duration: Duration | number, options?: GetDurationPartsOptions): DurationParts;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Duration } from '../index.js';
|
|
2
|
+
const SECOND = 1000;
|
|
3
|
+
const MINUTE = 60 * SECOND;
|
|
4
|
+
const HOUR = 60 * MINUTE;
|
|
5
|
+
const DAY = 24 * HOUR;
|
|
6
|
+
const WEEK = 7 * DAY;
|
|
7
|
+
const APPROXIMATE_YEAR = 365 * DAY;
|
|
8
|
+
const APPROXIMATE_MONTH = 30 * DAY;
|
|
9
|
+
/** The Gregorian mean year — 365.2425 days, the calendar's actual average. */
|
|
10
|
+
const EXACT_YEAR = 365.2425 * DAY;
|
|
11
|
+
const EXACT_MONTH = EXACT_YEAR / 12;
|
|
12
|
+
/** Units from the longest to the shortest, in both approximation modes. */
|
|
13
|
+
const unitsLongestFirst = ['Y', 'M', 'w', 'd', 'h', 'm', 's', 'ms'];
|
|
14
|
+
/** How many milliseconds each unit is worth, under the chosen approximation. */
|
|
15
|
+
function unitLengths(useApproximateMonthAndYear) {
|
|
16
|
+
return {
|
|
17
|
+
ms: 1,
|
|
18
|
+
s: SECOND,
|
|
19
|
+
m: MINUTE,
|
|
20
|
+
h: HOUR,
|
|
21
|
+
d: DAY,
|
|
22
|
+
w: WEEK,
|
|
23
|
+
M: useApproximateMonthAndYear ? APPROXIMATE_MONTH : EXACT_MONTH,
|
|
24
|
+
Y: useApproximateMonthAndYear ? APPROXIMATE_YEAR : EXACT_YEAR
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The requested units, deduplicated and ordered longest first. Falls back to the
|
|
29
|
+
* single largest unit the duration reaches, and to `'ms'` for a duration shorter
|
|
30
|
+
* than a millisecond.
|
|
31
|
+
*/
|
|
32
|
+
function resolveUnits(units, absoluteMs, lengths) {
|
|
33
|
+
if (units !== undefined && units.length > 0) {
|
|
34
|
+
return unitsLongestFirst.filter(unit => units.includes(unit));
|
|
35
|
+
}
|
|
36
|
+
return [unitsLongestFirst.find(unit => absoluteMs >= lengths[unit]) ?? 'ms'];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Breaks a duration down into its parts, cascading the remainder from each unit
|
|
40
|
+
* to the next.
|
|
41
|
+
*
|
|
42
|
+
* With several units, each one takes its whole part and hands the rest down, so
|
|
43
|
+
* `['M', 'd', 's']` reads as *X months and Y days and Z.mmm seconds*. With a
|
|
44
|
+
* single unit — including the one picked when `units` is omitted — that unit
|
|
45
|
+
* carries the whole duration, fraction included.
|
|
46
|
+
*
|
|
47
|
+
* @param duration - The duration to break down, as a {@link Duration} or a
|
|
48
|
+
* number of milliseconds.
|
|
49
|
+
* @param options - Which units to use, and how to handle the remainder.
|
|
50
|
+
* @returns The parts, each one reachable under its unit token and under its
|
|
51
|
+
* readable name. Units left out of the breakdown are `0`.
|
|
52
|
+
* @see {@link DurationParts}
|
|
53
|
+
*
|
|
54
|
+
* @remarks
|
|
55
|
+
* A negative duration is broken down by magnitude, then every part is negated.
|
|
56
|
+
* `floorSmallestUnit` truncates the duration *before* the cascade, so the whole
|
|
57
|
+
* breakdown describes the truncated duration rather than the original one —
|
|
58
|
+
* `1m 59.6s` gives `1m 59s`, and the missing `.6s` is gone from every part.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* getDurationParts(days(400), { units: ['Y', 'd'] })
|
|
62
|
+
* // => { Y: 1, years: 1, d: 35, days: 35, … every other unit at 0 }
|
|
63
|
+
*
|
|
64
|
+
* getDurationParts(days(400))
|
|
65
|
+
* // => { Y: 1.0958…, years: 1.0958…, … } — the largest unit it reaches
|
|
66
|
+
*/
|
|
67
|
+
export function getDurationParts(duration, options = {}) {
|
|
68
|
+
const { units, floorSmallestUnit = false, useApproximateMonthAndYear = true } = options;
|
|
69
|
+
const totalMs = duration instanceof Duration ? duration.toMs() : duration;
|
|
70
|
+
const sign = totalMs < 0 ? -1 : 1;
|
|
71
|
+
const lengths = unitLengths(useApproximateMonthAndYear);
|
|
72
|
+
const usedUnits = resolveUnits(units, Math.abs(totalMs), lengths);
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
74
|
+
const smallestUnit = usedUnits[usedUnits.length - 1];
|
|
75
|
+
const smallestLength = lengths[smallestUnit];
|
|
76
|
+
let remaining = Math.abs(totalMs);
|
|
77
|
+
if (floorSmallestUnit) {
|
|
78
|
+
remaining = Math.floor(remaining / smallestLength) * smallestLength;
|
|
79
|
+
}
|
|
80
|
+
const values = {
|
|
81
|
+
ms: 0, s: 0, m: 0, h: 0, d: 0, w: 0, M: 0, Y: 0
|
|
82
|
+
};
|
|
83
|
+
usedUnits.forEach((unit, pos) => {
|
|
84
|
+
const isSmallest = pos === usedUnits.length - 1;
|
|
85
|
+
const exact = remaining / lengths[unit];
|
|
86
|
+
// Math.round, not Math.floor, and deliberately so: the truncation already
|
|
87
|
+
// happened above, on the whole duration, so this value is a whole one by
|
|
88
|
+
// now. Rounding only clears the floating-point dust the divisions leave —
|
|
89
|
+
// flooring here would turn a 29.999999999 that means 30 into a 29.
|
|
90
|
+
const value = isSmallest
|
|
91
|
+
? (floorSmallestUnit ? Math.round(exact) : exact)
|
|
92
|
+
: Math.floor(exact);
|
|
93
|
+
values[unit] = value === 0 ? 0 : value * sign;
|
|
94
|
+
remaining -= value * lengths[unit];
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
...values,
|
|
98
|
+
milliseconds: values.ms,
|
|
99
|
+
seconds: values.s,
|
|
100
|
+
minutes: values.m,
|
|
101
|
+
hours: values.h,
|
|
102
|
+
days: values.d,
|
|
103
|
+
weeks: values.w,
|
|
104
|
+
months: values.M,
|
|
105
|
+
years: values.Y
|
|
106
|
+
};
|
|
107
|
+
}
|