Skip to content

Commit 5950d1f

Browse files
authored
Merge pull request #8 from atlassian/issue/add-formatDurationByOptions
Fix implementation to use fallback if DurationFormat is not available
2 parents c1b0436 + a88f47c commit 5950d1f

File tree

3 files changed

+206
-28
lines changed

3 files changed

+206
-28
lines changed

src/index.test.ts

Lines changed: 135 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ describe("date-time", () => {
401401
new Date(2000, 0, 1, 0, 0, 0, 2),
402402
"en-GB"
403403
)
404-
).toStrictEqual("");
404+
).toStrictEqual("0 seconds");
405405
});
406406

407407
test("should format duration by locale", () => {
@@ -434,13 +434,139 @@ describe("date-time", () => {
434434
).toStrictEqual("-1 day");
435435
});
436436

437-
test("should format duration by options", () => {
438-
const startDate = new Date("2024-01-01T00:00:00Z");
439-
const endDate = new Date("2024-01-01T01:02:03Z");
440-
const options: Intl.DurationFormatOptions = { style: "narrow" };
437+
describe('formatDurationByOptions', () => {
438+
const baseDate = new Date('2024-01-01T00:00:00Z');
439+
const laterDate = new Date('2024-01-02T01:02:03Z');
440+
441+
test('with long style', () => {
442+
const options: Intl.DurationFormatOptions = {
443+
style: 'long',
444+
}
445+
expect(dateTime.formatDurationByOptions(options, baseDate, laterDate)).toBe('1 day, 1 hour, 2 minutes, 3 seconds');
446+
});
441447

442-
expect(dateTime.formatDurationByOptions(options, startDate, endDate)).toBe(
443-
"1h 2m 3s"
444-
);
445-
});
448+
test('with long style, 0 duration', () => {
449+
let options: Intl.DurationFormatOptions = {
450+
style: 'long',
451+
}
452+
expect(dateTime.formatDurationByOptions(options, baseDate, baseDate, 'en-US')).toBe('0 seconds');
453+
options = {
454+
style: 'narrow',
455+
}
456+
expect(dateTime.formatDurationByOptions(options, baseDate, baseDate, 'en-US')).toBe('0s');
457+
options = {
458+
style: 'narrow',
459+
minutesDisplay: 'always',
460+
} as Intl.DurationFormatOptions;
461+
expect(dateTime.formatDurationByOptions(options, baseDate, baseDate, 'en-US')).toBe('0m');
462+
});
463+
464+
test('with different style', () => {
465+
const options: Intl.DurationFormatOptions = {
466+
style: 'narrow',
467+
};
468+
expect(dateTime.formatDurationByOptions(options, baseDate, laterDate)).toBe('1d 1h 2m 3s');
469+
});
470+
471+
test('with US locale', () => {
472+
const options: Intl.DurationFormatOptions = {
473+
style: 'narrow',
474+
}
475+
expect(dateTime.formatDurationByOptions(options, baseDate, laterDate, 'en-US')).toBe('1d 1h 2m 3s');
476+
});
477+
478+
test('with CN locale', () => {
479+
const options: Intl.DurationFormatOptions = {
480+
style: 'narrow',
481+
}
482+
expect(dateTime.formatDurationByOptions(options, baseDate, laterDate, 'zh-CN')).toBe('1天1小时2分钟3秒');
483+
});
484+
485+
test('with JP locale', () => {
486+
// Japanese locale needs to display unit as long, as narrow is wrong, returns english, we override to long in the implementation
487+
const options: Intl.DurationFormatOptions = {
488+
style: 'narrow',
489+
}
490+
expect(dateTime.formatDurationByOptions(options, baseDate, laterDate, 'ja-JP')).toBe('1日1時間2分3秒');
491+
});
492+
493+
test('with KR locale', () => {
494+
const options: Intl.DurationFormatOptions = {
495+
style: 'narrow',
496+
}
497+
expect(dateTime.formatDurationByOptions(options, baseDate, laterDate, 'ko-KR')).toBe('1일1시간2분3초');
498+
});
499+
500+
test('with DE locale', () => {
501+
const options: Intl.DurationFormatOptions = {
502+
style: 'narrow',
503+
}
504+
expect(dateTime.formatDurationByOptions(options, baseDate, laterDate, 'de-DE')).toBe('1 T, 1 Std., 2 Min. und 3 Sek.');
505+
});
506+
507+
});
508+
describe('formatDurationByOptions fallback', () => {
509+
const originalDurationFormat = Intl.DurationFormat;
510+
511+
beforeEach(() => {
512+
// Mock DurationFormat as undefined using TypeScript type assertions
513+
(Intl as any).DurationFormat = undefined;
514+
});
515+
516+
afterEach(() => {
517+
// Restore the original implementation
518+
(Intl as any).DurationFormat = originalDurationFormat;
519+
});
520+
521+
const baseDate = new Date('2024-01-01T00:00:00Z');
522+
const laterDate = new Date('2024-01-02T01:02:03Z');
523+
524+
test('with default options', () => {
525+
const options: Intl.DurationFormatOptions = {
526+
style: 'long',
527+
};
528+
529+
// This should use the fallback implementation
530+
const result = dateTime.formatDurationByOptions(options, baseDate, laterDate);
531+
expect(result).toStrictEqual('1 day 1 hour 2 minutes 3 seconds');
532+
expect(result).toBeDefined();
533+
// Add more specific expectations based on your fallback implementation
534+
});
535+
536+
test('throws error with null options', () => {
537+
expect(() => {
538+
// @ts-ignore - Deliberately testing invalid input
539+
dateTime.formatDurationByOptions(null, baseDate, laterDate);
540+
}).toThrow('Please use formatDuration instead');
541+
});
542+
543+
test('with US locale', () => {
544+
let options: Intl.DurationFormatOptions = {
545+
style: 'long',
546+
}
547+
expect(dateTime.formatDurationByOptions(options, baseDate, laterDate, 'en-US')).toStrictEqual('1 day 1 hour 2 minutes 3 seconds');
548+
options.style = 'narrow';
549+
expect(dateTime.formatDurationByOptions(options, baseDate, laterDate, 'en-US')).toStrictEqual('1d 1h 2m 3s');
550+
});
551+
552+
test('with CN locale', () => {
553+
let options: Intl.DurationFormatOptions = {
554+
style: 'long',
555+
}
556+
expect(dateTime.formatDurationByOptions(options, baseDate, laterDate, 'zh-CN')).toStrictEqual('1天1小时2分钟3秒钟');
557+
options.style = 'narrow';
558+
expect(dateTime.formatDurationByOptions(options, baseDate, laterDate, 'zh-CN')).toStrictEqual('1天1小时2分钟3秒');
559+
});
560+
561+
test('with JP locale', () => {
562+
// For Japanese locale, narrow is wrong, returns english, so we override to long in the fallback implementation
563+
let options: Intl.DurationFormatOptions = {
564+
style: 'long',
565+
}
566+
expect(dateTime.formatDurationByOptions(options, baseDate, laterDate, 'ja-JP')).toStrictEqual('1日1時間2分3秒');
567+
options.style = 'narrow';
568+
expect(dateTime.formatDurationByOptions(options, baseDate, laterDate, 'ja-JP')).toStrictEqual('1日1時間2分3秒');
569+
});
570+
571+
})
446572
});

