Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Blocked Days Calculation - Output Date Format #59

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,22 @@ The Criteria Section of the config file is simply named "Criteria" (without the
- Filters: a list of the names of the filters you want to apply
- Start Date: a date filter in the format YYYY-MM-DD which will exclude issues with resolved dates before the provided date (optional)
- End Date: a date filter in the format YYYY-MM-DD which will include issues with resolved dates before the provided date (optional)
- BlockedAttributes: This optional feature will add and value the ‘Days Blocked’ column to the CSV extract with the number of days blocked for each issue.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not adding a "Days Blocked" column to my output.csv file, is it only going to add the column if there is data returned, or should it at least return 0 for every issue (with or without blocked data)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Days Blocked column will only be added if there is a field listed in the BlockedAttributes section of the Criteria. If it is not configured, then it will not put the column in the output. Also, I believe that it will not put anything in the csv for blocked days if an issue has not been "blocked" Of course, the definition of blocked is really that configurable field (i.e. Impediment). I'm totally okay will defaulting to 0 if no blocked data is present for an issue. Is that perferred? Let me know and I'll work on changing it.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be great if it would default to 0 if there is no blocked data rather than just having a null field. This could also signify that the addition of the "BlockedAttributes" option was successful.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I'll look into make the change and defaulting.

- The number of days blocked is calculated by using the workflow function to block and unblock an issue.
Days Blocked is calculated down to the tenth of a day. For example, the Days Blocked might calculate to 15.4.
- Number of blocked days = the total of all blocked days across all status values included in the filter.
- Multiple block and unblocks are allowed.
- When enabled, the block values are used in the ScatterPlot, Cycle Time Histogram, and Flow Efficiency.
o Cycle Time:
 issues are displayed as a red dot instead of a black dot.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not proper markdown and does not render correctly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll work on fixing it and providing the correct markdown.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, thanks! Not a super big deal, but thought I'd call it out.

 Hover over the issue displays the issue detail, including number of blocked days
 Option has been added to Include/Exclude Blocked days
o Cycle Time Histogram
 Option has been added to Include/Exclude Blocked days
o Flow Efficiency
 Calculation: FE = (Total Cycle time – Blocked Days – Queued Days enabled in Queuing Stages) / Total Cycle Time
 Option has been added to Include/Exclude Blocked days in the calculation
- OutputDateFormat: an optional way to output a particular date format. This date format can be specified using normal date conventions (i.e., YYYY-MM-DD, MM/DD/YYYY, Mon-DD-YYYY, etc.). If this is not specified, then the standard date output is used.

An example of what this section might look like would be:

Expand All @@ -129,6 +145,8 @@ Criteria:
- User Story
Start Date: 2001-01-23
End Date: 2019-12-30
BlockedAttributes: Blocked, Blocked-In Progress

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are "blocked" and "blocked-in progress" workflow states, or is this looking at the "Flagged" attribute in JIRA?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, the way this works is that it will evaluate each field in the change log and if it matches any of the entries in the BlockedAttributes criteria, it will calculate the number of blocked days based upon the on/off of that field value. I believe that if you wanted to calculate the blocked days based upon the "Flagged" condition, you would just put the field "Impediment" in the BlockedAttributes section. Like this: BlockedAttributes: Impediment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood, unfortunately, that is not working for me. I tried adding Impediment, Flagged, Blocked, and none worked. There is no Blocked Days column being added to the output file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay, I'll see what I can do. I'll test specifically with Impediment and Flagged.

OutputDateFormat: YYYYMMDD

```

Expand Down
25 changes: 23 additions & 2 deletions src/components/jira-work-item.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { JiraExtractorConfig } from '../types';
import * as moment from 'moment';

class JiraWorkItem {
id: string;
stageDates: Array<string>;
name: string;
type: string;
daysBlocked: number;
attributes: {
[val: string]: string;
};
constructor(id: string = '', stageDates: string[] = [], name: string = '', type: string = '', attributes: {} = {}) {
constructor(id: string = '', stageDates: string[] = [], name: string = '', type: string = '', daysBlocked: number = 0, attributes: {} = {}) {
this.id = id;
this.stageDates = stageDates;
this.name = name;
this.type = type;
this.daysBlocked = daysBlocked;
this.attributes = attributes;
}

Expand All @@ -27,7 +30,21 @@ class JiraWorkItem {
}

s += `${(JiraWorkItem.cleanString(this.name))}`;
this.stageDates.forEach(stageDate => s += `,${stageDate}`);
if (isEmpty(config.outputDateformat)) {
this.stageDates.forEach(stageDate => s += `,${(stageDate)}`);
}
else {
this.stageDates.forEach(stageDate => {
if (!isEmpty(stageDate)){
s += `,${moment(stageDate).format(config.outputDateformat)}`;
} else {
s += ',';
}
});
}
if (config.blockedAttributes.length > 0){
s += `,${this.daysBlocked}`;
}
s += `,${this.type}`;

const attributeKeys = this.attributes ? Object.keys(this.attributes) : [];
Expand Down Expand Up @@ -69,6 +86,10 @@ class JiraWorkItem {
};
};

function isEmpty(str) {
return (!str || 0 === str.length);
};

export {
JiraWorkItem,
};
79 changes: 75 additions & 4 deletions src/components/staging-parser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { JiraApiIssue, Workflow } from '../types';

var daysBlocked = 0;

const addCreatedToFirstStage = (issue: JiraApiIssue, stageBins: string[][]) => {
const creationDate: string = issue.fields['created'];
stageBins[0].push(creationDate);
Expand All @@ -19,7 +21,33 @@ const addResolutionDateToClosedStage = (issue: JiraApiIssue, stageMap, stageBins
return stageBins;
};

const populateStages = (issue: JiraApiIssue, stageMap, stageBins, unusedStages = new Map<string, number>()) => {
const calculateDaysBlockedRounded = (blockedDifferenceInDays: number) => {
// mulitply the blocked difference as a decimal of days by 100 and round up to the nearest whole number
var roundedBlockedDiffToTwoDecimalPlaces = (Math.round((blockedDifferenceInDays*100)));
// peel off the last digit, which would be the unit place value
var lastdigit = roundedBlockedDiffToTwoDecimalPlaces.toString().split('').pop();
if (lastdigit === "9") {
// divide the whole unit difference by 10 round up to the nearest whole number and divide by 10
blockedDifferenceInDays = Math.ceil(roundedBlockedDiffToTwoDecimalPlaces / 10)/10;
}
else {
// divide the whole unit difference by 10 round down to the nearest whole number and divide by 10
blockedDifferenceInDays = Math.floor(roundedBlockedDiffToTwoDecimalPlaces / 10)/10;
}
// using the above logic rounds the difference in days up by one 10th if the first two decimals are .85 of one day or higher.
// it rounds down by one 10th if less than 0.85 of one day
return blockedDifferenceInDays;
}

const populateStages = (issue: JiraApiIssue, stageMap, stageBins, blockedAttributes: Array<string>, unusedStages = new Map<string, number>()) => {
var daysBlockedForCurrentIssue = 0;
var currentBlockedDate = null;
var blockedDateDifference = 0;

var msecPerMinute = 1000 * 60;
var msecPerHour = msecPerMinute * 60;
var msecPerDay = msecPerHour * 24;

// sort status changes into stage bins
issue.changelog.histories.forEach(history => {
history.items.forEach(historyItem => {
Expand All @@ -34,6 +62,24 @@ const populateStages = (issue: JiraApiIssue, stageMap, stageBins, unusedStages =
unusedStages.set(stageName, count + 1);
}
}
if (blockedAttributes.length > 0){
if (blockedAttributes.indexOf(historyItem['field']) > -1) {
const fromString = historyItem['fromString'];
const toString = historyItem['toString'];
if (fromString === null && (toString != null || toString != "")) {
currentBlockedDate = new Date(history['created']);
}
else if ((fromString != null || fromString != "") && (toString === null || toString === "")) {
const endBlockedDate = new Date (history['created']);
if (currentBlockedDate != null) {
blockedDateDifference = endBlockedDate.valueOf() - currentBlockedDate.valueOf();
daysBlockedForCurrentIssue += calculateDaysBlockedRounded(blockedDateDifference / msecPerDay);
}
currentBlockedDate = null;
}
}
}

// naive solution, does not differentiate between epic status stage or status stage/
// (lumpsthem together);
const customAttributes = ['Epic Status'];
Expand All @@ -50,6 +96,12 @@ const populateStages = (issue: JiraApiIssue, stageMap, stageBins, unusedStages =
}
});
});
if (currentBlockedDate != null) {
const endBlockedDate = new Date();
blockedDateDifference = endBlockedDate.valueOf() - currentBlockedDate.valueOf();
daysBlockedForCurrentIssue += calculateDaysBlockedRounded(blockedDateDifference / msecPerDay);
}
daysBlocked = daysBlockedForCurrentIssue;
return stageBins;
};

Expand All @@ -72,7 +124,20 @@ const filterAndFlattenStagingDates = (stageBins: string[][]) => {
return stagingDates;
};

const getStagingDates = (issue: JiraApiIssue, workflow: Workflow): string[] => {
const fillInBlankStageDatesFromLatestDate = (stagingDates: string[]) => {
var latestDate = '';
for (var _i = stagingDates.length-1; _i >=0; _i--) {
const currentDate = stagingDates[_i];
if ( currentDate ) {
latestDate = currentDate;
} else {
stagingDates[_i] = latestDate;
}
}
return stagingDates;
}

const getStagingDates = (issue: JiraApiIssue, workflow: Workflow, blockedAttributes: Array<string>): string[] => {
const createInFirstStage = workflow[Object.keys(workflow)[0]].includes('(Created)');
const resolvedInLastStage = workflow[Object.keys(workflow)[Object.keys(workflow).length - 1]].includes('(Resolved)');

Expand All @@ -91,11 +156,17 @@ const getStagingDates = (issue: JiraApiIssue, workflow: Workflow): string[] => {
if (resolvedInLastStage) {
stageBins = addResolutionDateToClosedStage(issue, stageMap, stageBins);
}
stageBins = populateStages(issue, stageMap, stageBins);
const stagingDates = filterAndFlattenStagingDates(stageBins);
stageBins = populateStages(issue, stageMap, stageBins, blockedAttributes);
var stagingDates = filterAndFlattenStagingDates(stageBins);
stagingDates = fillInBlankStageDatesFromLatestDate(stagingDates);
return stagingDates;
};

const getDaysBlocked = () => {
return daysBlocked;
};

export {
getStagingDates,
getDaysBlocked,
};
12 changes: 12 additions & 0 deletions src/components/yaml-converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ const convertCsvStringToArray = (s: string): string[] => {
}
};

const replaceDashInBlockedAttributes = (blockedAttributes): string => {
var re = new RegExp(String.fromCharCode(8211), 'gi');
return blockedAttributes.replace(re, String.fromCharCode(45));
};

const convertYamlToJiraSettings = (config: any): JiraExtractorConfig => {
const c: JiraExtractorConfig = {};

Expand All @@ -50,6 +55,13 @@ const convertYamlToJiraSettings = (config: any): JiraExtractorConfig => {
c.projects = config.legacy ? convertCsvStringToArray(config.Criteria.Projects) : convertToArray(config.Criteria.Project);
c.issueTypes = config.legacy ? convertCsvStringToArray(config.Criteria['Types']) : convertToArray(config.Criteria['Issue types']);
c.filters = config.legacy ? convertCsvStringToArray(config.Criteria.Filters) : convertToArray(config.Criteria.Filters);
if ((config.Criteria['BlockedAttributes'] == undefined) || (config.Criteria['BlockedAttributes'] == null)) {
c.blockedAttributes = config.legacy ? convertCsvStringToArray((config.Criteria['BlockedAttributes'])) : convertToArray(config.Criteria['BlockedAttributes']);
} else {
c.blockedAttributes = config.Criteria['BlockedAttributes'].indexOf(",") ? convertCsvStringToArray(replaceDashInBlockedAttributes(config.Criteria['BlockedAttributes'])) : convertToArray(config.Criteria['BlockedAttributes']);
}

c.outputDateformat = config.Criteria['OutputDateFormat'] || null;
c.startDate = config.Criteria['Start Date'] || null;
c.endDate = config.Criteria['End Date'] || null;
c.customJql = config.Criteria.JQL ? config.Criteria.JQL : ''; // fix this, need to put this in an Array
Expand Down
12 changes: 9 additions & 3 deletions src/extractor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getStagingDates } from './components/staging-parser';
import { getDaysBlocked } from './components/staging-parser';
import { getAttributes, parseAttributeArray } from './components/attribute-parser';
import { JiraWorkItem } from './components/jira-work-item';
import { getJson } from './components/jira-adapter';
Expand Down Expand Up @@ -112,7 +113,11 @@ class JiraExtractor {
let stages = Object.keys(this.config.workflow);
let config = this.config;

const header = `ID,Link,Name,${stages.join(',')},Type,${Object.keys(attributes).join(',')}${this.config.teams ? ',Team' : ''}`;
var header = `ID,Link,Name,${stages.join(',')},`;
if (config.blockedAttributes.length > 0){
header += `Days Blocked,`;
}
header += `Type,${Object.keys(attributes).join(',')}${this.config.teams ? ',Team' : ''}`;
const body = workItems.map(item => item.toCSV(config)).reduce((res, cur) => `${res + cur}\n`, '');
const csv: string = `${header}\n${body}`;
return csv;
Expand Down Expand Up @@ -161,8 +166,9 @@ class JiraExtractor {
const teams = this.config.teams;
const key: string = issue.key;
const name: string = issue.fields['summary'];
const stagingDates: string[] = getStagingDates(issue, workflow);
const stagingDates: string[] = getStagingDates(issue, workflow, this.config.blockedAttributes);
const type: string = issue.fields.issuetype.name ? issue.fields.issuetype.name : '';
const daysBlocked: number = getDaysBlocked();

let attributesKeyVal = {};
if (attributes) {
Expand All @@ -185,7 +191,7 @@ class JiraExtractor {

}

return new JiraWorkItem(key, stagingDates, name, type, attributesKeyVal);
return new JiraWorkItem(key, stagingDates, name, type, daysBlocked, attributesKeyVal);
};
};

Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ export interface JiraExtractorConfig {
projects?: Array<string>;
issueTypes?: Array<string>;
filters?: Array<string>;
blockedAttributes?: Array<string>;
outputDateformat?: string;
startDate?: Date;
endDate?: Date;
customJql?: string;
Expand Down