Scripted field for Time in Multiple Statuses

Donald Kelley April 5, 2017

I am trying to create a field called Time in Engineering using a scripted field.  I have done this based off the example script provided to get the time of a particular status.

 

MY problem stems from the fact that if I try to use a particular status, and only that status it does not return a value it is always blank

This status is generally the first status of any issue (however some issuetypes bypass it) but the status can also be used on a reopen or it can be sent back to from a variety of other statuses.  I know plugins can assist further with this but those options are not short terms solutions.  I can get all other statuses to report correctly.  

Any assistance would be greatly appreciated.  The script I am using is below.  The status with the issue is "Awaiting Triage"  If this status is included the return value is blank.  If it is removed from the script the sum of the other statues are returned.

import com.atlassian.core.util.DateUtils
import com.atlassian.jira.ComponentManager
import com.atlassian.jira.issue.history.ChangeItemBean

def componentManager = ComponentManager.getInstance()
def changeHistoryManager = componentManager.getChangeHistoryManager()

def inProgressName = "Awaiting Triage"
def inClosed = "Closed"
def inAssigned = "Assigned"
def inFix = "Fix"
def inRFT = "Ready for Test"
def inPromote = "Approved to Promote"
def inInvestigation = "Investigation"

def rt = [0]
changeHistoryManager.getChangeItemsForField (issue, "status").reverse().each {ChangeItemBean item ->

def timeDiff = System.currentTimeMillis() - item.created.getTime()

if (item.fromString == inAssigned) {
rt << -timeDiff
}
if (item.toString == inAssigned){
rt << timeDiff
}
if (item.fromString == inFix){
rt << -timeDiff
}
if (item.toString == inFix){
rt << timeDiff
}
if (item.fromString == inRFT){
rt << -timeDiff
}
// if (item.toString == inRFT){
// rt << timeDiff
// }
// if (item.fromString == inPromote){
// rt <<-timeDiff
// }
if (item.toString == inPromote){
rt <<timeDiff
}
if (item.fromString == inInvestigation){
rt << -timeDiff
}
if (item.toString == inInvestigation){
rt << timeDiff
}
if (item.fromString == inProgressName) {
rt << -timeDiff
}
if (item.toString == inProgressName){
rt << timeDiff
}

}

// NOTE: doesn't show anything if less than 60 seconds
DateUtils.getDurationString(Math.round(rt.sum() / 1000))

 

2 answers

0 votes
shiva November 19, 2020

@Jonny Carter @Donald Kelley 

The script works!

Any chance we can count the time in week days, excluding weekends ?

Jonny Carter
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
January 13, 2021

Sure! Most Java solutions to the problem can be easily adapted to Groovy. See, for example, https://stackoverflow.com/questions/8171968/calculating-dates-given-two-dates-excluding-weekend .

The Java LocalDate API is pretty useful for things of this nature.

0 votes
Jonny Carter
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
April 21, 2017

Side note: one thing I notice is that you're using the ComponentManager class to get the change history manager. Might switch to the ComponentAccessor, per the docs. Even if you're not on JIRA 7 yet, this will help future-proof you.

Anyway, the example script for determining how long something is in progress presumes that the status isn't the first one in your workflow. If that's the case, you'll need to add the time from when the issue gets created to the first time the status changes. Also, if an issue has never had its status updated, you'll need to add the time in.

Something like

import com.atlassian.core.util.DateUtils
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.history.ChangeItemBean

def issueService = ComponentAccessor.issueService
def user = ComponentAccessor.jiraAuthenticationContext.loggedInUser
def issue = issueService.getIssue(user, "SSPA-26").issue
def changeHistoryManager = ComponentAccessor.getChangeHistoryManager()
def awaitingTriageStatus  = "Awaiting Triage"
def rt = [0L]
def changeItems = changeHistoryManager.getChangeItemsForField(issue, "status")

changeItems.reverse().each { ChangeItemBean item ->
    log.debug("Change item: from ${item.fromString} to ${item.toString}")
    def timeDiff = System.currentTimeMillis() - item.created.getTime()

    if (item.fromString == awaitingTriageStatus ) {
        rt << -timeDiff
    }
    if (item.toString == awaitingTriageStatus ) {
        rt << timeDiff
    }
}

if (! changeItems) {
    rt << (System.currentTimeMillis() - issue.getCreated().getTime())
}

// NOTE: doesn't show anything if less than 60 seconds
def total = DateUtils.getDurationString(Math.round(rt.sum() / 1000))
log.debug(total)
return total

 

Donald Kelley May 25, 2017

Thank you that worked.  What if I wanted to get something that showed me all time except for a set of statuses.  For example we have 4 statuses that we consider outside of development time and want to calculate only time in development.  Could we use something to calculate the total time and subtract time spent in a specific status?

Isabella Frari
I'm New Here
I'm New Here
Those new to the Atlassian Community have posted less than three times. Give them a warm welcome!
April 8, 2022