src/index.ts

Lines changed: 70 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -179,26 +179,78 @@ export function formatDurationByOptions(options: Intl.DurationFormatOptions, fro
179179
throw new Error('Please use formatDuration instead');
180180
}
181181

182-
const diffInMs = to.getTime() - from.getTime();
183-
const diffInSeconds = Math.floor(diffInMs / 1000);
184-
185-
const days = Math.floor(diffInSeconds / (24 * 60 * 60));
186-
const hours = Math.floor((diffInSeconds % (24 * 60 * 60)) / (60 * 60));
187-
const minutes = Math.floor((diffInSeconds % (60 * 60)) / 60);
188-
const seconds = diffInSeconds % 60;
189-
190-
const duration = {
191-
days,
192-
hours,
193-
minutes,
194-
seconds
195-
};
182+
// Legacy implementation
183+
const getNumberFormat = (unit: string, number: number | bigint) => {
184+
return new Intl.NumberFormat(locale, {
185+
style: "unit",
186+
unit,
187+
unitDisplay: options.style === 'long' ? 'long' : 'narrow'
188+
}).format(number);
189+
}
190+
191+
const milliseconds = to.getTime() - from.getTime();
192+
193+
const isCJKLocale = locale.startsWith('zh') || locale.startsWith('ja') || locale.startsWith('ko');
194+
// Japanese locale needs to display unit as long, as narrow is wrong, returns english
195+
// "narrow" returns english on Chrome 134.0.6998.166 and Firefox 136.0.4, remove this once fixed
196+
if (locale === 'ja-JP') {
197+
options.style = 'long';
198+
}
199+
200+
const dayInMs = 24 * 60 * 60 * 1000;
201+
const hourInMs = 60 * 60 * 1000;
202+
const minuteInMs = 60 * 1000;
203+
const secondInMs = 1000;
204+
196205

