@sproutsocial/seeds-react-duration 1.0.0

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.
@@ -0,0 +1,82 @@
1
+ import testDuration from "./testDuration";
2
+
3
+ describe("When using a negative value...", () => {
4
+ describe("... it renders properly", () => {
5
+ testDuration(-1, {}, { text: "0s" });
6
+ testDuration(
7
+ -1,
8
+ {
9
+ displayUnits: {
10
+ days: true,
11
+ hours: true,
12
+ minutes: true,
13
+ seconds: true,
14
+ milliseconds: true,
15
+ },
16
+ },
17
+ { text: "0ms" }
18
+ );
19
+ testDuration(
20
+ -1,
21
+ {
22
+ displayUnits: { days: true, hours: true, minutes: true, seconds: true },
23
+ },
24
+ { text: "0s" }
25
+ );
26
+ testDuration(
27
+ -1,
28
+ { displayUnits: { days: true, hours: true, minutes: true } },
29
+ { text: "0m" }
30
+ );
31
+ testDuration(
32
+ -1,
33
+ { displayUnits: { days: true, hours: true } },
34
+ { text: "0h" }
35
+ );
36
+ testDuration(-1, { displayUnits: { days: true } }, { text: "0d" });
37
+ });
38
+
39
+ describe("... should work with display long", () => {
40
+ testDuration(-1, { display: "long" }, { text: "0 seconds" });
41
+ testDuration(
42
+ -1,
43
+ {
44
+ display: "long",
45
+ displayUnits: {
46
+ days: true,
47
+ hours: true,
48
+ minutes: true,
49
+ seconds: true,
50
+ milliseconds: true,
51
+ },
52
+ },
53
+ { text: "0 milliseconds" }
54
+ );
55
+ testDuration(
56
+ -1,
57
+ {
58
+ display: "long",
59
+ displayUnits: { days: true, hours: true, minutes: true, seconds: true },
60
+ },
61
+ { text: "0 seconds" }
62
+ );
63
+ testDuration(
64
+ -1,
65
+ {
66
+ display: "long",
67
+ displayUnits: { days: true, hours: true, minutes: true },
68
+ },
69
+ { text: "0 minutes" }
70
+ );
71
+ testDuration(
72
+ -1,
73
+ { display: "long", displayUnits: { days: true, hours: true } },
74
+ { text: "0 hours" }
75
+ );
76
+ testDuration(
77
+ -1,
78
+ { display: "long", displayUnits: { days: true } },
79
+ { text: "0 days" }
80
+ );
81
+ });
82
+ });
@@ -0,0 +1,37 @@
1
+ /* eslint-env jest, node */
2
+ import React from "react";
3
+ import { render, screen } from "@sproutsocial/seeds-react-testing-library";
4
+ import Duration, { formatDuration } from "../../Duration";
5
+
6
+ jest.mock("../../constants.ts", () => ({
7
+ ...jest.requireActual("../../constants.ts"),
8
+ MEMO_CACHE_SIZE: 0,
9
+ COMPARE_OBJECTS: false,
10
+ }));
11
+
12
+ // @ts-ignore TS error later
13
+ const testDuration = (milliseconds, options = {}, texts) => {
14
+ const { text: mainText, label: labelText, format: formatText } = texts;
15
+
16
+ test(`Should display ${milliseconds} as -> ${mainText}`, () => {
17
+ render(<Duration milliseconds={milliseconds} {...options} />);
18
+ const duration = screen.getByText(mainText);
19
+ expect(duration).toBeInTheDocument();
20
+ if (labelText) {
21
+ expect(screen.getByText(labelText)).toBeInTheDocument();
22
+ }
23
+
24
+ // formatDuration test
25
+ if (formatText) {
26
+ expect(
27
+ formatDuration({ milliseconds: milliseconds, ...options })
28
+ ).toEqual(formatText);
29
+ } else {
30
+ expect(
31
+ formatDuration({ milliseconds: milliseconds, ...options })
32
+ ).toEqual(mainText);
33
+ }
34
+ });
35
+ };
36
+
37
+ export default testDuration;
@@ -0,0 +1,260 @@
1
+ import { getDurationMaxDisplayUnits } from "../../index";
2
+
3
+ // full
4
+ const millisecondsDuration = 123; // 123ms
5
+ const secondsDuration = 12345; // 12s 345ms
6
+ const minutesDuration = 1234567; // 20m 34s 567ms
7
+ const hoursDuration = 12345678; // 3h 25m 45s 678ms
8
+ const daysDuration = 123456789; // 1d 10h 17m 36s 789ms
9
+
10
+ // partial
11
+ const minutesPartialDuration = 1200567; // 20m 567ms
12
+ const hoursPartialDuration = 10845000; // 3h 45s
13
+ const daysPartialDuration = 87420789; // 1d 17m 789ms
14
+
15
+ describe("When calling getDurationMaxDisplayUnits...", () => {
16
+ describe("... it returns the correct value ...", () => {
17
+ test.each([
18
+ // null
19
+ { milliseconds: null, maxDisplayUnits: 1, expected: {} },
20
+ // zero
21
+ { milliseconds: 0, maxDisplayUnits: 1, expected: {} },
22
+ // milliseconds
23
+ { milliseconds: millisecondsDuration, maxDisplayUnits: 0, expected: {} },
24
+ {
25
+ milliseconds: millisecondsDuration,
26
+ maxDisplayUnits: 1,
27
+ expected: { milliseconds: true },
28
+ },
29
+ {
30
+ milliseconds: millisecondsDuration,
31
+ maxDisplayUnits: 2,
32
+ expected: { milliseconds: true },
33
+ },
34
+ // seconds
35
+ { milliseconds: secondsDuration, maxDisplayUnits: 0, expected: {} },
36
+ {
37
+ milliseconds: secondsDuration,
38
+ maxDisplayUnits: 1,
39
+ expected: { seconds: true },
40
+ },
41
+ {
42
+ milliseconds: secondsDuration,
43
+ maxDisplayUnits: 2,
44
+ expected: { seconds: true, milliseconds: true },
45
+ },
46
+ {
47
+ milliseconds: secondsDuration,
48
+ maxDisplayUnits: 3,
49
+ expected: { seconds: true, milliseconds: true },
50
+ },
51
+ // minutes
52
+ { milliseconds: minutesDuration, maxDisplayUnits: 0, expected: {} },
53
+ {
54
+ milliseconds: minutesDuration,
55
+ maxDisplayUnits: 1,
56
+ expected: { minutes: true },
57
+ },
58
+ {
59
+ milliseconds: minutesDuration,
60
+ maxDisplayUnits: 2,
61
+ expected: { minutes: true, seconds: true },
62
+ },
63
+ {
64
+ milliseconds: minutesDuration,
65
+ maxDisplayUnits: 3,
66
+ expected: { minutes: true, seconds: true, milliseconds: true },
67
+ },
68
+ {
69
+ milliseconds: minutesDuration,
70
+ maxDisplayUnits: 4,
71
+ expected: { minutes: true, seconds: true, milliseconds: true },
72
+ },
73
+ // hours
74
+ { milliseconds: hoursDuration, maxDisplayUnits: 0, expected: {} },
75
+ {
76
+ milliseconds: hoursDuration,
77
+ maxDisplayUnits: 1,
78
+ expected: { hours: true },
79
+ },
80
+ {
81
+ milliseconds: hoursDuration,
82
+ maxDisplayUnits: 2,
83
+ expected: { hours: true, minutes: true },
84
+ },
85
+ {
86
+ milliseconds: hoursDuration,
87
+ maxDisplayUnits: 3,
88
+ expected: { hours: true, minutes: true, seconds: true },
89
+ },
90
+ {
91
+ milliseconds: hoursDuration,
92
+ maxDisplayUnits: 4,
93
+ expected: {
94
+ hours: true,
95
+ minutes: true,
96
+ seconds: true,
97
+ milliseconds: true,
98
+ },
99
+ },
100
+ {
101
+ milliseconds: hoursDuration,
102
+ maxDisplayUnits: 5,
103
+ expected: {
104
+ hours: true,
105
+ minutes: true,
106
+ seconds: true,
107
+ milliseconds: true,
108
+ },
109
+ },
110
+ // days
111
+ { milliseconds: daysDuration, maxDisplayUnits: 0, expected: {} },
112
+ {
113
+ milliseconds: daysDuration,
114
+ maxDisplayUnits: 1,
115
+ expected: { days: true },
116
+ },
117
+ {
118
+ milliseconds: daysDuration,
119
+ maxDisplayUnits: 2,
120
+ expected: { days: true, hours: true },
121
+ },
122
+ {
123
+ milliseconds: daysDuration,
124
+ maxDisplayUnits: 3,
125
+ expected: { days: true, hours: true, minutes: true },
126
+ },
127
+ {
128
+ milliseconds: daysDuration,
129
+ maxDisplayUnits: 4,
130
+ expected: { days: true, hours: true, minutes: true, seconds: true },
131
+ },
132
+ {
133
+ milliseconds: daysDuration,
134
+ maxDisplayUnits: 5,
135
+ expected: {
136
+ days: true,
137
+ hours: true,
138
+ minutes: true,
139
+ seconds: true,
140
+ milliseconds: true,
141
+ },
142
+ },
143
+ {
144
+ milliseconds: daysDuration,
145
+ maxDisplayUnits: 6,
146
+ expected: {
147
+ days: true,
148
+ hours: true,
149
+ minutes: true,
150
+ seconds: true,
151
+ milliseconds: true,
152
+ },
153
+ },
154
+ // partial minutes
155
+ {
156
+ milliseconds: minutesPartialDuration,
157
+ maxDisplayUnits: 0,
158
+ expected: {},
159
+ },
160
+ {
161
+ milliseconds: minutesPartialDuration,
162
+ maxDisplayUnits: 1,
163
+ expected: {
164
+ minutes: true,
165
+ },
166
+ },
167
+ {
168
+ milliseconds: minutesPartialDuration,
169
+ maxDisplayUnits: 2,
170
+ expected: {
171
+ minutes: true,
172
+ milliseconds: true,
173
+ },
174
+ },
175
+ {
176
+ milliseconds: minutesPartialDuration,
177
+ maxDisplayUnits: 3,
178
+ expected: {
179
+ minutes: true,
180
+ milliseconds: true,
181
+ },
182
+ },
183
+ // partial hours
184
+ {
185
+ milliseconds: hoursPartialDuration,
186
+ maxDisplayUnits: 0,
187
+ expected: {},
188
+ },
189
+ {
190
+ milliseconds: hoursPartialDuration,
191
+ maxDisplayUnits: 1,
192
+ expected: {
193
+ hours: true,
194
+ },
195
+ },
196
+ {
197
+ milliseconds: hoursPartialDuration,
198
+ maxDisplayUnits: 2,
199
+ expected: {
200
+ hours: true,
201
+ seconds: true,
202
+ },
203
+ },
204
+ {
205
+ milliseconds: hoursPartialDuration,
206
+ maxDisplayUnits: 3,
207
+ expected: {
208
+ hours: true,
209
+ seconds: true,
210
+ },
211
+ },
212
+ // partial days
213
+ {
214
+ milliseconds: daysPartialDuration,
215
+ maxDisplayUnits: 0,
216
+ expected: {},
217
+ },
218
+ {
219
+ milliseconds: daysPartialDuration,
220
+ maxDisplayUnits: 1,
221
+ expected: {
222
+ days: true,
223
+ },
224
+ },
225
+ {
226
+ milliseconds: daysPartialDuration,
227
+ maxDisplayUnits: 2,
228
+ expected: {
229
+ days: true,
230
+ minutes: true,
231
+ },
232
+ },
233
+ {
234
+ milliseconds: daysPartialDuration,
235
+ maxDisplayUnits: 3,
236
+ expected: {
237
+ days: true,
238
+ minutes: true,
239
+ milliseconds: true,
240
+ },
241
+ },
242
+ {
243
+ milliseconds: daysPartialDuration,
244
+ maxDisplayUnits: 4,
245
+ expected: {
246
+ days: true,
247
+ minutes: true,
248
+ milliseconds: true,
249
+ },
250
+ },
251
+ ])(
252
+ "given milliseconds: $milliseconds and maxDisplayUnits: $maxDisplayUnits",
253
+ ({ milliseconds, maxDisplayUnits, expected }) => {
254
+ expect(
255
+ getDurationMaxDisplayUnits({ milliseconds, maxDisplayUnits })
256
+ ).toEqual(expected);
257
+ }
258
+ );
259
+ });
260
+ });
@@ -0,0 +1,82 @@
1
+ import testDuration from "./testDuration";
2
+
3
+ describe("When using a zero value...", () => {
4
+ describe("... it renders properly", () => {
5
+ testDuration(0.0, {}, { text: "0s" });
6
+ testDuration(
7
+ 0.0,
8
+ {
9
+ displayUnits: {
10
+ days: true,
11
+ hours: true,
12
+ minutes: true,
13
+ seconds: true,
14
+ milliseconds: true,
15
+ },
16
+ },
17
+ { text: "0ms" }
18
+ );
19
+ testDuration(
20
+ 0.0,
21
+ {
22
+ displayUnits: { days: true, hours: true, minutes: true, seconds: true },
23
+ },
24
+ { text: "0s" }
25
+ );
26
+ testDuration(
27
+ 0.0,
28
+ { displayUnits: { days: true, hours: true, minutes: true } },
29
+ { text: "0m" }
30
+ );
31
+ testDuration(
32
+ 0.0,
33
+ { displayUnits: { days: true, hours: true } },
34
+ { text: "0h" }
35
+ );
36
+ testDuration(0.0, { displayUnits: { days: true } }, { text: "0d" });
37
+ });
38
+
39
+ describe("... should work with display long", () => {
40
+ testDuration(0.0, { display: "long" }, { text: "0 seconds" });
41
+ testDuration(
42
+ 0.0,
43
+ {
44
+ display: "long",
45
+ displayUnits: {
46
+ days: true,
47
+ hours: true,
48
+ minutes: true,
49
+ seconds: true,
50
+ milliseconds: true,
51
+ },
52
+ },
53
+ { text: "0 milliseconds" }
54
+ );
55
+ testDuration(
56
+ 0.0,
57
+ {
58
+ display: "long",
59
+ displayUnits: { days: true, hours: true, minutes: true, seconds: true },
60
+ },
61
+ { text: "0 seconds" }
62
+ );
63
+ testDuration(
64
+ 0.0,
65
+ {
66
+ display: "long",
67
+ displayUnits: { days: true, hours: true, minutes: true },
68
+ },
69
+ { text: "0 minutes" }
70
+ );
71
+ testDuration(
72
+ 0.0,
73
+ { display: "long", displayUnits: { days: true, hours: true } },
74
+ { text: "0 hours" }
75
+ );
76
+ testDuration(
77
+ 0.0,
78
+ { display: "long", displayUnits: { days: true } },
79
+ { text: "0 days" }
80
+ );
81
+ });
82
+ });
@@ -0,0 +1,41 @@
1
+ export const COMPARE_OBJECTS = true;
2
+ export const MEMO_CACHE_SIZE = 10;
3
+
4
+ export const UNITS = {
5
+ days: "days",
6
+ hours: "hours",
7
+ minutes: "minutes",
8
+ seconds: "seconds",
9
+ milliseconds: "milliseconds",
10
+ };
11
+
12
+ export const MILLISECONDS_IN = {
13
+ [UNITS.days]: 1 * 1000 * 60 * 60 * 24,
14
+ [UNITS.hours]: 1 * 1000 * 60 * 60,
15
+ [UNITS.minutes]: 1 * 1000 * 60,
16
+ [UNITS.seconds]: 1 * 1000,
17
+ [UNITS.milliseconds]: 1,
18
+ };
19
+
20
+ export const ORDERED_UNITS = [
21
+ UNITS.days,
22
+ UNITS.hours,
23
+ UNITS.minutes,
24
+ UNITS.seconds,
25
+ UNITS.milliseconds,
26
+ ];
27
+
28
+ // Duration props defaults
29
+ export const DEFAULT_DISPLAY = "narrow";
30
+ export const DEFAULT_DISPLAY_UNITS = {
31
+ [UNITS.days]: true,
32
+ [UNITS.hours]: true,
33
+ [UNITS.minutes]: true,
34
+ [UNITS.seconds]: true,
35
+ [UNITS.milliseconds]: false,
36
+ };
37
+
38
+ export const DEFAULT_LOCALE = "en-US";
39
+ export const DEFAULT_MILLISECONDS = null;
40
+
41
+ export const EM_DASH = "—"; // shift + option + hyphen on a mac keyboard
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ import Duration, { formatDuration } from "./Duration";
2
+ import {
3
+ getDurationMaxDisplayUnits,
4
+ type TypeGetDurationMaxDisplayUnitsProps,
5
+ } from "./utils";
6
+
7
+ export default Duration;
8
+ export { Duration };
9
+ export { formatDuration };
10
+ export { getDurationMaxDisplayUnits };
11
+ export type { TypeGetDurationMaxDisplayUnitsProps };
12
+ export * from "./DurationTypes";
package/src/styles.ts ADDED
@@ -0,0 +1,6 @@
1
+ import styled from "styled-components";
2
+ import Text from "@sproutsocial/seeds-react-text";
3
+
4
+ export const Container = styled(Text)`
5
+ font-variant-numeric: tabular-nums;
6
+ `;
package/src/utils.ts ADDED
@@ -0,0 +1,89 @@
1
+ import type {
2
+ TypeDurationDisplayUnits,
3
+ TypeDurationMilliseconds,
4
+ } from "./DurationTypes";
5
+ import { MILLISECONDS_IN, ORDERED_UNITS, UNITS } from "./constants";
6
+
7
+ export const getLowestUnit = (displayUnits: TypeDurationDisplayUnits) =>
8
+ [...ORDERED_UNITS]
9
+ .reverse()
10
+ .find((unit) => displayUnits[unit as keyof TypeDurationDisplayUnits]) ||
11
+ UNITS.milliseconds;
12
+
13
+ export const splitMillisecondsIntoUnits = (
14
+ milliseconds: number,
15
+ displayUnits: TypeDurationDisplayUnits
16
+ ): {
17
+ days?: number;
18
+ hours?: number;
19
+ minutes?: number;
20
+ seconds?: number;
21
+ milliseconds?: number;
22
+ } => {
23
+ const lowestUnit = getLowestUnit(displayUnits);
24
+
25
+ // @ts-ignore TS error later
26
+ const remainder = milliseconds % MILLISECONDS_IN[lowestUnit];
27
+ // @ts-ignore TS error later
28
+ if (2 * remainder >= MILLISECONDS_IN[lowestUnit]) {
29
+ // if the remainder is large, add enough seconds to increse the lowest unit
30
+ // @ts-ignore TS error later
31
+ milliseconds += MILLISECONDS_IN[lowestUnit] - remainder;
32
+ }
33
+
34
+ const units = {};
35
+
36
+ ORDERED_UNITS.forEach((unit) => {
37
+ // @ts-ignore TS error later
38
+ if (displayUnits[unit]) {
39
+ // @ts-ignore TS error later
40
+ units[unit] = Math.floor(milliseconds / MILLISECONDS_IN[unit]);
41
+ // @ts-ignore TS error later
42
+ milliseconds -= units[unit] * MILLISECONDS_IN[unit];
43
+ }
44
+ });
45
+
46
+ return units;
47
+ };
48
+
49
+ export const isValidNumber = (value: unknown): boolean =>
50
+ typeof value === "number" && isFinite(value);
51
+
52
+ export interface TypeGetDurationMaxDisplayUnitsProps {
53
+ milliseconds: TypeDurationMilliseconds;
54
+ maxDisplayUnits: number;
55
+ }
56
+
57
+ export const getDurationMaxDisplayUnits = ({
58
+ milliseconds,
59
+ maxDisplayUnits = ORDERED_UNITS.length,
60
+ }: TypeGetDurationMaxDisplayUnitsProps): TypeDurationDisplayUnits => {
61
+ const displayUnits = {};
62
+
63
+ if (!isValidNumber(milliseconds)) {
64
+ return displayUnits;
65
+ }
66
+
67
+ for (const unit of ORDERED_UNITS) {
68
+ if (Object.keys(displayUnits).length >= maxDisplayUnits) {
69
+ break;
70
+ }
71
+
72
+ // @ts-expect-error - stupid typescript isn't smart enough to check the isValidNumber check above ¯\_(ツ)_/¯
73
+ const millisecondsByUnit = splitMillisecondsIntoUnits(milliseconds, {
74
+ days: true,
75
+ hours: true,
76
+ minutes: true,
77
+ seconds: true,
78
+ milliseconds: true,
79
+ });
80
+
81
+ // @ts-expect-error - stupid typescript isn't smart enough to check the isValidNumber check above ¯\_(ツ)_/¯
82
+ if (milliseconds > MILLISECONDS_IN[unit] && millisecondsByUnit[unit] > 0) {
83
+ // @ts-ignore TS error later
84
+ displayUnits[unit] = true;
85
+ }
86
+ }
87
+
88
+ return displayUnits;
89
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "@sproutsocial/seeds-tsconfig/bundler/dom/library-monorepo",
3
+ "compilerOptions": {
4
+ "jsx": "react-jsx",
5
+ "module": "esnext"
6
+ },
7
+ "include": ["src/**/*"],
8
+ "exclude": ["node_modules", "dist", "coverage"]
9
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig((options) => ({
4
+ entry: ["src/index.ts"],
5
+ format: ["cjs", "esm"],
6
+ clean: true,
7
+ legacyOutput: true,
8
+ dts: options.dts,
9
+ external: ["react"],
10
+ sourcemap: true,
11
+ metafile: options.metafile,
12
+ }));