From e4900d50bfafc22fbc958cc17c1b8b4b9703ce59 Mon Sep 17 00:00:00 2001 From: Jamie Mason Date: Mon, 11 Mar 2019 12:50:57 +0000 Subject: [PATCH] feat(core): add outsideOfficeHours --- README.md | 37 ++++++++++- src/index.ts | 6 +- ...{index.spec.ts => is-office-hours.spec.ts} | 8 +-- src/is-office-hours.ts | 16 +++++ src/lib/pad.ts | 1 + src/lib/range.ts | 7 +++ src/outside-office-hours.spec.ts | 41 +++++++++++++ src/outside-office-hours.ts | 61 +++++++++++++++++++ 8 files changed, 164 insertions(+), 13 deletions(-) rename src/{index.spec.ts => is-office-hours.spec.ts} (87%) create mode 100644 src/is-office-hours.ts create mode 100644 src/lib/pad.ts create mode 100644 src/lib/range.ts create mode 100644 src/outside-office-hours.spec.ts create mode 100644 src/outside-office-hours.ts diff --git a/README.md b/README.md index 997cdc3..da83799 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,14 @@ npm install --save is-office-hours ## :memo: Usage -Always `false` on Saturday and Sunday, `true` from 9:00am until 4:59pm weekdays. -Does not take into account public holidays. +### isOfficeHours + +Returns `true` if the provided `Date` falls within Monday to Friday 9:00am to +4:59pm. ```js +import { isOfficeHours } from 'is-office-hours'; + isOfficeHours(new Date('2019-03-04T08:59:59.000Z')); // false isOfficeHours(new Date('2019-03-04T09:00:00.000Z')); @@ -39,3 +43,32 @@ isOfficeHours(new Date('2019-03-04T17:00:00.000Z')); isOfficeHours(new Date('2019-03-04T17:01:00.000Z')); // false ``` + +### outsideOfficeHours + +If the provided date falls within Office Hours, a new date is returned with the +time adjusted to fall outside Office Hours. If the provided date falls outside +Office Hours, it is returned unchanged. + +The provided date is never mutated. + +```js +import { outsideOfficeHours } from 'is-office-hours'; + +outsideOfficeHours(new Date('2019-03-04T09:00:00.000Z')); +// Date(2019-03-04T18:00:00.000Z) +outsideOfficeHours(new Date('2019-03-04T10:00:00.000Z')); +// Date(2019-03-04T18:37:29.000Z) +outsideOfficeHours(new Date('2019-03-04T11:00:00.000Z')); +// Date(2019-03-04T19:14:59.000Z) +outsideOfficeHours(new Date('2019-03-04T12:00:00.000Z')); +// Date(2019-03-04T19:52:29.000Z) +outsideOfficeHours(new Date('2019-03-04T13:00:00.000Z')); +// Date(2019-03-04T20:29:59.000Z) +outsideOfficeHours(new Date('2019-03-04T14:00:00.000Z')); +// Date(2019-03-04T21:07:29.000Z) +outsideOfficeHours(new Date('2019-03-04T15:00:00.000Z')); +// Date(2019-03-04T21:44:59.000Z) +outsideOfficeHours(new Date('2019-03-04T16:00:00.000Z')); +// Date(2019-03-04T22:22:29.000Z) +``` diff --git a/src/index.ts b/src/index.ts index 3a52a2e..fbd0239 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,2 @@ -const isWorkingDay = (date: Date) => date.getDay() >= 1 && date.getDay() <= 5; - -export const isOfficeHours = (date: Date): boolean => - isWorkingDay(date) && date.getHours() >= 9 && date.getHours() < 17; +export { isOfficeHours } from './is-office-hours'; +export { outsideOfficeHours } from './outside-office-hours'; diff --git a/src/index.spec.ts b/src/is-office-hours.spec.ts similarity index 87% rename from src/index.spec.ts rename to src/is-office-hours.spec.ts index 256ae6b..806f37e 100644 --- a/src/index.spec.ts +++ b/src/is-office-hours.spec.ts @@ -29,17 +29,11 @@ const checkDate = (isoDate: string, isoTime: string) => [['Saturday', '2019-03-09'], ['Sunday', '2019-03-10']].forEach( ([restDay, isoDate]) => { - it(`Returns false until 9:00am ${restDay}`, () => { + it(`Returns false on ${restDay}`, () => { expect(checkDate(isoDate, '08:59:59')).toBeFalse(); - }); - - it(`Returns false between 9:00am and 4:59pm ${restDay}`, () => { expect(checkDate(isoDate, '09:00:00')).toBeFalse(); expect(checkDate(isoDate, '09:01:00')).toBeFalse(); expect(checkDate(isoDate, '16:59:59')).toBeFalse(); - }); - - it(`Returns false from 5:00pm ${restDay}`, () => { expect(checkDate(isoDate, '17:00:00')).toBeFalse(); expect(checkDate(isoDate, '17:01:00')).toBeFalse(); }); diff --git a/src/is-office-hours.ts b/src/is-office-hours.ts new file mode 100644 index 0000000..d68e6d1 --- /dev/null +++ b/src/is-office-hours.ts @@ -0,0 +1,16 @@ +const isWorkingDay = (day: number) => day >= 1 && day <= 5; + +/** @member hour @member minute @member second */ +export type Time = [number, number, number]; + +export const stintStart: Time = [9, 0, 0]; +export const stintEnd: Time = [16, 59, 59]; + +/** + * Returns `true` if the provided `Date` falls within Monday to Friday 9:00am to + * 4:59pm. + */ +export const isOfficeHours = (date: Date): boolean => + isWorkingDay(date.getDay()) && + date.getHours() >= stintStart[0] && + date.getHours() <= stintEnd[0]; diff --git a/src/lib/pad.ts b/src/lib/pad.ts new file mode 100644 index 0000000..23d12a7 --- /dev/null +++ b/src/lib/pad.ts @@ -0,0 +1 @@ +export const pad = (n: number) => (n < 10 ? `0${n}` : `${n}`); diff --git a/src/lib/range.ts b/src/lib/range.ts new file mode 100644 index 0000000..0bff87d --- /dev/null +++ b/src/lib/range.ts @@ -0,0 +1,7 @@ +export const range = (floor: number, ceiling: number) => { + const array = []; + while (floor <= ceiling) { + array.push(floor++); + } + return array; +}; diff --git a/src/outside-office-hours.spec.ts b/src/outside-office-hours.spec.ts new file mode 100644 index 0000000..c952f41 --- /dev/null +++ b/src/outside-office-hours.spec.ts @@ -0,0 +1,41 @@ +import 'expect-more-jest'; +import { outsideOfficeHours } from './'; +import { pad } from './lib/pad'; +import { range } from './lib/range'; + +describe('Dates during office hours', () => { + range(9, 16).forEach((hour) => { + range(0, 59).forEach((minute) => { + const eveStart = new Date('2019-03-04T18:00:00.000Z'); + const eveEnd = new Date('2019-03-04T22:59:59.000Z'); + const iso8601 = `2019-03-04T${pad(hour)}:${pad(minute)}:00.000Z`; + const date = new Date(iso8601); + const epoch = date.getTime(); + const nextDate = outsideOfficeHours(date); + + it(`Moves ${iso8601}`, () => { + expect(nextDate.getTime()).toBeLessThanOrEqual(eveEnd.getTime()); + expect(nextDate.getTime()).toBeGreaterThanOrEqual(eveStart.getTime()); + }); + + it('Does not mutate the original Date', () => { + expect(date.getTime()).toEqual(epoch); + }); + }); + }); +}); + +describe('Dates outside office hours', () => { + range(0, 8) + .concat(range(17, 23)) + .forEach((hour) => { + range(0, 59).forEach((minute) => { + const iso8601 = `2019-03-04T${pad(hour)}:${pad(minute)}:00.000Z`; + const date = new Date(iso8601); + + it(`Leaves ${iso8601} untouched`, () => { + expect(outsideOfficeHours(date)).toBe(date); + }); + }); + }); +}); diff --git a/src/outside-office-hours.ts b/src/outside-office-hours.ts new file mode 100644 index 0000000..bb73806 --- /dev/null +++ b/src/outside-office-hours.ts @@ -0,0 +1,61 @@ +import { isOfficeHours, stintEnd, stintStart, Time } from './is-office-hours'; + +const getPercentOfPart = (part: number, whole: number) => (part / whole) * 100; + +const getPartFromPercent = (percent: number, whole: number) => + whole * (percent / 100); + +const getMsSinceMidnight = ([hours, minutes, seconds]: Time) => { + const date = new Date(0); + date.setHours(hours); + date.setMinutes(minutes); + date.setSeconds(seconds); + return date.getTime(); +}; + +const getTimeFromDate = (date: Date): Time => [ + date.getHours(), + date.getMinutes(), + date.getSeconds() +]; + +const getDateWithTime = (date: Date, [hours, minutes, seconds]: Time) => { + const nextDate = new Date(date.getTime()); + nextDate.setHours(hours); + nextDate.setMinutes(minutes); + nextDate.setSeconds(seconds); + return nextDate; +}; + +const getMsBetween = (start: Time, end: Time) => + getMsSinceMidnight(end) - getMsSinceMidnight(start); + +const eveningStart: Time = [18, 0, 0]; +const eveningEnd: Time = [22, 59, 59]; +const eveningLength = getMsBetween(eveningStart, eveningEnd); +const stintLength = getMsBetween(stintStart, stintEnd); +const eveningStartMs = getMsSinceMidnight(eveningStart); +const stintStartMs = getMsSinceMidnight(stintStart); + +/** + * If the provided `Date` falls within Office Hours, a new `Date` is returned + * with the time adjusted to fall outside Office Hours. If the provided `Date` + * falls outside Office Hours, it is returned unchanged. + * + * The provided `Date` is never mutated. + */ +export const outsideOfficeHours = (date: Date): Date => { + if (isOfficeHours(date)) { + const eventTime = getTimeFromDate(date); + const eventStartMs = getMsSinceMidnight(eventTime); + const eventDistance = eventStartMs - stintStartMs; + if (eventDistance === 0) { + return getDateWithTime(date, eveningStart); + } + const eventPercent = getPercentOfPart(eventDistance, stintLength); + const eveningDistance = getPartFromPercent(eventPercent, eveningLength); + const nextDate = new Date(eveningDistance + eveningStartMs); + return getDateWithTime(date, getTimeFromDate(nextDate)); + } + return date; +};