Skip to content

Commit

Permalink
feat(Duration): add format method
Browse files Browse the repository at this point in the history
  • Loading branch information
ValeraS committed May 21, 2024
1 parent 155e56e commit 5c1bef1
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 12 deletions.
121 changes: 121 additions & 0 deletions src/duration/__tests__/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,124 @@ test('Duration#humanize ru language', async () => {
expect(duration({seconds: -44}, {lang: 'ru'}).humanize(true)).toBe('несколько секунд назад');
expect(duration({seconds: +44}, {lang: 'ru'}).humanize(true)).toBe('через несколько секунд');
});

//------
// #format()
//------

test("Duration#format('S') returns milliseconds", () => {
expect(dur().format('S')).toBe('37695150007');

const lil = duration(5);
expect(lil.format('S')).toBe('5');
expect(lil.format('SS')).toBe('05');
expect(lil.format('SSSSS')).toBe('00005');
});

test("Duration#format('s') returns seconds", () => {
expect(dur().format('s')).toBe('37695150');
expect(dur().format('s', {floor: false})).toBe('37695150.007');
expect(dur().format('s.SSS')).toBe('37695150.007');

const lil = duration({seconds: 6});
expect(lil.format('s')).toBe('6');
expect(lil.format('ss')).toBe('06');
expect(lil.format('sss')).toBe('006');
expect(lil.format('ssss')).toBe('0006');
});

test("Duration#format('m') returns minutes", () => {
expect(dur().format('m')).toBe('628252');
expect(dur().format('m', {floor: false})).toBe('628252.5');
expect(dur().format('m:ss')).toBe('628252:30');
expect(dur().format('m:ss.SSS')).toBe('628252:30.007');

const lil = duration({minutes: 6});
expect(lil.format('m')).toBe('6');
expect(lil.format('mm')).toBe('06');
expect(lil.format('mmm')).toBe('006');
expect(lil.format('mmmm')).toBe('0006');
});

test("Duration#format('h') returns hours", () => {
expect(dur().format('h')).toBe('10470');
expect(dur().format('h', {floor: false})).toBe('10470.875');
expect(dur().format('h:ss')).toBe('10470:3150');
expect(dur().format('h:mm:ss.SSS')).toBe('10470:52:30.007');

const lil = duration({hours: 6});
expect(lil.format('h')).toBe('6');
expect(lil.format('hh')).toBe('06');
expect(lil.format('hhh')).toBe('006');
expect(lil.format('hhhh')).toBe('0006');
});

test("Duration#format('d') returns days", () => {
expect(dur().format('d')).toBe('436');
expect(dur().format('d', {floor: false})).toBe('436.286');
expect(dur().format('d:h:ss')).toBe('436:6:3150');
expect(dur().format('d:h:mm:ss.SSS')).toBe('436:6:52:30.007');

const lil = duration({days: 6});
expect(lil.format('d')).toBe('6');
expect(lil.format('dd')).toBe('06');
expect(lil.format('ddd')).toBe('006');
expect(lil.format('dddd')).toBe('0006');
});

test("Duration#format('w') returns weeks", () => {
expect(dur().format('w')).toBe('62');
expect(dur().format('w', {floor: false})).toBe('62.327');
expect(dur().format('w:s')).toBe('62:197550');
expect(dur().format('w:dd:h:mm:ss.SSS')).toBe('62:02:6:52:30.007');

const lil = duration({weeks: 6});
expect(lil.format('w')).toBe('6');
expect(lil.format('ww')).toBe('06');
expect(lil.format('www')).toBe('006');
expect(lil.format('wwww')).toBe('0006');
});

test("Duration#format('M') returns months", () => {
expect(dur().format('M')).toBe('14');
expect(dur().format('M', {floor: false})).toBe('14.334');
expect(dur().format('M:s')).toBe('14:878706');
expect(dur().format('M:dd:h:mm:ss.SSS')).toBe('14:10:4:05:06.007');

const lil = duration({months: 6});
expect(lil.format('M')).toBe('6');
expect(lil.format('MM')).toBe('06');
expect(lil.format('MMM')).toBe('006');
expect(lil.format('MMMM')).toBe('0006');
});

test("Duration#format('y') returns years", () => {
expect(dur().format('y')).toBe('1');
expect(dur().format('y', {floor: false})).toBe('1.195');
expect(dur().format('y:m')).toBe('1:102303');
expect(dur().format('y:M:dd:h:mm:ss.SSS')).toBe('1:2:10:4:05:06.007');

const lil = duration({years: 5});
expect(lil.format('y')).toBe('5');
expect(lil.format('yy')).toBe('05');
expect(lil.format('yyyyy')).toBe('00005');
});

test('Duration#format leaves in zeros', () => {
const tiny = duration({seconds: 5});
expect(tiny.format('hh:mm:ss')).toBe('00:00:05');
expect(tiny.format('hh:mm:ss.SSS')).toBe('00:00:05.000');
});

test('Duration#format rounds down', () => {
const tiny = duration({seconds: 5.7});
expect(tiny.format('s')).toBe('5');

const unpromoted = duration({seconds: 59.7});
expect(unpromoted.format('mm:ss')).toBe('00:59');
});