197-
const formatter = new Intl.DurationFormat(locale, options);
206+
const days = Math.floor(milliseconds / dayInMs);
207+
const hours = Math.floor((milliseconds - days * dayInMs) / hourInMs);
208+
const minutes = Math.floor((milliseconds - days * dayInMs - hours * hourInMs) / minuteInMs);
209+
const seconds = Math.floor((milliseconds - days * dayInMs - hours * hourInMs - minutes * minuteInMs) / secondInMs);
198210

199-
if (locale.startsWith('zh') || locale.startsWith('ja') || locale.startsWith('ko')) {
200-
return formatter.format(duration).replace(/\s+/g, '');
211+
// Feature detection for Intl.DurationFormat, not supported in older browsers
212+
if ('DurationFormat' in Intl) {
213+
try {
214+
const duration = {
215+
days,
216+
hours,
217+
minutes,
218+
seconds
219+
};
220+
let durationString = new Intl.DurationFormat(locale, options).format(duration);
221+
if (durationString === '') {
222+
// Use legacy implementation if Intl.DurationFormat returns empty string (duration less than 1 second)
223+
durationString = getNumberFormat('second', seconds);
224+
}
225+
if (isCJKLocale) {
226+
durationString = durationString.normalize('NFD').replace(/\s+/g, '');
227+
}
228+
return durationString;
229+
} catch (error) {
230+
console.warn('Intl.DurationFormat failed, falling back to custom implementation', error);
231+
// Fall through to legacy implementation
232+
}
233+
}
234+
235+
const parts = [];
236+
237+
if (days) {
238+
parts.push(getNumberFormat('day', days));
201239
}
240+
if (hours) {
241+
parts.push(getNumberFormat('hour', hours));
242+
}
243+
if (minutes) {
244+
parts.push(getNumberFormat('minute', minutes));
245+
}
246+
parts.push(getNumberFormat('second', seconds));
247+
248+
// Normalize and replace spaces
249+
let result = parts.join(' ').normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/\s+/g, ' ');
202250

203-
return formatter.format(duration);
251+
if (isCJKLocale) {
252+
return result.replace(/\s+/g, '');
253+
}
254+
255+
return result;
204256
}

src/test-dates.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,6 @@
158158
"pattern": "yyyy. mm. dd.",
159159
"numericDate": "2004. 8. 3.",
160160
"cldrDate": "2004. 8. 3.",
161-
"duration": "1일1시간1분1초"
161+
"duration": "1일1시간1분1초"
162162
}
163163
]

0 commit comments

Comments
 (0)