Skip to content

Commit

Permalink
chore(refactor): extract date utility functions
Browse files Browse the repository at this point in the history
- cleanup utility functions in CalOohPay.ts
  • Loading branch information
lonelydev committed Oct 16, 2024
1 parent 4466375 commit 7cf6ae5
Show file tree
Hide file tree
Showing 10 changed files with 166 additions and 92 deletions.
6 changes: 5 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,8 @@ jobs:

- name: Validate PR commits with commitlint
if: github.event_name == 'pull_request'
run: npx commitlint --from ${{ github.event.pull_request.head.sha }}~${{ github.event.pull_request.commits }} --to ${{ github.event.pull_request.head.sha }} --verbose
run: npx commitlint --from ${{ github.event.pull_request.head.sha }}~${{ github.event.pull_request.commits }} --to ${{ github.event.pull_request.head.sha }} --verbose

- name: Run Jest tests
if: github.event_name == 'pull_request'
run: npm test
46 changes: 20 additions & 26 deletions src/CalOohPay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,11 @@ import { FinalSchedule } from './FinalSchedule';
import { OnCallPaymentsCalculator } from './OnCallPaymentsCalculator';
import { ScheduleEntry } from './ScheduleEntry';
import { CommandLineOptions } from './CommandLineOptions.js';
import { Environment, sanitiseEnvVariable } from './EnvironmentController.js';
import { toLocaTzIsoStringWithOffset } from './DateUtilities.js';

dotenv.config();

interface Environment {
API_TOKEN: string;
}

function sanitiseEnvVariable(envVars: NodeJS.ProcessEnv): Environment {
if (!envVars.API_TOKEN) {
throw new Error("API_TOKEN not defined");
}
return {
API_TOKEN: envVars.API_TOKEN,
};
}

function toLocalIsoStringWithOffset(date: Date): string {
var timezoneOffsetInMilliseconds = date.getTimezoneOffset() * 60000;
var localISOTime = (new Date(date.getTime() - timezoneOffsetInMilliseconds)).toISOString().slice(0, -5);
let timezoneOffsetInHours = - (timezoneOffsetInMilliseconds / 3600000);
let localISOTimeWithOffset =
localISOTime +
(timezoneOffsetInHours >= 0 ? '+' : '-') +
(Math.abs(timezoneOffsetInHours) < 10 ? '0' : '') +
timezoneOffsetInHours + ':00';
return localISOTimeWithOffset;
}
const sanitisedEnvVars: Environment = sanitiseEnvVariable(process.env);

