diff --git a/README.md b/README.md index cf01123..1434dd9 100644 --- a/README.md +++ b/README.md @@ -320,6 +320,7 @@ Below is a table outlining the various configuration parameters available for ** | `CORE_HOURS_START` | Start of core hours. Excludes non-working hours from the calculations of time-related metrics. By default, a full day is counted. Time should be entered in the format **HH:mm**. The timezone corresponds to that specified in the `TIMEZONE` input (default is UTC). For correct operation, `CORE_HOURS_END` must also be specified and must be later than `CORE_HOURS_START`. Example: `10:00` | - | | `CORE_HOURS_END` | End of core hours. Excludes non-working hours from the calculations of time-related metrics. By default, a full day is counted. Time should be entered in the format **HH:mm**. The timezone corresponds to that specified in the `TIMEZONE` input (default is UTC). For correct operation, `CORE_HOURS_END` must also be specified and must be later than `CORE_HOURS_START`. Example: `19:00` | - | | `HOLIDAYS` | Dates to be excluded from the calculations of time-related metrics. Saturday and Sunday are already excluded by default. Dates should be entered in the format **d/MM/yyyy**, separated by commas. Example: `01/01/2024, 08/03/2024` | - | +| `WEEKENDS` | Specifies the days of the week considered as weekends. Values are represented as numbers, where 0 corresponds to Sunday | `0,6` | | `TIMEZONE` | Timezone that will be used in action. Examples: `Europe/Berlin` or `America/New_York`. See the full list of time zones [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) | `UTC` | | `PERCENTILE` | Percentile value for timeline. This parameter is mandatory if `percentile` is specified in the `SHOW_STATS_TYPES` input. | `75` | | `ISSUE_TITLE` | Title for the created/updated issue with report | `Pull requests report(d/MM/yyyy HH:mm)` | diff --git a/action.yml b/action.yml index 60e0912..773eebd 100644 --- a/action.yml +++ b/action.yml @@ -43,6 +43,10 @@ inputs: CORE_HOURS_END: description: "End time of core hours(HH:mm)" required: false + WEEKENDS: + description: "Specifies the days of the week considered as weekends. Values are represented as numbers, where 0 corresponds to Sunday" + required: false + default: "0,6" HOLIDAYS: description: "Holidays separated by comma(d/MM/yyyy)" required: false diff --git a/build/index.js b/build/index.js index 041757a..e013255 100644 --- a/build/index.js +++ b/build/index.js @@ -606,6 +606,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.calcNonWorkingHours = void 0; const date_fns_1 = __nccwpck_require__(73314); const isHoliday_1 = __nccwpck_require__(33345); +const checkWeekend_1 = __nccwpck_require__(994); const calcNonWorkingHours = (firstDate, secondDate, { startOfWorkingTime, endOfWorkingTime }, holidays) => { const daysOfInterval = (0, date_fns_1.eachDayOfInterval)({ start: firstDate, @@ -619,7 +620,7 @@ const calcNonWorkingHours = (firstDate, secondDate, { startOfWorkingTime, endOfW const startOfWorkingHours = (0, date_fns_1.setMinutes)((0, date_fns_1.setHours)(day, startHours), startMinutes); const endOfWorkingHours = (0, date_fns_1.setMinutes)((0, date_fns_1.setHours)(day, endHours), endMinutes); const endOfDay = (0, date_fns_1.setMinutes)((0, date_fns_1.setHours)(day, 23), 59); - if ((0, date_fns_1.isWeekend)(day) || (0, isHoliday_1.isHoliday)(day, holidays)) + if ((0, checkWeekend_1.checkWeekend)(day) || (0, isHoliday_1.isHoliday)(day, holidays)) return acc; if (arr.length === 1) { if ((0, date_fns_1.isBefore)(secondDate, startOfWorkingHours) || @@ -711,24 +712,22 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.calcWeekendMinutes = void 0; const date_fns_1 = __nccwpck_require__(73314); const isHoliday_1 = __nccwpck_require__(33345); +const checkWeekend_1 = __nccwpck_require__(994); const calcWeekendMinutes = (firstDate, secondDate, holidays) => { - const weekendsOfInterval = (0, date_fns_1.eachWeekendOfInterval)({ - start: firstDate, - end: secondDate, - }); const days = (0, date_fns_1.eachDayOfInterval)({ start: firstDate, end: secondDate, }); + const weekendsOfInterval = days.filter((day) => (0, checkWeekend_1.checkWeekend)(day)); const minutesOnHolidays = days.reduce((acc, day) => { - if ((0, isHoliday_1.isHoliday)(day, holidays) && !(0, date_fns_1.isWeekend)(day)) + if ((0, isHoliday_1.isHoliday)(day, holidays) && !(0, checkWeekend_1.checkWeekend)(day)) return acc + 24 * 60; return acc; }, 0); let minutesInWeekend = 24 * 60 * weekendsOfInterval.length + (minutesOnHolidays || 0); - const isStartedAtWeekend = (0, date_fns_1.isWeekend)(firstDate); + const isStartedAtWeekend = (0, checkWeekend_1.checkWeekend)(firstDate); const isStartedOnHoliday = (0, isHoliday_1.isHoliday)(firstDate, holidays); - const isEndedAtWeekend = (0, date_fns_1.isWeekend)(secondDate); + const isEndedAtWeekend = (0, checkWeekend_1.checkWeekend)(secondDate); const isEndedOnHoliday = (0, isHoliday_1.isHoliday)(secondDate, holidays); if (isStartedAtWeekend) { minutesInWeekend -= (0, date_fns_1.differenceInMinutes)(firstDate, weekendsOfInterval[0]); @@ -749,6 +748,25 @@ const calcWeekendMinutes = (firstDate, secondDate, holidays) => { exports.calcWeekendMinutes = calcWeekendMinutes; +/***/ }), + +/***/ 994: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.checkWeekend = void 0; +const date_fns_1 = __nccwpck_require__(73314); +const utils_1 = __nccwpck_require__(41002); +const checkWeekend = (date) => { + const currentDay = (0, date_fns_1.getDay)(date); + const weekends = (0, utils_1.getMultipleValuesInput)('WEEKENDS').map((el) => parseInt(el)); + return weekends.includes(currentDay); +}; +exports.checkWeekend = checkWeekend; + + /***/ }), /***/ 51666: @@ -2843,6 +2861,7 @@ ${[ "TIMEZONE", "CORE_HOURS_START", "CORE_HOURS_END", + "WEEKENDS", "HOLIDAYS", "REPORT_PERIOD", "REPORT_DATE_START", diff --git a/package.json b/package.json index e370b04..55d22db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pull-request-analytics-action", - "version": "4.5.0", + "version": "4.6.0", "description": "Generates detailed PR analytics reports within GitHub, focusing on review efficiency and team performance.", "main": "build/index.js", "scripts": { @@ -8,8 +8,8 @@ "build": "tsc && ncc build ./dist/index.js -o build", "tsc": "tsc", "lint": "eslint ./src/**/*.ts", - "test": "TZ=utc jest", - "test:watch": "TZ=utc jest --watch", + "test": "TZ=utc WEEKENDS=0,6 jest", + "test:watch": "TZ=utc WEEKENDS=0,6 jest --watch", "prepare": "husky install" }, "author": "Aleksei Simatov", diff --git a/src/converters/utils/calculations/calcAverageValue.spec.ts b/src/converters/utils/calculations/calcAverageValue.spec.ts new file mode 100644 index 0000000..acede51 --- /dev/null +++ b/src/converters/utils/calculations/calcAverageValue.spec.ts @@ -0,0 +1,33 @@ +import { calcAverageValue } from './calcAverageValue'; + +describe('calcAverageValue', () => { + it('should return 0 if no values are provided', () => { + expect(calcAverageValue()).toBe(0); + }); + + it('should return 0 if an empty array is provided', () => { + expect(calcAverageValue([])).toBe(0); + }); + + it('should return the average value of the provided numbers', () => { + expect(calcAverageValue([1, 2, 3, 4, 5])).toBe(3); + expect(calcAverageValue([10, 20, 30])).toBe(20); + expect(calcAverageValue([1, 1, 1, 1, 1])).toBe(1); + }); + + it('should return the ceiling of the average value', () => { + expect(calcAverageValue([1, 2, 3])).toBe(2); + expect(calcAverageValue([1, 2, 2])).toBe(2); + expect(calcAverageValue([1, 1, 2])).toBe(2); + }); + + it('should handle negative numbers correctly', () => { + expect(calcAverageValue([-1, -2, -3, -4, -5])).toBe(-3); + expect(calcAverageValue([-10, -20, -30])).toBe(-20); + }); + + it('should handle a mix of positive and negative numbers', () => { + expect(calcAverageValue([-1, 1, -1, 1])).toBe(0); + expect(calcAverageValue([-10, 10, -10, 10])).toBe(0); + }); +}); \ No newline at end of file diff --git a/src/converters/utils/calculations/calcDraftTime.spec.ts b/src/converters/utils/calculations/calcDraftTime.spec.ts new file mode 100644 index 0000000..b477c34 --- /dev/null +++ b/src/converters/utils/calculations/calcDraftTime.spec.ts @@ -0,0 +1,101 @@ +import { calcDraftTime } from "./calcDraftTime"; +import { + convertToDraftTimelineEvent, + readyForReviewTimelineEvent, +} from "../../constants"; + +describe("calcDraftTime", () => { + it("should return an empty array if statuses is empty", () => { + const result = calcDraftTime( + "2021-01-01T00:00:00Z", + "2021-01-02T00:00:00Z", + [] + ); + expect(result).toEqual([]); + }); + + it("should calculate draft time correctly when there is only one readyForReviewTimelineEvent", () => { + const statuses = [ + { + event: readyForReviewTimelineEvent, + created_at: "2021-01-01T12:00:00Z", + }, + ]; + const result = calcDraftTime( + "2021-01-01T00:00:00Z", + "2021-01-02T00:00:00Z", + statuses + ); + expect(result).toEqual([["2021-01-01T00:00:00Z", "2021-01-01T12:00:00Z"]]); + }); + + it("should calculate draft time correctly when there are multiple events", () => { + const statuses = [ + { + event: convertToDraftTimelineEvent, + created_at: "2021-01-01T12:00:00Z", + }, + { + event: readyForReviewTimelineEvent, + created_at: "2021-01-01T14:00:00Z", + }, + { + event: convertToDraftTimelineEvent, + created_at: "2021-01-01T16:00:00Z", + }, + { + event: readyForReviewTimelineEvent, + created_at: "2021-01-01T18:00:00Z", + }, + ]; + const result = calcDraftTime( + "2021-01-01T00:00:00Z", + "2021-01-02T00:00:00Z", + statuses + ); + expect(result).toEqual([ + ["2021-01-01T12:00:00Z", "2021-01-01T14:00:00Z"], + ["2021-01-01T16:00:00Z", "2021-01-01T18:00:00Z"], + ]); + }); + + it("should include the closedAt time if the last event is convertToDraftTimelineEvent", () => { + const statuses = [ + { + event: readyForReviewTimelineEvent, + created_at: "2021-01-01T12:00:00Z", + }, + { + event: convertToDraftTimelineEvent, + created_at: "2021-01-01T14:00:00Z", + }, + ]; + const result = calcDraftTime( + "2021-01-01T00:00:00Z", + "2021-01-02T00:00:00Z", + statuses + ); + expect(result).toEqual([ + ["2021-01-01T00:00:00Z", "2021-01-01T12:00:00Z"], + ["2021-01-01T14:00:00Z", "2021-01-02T00:00:00Z"], + ]); + }); + + it("should handle undefined createdAt and closedAt correctly", () => { + const statuses = [ + { + event: readyForReviewTimelineEvent, + created_at: "2021-01-01T12:00:00Z", + }, + { + event: convertToDraftTimelineEvent, + created_at: "2021-01-01T14:00:00Z", + }, + ]; + const result = calcDraftTime(undefined, undefined, statuses); + expect(result).toEqual([ + [undefined, "2021-01-01T12:00:00Z"], + ["2021-01-01T14:00:00Z", undefined], + ]); + }); +}); diff --git a/src/converters/utils/calculations/calcNonWorkingHours.ts b/src/converters/utils/calculations/calcNonWorkingHours.ts index 7522687..8655e51 100644 --- a/src/converters/utils/calculations/calcNonWorkingHours.ts +++ b/src/converters/utils/calculations/calcNonWorkingHours.ts @@ -2,11 +2,11 @@ import { differenceInMinutes, eachDayOfInterval, isBefore, - isWeekend, setHours, setMinutes, } from "date-fns"; import { isHoliday } from "./isHoliday"; +import { checkWeekend } from "./checkWeekend"; export type CoreHours = { startOfWorkingTime: string; @@ -36,7 +36,7 @@ export const calcNonWorkingHours = ( const endOfWorkingHours = setMinutes(setHours(day, endHours), endMinutes); const endOfDay = setMinutes(setHours(day, 23), 59); - if (isWeekend(day) || isHoliday(day, holidays)) return acc; + if (checkWeekend(day) || isHoliday(day, holidays)) return acc; if (arr.length === 1) { if ( isBefore(secondDate, startOfWorkingHours) || diff --git a/src/converters/utils/calculations/calcWeekendMinutes.ts b/src/converters/utils/calculations/calcWeekendMinutes.ts index bac79f1..bd62425 100644 --- a/src/converters/utils/calculations/calcWeekendMinutes.ts +++ b/src/converters/utils/calculations/calcWeekendMinutes.ts @@ -1,39 +1,36 @@ import { differenceInMinutes, eachDayOfInterval, - eachWeekendOfInterval, - isWeekend, setHours, setMinutes, } from "date-fns"; import { isHoliday } from "./isHoliday"; +import { checkWeekend } from "./checkWeekend"; export const calcWeekendMinutes = ( firstDate: Date, secondDate: Date, holidays?: string[] ) => { - const weekendsOfInterval = eachWeekendOfInterval({ - start: firstDate, - end: secondDate, - }); - const days = eachDayOfInterval({ start: firstDate, end: secondDate, }); + + const weekendsOfInterval = days.filter((day) => checkWeekend(day)); + const minutesOnHolidays = days.reduce((acc, day) => { - if (isHoliday(day, holidays) && !isWeekend(day)) return acc + 24 * 60; + if (isHoliday(day, holidays) && !checkWeekend(day)) return acc + 24 * 60; return acc; }, 0); let minutesInWeekend = 24 * 60 * weekendsOfInterval.length + (minutesOnHolidays || 0); - const isStartedAtWeekend = isWeekend(firstDate); + const isStartedAtWeekend = checkWeekend(firstDate); const isStartedOnHoliday = isHoliday(firstDate, holidays); - const isEndedAtWeekend = isWeekend(secondDate); + const isEndedAtWeekend = checkWeekend(secondDate); const isEndedOnHoliday = isHoliday(secondDate, holidays); diff --git a/src/converters/utils/calculations/checkUserInclusive.ts b/src/converters/utils/calculations/checkUserInclusive.ts new file mode 100644 index 0000000..972fab1 --- /dev/null +++ b/src/converters/utils/calculations/checkUserInclusive.ts @@ -0,0 +1,4 @@ +import { getMultipleValuesInput } from "../../../common/utils"; +export const checkUserInclusive = (name: string) => { + return !getMultipleValuesInput('EXCLUDE_USERS').includes(name) ; +}; \ No newline at end of file diff --git a/src/converters/utils/calculations/checkWeekend.spec.ts b/src/converters/utils/calculations/checkWeekend.spec.ts new file mode 100644 index 0000000..05e1652 --- /dev/null +++ b/src/converters/utils/calculations/checkWeekend.spec.ts @@ -0,0 +1,52 @@ +import { checkWeekend } from "./checkWeekend"; +import { getMultipleValuesInput } from "../../../common/utils"; + +jest.mock("../../../common/utils", () => ({ + getMultipleValuesInput: jest.fn(), +})); + +describe("checkWeekend", () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it("should return true if the date is a weekend", () => { + (getMultipleValuesInput as jest.Mock).mockReturnValue(["0", "6"]); // Sunday and Saturday + + const date = new Date("2021-01-02T00:00:00Z"); // Saturday + const result = checkWeekend(date); + expect(result).toBe(true); + }); + + it("should return false if the date is not a weekend", () => { + (getMultipleValuesInput as jest.Mock).mockReturnValue(["0", "6"]); // Sunday and Saturday + + const date = new Date("2021-01-01T00:00:00Z"); // Friday + const result = checkWeekend(date); + expect(result).toBe(false); + }); + + it("should handle numeric input for date", () => { + (getMultipleValuesInput as jest.Mock).mockReturnValue(["0", "6"]); // Sunday and Saturday + + const date = new Date("2021-01-02T00:00:00Z").getTime(); // Saturday + const result = checkWeekend(date); + expect(result).toBe(true); + }); + + it("should return false if WEEKENDS environment variable is not set", () => { + (getMultipleValuesInput as jest.Mock).mockReturnValue([]); + + const date = new Date("2021-01-02T00:00:00Z"); // Saturday + const result = checkWeekend(date); + expect(result).toBe(false); + }); + + it("should return true for custom weekend days", () => { + (getMultipleValuesInput as jest.Mock).mockReturnValue(["1", "2"]); // Monday and Tuesday + + const date = new Date("2021-01-04T00:00:00Z"); // Monday + const result = checkWeekend(date); + expect(result).toBe(true); + }); +}); \ No newline at end of file diff --git a/src/converters/utils/calculations/checkWeekend.ts b/src/converters/utils/calculations/checkWeekend.ts new file mode 100644 index 0000000..ead39a1 --- /dev/null +++ b/src/converters/utils/calculations/checkWeekend.ts @@ -0,0 +1,8 @@ +import { getDay } from "date-fns"; +import { getMultipleValuesInput } from "../../../common/utils"; + +export const checkWeekend = (date: Date | number) => { + const currentDay = getDay(date); + const weekends = getMultipleValuesInput('WEEKENDS').map((el) => parseInt(el)); + return weekends.includes(currentDay); +}; diff --git a/src/view/utils/createConfigParamsCode.ts b/src/view/utils/createConfigParamsCode.ts index 0763f09..ca9746c 100644 --- a/src/view/utils/createConfigParamsCode.ts +++ b/src/view/utils/createConfigParamsCode.ts @@ -25,6 +25,7 @@ ${[ "TIMEZONE", "CORE_HOURS_START", "CORE_HOURS_END", + "WEEKENDS", "HOLIDAYS", "REPORT_PERIOD", "REPORT_DATE_START",