Add/subtract results from scripted fields

- March 12, 2021

Hi all,

This post relates to one of my other posts (https://community.atlassian.com/t5/Marketplace-Apps-Integrations/Scripted-fields-for-time-in-statuses/qaq-p/1624738).

This is a question merely out of curiosity. I'm pretty sure it isn't possible...but I'm wondering if it's possible to take the result values of scripted fields made in scriptrunner, and add them together, or subtract one result value from the other.

To elaborate further, I have three scripted fields: one that calculates time spent in To Do-category statuses (like 'To Do', 'Open', 'New', 'Selected for Dev', etc..), one that calculates time spent in In Progress-category statuses ('In Progress', 'Ready for Work', 'In Review', 'With Reporter', etc...), and one that calcualtes time in statuses that indicate that progress has been halted (namely. 'On Hold', 'Blocked', and 'Stalled').

Here are the scripts for each scripted field...

Time in TODO

import com.atlassian.jira.component.ComponentAccessor;
import com.atlassian.jira.issue.history.ChangeItemBean;
import com.atlassian.jira.issue.Issue;

def changeHistoryManager = ComponentAccessor.getChangeHistoryManager()

def statusName = "New"
def createdDateDiff = System.currentTimeMillis() - issue.getCreated().getTime()

List<Long> rt = [0L]

// Adding time from when issue is created to the first time the status changes
rt << createdDateDiff

def changeItems = changeHistoryManager.getChangeItemsForField(issue, "status")
changeItems.reverse().each {ChangeItemBean item -> item.fromString

// Get the time passed since status change
def timeDiff = System.currentTimeMillis() - item.created.getTime()

// If the status change is from 'New', subtract the time passed since then
if (item.fromString == statusName) {
rt << -timeDiff
}

// If the status change goes to our status, we want to add the time passed since then
if (item.toString == statusName){
rt << timeDiff
}
}

def intakeReview = "Intake Review"
def rt2 = [0L]

changeHistoryManager.getChangeItemsForField (issue, "status")
changeItems.reverse().each {ChangeItemBean item ->
def timeDiff = System.currentTimeMillis() - item.created.time
// Subtract time if the "from" status is equal to the status to be checked and from and to statuses are different.
// This allows to count the time the issue is in the state for the first time
if (item.fromString == intakeReview && item.fromString != item.toString) {
rt2 << -timeDiff
}
// Add time if the "to" status is equal to the status to be checked
if (item.toString == intakeReview) {
rt2 << timeDiff
}
}

def backlogged = "Backlogged"
def rt3 = [0L]

changeHistoryManager.getChangeItemsForField (issue, "status")
changeItems.reverse().each {ChangeItemBean item ->
def timeDiff = System.currentTimeMillis() - item.created.time
// Subtract time if the "from" status is equal to the status to be checked and from and to statuses are different.
// This allows to count the time the issue is in the state for the first time
if (item.fromString == backlogged && item.fromString != item.toString) {
rt3 << -timeDiff
}
// Add time if the "to" status is equal to the status to be checked
if (item.toString == backlogged) {
rt3 << timeDiff
}
}


List<Long> rtTotal = new ArrayList<Long>()
rtTotal.addAll(rt)
rtTotal.addAll(rt2)
rtTotal.addAll(rt3)

def total = (rtTotal.sum() as Long) / 1000
return Math.round(total) ?: 0

 

Time in IN PROGRESS

import com.atlassian.jira.component.ComponentAccessor;
import com.atlassian.jira.issue.history.ChangeItemBean;
import com.atlassian.jira.issue.Issue;

def changeHistoryManager = ComponentAccessor.getChangeHistoryManager()
def changeItems = changeHistoryManager.getChangeItemsForField(issue, "status")

def inProgress = "In Progress"
def rt = [0L]

changeHistoryManager.getChangeItemsForField (issue, "status")
changeItems.reverse().each {ChangeItemBean item ->
def timeDiff = System.currentTimeMillis() - item.created.time
// Subtract time if the "from" status is equal to the status to be checked and from and to statuses are different.
// This allows to count the time the issue is in the state for the first time
if (item.fromString == inProgress && item.fromString != item.toString) {
rt << -timeDiff
}
// Add time if the "to" status is equal to the status to be checked
if (item.toString == inProgress) {
rt << timeDiff
}
}

def rdyForWork = "Ready for Work"
def rt2 = [0L]

changeHistoryManager.getChangeItemsForField (issue, "status")
changeItems.reverse().each {ChangeItemBean item ->
def timeDiff = System.currentTimeMillis() - item.created.time
// Subtract time if the "from" status is equal to the status to be checked and from and to statuses are different.
// This allows to count the time the issue is in the state for the first time
if (item.fromString == rdyForWork && item.fromString != item.toString) {
rt2 << -timeDiff
}
// Add time if the "to" status is equal to the status to be checked
if (item.toString == rdyForWork) {
rt2 << timeDiff
}
}

def inAnalysis = "In Analysis"
def rt3 = [0L]

changeHistoryManager.getChangeItemsForField (issue, "status")
changeItems.reverse().each {ChangeItemBean item ->
def timeDiff = System.currentTimeMillis() - item.created.time
// Subtract time if the "from" status is equal to the status to be checked and from and to statuses are different.
// This allows to count the time the issue is in the state for the first time
if (item.fromString == inAnalysis && item.fromString != item.toString) {
rt3 << -timeDiff
}
// Add time if the "to" status is equal to the status to be checked
if (item.toString == inAnalysis) {
rt3 << timeDiff
}
}

def inReview = "In Review"
def rt4 = [0L]

changeHistoryManager.getChangeItemsForField (issue, "status")
changeItems.reverse().each {ChangeItemBean item ->
def timeDiff = System.currentTimeMillis() - item.created.time
// Subtract time if the "from" status is equal to the status to be checked and from and to statuses are different.
// This allows to count the time the issue is in the state for the first time
if (item.fromString == inReview && item.fromString != item.toString) {
rt4 << -timeDiff
}
// Add time if the "to" status is equal to the status to be checked
if (item.toString == inReview) {
rt4 << timeDiff
}
}

def codeReview = "Code Review"
def rt5 = [0L]

changeHistoryManager.getChangeItemsForField (issue, "status")
changeItems.reverse().each {ChangeItemBean item ->
def timeDiff = System.currentTimeMillis() - item.created.time
// Subtract time if the "from" status is equal to the status to be checked and from and to statuses are different.
// This allows to count the time the issue is in the state for the first time
if (item.fromString == codeReview && item.fromString != item.toString) {
rt5 << -timeDiff
}
// Add time if the "to" status is equal to the status to be checked
if (item.toString == codeReview) {
rt5 << timeDiff
}
}

def pending = "Pending"
def rt6 = [0L]

changeHistoryManager.getChangeItemsForField (issue, "status")
changeItems.reverse().each {ChangeItemBean item ->
def timeDiff = System.currentTimeMillis() - item.created.time
// Subtract time if the "from" status is equal to the status to be checked and from and to statuses are different.
// This allows to count the time the issue is in the state for the first time
if (item.fromString == pending && item.fromString != item.toString) {
rt6 << -timeDiff
}
// Add time if the "to" status is equal to the status to be checked
if (item.toString == pending) {
rt6 << timeDiff
}
}

def withReporter = "With Reporter"
def rt7 = [0L]

changeHistoryManager.getChangeItemsForField (issue, "status")
changeItems.reverse().each {ChangeItemBean item ->
def timeDiff = System.currentTimeMillis() - item.created.time
// Subtract time if the "from" status is equal to the status to be checked and from and to statuses are different.
// This allows to count the time the issue is in the state for the first time
if (item.fromString == withReporter && item.fromString != item.toString) {
rt7 << -timeDiff
}
// Add time if the "to" status is equal to the status to be checked
if (item.toString == withReporter) {
rt7 << timeDiff
}
}

def withTeam = "With Team"
def rt8 = [0L]

changeHistoryManager.getChangeItemsForField (issue, "status")
changeItems.reverse().each {ChangeItemBean item ->
def timeDiff = System.currentTimeMillis() - item.created.time
// Subtract time if the "from" status is equal to the status to be checked and from and to statuses are different.
// This allows to count the time the issue is in the state for the first time
if (item.fromString == withTeam && item.fromString != item.toString) {
rt8 << -timeDiff
}
// Add time if the "to" status is equal to the status to be checked
if (item.toString == withTeam) {
rt8 << timeDiff
}
}

def rdyForAcceptance = "Ready for Acceptance"
def rt9 = [0L]

changeHistoryManager.getChangeItemsForField (issue, "status")
changeItems.reverse().each {ChangeItemBean item ->
def timeDiff = System.currentTimeMillis() - item.created.time
// Subtract time if the "from" status is equal to the status to be checked and from and to statuses are different.
// This allows to count the time the issue is in the state for the first time
if (item.fromString == rdyForAcceptance && item.fromString != item.toString) {
rt9 << -timeDiff
}
// Add time if the "to" status is equal to the status to be checked
if (item.toString == rdyForAcceptance) {
rt9 << timeDiff
}
}

def overdue = "Overdue"
def rt10 = [0L]

changeHistoryManager.getChangeItemsForField (issue, "status")
changeItems.reverse().each {ChangeItemBean item ->
def timeDiff = System.currentTimeMillis() - item.created.time
// Subtract time if the "from" status is equal to the status to be checked and from and to statuses are different.
// This allows to count the time the issue is in the state for the first time
if (item.fromString == overdue && item.fromString != item.toString) {
rt10 << -timeDiff
}
// Add time if the "to" status is equal to the status to be checked
if (item.toString == overdue) {
rt10 << timeDiff
}
}

List<Long> rtTotal = new ArrayList<Long>()
rtTotal.addAll(rt)
rtTotal.addAll(rt2)
rtTotal.addAll(rt3)
rtTotal.addAll(rt4)
rtTotal.addAll(rt5)
rtTotal.addAll(rt6)
rtTotal.addAll(rt7)
rtTotal.addAll(rt8)
rtTotal.addAll(rt9)
rtTotal.addAll(rt10)

def total = (rtTotal.sum() as Long) / 1000
return Math.round(total) ?: 0

 

Time PROGRESS HALTED

import com.atlassian.jira.component.ComponentAccessor;
import com.atlassian.jira.issue.history.ChangeItemBean;
import com.atlassian.jira.issue.Issue;

def changeHistoryManager = ComponentAccessor.getChangeHistoryManager()
def changeItems = changeHistoryManager.getChangeItemsForField(issue, "status")

def onHold = "On Hold"
def rt = [0L]

changeHistoryManager.getChangeItemsForField (issue, "status")
changeItems.reverse().each {ChangeItemBean item ->
def timeDiff = System.currentTimeMillis() - item.created.time
// Subtract time if the "from" status is equal to the status to be checked and from and to statuses are different.
// This allows to count the time the issue is in the state for the first time
if (item.fromString == onHold && item.fromString != item.toString) {
rt << -timeDiff
}
// Add time if the "to" status is equal to the status to be checked
if (item.toString == onHold) {
rt << timeDiff
}
}

def blocked = "Blocked"
def rt2 = [0L]

changeHistoryManager.getChangeItemsForField (issue, "status")
changeItems.reverse().each {ChangeItemBean item ->
def timeDiff = System.currentTimeMillis() - item.created.time
// Subtract time if the "from" status is equal to the status to be checked and from and to statuses are different.
// This allows to count the time the issue is in the state for the first time
if (item.fromString == blocked && item.fromString != item.toString) {
rt2 << -timeDiff
}
// Add time if the "to" status is equal to the status to be checked
if (item.toString == blocked) {
rt2 << timeDiff
}
}

def stalled = "Stalled"
def rt3 = [0L]

changeHistoryManager.getChangeItemsForField (issue, "status")
changeItems.reverse().each {ChangeItemBean item ->
def timeDiff = System.currentTimeMillis() - item.created.time
// Subtract time if the "from" status is equal to the status to be checked and from and to statuses are different.
// This allows to count the time the issue is in the state for the first time
if (item.fromString == stalled && item.fromString != item.toString) {
rt3 << -timeDiff
}
// Add time if the "to" status is equal to the status to be checked
if (item.toString == stalled) {
rt3 << timeDiff
}
}

List<Long> rtTotal = new ArrayList<Long>()
rtTotal.addAll(rt)
rtTotal.addAll(rt2)
rtTotal.addAll(rt3)

def total = (rtTotal.sum() as Long) / 1000
return Math.round(total) ?: 0

 

The scripted fields all work as intended...although I'd preferably like for them to look into the scope of the workflow in context of the issue in concern and add all workflow statuses within respective issue categories, rather than me just relying on a set list of statuses. If anyone had any suggestions/tips on improving the scripts to follow this logic, that'd be great too.

But more importantly, I want to take the (Duration) results of each of these scripted fields and add them together. Is that at all possible?

2 answers

0 votes
- March 19, 2021

Thank you both @Nic Brough -Adaptavist- @Mario Carabelli !

My apologies for the delay. There was a problem where we couldn't preview test over issues in our scripted fields and couldn't even create issues in our sandbox instance! But now that that's over with...

Thanks both for the input. @Mario Carabelli I would consider this approach if push came to shove, although I would rather reference the scripted fields by ID if possible.

Working off of that, @Nic Brough -Adaptavist- I found some code to reference from and edited it a little to reference the scripted fields by ID. Ideally, I'd like it to take the values from each field over a test issue, and add them together. Here's what I have so far for the script I want for the TOTAL TIME field:

import com.atlassian.jira.ComponentAccessor
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.CustomFieldManager
import com.atlassian.jira.issue.fields.CustomField
import com.atlassian.jira.issue.IssueManager
import com.atlassian.jira.issue.Issue

CustomFieldManager customFieldManager = ComponentAccessor.getCustomFieldManager()
CustomField toDo = customFieldManager.getCustomFieldObject("customfield_20900"); // Time in TODO
CustomField inProgress = customFieldManager.getCustomFieldObject("customfield_21100"); // Time in IN PROGRESS

def toDoInt = issue.getCustomFieldValue(toDo);
def inProgressInt = issue.getCustomFieldValue(inProgress);

if (toDoInt == null) { toDoInt = 0 }
if (inProgressInt == null) { inProgressInt = 0 }

def total = (toDoInt + inProgressInt)

return total

I assumed that the script would take the values from the two scripted fields (82,182 or 22 hours, 49 minutes for Time in TODO, and  358,520 or 4 days, 3 hours, 35 minutes for Time in IN PROGRESS respectively) and add them together. But for some reason when previewing, I'm getting the following error logged:

2021-03-19 21:19:14,521 ERROR [customfield.GroovyCustomField]: ************************************************************************************* 2021-03-19 21:19:14,521 ERROR [customfield.GroovyCustomField]: Script field failed on issue: MULE-1039, field: TOTAL TIME (Actual) java.lang.NullPointerException at com.atlassian.jira.issue.IssueImpl.getCustomFieldValue(IssueImpl.java:951) at com.atlassian.jira.issue.Issue$getCustomFieldValue$1.call(Unknown Source) at Script273.run(Script273.groovy:12)

 

I'm confused as to why I'm getting a null pointer exception when trying to get the value of the Time in TODO field, even though when I test that field itself, I get a value of 82,182 as expected.

Nic Brough -Adaptavist-
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
March 19, 2021

I don't think the problem is the content of the field, I think it's that the field itself is not being fetched.  .getCustomFieldObject(string) does not look right to me.

I usually fetch custom fields by a name search like:

CustomField toDo = ComponentAccessor.customFieldManager.getCustomFieldObjects(issue).findByName("To do")

Obviously, if you're not getting a field object into the toDo variable, the value will be null on trying to fetch the contents, hence the error on line 12

I suspect your script will work fine if you replace both of the "find a custom field header object" lines

- March 22, 2021

@Nic Brough -Adaptavist- 

That seems to be a step in the right direction. Thanks!

I've updated the code per your advice as follows:

import com.atlassian.jira.ComponentAccessor
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.CustomFieldManager
import com.atlassian.jira.issue.fields.CustomField
import com.atlassian.jira.issue.IssueManager
import com.atlassian.jira.issue.Issue

CustomFieldManager customFieldManager = ComponentAccessor.getCustomFieldManager()
CustomField toDo = ComponentAccessor.customFieldManager.getCustomFieldObjects(issue).findByName("To Do")
CustomField inProgress = ComponentAccessor.customFieldManager.getCustomFieldObjects(issue).findByName("Time IN PROGRESS")

def toDoInt = issue.getCustomFieldValue(toDo);
def inProgressInt = issue.getCustomFieldValue(inProgress);

if (toDoInt == null) { toDoInt = 0 }
if (inProgressInt == null) { inProgressInt = 0 }

def total = (toDoInt + inProgressInt)

return total

So now the null pointer exception error is gone and I actually get a result...but now there's a new problem, and this time it's with the result itself.

The way I thought this script would work is that it would take the Long values from the To Do and Time IN PROGRESS fields for the test issue ,add them together, and produce the result of the sum. The value I'm looking to have returned in this instance would ideally be 1 week, 1 day, 1 hour, 5 minutes (which is [1 week, 2 hours, 16 minutes] + [22 hours, 49 minutes]), or 694,565 in text field format.

But somehow, when testing the code above for the same test issue, it's not returning the expected result, but rather producing one of the following two results interchangeably:

  • 0 minutes (000000..... in text format)
  • 2613 years, 7 weeks,  6 days, 15 hours, 29 minutes (82182612336 in-text format)

Not sure why this is happening.

Nic Brough -Adaptavist-
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
March 23, 2021

I would guess that your fields don't hold the data in the way you're expecting them to.

Two things to do:

  • Look at both field definitions in the list of custom fields - what type of field is each?  (Number, date, date/time, etc)
  • Brute force - try returning each value (toDoInt and inProgressInt) while testing the script.  Are they similarly sized numbers?   (Scripted fields have the nice little tester where you can give it an issue to test against and you run the script to get the result, makes it easy to mod and retry.)
- March 23, 2021

@Nic Brough -Adaptavist- 

Regarding the field definitions, they're both scripted fields, but each are set at the end to return a Long (representing duration in seconds).

When testing just the toDoInt value, interchangeably (each time I hit Preview) it returns either 22 hours, 49 minutes (as expected), or null. When testing the value of inProgressInt, same weird thing. It'll return either 1 week, 19 hours, 53 minutes,   or null.

- March 23, 2021

@Nic Brough -Adaptavist- 

I was able to convert the values of toDoInt and inProgressInt to ints by with parseInt:

import com.atlassian.jira.ComponentAccessor
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.CustomFieldManager
import com.atlassian.jira.issue.fields.CustomField
import com.atlassian.jira.issue.IssueManager
import com.atlassian.jira.issue.Issue


CustomFieldManager customFieldManager = ComponentAccessor.getCustomFieldManager()
CustomField toDo = ComponentAccessor.customFieldManager.getCustomFieldObjects(issue).findByName("To Do")
CustomField inProgress = ComponentAccessor.customFieldManager.getCustomFieldObjects(issue).findByName("Time IN PROGRESS")

def toDoVal = issue.getCustomFieldValue(toDo);
def inProgressVal = issue.getCustomFieldValue(inProgress);

int toDoInt = Integer.parseInt(toDoVal);
int inProgressInt = Integer.parseInt(inProgressVal);

if (toDoInt == null) { toDoInt = 0 }
if (inProgressInt == null) { inProgressInt = 0 }

def total = (toDoInt + inProgressInt)

return total

 

Now previewing over the test ticket, the script adds the values of the two fields together (as int) and I get the expected result of 1 week, 1 day, 20 hours, 26 minutes.

The only problem now is that every other time I preview,  I get this error:

java.lang.NumberFormatException: null at java_lang_Integer$parseInt.call(Unknown Source) at Script385.run(Script385.groovy:16)
Nic Brough -Adaptavist-
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
March 23, 2021

Ok, there are still a couple of things to look at.

First was the bit from my original answer - are your scripted fields being pulled from a different issue?  If they are not, then you may have a random problem where you have no way to know which field will be populated first. 

As above, if you have fields A, B, C, D, with A and B being numbers, and C and D being scripted, you can do C = A + B, but you must not do D = C + 4 - when it calculates D, it might be getting the old value, the new value or even a null, depending on where it executes.   Your script should do D = A + B + 4, not rely on C.  Do not read a scripted field from the current issue while calculating another.

 

Second, although scripted fields are of a type "scripted field", I suspect you'll find that you have set up yours with different "templates" - these tell Jira what format the output is, and are found in the same admin place as the script for them.  My guess is you'll find one of them is set to "number" template (so you don't need to "parseint" on that one) and the other is a "string" or possibly other format, so that's why the parseint now gets something for it.

- March 23, 2021

@Nic Brough -Adaptavist- 

Ah, I see. In this case, A (To Do), B (Time IN PROGRESS),  C (Time PROGRESS HALTED) and D (TOTAL TIME) are all scripted fields. And for TOTAL TIME, it's basically trying to read two scripted fields while calculating the value of itself. So that doesn't work out..

I'll probably have to redo the TOTAL TIME script and make it so that it calculates the time difference in between now and when the issue was created, and stop calculating when the issue reaches the "Done" status. 

Like Nic Brough -Adaptavist- likes this
0 votes
Nic Brough -Adaptavist-
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
March 12, 2021

Possible, yes, but you do need to be careful about where you do it.

You can get the values for a scripted field in the same way you get a value for any other field.

But.  The easiest way to explain this is to say that "scripted field values are built when an issue is indexed".   This is not the truth.  But it is an oversimplification which we can build on, because it does look like it works that way.

The problem we have with scripted fields is that you have no way of knowing what order they might be calculated in. 

Imagine you have four fields, two simple numbers A and B are standard number fields, field C is scripted and works out as "A + B".  D is also scripted and is "C + 4".

That sounds quite simple, but if you edit an issue and set A = 1 and B = 2, you'll get C = 3 as you expect, but D may come out as 4, null or 7.  Mostly, you will get 7 (the right answer), if you have added the fields C and D in order, but not reliably.

So, yes, you can calculate from your scripted fields, but you need to think about the timing of where you do it.

Mario Carabelli March 13, 2021

Another approach would be to copy the code of all the custom fields you want to add/subtract in one big script field, calculate the results of the script fieldsinside this Script and show the results in the field. This would be less complex but had the disadvantage of code duplication.

Nic Brough -Adaptavist-
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
March 19, 2021

Well, what I'd do for my example is continue to use C and D as separate scripted fields (because I don't want one combined scripted field).  But, yes, you're absolutely right about doing the calculation in one go.

So, I'd replace:

  • C = A + B
  • D = C + 4

with:

  • C = A + B
  • D = A + B + 4

Suggest an answer

Log in or Sign up to answer