Skip to content

Commit

Permalink
feat(duration): add units on Duration (#561)
Browse files Browse the repository at this point in the history
  • Loading branch information
kyranet committed Mar 15, 2023
1 parent ddebb70 commit eac39af
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 85 deletions.
213 changes: 139 additions & 74 deletions packages/duration/src/lib/Duration.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,72 @@
import { Time } from './constants';

const tokens = new Map([
['nanosecond', 1 / 1e6],
['nanoseconds', 1 / 1e6],
['ns', 1 / 1e6],

['millisecond', 1],
['milliseconds', 1],
['ms', 1],

['second', 1000],
['seconds', 1000],
['sec', 1000],
['secs', 1000],
['s', 1000],

['minute', 1000 * 60],
['minutes', 1000 * 60],
['min', 1000 * 60],
['mins', 1000 * 60],
['m', 1000 * 60],

['hour', 1000 * 60 * 60],
['hours', 1000 * 60 * 60],
['hr', 1000 * 60 * 60],
['hrs', 1000 * 60 * 60],
['h', 1000 * 60 * 60],

['day', 1000 * 60 * 60 * 24],
['days', 1000 * 60 * 60 * 24],
['d', 1000 * 60 * 60 * 24],

['week', 1000 * 60 * 60 * 24 * 7],
['weeks', 1000 * 60 * 60 * 24 * 7],
['wk', 1000 * 60 * 60 * 24 * 7],
['wks', 1000 * 60 * 60 * 24 * 7],
['w', 1000 * 60 * 60 * 24 * 7],

['month', 1000 * 60 * 60 * 24 * (365.25 / 12)],
['months', 1000 * 60 * 60 * 24 * (365.25 / 12)],
['b', 1000 * 60 * 60 * 24 * (365.25 / 12)],
['mo', 1000 * 60 * 60 * 24 * (365.25 / 12)],

['year', 1000 * 60 * 60 * 24 * 365.25],
['years', 1000 * 60 * 60 * 24 * 365.25],
['yr', 1000 * 60 * 60 * 24 * 365.25],
['yrs', 1000 * 60 * 60 * 24 * 365.25],
['y', 1000 * 60 * 60 * 24 * 365.25]
['nanosecond', Time.Nanosecond],
['nanoseconds', Time.Nanosecond],
['ns', Time.Nanosecond],

['microsecond', Time.Microsecond],
['microseconds', Time.Microsecond],
['μs', Time.Microsecond],
['us', Time.Microsecond],

['millisecond', Time.Millisecond],
['milliseconds', Time.Millisecond],
['ms', Time.Millisecond],

['second', Time.Second],
['seconds', Time.Second],
['sec', Time.Second],
['secs', Time.Second],
['s', Time.Second],

['minute', Time.Minute],
['minutes', Time.Minute],
['min', Time.Minute],
['mins', Time.Minute],
['m', Time.Minute],

['hour', Time.Hour],
['hours', Time.Hour],
['hr', Time.Hour],
['hrs', Time.Hour],
['h', Time.Hour],

['day', Time.Day],
['days', Time.Day],
['d', Time.Day],

['week', Time.Week],
['weeks', Time.Week],
['wk', Time.Week],
['wks', Time.Week],
['w', Time.Week],

['month', Time.Month],
['months', Time.Month],
['b', Time.Month],
['mo', Time.Month],

['year', Time.Year],
['years', Time.Year],
['yr', Time.Year],
['yrs', Time.Year],
['y', Time.Year]
]);

const mappings = new Map([
[Time.Nanosecond, 'nanoseconds'],
[Time.Microsecond, 'microseconds'],
[Time.Millisecond, 'milliseconds'],
[Time.Second, 'seconds'],
[Time.Minute, 'minutes'],
[Time.Hour, 'hours'],
[Time.Day, 'days'],
[Time.Week, 'weeks'],
[Time.Month, 'months'],
[Time.Year, 'years']
] as const);

/**
* Converts duration strings into ms and future dates
*/
Expand All @@ -57,66 +77,111 @@ export class Duration {
public offset: number;

/**
* Create a new Duration instance
* @param pattern The string to parse
* The amount of nanoseconds extracted from the text.
*/
public constructor(pattern: string) {
this.offset = Duration.parse(pattern.toLowerCase());
}
public nanoseconds = 0;

/**
* Get the date from now
* The amount of microseconds extracted from the text.
*/
public get fromNow(): Date {
return this.dateFrom(new Date());
}
public microseconds = 0;

/**
* Get the date from
* @param date The Date instance to get the date from
* The amount of milliseconds extracted from the text.
*/
public dateFrom(date: Date): Date {
return new Date(date.getTime() + this.offset);
}
public milliseconds = 0;

/**
* The RegExp used for the pattern parsing
* The amount of seconds extracted from the text.
*/
private static readonly kPatternRegex = /(-?\d*\.?\d+(?:e[-+]?\d+)?)\s*([a-zμ]*)/gi;
public seconds = 0;

/**
* The RegExp used for removing commas
* The amount of minutes extracted from the text.
*/
private static readonly kCommaRegex = /,/g;
public minutes = 0;

/**
* The RegExp used for replacing a/an with 1
* The amount of hours extracted from the text.
*/
public hours = 0;

/**
* The amount of days extracted from the text.
*/
public days = 0;

/**
* The amount of weeks extracted from the text.
*/
public weeks = 0;

/**
* The amount of months extracted from the text.
*/
public months = 0;

/**
* The amount of years extracted from the text.
*/
private static readonly kAanRegex = /\ban?\b/gi;
public years = 0;

/**
* Parse the pattern
* @param pattern The pattern to parse
* Create a new Duration instance
* @param pattern The string to parse
*/
private static parse(pattern: string): number {
public constructor(pattern: string) {
let result = 0;
let valid = false;

pattern
.toLowerCase()
// ignore commas
.replace(Duration.kCommaRegex, '')
.replace(Duration.commaRegex, '')
// a / an = 1
.replace(Duration.kAanRegex, '1')
.replace(Duration.aAndAnRegex, '1')
// do math
.replace(Duration.kPatternRegex, (_, i, units) => {
.replace(Duration.patternRegex, (_, i, units) => {
const token = tokens.get(units);
if (token !== undefined) {
result += Number(i) * token;
const n = Number(i);
result += n * token;
this[mappings.get(token)!] += n;
valid = true;
}
return '';
});

return valid ? result : NaN;
this.offset = valid ? result : NaN;
}

/**
* Get the date from now
*/
public get fromNow(): Date {
return this.dateFrom(new Date());
}

/**
* Get the date from
* @param date The Date instance to get the date from
*/
public dateFrom(date: Date): Date {
return new Date(date.getTime() + this.offset);
}

/**
* The RegExp used for the pattern parsing
*/
private static readonly patternRegex = /(-?\d*\.?\d+(?:e[-+]?\d+)?)\s*([a-zμ]*)/gi;

/**
* The RegExp used for removing commas
*/
private static readonly commaRegex = /,/g;

/**
* The RegExp used for replacing a/an with 1
*/
private static readonly aAndAnRegex = /\ban?\b/gi;
}
89 changes: 80 additions & 9 deletions packages/duration/tests/lib/Duration.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,85 @@
import { Duration } from '../../src';
import { Duration, Time } from '../../src';

describe('Duration', () => {
test('GIVEN duration with an offset of 0s, THEN shows 0ms', () => {
const duration = new Duration('0s');
expect(duration.offset).toEqual(0);
});
describe('units', () => {
function expectDurationUnits(duration: Duration, units: Partial<Record<keyof Duration, number>> = {}) {
expect(duration.nanoseconds).toEqual(units.nanoseconds ?? 0);
expect(duration.microseconds).toEqual(units.microseconds ?? 0);
expect(duration.milliseconds).toEqual(units.milliseconds ?? 0);
expect(duration.seconds).toEqual(units.seconds ?? 0);
expect(duration.minutes).toEqual(units.minutes ?? 0);
expect(duration.hours).toEqual(units.hours ?? 0);
expect(duration.days).toEqual(units.days ?? 0);
expect(duration.weeks).toEqual(units.weeks ?? 0);
expect(duration.months).toEqual(units.months ?? 0);
expect(duration.years).toEqual(units.years ?? 0);
}

test('GIVEN duration with an offset of 1s, THEN shows 1000ms', () => {
const duration = new Duration('a second');
expect(duration.offset).toEqual(1000);
test.each(['0ns', '0us', '0μs', '0ms', '0s', '0m', '0h', '0d', '0w', '0mo', '0yr'])('GIVEN %s THEN shows 0ms', (pattern) => {
const duration = new Duration(pattern);
expect(duration.offset).toEqual(0);
expectDurationUnits(duration, {});
});

test.each(['a nanosecond', '1ns'])('GIVEN %s THEN shows 1ns', (pattern) => {
const duration = new Duration(pattern);
expect(duration.offset).toEqual(Time.Nanosecond);
expectDurationUnits(duration, { nanoseconds: 1 });
});

test.each(['a microsecond', '1us', '1μs'])('GIVEN %s THEN shows 1μs', (pattern) => {
const duration = new Duration(pattern);
expect(duration.offset).toEqual(Time.Microsecond);
expectDurationUnits(duration, { microseconds: 1 });
});

test.each(['a millisecond', '1ms'])('GIVEN %s THEN shows 1ms', (pattern) => {
const duration = new Duration(pattern);
expect(duration.offset).toEqual(Time.Millisecond);
expectDurationUnits(duration, { milliseconds: 1 });
});

test.each(['a second', '1s'])('GIVEN %s THEN shows 1s', (pattern) => {
const duration = new Duration(pattern);
expect(duration.offset).toEqual(Time.Second);
expectDurationUnits(duration, { seconds: 1 });
});

test.each(['a minute', '1m'])('GIVEN %s THEN shows 1m', (pattern) => {
const duration = new Duration(pattern);
expect(duration.offset).toEqual(Time.Minute);
expectDurationUnits(duration, { minutes: 1 });
});

test.each(['a hour', '1h'])('GIVEN %s THEN shows 1h', (pattern) => {
const duration = new Duration(pattern);
expect(duration.offset).toEqual(Time.Hour);
expectDurationUnits(duration, { hours: 1 });
});

test.each(['a day', '1d'])('GIVEN %s THEN shows 1d', (pattern) => {
const duration = new Duration(pattern);
expect(duration.offset).toEqual(Time.Day);
expectDurationUnits(duration, { days: 1 });
});

test.each(['a week', '1w'])('GIVEN %s THEN shows 1w', (pattern) => {
const duration = new Duration(pattern);
expect(duration.offset).toEqual(Time.Week);
expectDurationUnits(duration, { weeks: 1 });
});

test.each(['a month', '1mo'])('GIVEN %s THEN shows 1mo', (pattern) => {
const duration = new Duration(pattern);
expect(duration.offset).toEqual(Time.Month);
expectDurationUnits(duration, { months: 1 });
});

test.each(['a year', '1yr'])('GIVEN %s THEN shows 1yr', (pattern) => {
const duration = new Duration(pattern);
expect(duration.offset).toEqual(Time.Year);
expectDurationUnits(duration, { years: 1 });
});
});

test('GIVEN invalid duration THEN show NaN', () => {
Expand All @@ -19,6 +90,6 @@ describe('Duration', () => {
test('GIVEN duration with offset, THEN dateFrom is valid', () => {
const duration = new Duration('a second');
const date = new Date();
expect(duration.dateFrom(date)).toEqual(new Date(date.getTime() + 1000));
expect(duration.dateFrom(date)).toEqual(new Date(date.getTime() + Time.Second));
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
// Vitest Snapshot v1

exports[`ESLint Config > should export rules 1`] = `
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
// Vitest Snapshot v1

exports[`Prettier Config > should export rules 1`] = `
{
Expand Down

0 comments on commit eac39af

Please sign in to comment.