/* Calculate the Sum of Durarion for time spent in group of Statuses "status1", "Status2", "Status3" append any status -- Get duration within Business Hours
*/

import com.atlassian.jira.issue.history.ChangeItemBean
import org.joda.time.DateTime
import org.joda.time.DateTimeZone

import java.sql.Timestamp
import java.time.*
import java.time.temporal.ChronoUnit
import java.util.stream.IntStream

final List<String> statuses = ["status1", "status2", "status3"]
final String statusName = "" //Any of myStatus to check
final List<ChangeItemBean> changeItems = issue.getFieldHistory("status")
if (changeItems.size() == 0) //issue status has never changed
if (issue.get("status").name == statusName)
return workingSecondsBetween(new Date().getTime(), issue.getCreated().getTime())
else
return null;
long totalTime = 0L;
Long startTime = null;
//iterate over changes
for (ChangeItemBean changeItemBean : changeItems) {
if (statuses.contains(changeItemBean.getFromString())) {
if (startTime == null) //Assuming this might have have been the starting status
startTime = issue.getCreated().getTime();
totalTime += workingSecondsBetween(changeItemBean.getCreated().getTime(), startTime);
startTime = null;
}
if (statuses.contains(changeItemBean.getToString())) {
startTime = changeItemBean.getCreated().getTime();
}
}
if (startTime != null)
totalTime += workingSecondsBetween(new Date().getTime(), startTime); //we are currently in the requested status
return totalTime


public class WorkingMinutesCalculator {
private static final int WORK_HOUR_START = 9; //first working hour in 24hr MyClock
private static final int WORK_HOUR_END = 17; //last working hour (work ends at the end of this hour)
private static final long SECONDS_PER_HOUR = 3600;

private static final long WORKING_HOURS_PER_DAY = WORK_HOUR_END - WORK_HOUR_START;
private static final long WORKING_SECONDS_PER_DAY = WORKING_HOURS_PER_DAY * SECONDS_PER_HOUR;

public int getWorkingSecondsSince(final Timestamp startTime) {
Timestamp now = Timestamp.from(Instant.now());
return getWorkingSeconds(startTime, now);
}

public int getWorkingSeconds(final Timestamp startTime, final Timestamp endTime) {
if (null == startTime || null == endTime) {
throw new IllegalStateException();
}
if (endTime.before(startTime)) {
return 0;
}

LocalDateTime from = startTime.toLocalDateTime();
LocalDateTime to = endTime.toLocalDateTime();

LocalDate fromDay = from.toLocalDate();
LocalDate toDay = to.toLocalDate();

int allDaysBetween = (int) (ChronoUnit.DAYS.between(fromDay, toDay) + 1);
long allWorkingSeconds = IntStream.range(0, allDaysBetween)
.filter{i -> isWorkingDay(from.plusDays(i))}
.count() * WORKING_SECONDS_PER_DAY ;

// MyClock from - working_day_from_start
long tailRedundantSeconds = 0;
if (isWorkingDay(from)) {
if (isWorkingHours(from)) {
tailRedundantSeconds = Duration.between(fromDay.atTime(WORK_HOUR_START, 0), from).toSeconds();
} else if (from.getHour() > WORK_HOUR_START) {
tailRedundantSeconds = WORKING_SECONDS_PER_DAY;
}
}

// MyClock working_day_end - to
long headRedundanSeconds = 0;
if (isWorkingDay(to)) {
if (isWorkingHours(to)) {
headRedundanSeconds = Duration.between(to, toDay.atTime(WORK_HOUR_END, 0)).toSeconds();
} else if (from.getHour() < WORK_HOUR_START) {
headRedundanSeconds = WORKING_SECONDS_PER_DAY;
}
}
return (int) (allWorkingSeconds - tailRedundantSeconds - headRedundanSeconds);
}

private boolean isWorkingDay(final LocalDateTime time) {
return time.getDayOfWeek().getValue() < DayOfWeek.SATURDAY.getValue();
}

private boolean isWorkingHours(final LocalDateTime time) {
int hour = time.getHour();
return WORK_HOUR_START <= hour && hour <= WORK_HOUR_END;
}
}

long workingSecondsBetween(long to, long from) {
//to and from are Unix time in the system time zone (not UTC)
final DateTimeZone timeZone = DateTimeZone.forID("America/Chicago") //adjusted for my time zone CST (Central)
DateTime fromDt = new DateTime(from + (new Date(from)).getTimezoneOffset()*60000, timeZone)
DateTime toDt = new DateTime(to + (new Date(to)).getTimezoneOffset()*60000, timeZone)
return (new WorkingMinutesCalculator()).getWorkingSeconds(new Timestamp(fromDt.toLocalDateTime().getLocalMillis()), new Timestamp(toDt.toLocalDateTime().getLocalMillis()))
}

Suggest an answer

Log in or Sign up to answer
TAGS
AUG Leaders

Atlassian Community Events