Skip to content

Commit ac515bd

Browse files
committed
Add option to count business hours only
1 parent db4dd22 commit ac515bd

File tree

3 files changed

+99
-27
lines changed

3 files changed

+99
-27
lines changed

src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ class PullRequestData {
66
differenceInSeconds: number;
77

88
constructor(private createdAt: moment.Moment, private closedAt: moment.Moment, private updatedAt: moment.Moment, private mergedAt: moment.Moment) {
9-
this.differenceInSeconds = durationInSecondsExcludingWeekends(createdAt, mergedAt);
9+
this.differenceInSeconds = durationInSecondsExcludingWeekends(createdAt, mergedAt, true);
1010
}
1111

1212
static from(createdAt: string, closedAt: string, updatedAt: string, mergedAt: string): PullRequestData {
@@ -40,4 +40,4 @@ if (!process.env.PULL_REQUESTS_DATABASE_PATH) {
4040
})
4141
})().catch(e => {
4242
console.error(e);
43-
});
43+
});

src/weekend.test.ts

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import * as moment from "moment";
2-
import {durationInSecondsExcludingWeekends} from "./weekend";
2+
import { durationInSecondsExcludingWeekends } from "./weekend";
33

4-
describe('Weekend exclusion', () => {
5-
it('handles dates on the same weekday', () => {
4+
describe("Weekend exclusion", () => {
5+
it("handles dates on the same weekday", () => {
66
const beginning = moment("2019-04-03T08:34:50Z");
77
const end = moment("2019-04-03T13:34:50Z");
88

@@ -11,7 +11,7 @@ describe('Weekend exclusion', () => {
1111
expect(duration).toEqual(18000);
1212
});
1313

14-
it('handles dates without weekends', () => {
14+
it("handles dates without weekends", () => {
1515
const beginning = moment("2019-04-03T08:34:50Z");
1616
const end = moment("2019-04-04T13:34:50Z");
1717

@@ -20,7 +20,7 @@ describe('Weekend exclusion', () => {
2020
expect(duration).toEqual(104400);
2121
});
2222

23-
it('handles whole weekends', () => {
23+
it("handles whole weekends", () => {
2424
const beginning = moment.utc("2019-04-06T00:00:00Z");
2525
const end = moment.utc("2019-04-08T00:00:00Z");
2626

@@ -29,7 +29,7 @@ describe('Weekend exclusion', () => {
2929
expect(duration).toEqual(0);
3030
});
3131

32-
it('handles partial weekends', () => {
32+
it("handles partial weekends", () => {
3333
const beginning = moment.utc("2019-04-06T08:00:00Z");
3434
const end = moment.utc("2019-04-07T08:00:00Z");
3535

@@ -38,7 +38,7 @@ describe('Weekend exclusion', () => {
3838
expect(duration).toEqual(0);
3939
});
4040

41-
it('does not exclude weekends from the calculation if the provided range does not contain any weekend days', () => {
41+
it("does not exclude weekends from the calculation if the provided range does not contain any weekend days", () => {
4242
const beginning = moment("2019-04-03T08:34:50Z");
4343
const end = moment("2019-04-05T13:34:51Z");
4444

@@ -47,24 +47,54 @@ describe('Weekend exclusion', () => {
4747
expect(duration).toEqual(190801);
4848
});
4949

50-
it('excludes weekends if the interval contains one or more whole weekends', () => {
50+
it("excludes weekends if the interval contains one or more whole weekends", () => {
5151
const beginning = moment("2019-04-03T08:34:50Z");
5252
const end = moment("2019-04-18T13:34:51Z");
5353

5454
const duration = durationInSecondsExcludingWeekends(beginning, end);
5555

56-
5756
// Without exclusion: 1314001 seconds
5857
// Excluded: two weekends, 96 hours, 172800 seconds
5958
expect(duration).toEqual(1314001 - 2 * 48 * 60 * 60);
6059
});
6160

62-
it('excludes weekends if the interval contains one or more whole weekends and a partial weekend', () => {
61+
it("excludes weekends if the interval contains one or more whole weekends and a partial weekend", () => {
6362
const beginning = moment("2019-04-03T08:34:50Z");
6463
const end = moment("2019-04-21T13:34:51Z");
6564

6665
const duration = durationInSecondsExcludingWeekends(beginning, end);
6766

68-
expect(duration).toEqual(1573201 - (2 * 48 * 60 * 60) - (24 * 60 * 60) - (13 * 60 * 60 + 34 * 60 + 51));
69-
})
70-
});
67+
expect(duration).toEqual(
68+
1573201 - 2 * 48 * 60 * 60 - 24 * 60 * 60 - (13 * 60 * 60 + 34 * 60 + 51)
69+
);
70+
});
71+
72+
it("accounts for business hours (09:00 - 17:00) only", () => {
73+
const usesCases = [
74+
{
75+
// Same day
76+
start: moment.utc("2019-04-01T01:00:00Z"),
77+
end: moment.utc("2019-04-01T23:00:00Z"),
78+
duration: 8 * 60 * 60
79+
},
80+
{
81+
// Spans over multiple days
82+
start: moment.utc("2019-04-01T01:00:00Z"),
83+
end: moment.utc("2019-04-02T23:00:00Z"),
84+
duration: 16 * 60 * 60
85+
},
86+
{
87+
// Spans over multiple weeks
88+
start: moment.utc("2019-04-01T01:00:00Z"),
89+
end: moment.utc("2019-04-30T23:00:00Z"),
90+
duration: 22 * 8 * 60 * 60
91+
}
92+
];
93+
94+
usesCases.forEach(usesCase => {
95+
expect(
96+
durationInSecondsExcludingWeekends(usesCase.start, usesCase.end, true)
97+
).toEqual(usesCase.duration);
98+
});
99+
});
100+
});

src/weekend.ts

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,74 @@
11
import * as moment from "moment";
22

3-
export function durationInSecondsExcludingWeekends(beginning: moment.Moment, end: moment.Moment): number {
4-
if (beginning.format("YYYY-MM-DD") == end.format("YYYY-MM-DD")) {
3+
export function durationInSecondsExcludingWeekends(
4+
beginning: moment.Moment,
5+
end: moment.Moment,
6+
countBusinessHoursOnly?: boolean
7+
): number {
8+
const dayStart = moment(beginning);
9+
dayStart.set({ hour: 9, minute: 0, second: 0, millisecond: 0 });
10+
11+
const dayEnd = moment(end);
12+
dayEnd.set({ hour: 17, minute: 0, second: 0, millisecond: 0 });
13+
14+
const isSameDay = beginning.format("YYYY-MM-DD") == end.format("YYYY-MM-DD");
15+
if (isSameDay) {
516
if (beginning.isoWeekday() == 6 || beginning.isoWeekday() == 7) {
617
return 0;
718
}
19+
20+
if (countBusinessHoursOnly) {
21+
if (0 < dayStart.diff(beginning)) {
22+
beginning.set({ hour: 9, minute: 0, second: 0, millisecond: 0 });
23+
}
24+
25+
if (0 < end.diff(dayEnd)) {
26+
end.set({ hour: 17, minute: 0, second: 0, millisecond: 0 });
27+
}
28+
}
29+
830
return moment.duration(moment(end).diff(moment(beginning))).asSeconds();
931
}
1032

11-
const dayBeforeEnd = moment(end).subtract(1, 'days');
33+
const dayBeforeEnd = moment(end).subtract(1, "days");
1234

1335
let duration = 0;
14-
// This is slow and needs to be refactored to some clever calculation. Deemed good enough for the time being.
15-
for (const actualDay = moment(beginning).add(1, 'days'); actualDay.isBefore(dayBeforeEnd); actualDay.add(1, 'days')) {
36+
// This is slow and needs to be refactored to some clever calculation.
37+
// Deemed good enough for the time being.
38+
for (
39+
const actualDay = moment(beginning).add(1, "days");
40+
actualDay.isBefore(dayBeforeEnd);
41+
actualDay.add(1, "days")
42+
) {
1643
if (actualDay.isoWeekday() != 6 && actualDay.isoWeekday() != 7) {
17-
duration += 24 * 60 * 60;
44+
if (countBusinessHoursOnly) {
45+
duration += 8 * 60 * 60;
46+
} else {
47+
duration += 24 * 60 * 60;
48+
}
1849
}
1950
}
2051

2152
if (beginning.isoWeekday() != 6 && beginning.isoWeekday() != 7) {
22-
let endOfFirstDay = moment.utc(beginning).endOf('day');
23-
duration += Math.round(moment.duration(endOfFirstDay.diff(moment(beginning))).asSeconds());
53+
const endOfFirstDay = countBusinessHoursOnly
54+
? dayStart.set({ hour: 17, minute: 0, second: 0, millisecond: 0 })
55+
: moment.utc(beginning).endOf("day");
56+
57+
duration += Math.round(
58+
moment.duration(endOfFirstDay.diff(moment(beginning))).asSeconds()
59+
);
2460
}
2561

26-
if (end.isoWeekday() != 6 && end.isoWeekday() != 7) {
27-
let beginningOfLastDay = moment.utc(end).startOf('day');
28-
duration += Math.round(moment.duration(moment(end).diff(beginningOfLastDay)).asSeconds())
62+
if (
63+
!countBusinessHoursOnly &&
64+
end.isoWeekday() != 6 &&
65+
end.isoWeekday() != 7
66+
) {
67+
const beginningOfLastDay = moment.utc(end).startOf("day");
68+
duration += Math.round(
69+
moment.duration(moment(end).diff(beginningOfLastDay)).asSeconds()
70+
);
2971
}
3072

3173
return duration;
32-
}
74+
}

0 commit comments

Comments
 (0)