Skip to content

Commit

Permalink
feat: add duration pipe
Browse files Browse the repository at this point in the history
  • Loading branch information
json-derulo committed Sep 1, 2023
1 parent 066077b commit 6ffe468
Show file tree
Hide file tree
Showing 11 changed files with 426 additions and 0 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Now you can use the pipes, see below.
- [Unit pipe](#unit-pipe)
- [List pipe](#list-pipe)
- [Relative Time (timeago) pipe](#relative-time-timeago-pipe)
- [Duration pipe](#duration-pipe)

### Date pipe

Expand Down Expand Up @@ -279,6 +280,27 @@ The following options are supported:

With the `INTL_RELATIVE_TIME_PIPE_DEFAULT_OPTIONS` injection token you can specify default options.

## Duration pipe

Use the duration pipe like the following:

```
{{ { hours: 2, minutes: 53 } | intlDuration: options }}
```

The input can be one of the following:

- [duration object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/format#parameters)
- null
- undefined

The following options are supported:

- [`style`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#style)
- [`fractionalDigits`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat#fractionalDigits)

For each duration unit, there is a style and display option.

## Browser compatibility

This library supports the latest major version of the following browsers:
Expand All @@ -302,6 +324,7 @@ In case you need to support older versions of that browsers, see the below table
| Unit | 77 | 78 | 14.1 |
| List | 72 | 78 | 14.1 |
| Relative Time | 71 | 65 | 14 |
| Duration | n/a | n/a | 16.4 |

## Angular compatibility table

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { InjectionToken } from '@angular/core';
import { IntlDurationPipeOptions } from './intl-duration.pipe';

export const INTL_DURATION_PIPE_DEFAULT_OPTIONS = new InjectionToken<
Omit<IntlDurationPipeOptions, 'locale'>
>('IntlDurationPipeDefaultOptions');
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { TestBed } from '@angular/core/testing';
import { INTL_LOCALES } from '../locale';
import { INTL_DURATION_PIPE_DEFAULT_OPTIONS } from './intl-duration-pipe-default-options';
import { IntlDurationPipe } from './intl-duration.pipe';

describe('IntlDurationPipe', () => {
let testUnit: IntlDurationPipe;

describe('parsing', () => {
beforeEach(() => {
testUnit = new IntlDurationPipe('en-US');
});

it('should create an instance', () => {
expect(testUnit).toBeTruthy();
});

it('should handle null values', () => {
expect(testUnit.transform(null)).toBeNull();
});

it('should handle undefined values', () => {
expect(testUnit.transform(undefined)).toBeNull();
});

it('should transform durations', () => {
expect(
testUnit.transform({
years: 2,
months: 11,
weeks: 2,
days: 1,
hours: 0,
minutes: 55,
seconds: 19,
milliseconds: 940,
microseconds: 10,
nanoseconds: 3,
}),
).toEqual(
'2 yrs, 11 mths, 2 wks, 1 day, 55 min, 19 sec, 940 ms, 10 μs, 3 ns',
);
});

it('should handle missing Intl.NumberFormat browser API', () => {
// @ts-expect-error Intl APIs are not expected to be undefined
spyOn(Intl, 'DurationFormat').and.returnValue(undefined);
const consoleError = spyOn(console, 'error');
expect(testUnit.transform({ years: 1 })).toBeNull();

expect(consoleError).toHaveBeenCalledTimes(1);
});
});

describe('internationalization', () => {
it('should respect the set locale', () => {
TestBed.configureTestingModule({
providers: [
IntlDurationPipe,
{
provide: INTL_LOCALES,
useValue: 'de-DE',
},
],
});
testUnit = TestBed.inject(IntlDurationPipe);

expect(testUnit.transform({ years: 1 })).toEqual('1 J');
});

it('should fall back to the browser default locale', () => {
TestBed.configureTestingModule({ providers: [IntlDurationPipe] });

const result1 = TestBed.inject(IntlDurationPipe).transform({ years: 1 });
const result2 = new IntlDurationPipe(navigator.language).transform({
years: 1,
});

expect(result1).toEqual(result2);
});
});

describe('options', () => {
it('should respect the setting from default config', () => {
TestBed.configureTestingModule({
providers: [
IntlDurationPipe,
{
provide: INTL_LOCALES,
useValue: 'en-US',
},
{
provide: INTL_DURATION_PIPE_DEFAULT_OPTIONS,
useValue: {
style: 'long',
},
},
],
});
testUnit = TestBed.inject(IntlDurationPipe);

expect(testUnit.transform({ years: 1 })).toEqual('1 year');
});

it('should give the user options a higher priority', () => {
TestBed.configureTestingModule({
providers: [
IntlDurationPipe,
{
provide: INTL_LOCALES,
useValue: 'en-US',
},
{
provide: INTL_DURATION_PIPE_DEFAULT_OPTIONS,
useValue: {
style: 'long',
},
},
],
});
testUnit = TestBed.inject(IntlDurationPipe);

expect(testUnit.transform({ years: 1 }, { style: 'narrow' })).toEqual(
'1y',
);
});
});

it('should respect locale option', () => {
TestBed.configureTestingModule({
providers: [
IntlDurationPipe,
{
provide: INTL_LOCALES,
useValue: 'en-US',
},
],
});
testUnit = TestBed.inject(IntlDurationPipe);

expect(testUnit.transform({ years: 1 }, { locale: 'de-DE' })).toEqual(
'1 J',
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Inject, Optional, Pipe, PipeTransform } from '@angular/core';
import { IntlPipeOptions } from '../intl-pipe-options';
import { INTL_LOCALES } from '../locale';
import { INTL_DURATION_PIPE_DEFAULT_OPTIONS } from './intl-duration-pipe-default-options';

// ToDo: remove once TypeScript includes official typings
// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace Intl {
export class DurationFormat {
constructor(locale?: string[] | string, options?: DurationFormatOptions);

format(duration: Duration): string;
}

export interface DurationFormatOptions {
style?: DurationItemStyle | 'digital';
years?: DurationItemStyle;
yearsDisplay?: DurationItemDisplay;
months?: DurationItemStyle;
monthsDisplay?: DurationItemDisplay;
weeks?: DurationItemStyle;
weeksDisplay?: DurationItemDisplay;
days?: DurationItemStyle;
daysDisplay?: DurationItemDisplay;
hours?: DurationItemStyle | 'numeric' | '2-digit';
hoursDisplay?: DurationItemDisplay;
minutes?: DurationItemStyle | 'numeric' | '2-digit';
minutesDisplay?: DurationItemDisplay;
seconds?: DurationItemStyle | 'numeric' | '2-digit';
secondsDisplay?: DurationItemDisplay;
milliseconds?: DurationItemStyle | 'numeric' | '2-digit';
millisecondsDisplay?: DurationItemDisplay;
microseconds?: DurationItemStyle | 'numeric';
microsecondsDisplay?: DurationItemDisplay;
nanoseconds?: DurationItemStyle | 'numeric';
nanosecondsDisplay?: DurationItemDisplay;
fractionalDigits?: number;
}

export type DurationItemStyle = 'long' | 'short' | 'narrow';
export type DurationItemDisplay = 'always' | 'auto';

export interface Duration {
years?: number;
months?: number;
weeks?: number;
days?: number;
hours?: number;
minutes?: number;
seconds?: number;
milliseconds?: number;
microseconds?: number;
nanoseconds?: number;
}
}

export type IntlDurationPipeOptions = Partial<Intl.DurationFormatOptions> &
IntlPipeOptions;

@Pipe({
name: 'intlDuration',
standalone: true,
})
export class IntlDurationPipe implements PipeTransform {
constructor(
@Optional()
@Inject(INTL_LOCALES)
readonly locale?: string | string[] | null,
@Optional()
@Inject(INTL_DURATION_PIPE_DEFAULT_OPTIONS)
readonly defaultOptions?: Omit<IntlDurationPipeOptions, 'locale'> | null,
) {}
transform(
value: Intl.Duration | null | undefined,
options?: IntlDurationPipeOptions,
): string | null {
if (!value) {
return null;
}

const { locale, ...intlOptions } = options ?? {};

try {
return new Intl.DurationFormat(locale ?? this.locale ?? undefined, {
...this.defaultOptions,
...intlOptions,
}).format(value);
} catch (e) {
console.error('Error while transforming the duration value', e);
return null;
}
}
}
3 changes: 3 additions & 0 deletions projects/angular-ecmascript-intl/src/lib/intl.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IntlCountryPipe } from './country/intl-country.pipe';
import { IntlCurrencyPipe } from './currency/intl-currency.pipe';
import { IntlDatePipe } from './date/intl-date.pipe';
import { IntlDecimalPipe } from './decimal/intl-decimal.pipe';
import { IntlDurationPipe } from './duration/intl-duration.pipe';
import { IntlLanguagePipe } from './language/intl-language.pipe';
import { IntlListPipe } from './list/intl-list.pipe';
import { IntlPercentPipe } from './percent/intl-percent.pipe';
Expand All @@ -20,6 +21,7 @@ import { IntlUnitPipe } from './unit/intl-unit.pipe';
IntlUnitPipe,
IntlListPipe,
IntlRelativeTimePipe,
IntlDurationPipe,
],
exports: [
IntlDatePipe,
Expand All @@ -31,6 +33,7 @@ import { IntlUnitPipe } from './unit/intl-unit.pipe';
IntlUnitPipe,
IntlListPipe,
IntlRelativeTimePipe,
IntlDurationPipe,
],
})
export class IntlModule {}
2 changes: 2 additions & 0 deletions projects/angular-ecmascript-intl/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export * from './lib/date/intl-date-pipe-default-options';
export * from './lib/date/intl-date.pipe';
export * from './lib/decimal/intl-decimal-pipe-default-options';
export * from './lib/decimal/intl-decimal.pipe';
export * from './lib/duration/intl-duration-pipe-default-options';
export * from './lib/duration/intl-duration.pipe';
export * from './lib/intl.module';
export * from './lib/language/intl-language-pipe-default-options';
export * from './lib/language/intl-language.pipe';
Expand Down
Loading

0 comments on commit 6ffe468

Please sign in to comment.