test('Duration#format localizes the numbers', async () => {
await settings.loadLocale('bn');
expect(dur().locale('bn').format('yy:MM:dd:h:mm:ss.SSS')).toBe('০১:০২:১০:৪:০৫:০৬.০০৭');
});
2 changes: 1 addition & 1 deletion src/duration/__tests__/units.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ test('Duration#shiftTo throws on invalid units', () => {
test('Duration#shiftTo tacks decimals onto the end', () => {
const dur = duration({minutes: 73}).shiftTo(['hours']);
expect(dur.isValid()).toBe(true);
expect(dur.hours()).toBeCloseTo(1.2167, 4);
expect(dur.hours()).toBeCloseTo(1.217, 3);
});

test('Duration#shiftTo deconstructs decimal inputs', () => {
Expand Down
87 changes: 86 additions & 1 deletion src/duration/duration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@

import {dateTimeUtc} from '../dateTime';
import {settings} from '../settings';
import type {Duration, DurationInput, DurationInputObject, DurationUnit} from '../typings';
import type {
Duration,
DurationInput,
DurationInputObject,
DurationUnit,
FormatOptions,
} from '../typings';
import {normalizeDateComponents, normalizeDurationUnit} from '../utils';
import {getListFormat, getNumberFormat} from '../utils/locale';

Expand Down Expand Up @@ -322,6 +328,62 @@ export class DurationImpl implements Duration {
}).format(l);
}

format(formatInput: string, options: FormatOptions & {forceSimple?: boolean} = {}): string {
if (!this.isValid()) {
return 'Invalid Duration';
}

const formattingTokens = /(\[[^[]*\])|y+|M+|w+|d+|h+|m+|s+|S+|./g;
const tokens: Array<
{literal: true; value: string} | {literal: false; padTo: number; unit: DurationUnit}
> = [];
const units: DurationUnit[] = [];
let match: RegExpMatchArray | null;
while ((match = formattingTokens.exec(formatInput))) {
const value = match[0];
const escaped = match[1];
const unit = tokenToField(value[0]);
if (unit) {
tokens.push({literal: false, padTo: value.length, unit});
units.push(unit);
} else if (escaped) {
tokens.push({literal: true, value: escaped.slice(1, -1)});
} else {
tokens.push({literal: true, value});
}
}

const dur = this.shiftTo(units);
let result = '';

const {floor = true, forceSimple, ...other} = options;
const useIntlFormatter = !forceSimple || Object.keys(other).length > 0;

for (const token of tokens) {
if (token.literal) {
result += token.value;
} else {
const val = dur.get(token.unit);

if (useIntlFormatter) {
const formatter = getNumberFormat(this._locale, {
useGrouping: false,
...other,
minimumIntegerDigits: token.padTo,
});
const fixed = floor ? Math.floor(val) : val;
result += formatter.format(fixed);
} else {
const fixed = floor ? Math.floor(val) : Math.round(val * 1000) / 1000;
result += `${fixed < 0 ? '-' : ''}${Math.abs(fixed)
.toString()
.padStart(token.padTo, '0')}`;
}
}
}
return result;
}

isValid(): boolean {
return this._isValid;
}
Expand Down Expand Up @@ -350,3 +412,26 @@ function monthsToDays(months: number) {
// the reverse of daysToMonths
return (months * 146097) / 4800;
}

function tokenToField(token: string) {
switch (token[0]) {
case 'S':
return 'millisecond';
case 's':
return 'second';
case 'm':
return 'minute';
case 'h':
return 'hour';
case 'd':
return 'day';
case 'w':
return 'week';
case 'M':
return 'month';
case 'y':
return 'year';
default:
return null;
}
}
27 changes: 17 additions & 10 deletions src/duration/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export function shiftTo(
}
const newValues: DurationValues = {};
const accumulated: DurationValues = {};
let lastUnit;
let lastUnit: keyof DurationValues | undefined;

for (const unit of orderedUnits) {
if (!units.includes(unit)) {
Expand Down Expand Up @@ -201,18 +201,25 @@ export function shiftTo(
const i = Math.trunc(own);
newValues[unit] = i;
accumulated[unit] = (own * 1000 - i * 1000) / 1000;
// console.log(newValues, accumulated);
}

// anything leftover becomes the decimal for the last unit
// lastUnit must be defined since units is not empty
for (const [key, value] of Object.entries(accumulated)) {
if (value !== 0) {
newValues[lastUnit as keyof DurationValues] =
(newValues[lastUnit as keyof DurationValues] ?? 0) +
(key === lastUnit
? value
: // @ts-expect-error
value / matrix[lastUnit][key]);
if (lastUnit) {
// anything leftover becomes the decimal for the last unit
for (const [key, value] of Object.entries(accumulated)) {
if (value !== 0) {
newValues[lastUnit] =
(newValues[lastUnit] ?? 0) +
(key === lastUnit
? value
: // @ts-expect-error
value / matrix[lastUnit][key]);
}
}
const v = newValues[lastUnit];
if (v) {
newValues[lastUnit] = Math.round(v * 1000) / 1000;
}
}

Expand Down
10 changes: 10 additions & 0 deletions src/typings/duration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export type DurationUnit = BaseUnit | QuarterUnit | WeekUnit;
export type DurationInputObject = Partial<Record<DurationUnit, number | string>>;
export type DurationInput = Duration | number | string | DurationInputObject | null | undefined;

export type FormatOptions = {
floor?: boolean;
} & Intl.NumberFormatOptions;

export interface Duration {
/** Return the length of the duration in the specified unit. */
as(unit: DurationUnit): number;
Expand Down Expand Up @@ -91,6 +95,12 @@ export interface Duration {
unitDisplay?: Intl.NumberFormatOptions['unitDisplay'];
}): string;

/** Returns a string representation of this Duration formatted according to the specified format string.
* Used tokens are S for milliseconds, s for seconds, m for minutes, h for hours, d for days, w for weeks, M for months, and y for years.
* Add padding by repeating the token, e.g. 'yy' pads the years to two digits, "hhhh" pads the hours to four digits.
*/
format(formatInput: string, options?: FormatOptions): string;

/** Reduce this Duration to its canonical representation in its current units. */
normalize(options?: {roundUp?: boolean}): Duration;

Expand Down

0 comments on commit 5c1bef1

Please sign in to comment.