const yargsInstance = yargs(hideBin(process.argv));
Expand Down Expand Up @@ -67,7 +45,7 @@ const argv: CommandLineOptions = yargsInstance
})
.default('s', function firstDayOfPreviousMonth(): string {
let today = new Date();
return toLocalIsoStringWithOffset(new Date(new Date(today.getFullYear(), (today.getMonth() - 1), 1)));
return toLocaTzIsoStringWithOffset(new Date(new Date(today.getFullYear(), (today.getMonth() - 1), 1)));
}, 'the first day of the previous month')
.option('until', {
type: 'string',
Expand All @@ -77,7 +55,7 @@ const argv: CommandLineOptions = yargsInstance
})
.default('u', function lastDayOfPreviousMonth(): string {
let today = new Date();
return toLocalIsoStringWithOffset(new Date(
return toLocaTzIsoStringWithOffset(new Date(
new Date(
today.getFullYear(),
today.getMonth(),
Expand Down Expand Up @@ -147,6 +125,7 @@ function extractOnCallUsersFromFinalSchedule(finalSchedule: FinalSchedule): Reco
}

function calOohPay(cliOptions: CommandLineOptions) {
console.table(cliOptions);
const pagerDutyApi = api({ token: sanitisedEnvVars.API_TOKEN });
for (let rotaId of cliOptions.rotaIds.split(',')) {
pagerDutyApi
Expand Down Expand Up @@ -185,3 +164,18 @@ function calOohPay(cliOptions: CommandLineOptions) {
);
}
}

function sanitiseInputDates(since: string, until: string): [string, string] {
let sinceDate = new Date(since);
let untilDate = new Date(until);
// if sinceDate has a time component, set it to 00:00:00
sinceDate.setHours(0, 0, 0, 0);
//if untilDate has a time component, set it to 23:59:59
untilDate.setHours(23, 59, 59, 999);
if (sinceDate > untilDate) {
throw new Error("since date cannot be greater than until date");
}
let sinceString = toLocaTzIsoStringWithOffset(sinceDate);
let untilString = toLocaTzIsoStringWithOffset(untilDate);
return [sinceString, untilString];
}
33 changes: 33 additions & 0 deletions src/DateUtilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Converts a given date to a local ISO string with timezone offset.
*
* This function takes a `Date` object and returns a string in the ISO 8601 format
* with the local timezone offset included. The resulting string will be in the format
* `YYYY-MM-DDTHH:mm:ss±HH:00`.
*
* @param date - The `Date` object to be converted.
* @returns A string representing the local ISO time with timezone offset.
*/
export function toLocaTzIsoStringWithOffset(date: Date): string {
var timezoneOffsetInMilliseconds = date.getTimezoneOffset() * 60000;
var localISOTime = (new Date(date.getTime() - timezoneOffsetInMilliseconds)).toISOString().slice(0, -5);
let timezoneOffsetInHours = -(timezoneOffsetInMilliseconds / 3600000);
let localISOTimeWithOffset = localISOTime +
(timezoneOffsetInHours >= 0 ? '+' : '-') +
(Math.abs(timezoneOffsetInHours) < 10 ? '0' : '') +
timezoneOffsetInHours + ':00';
return localISOTimeWithOffset;
}

/**
* Converts a given date to a specified timezone.
*
* @param date - The date to be converted. Can be a Date object or a string representing a date.
* @param timeZoneId - The IANA timezone identifier (e.g., "America/New_York", "Europe/London").
* @returns A new Date object representing the same moment in time in the specified timezone.
*/
export function convertTimezone(date: Date, timeZoneId: string): Date {
return new Date(
(typeof date === "string" ? new Date(date) : date)
.toLocaleString("en-GB", {timeZone: timeZoneId}));
}
13 changes: 13 additions & 0 deletions src/EnvironmentController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

export interface Environment {
API_TOKEN: string;
}

export function sanitiseEnvVariable(envVars: NodeJS.ProcessEnv): Environment {
if (!envVars.API_TOKEN) {
throw new Error("API_TOKEN not defined");
}
return {
API_TOKEN: envVars.API_TOKEN,
};
}
26 changes: 11 additions & 15 deletions src/OnCallPeriod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export class OnCallPeriod {
readonly until: Date;

private _numberOfOohWeekDays: number = 0;
private _numberOfOohWeekendDays: number = 0;
private _numberOfOohWeekends: number = 0;

constructor(s: Date, u: Date) {
this.since = new Date(s);
Expand All @@ -19,7 +19,7 @@ export class OnCallPeriod {
if (OnCallPeriod.isWeekDay(curDate.getDay())) {
this._numberOfOohWeekDays++;
} else {
this._numberOfOohWeekendDays++;
this._numberOfOohWeekends++;
}
}
curDate.setDate(curDate.getDate() + 1);
Expand All @@ -30,10 +30,16 @@ export class OnCallPeriod {
return this._numberOfOohWeekDays;
}

public get numberOfOohWeekendDays(): number {
return this._numberOfOohWeekendDays;
public get numberOfOohWeekends(): number {
return this._numberOfOohWeekends;
}

/**
* Determines if the given day number corresponds to a weekday.
*
* @param dayNum - The number representing the day of the week (0 for Sunday, 1 for Monday, ..., 6 for Saturday).
* @returns `true` if the day number corresponds to a weekday (Monday to Thursday), otherwise `false`.
*/
private static isWeekDay(dayNum: number): boolean {
return dayNum > 0 && dayNum < 5;
}
Expand All @@ -46,10 +52,6 @@ export class OnCallPeriod {
* @returns true if the person was on call OOH, false otherwise
*/
private static wasPersonOnCallOOH(since: Date, until: Date): boolean {
/**
* if dateToCheck in the evening after 6pm and onCallUntilDate is at least 12 hours
* longer than dateToCheck, then the person was on call OOH
*/
return (OnCallPeriod.doesShiftSpanEveningTillNextDay(since, until) &&
OnCallPeriod.isShiftLongerThan6Hours(since, until));
}
Expand Down Expand Up @@ -78,15 +80,9 @@ export class OnCallPeriod {
return (since.getDate() !== until.getDate());
}

private static convertTZ(date: Date, timeZoneId: string): Date {
return new Date(
(typeof date === "string" ? new Date(date) : date)
.toLocaleString("en-GB", {timeZone: timeZoneId}));
}

toString() {
console.log("On call period from %s to %s", this.since, this.until);
console.log("Number of OOH Weekdays (Mon-Thu): %d", this.numberOfOOhWeekDays);
console.log("Number of OOH Weekends (Fri-Sun): %d", this.numberOfOohWeekendDays);
console.log("Number of OOH Weekends (Fri-Sun): %d", this.numberOfOohWeekends);
}
}
2 changes: 1 addition & 1 deletion src/OnCallUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ export class OnCallUser {
}

public getTotalOohWeekendDays(): number {
return this._onCallPeriods.reduce((acc, ocp) => acc + ocp.numberOfOohWeekendDays, 0);
return this._onCallPeriods.reduce((acc, ocp) => acc + ocp.numberOfOohWeekends, 0);
}
}
7 changes: 7 additions & 0 deletions src/PagerdutySchedule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { FinalSchedule } from "./FinalSchedule.js";

export interface PagerdutySchedule {
name: string;
html_url: string;
final_schedule: FinalSchedule;
}
25 changes: 25 additions & 0 deletions test/DateUtilities.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { toLocaTzIsoStringWithOffset } from '../src/DateUtilities';
import {describe, expect, test} from '@jest/globals';

describe('DateUtilities.toLocaTzIsoStringWithOffset', () => {
test('should convert UTC date to local ISO string with timezone offset', () => {
const date = new Date('2023-10-01T12:00:00Z');
const result = toLocaTzIsoStringWithOffset(date);
const expectedLocalISOTime = '2023-10-01T13:00:00+01:00';
expect(result).toBe(expectedLocalISOTime);
});

test('should handle dates with positive timezone offsets', () => {
const date = new Date('2023-10-01T12:00:00+02:00');
const result = toLocaTzIsoStringWithOffset(date);
const expectedLocalISOTime = '2023-10-01T11:00:00+01:00';
expect(result).toBe(expectedLocalISOTime);
});

test('should handle dates with negative timezone offsets', () => {
const date = new Date('2023-10-01T12:00:00-05:00');
const result = toLocaTzIsoStringWithOffset(date);
const expectedLocalISOTime = '2023-10-01T18:00:00+01:00';
expect(result).toBe(expectedLocalISOTime);
});
});
53 changes: 4 additions & 49 deletions test/OnCallPaymentCalculator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,6 @@ import { OnCallPaymentsCalculator } from "../src/OnCallPaymentsCalculator";
import { OnCallPeriod } from '../src/OnCallPeriod';
import { OnCallUser } from '../src/OnCallUser';

describe('should be able to initialise OnCallPeriod', () => {
test('- when OnCallPeriod is initialised', () => {
const onCallPeriod = new OnCallPeriod(new Date('2024-08-01T00:00:00+01:00'), new Date('2024-08-12T10:00:00+01:00'));
expect(onCallPeriod.since).toStrictEqual(new Date('2024-08-01T00:00:00+01:00'));
expect(onCallPeriod.until).toStrictEqual(new Date('2024-08-12T10:00:00+01:00'));
});

test('- same day, 2 hours in the evening on call period', () => {
const onCallPeriod = new OnCallPeriod(
new Date('2024-09-20T16:30:00+01:00'),
new Date('2024-09-20T18:30:00+01:00'),
);
expect(onCallPeriod.since).toStrictEqual(new Date('2024-09-20T16:30:00+01:00'));
expect(onCallPeriod.until).toStrictEqual(new Date('2024-09-20T18:30:00+01:00'));
expect(onCallPeriod.numberOfOOhWeekDays).toBe(0);
expect(onCallPeriod.numberOfOohWeekendDays).toBe(0);
});

test('- from Friday 8pm to Monday morning, numberOfOohWeekendDays must be 3', () => {
const since = new Date('2024-09-20T20:00:00+01:00');
const until = new Date('2024-09-23T10:00:00+01:00');
const onCallPeriod = new OnCallPeriod(
since,
until,
);
expect(onCallPeriod.since).toStrictEqual(since);
expect(onCallPeriod.until).toStrictEqual(until);
expect(onCallPeriod.numberOfOOhWeekDays).toBe(0);
expect(onCallPeriod.numberOfOohWeekendDays).toBe(3);
});

test('- from on-call from 28th of Month 10am to 2nd of next month', () => {
const since = new Date('2024-08-28T10:00:00+01:00');
const until = new Date('2024-09-02T10:00:00+01:00');
const onCallPeriod = new OnCallPeriod(
since,
until,
);
expect(onCallPeriod.since).toStrictEqual(since);
expect(onCallPeriod.until).toStrictEqual(until);
expect(onCallPeriod.numberOfOOhWeekDays).toBe(2);
expect(onCallPeriod.numberOfOohWeekendDays).toBe(3);
});
})

describe('should calculate the payment for an on call user', () => {

test('- when person continues to be on-call from end of Month to 12th of subsequent month', () => {
Expand All @@ -68,7 +23,7 @@ describe('should calculate the payment for an on call user', () => {
expect(onCallUser.onCallPeriods[0].since).toEqual(since);
expect(onCallUser.onCallPeriods[0].until).toEqual(until);
expect(onCallUser.onCallPeriods[0].numberOfOOhWeekDays).toBe(5);
expect(onCallUser.onCallPeriods[0].numberOfOohWeekendDays).toBe(6);
expect(onCallUser.onCallPeriods[0].numberOfOohWeekends).toBe(6);
expect(calculator.calculateOnCallPayment(onCallUser)).toBe(700);
});

Expand All @@ -89,7 +44,7 @@ describe('should calculate the payment for an on call user', () => {
expect(onCallUser.onCallPeriods[0].since).toEqual(since);
expect(onCallUser.onCallPeriods[0].until).toEqual(until);
expect(onCallUser.onCallPeriods[0].numberOfOOhWeekDays).toBe(5);
expect(onCallUser.onCallPeriods[0].numberOfOohWeekendDays).toBe(6);
expect(onCallUser.onCallPeriods[0].numberOfOohWeekends).toBe(6);
expect(calculator.calculateOnCallPayment(onCallUser)).toBe(700);
});

Expand All @@ -110,7 +65,7 @@ describe('should calculate the payment for an on call user', () => {
expect(onCallUser.onCallPeriods[0].since).toEqual(since);
expect(onCallUser.onCallPeriods[0].until).toEqual(until);
expect(onCallUser.onCallPeriods[0].numberOfOOhWeekDays).toBe(2);
expect(onCallUser.onCallPeriods[0].numberOfOohWeekendDays).toBe(1);
expect(onCallUser.onCallPeriods[0].numberOfOohWeekends).toBe(1);
expect(calculator.calculateOnCallPayment(onCallUser)).toBe(175);
});

Expand All @@ -131,7 +86,7 @@ describe('should calculate the payment for an on call user', () => {
expect(onCallUser.onCallPeriods[0].since).toEqual(since);
expect(onCallUser.onCallPeriods[0].until).toEqual(until);
expect(onCallUser.onCallPeriods[0].numberOfOOhWeekDays).toBe(2);
expect(onCallUser.onCallPeriods[0].numberOfOohWeekendDays).toBe(3);
expect(onCallUser.onCallPeriods[0].numberOfOohWeekends).toBe(3);
expect(calculator.calculateOnCallPayment(onCallUser)).toBe(325);
});

Expand Down
47 changes: 47 additions & 0 deletions test/OnCallPeriod.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {describe, expect, test} from '@jest/globals';
import { OnCallPeriod } from '../src/OnCallPeriod';

describe('should initialise OnCallPeriod with the right number of weekdays and weekends', () => {
test('- when the shift starts 1st of month and is until the 12th of the month', () => {
const onCallPeriod = new OnCallPeriod(new Date('2024-08-01T00:00:00+01:00'), new Date('2024-08-12T10:00:00+01:00'));
expect(onCallPeriod.since).toStrictEqual(new Date('2024-08-01T00:00:00+01:00'));
expect(onCallPeriod.until).toStrictEqual(new Date('2024-08-12T10:00:00+01:00'));
});

test('- when the shift starts and ends on the same day, just 2 hours in the evening', () => {
const onCallPeriod = new OnCallPeriod(
new Date('2024-09-20T16:30:00+01:00'),
new Date('2024-09-20T18:30:00+01:00'),
);
expect(onCallPeriod.since).toStrictEqual(new Date('2024-09-20T16:30:00+01:00'));
expect(onCallPeriod.until).toStrictEqual(new Date('2024-09-20T18:30:00+01:00'));
expect(onCallPeriod.numberOfOOhWeekDays).toBe(0);
expect(onCallPeriod.numberOfOohWeekends).toBe(0);
});

test('- when the shift starts on Friday 8pm and extends until Monday morning, numberOfOohWeekends must be 3', () => {
const since = new Date('2024-09-20T20:00:00+01:00');
const until = new Date('2024-09-23T10:00:00+01:00');
const onCallPeriod = new OnCallPeriod(
since,
until,
);
expect(onCallPeriod.since).toStrictEqual(since);
expect(onCallPeriod.until).toStrictEqual(until);
expect(onCallPeriod.numberOfOOhWeekDays).toBe(0);
expect(onCallPeriod.numberOfOohWeekends).toBe(3);
});

test('- when the shift starts on 2024-08-28T10:00:00+01:00 and extends until 2024-09-02T10:00:00+01:00', () => {
const since = new Date('2024-08-28T10:00:00+01:00');
const until = new Date('2024-09-02T10:00:00+01:00');
const onCallPeriod = new OnCallPeriod(
since,
until,
);
expect(onCallPeriod.since).toStrictEqual(since);
expect(onCallPeriod.until).toStrictEqual(until);
expect(onCallPeriod.numberOfOOhWeekDays).toBe(2);
expect(onCallPeriod.numberOfOohWeekends).toBe(3);
});
})

0 comments on commit 7cf6ae5

Please sign in to comment.