Jira 的 Structure 插件对于日常任务及其分析非常有用;它将 Jira 票据的可视化和结构化提升到了一个新的水平,并且开箱即用。
而且,并不是每个人都知道这一点,但结构公式的功能可能会让您大吃一惊。使用公式,您可以创建非常有用的表格,可以极大地简化任务工作,最重要的是,它们对于对版本、史诗和项目进行更深入的分析非常有用。
在本文中,您将了解如何创建自己的公式,从最简单的示例开始,到复杂但相当有用的案例结束。
那么,这段文字是写给谁的呢?有人可能会想,当 ALM Works 网站上的官方文档就在那里等待读者挖掘时,为什么还要写一篇文章。确实如此。然而,我是那些根本不知道 Structure 隐藏了如此广泛的功能的人之一:“等等,这一直是一个选项?!”这种认识让我思考,可能还有其他人仍然不知道他们可以用公式和结构做哪些事情。
本文对于那些已经熟悉公式的人也很有用。您将学习一些使用自定义字段的有趣实用选项,也许还可以为您的项目借用其中的一些选项。顺便说一句,如果您有自己的任何有趣的示例,如果您在评论中分享它们,我将很高兴。
每个例子都分析的很详细,从问题的描述到代码的解释,足够透彻,不留任何疑问。当然,除了解释之外,每个示例都通过代码进行说明,您可以自己尝试,而无需深入分析。
如果您不想阅读,但对公式感兴趣,请查看ALM Works 网络研讨会。这些在 40 分钟内解释了基础知识;信息以非常压缩的方式呈现在那里。
您不需要任何额外的知识来理解这些示例,因此使用过 Jira 和 Structure 的任何人都可以毫无问题地重复其表中的示例。
开发人员使用他们的 Expr 语言提供了相当灵活的语法。基本上,这里的理念是“随心所欲地编写,它就会起作用”。
那么,让我们开始吧!
那么,我们为什么要使用公式呢?嗯,有时事实证明我们没有足够的标准 Jira 字段,例如“受让人”、“故事点”等。或者我们需要计算某些字段的金额,按版本显示剩余容量,并找出任务更改状态的次数。也许我们甚至想将多个字段合并为一个,以使我们的结构更易于阅读。
为了解决这些问题,我们需要公式,我们将使用它们来创建自定义字段。
我们需要做的第一件事是理解公式是如何工作的。它允许我们对字符串应用某种操作。由于我们将许多任务上传到结构中,因此该公式将应用于整个表的每一行。通常,其所有操作都是为了处理这些方面的任务。
因此,如果我们要求公式显示某些 Jira 字段,例如“受让人”,那么该公式将应用于每个任务,并且我们将有另一个“受让人”列。
公式由几个基本实体组成:
我们将通过一些示例更加熟悉公式及其语法,并且我们将介绍六个实际案例。
在查看每个示例之前,我们将指出我们正在使用哪些结构功能;尚未解释的新功能将以粗体显示。以下每个示例的复杂性都会不断增加。它们的排列是为了逐步向您介绍重要的公式功能。
这是您每次都会看到的基本结构:
这些示例涵盖从变量映射到复杂数组的主题:
首先,让我们了解如何使用公式创建自定义字段。在“结构”的右上角,所有列的末尾,有一个“+”图标 - 单击它。在出现的字段中,写下“公式...”并选择适当的项目。
让我们讨论一下保存公式。不幸的是,仍然无法将特定公式单独保存在某处(只能在笔记本中,就像我一样)。在 ALM Works 网络研讨会上,团队提到他们正在研究一组公式,但目前保存它们的唯一方法是保存整个视图以及公式。
当我们完成公式的工作后,我们需要单击结构的视图(它很可能标有蓝色星号),然后单击“保存”以覆盖当前视图。或者您可以单击“另存为...”来创建新视图。 (不要忘记将其提供给其他 Jira 用户,因为默认情况下新视图是私有的。)
该公式将保存到特定视图中的其余字段中,您可以在“查看详细信息”菜单的“高级”选项卡中看到它。
从版本 8.2 开始,Structure 现在能够通过 3 次快速单击来保存公式。
公式编辑窗口中提供了保存对话框。如果该窗口未打开,只需单击所需列中的三角形 ▼ 图标。
在编辑窗口中,我们看到“已保存列”字段,右侧有一个带有蓝色通知的图标,这意味着公式中的更改尚未保存。单击此图标并选择“另存为...”选项。
然后输入我们的列(公式)的名称并选择将其保存在哪个空间中。如果我们想将其保存在个人列表中,请选择“我的专栏”。 “全局”,以便公式将保存在常规列表中,结构的所有用户都可以在其中编辑。单击“保存”。
现在我们的公式已保存。我们可以将其加载到任何结构中或从任何地方重新保存它。通过重新保存公式,它将在使用该公式的所有结构中进行更新。
变量映射也与公式一起保存,但我们稍后会讨论映射。
现在,让我们继续我们的示例!
我们需要一个包含任务列表的表格,以及处理这些任务的开始和结束日期。我们还需要该表将其导出到单独的 Excel 甘特图。不幸的是,Jira 和 Structure 不知道如何立即提供此类日期。
开始日期和结束日期是过渡到特定状态的日期,在我们的例子中,这些日期是“进行中”和“已关闭”。我们需要获取这些日期,并将它们显示在单独的字段中(这是进一步导出到甘特图所必需的)。因此,我们将有两个字段(两个公式)。
使用的结构特征
代码示例
开始日期字段:
firstTransitionToStart
结束日期字段:
latestTransitionToDone
在本例中,代码是单个变量,firstTransitionToStart(用于开始日期字段)和latestTransitionToDone(用于第二个字段)。
现在让我们关注开始日期字段。我们的目标是获取任务转换到“进行中”状态的日期(这对应于任务的逻辑开始),因此变量的命名非常明确,以防止以后猜测,因为“第一次转换到开始”。
为了将日期变成变量,我们转向变量映射。让我们通过单击“保存”按钮来保存公式。
我们的变量出现在“变量”部分,旁边有一个感叹号。结构表明它无法将变量链接到 Jira 中的字段,我们必须自己完成(即映射它)。
点击变量,进入映射界面。选择字段或所需操作 - 查找操作“转换日期...”。为此,请在选择字段中写入“过渡”。您将立即获得多个选项,其中一个适合我们:“第一次过渡到进行中”。但为了演示映射的工作原理,我们选择“Transition Date ...”选项。
之后,您需要选择转换发生的状态以及此转换的顺序 - 第一个或最后一个。
选择或输入“状态” - “状态:进行中”(或工作流程中的相应状态),并在“转换” - “第一次转换到状态”中,因为任务工作的开始是第一个转换到相应的状态。
如果我们选择最初提出的选项“第一次过渡到进行中”而不是“过渡日期...”,那么结果几乎是相同的 - 结构将为我们选择必要的参数。唯一的问题是,我们将拥有“类别:进行中”,而不是“状态:进行中”。
让我注意一个重要特征:状态和类别是两个不同的东西。状态是一种特定的状态,它是明确的,但一个类别可以包含多个状态。只有三个类别:“待办事项”、“进行中”和“已完成”。在 Jira 中,它们通常分别用灰色、蓝色和绿色标记。该状态必须属于这些类别之一。
我建议在此类情况下指示特定状态,以避免与同一类别的状态混淆。例如,我们项目上的“待办事项”类别有两种状态,“开放”和“QA 队列”。
让我们回到我们的例子。
一旦我们选择了必要的选项,我们就可以单击“<返回变量列表”来完成firstTransitionToStart变量的映射选项。如果一切正确,我们会看到一个绿色的复选标记。
同时,在我们的自定义字段中,我们看到一些奇怪的数字,看起来根本不像日期。在我们的例子中,公式的结果将是firstTransitionToStart变量的值,它的值是自1970年1月以来的毫秒数。为了获得正确的日期,我们需要选择特定的公式显示格式。
格式选择位于编辑窗口的最底部。默认情况下选择“常规”。我们需要“日期/时间”来正确显示日期。
对于第二个字段,latestTransitionToDone,我们将执行相同的操作。唯一的区别是,在映射时我们已经可以选择“完成”类别,而不是状态(因为通常只有一个明确的任务完成状态)。我们选择“Latest Transition”作为转换参数,因为我们对最近到“Done”类别的转换感兴趣。
这两个字段的最终结果将如下所示。
现在让我们看看如何使用我们自己的显示格式来实现相同的结果。
我们对上一个示例中的日期显示格式不满意,因为我们需要一个特殊的甘特表 - “01.01.2022”。
让我们使用 Structure 中内置的函数来显示日期,并指定适合我们的格式。
使用的结构特点
代码示例
FORMAT_DATETIME(firstTransitionToStart;"dd.MM.yyyy")
开发人员提供了许多不同的函数,包括一个单独的函数,用于以我们自己的格式显示日期:FORMAT_DATETIME;这就是我们要使用的。该函数使用两个参数:日期和所需格式的字符串。
我们使用与上一个示例中相同的映射规则来设置firstTransitionToStart变量(第一个参数)。第二个参数是指定格式的字符串,我们这样定义它:“dd.MM.yyyy”。这对应于我们想要的形式“01.01.2022”。
因此,我们的公式将立即给出所需形式的结果。因此,我们可以在字段设置中保留“常规”选项。
包含工作结束日期的第二个字段以相同的方式完成。因此,结构应如下图所示。
原则上,使用公式语法没有重大困难。如果需要变量,写下它的名字;如果您需要一个函数,只需写下它的名称并传递参数(如果需要)。
当 Structure 遇到未知名称时,它会假设它是一个变量并尝试自行映射它,或者向我们寻求帮助。
顺便说一下,一个重要的注意事项:结构不区分大小写,因此firstTransitionToStart、firsttransitiontostart 和firSttrAnsItiontOStarT 是同一个变量。同样的规则也适用于函数。为了实现明确的代码风格,在示例中我们将尝试遵守 MSDN 的大写约定规则。
现在让我们深入研究语法并查看显示结果的特殊格式。
我们处理常规任务(任务、Bug 等)以及具有子任务的故事类型任务。在某些时候,我们需要找出员工在特定时期内执行的任务和子任务。
问题在于,许多子任务不提供有关故事本身的信息,因为它们被称为“处理故事”、“设置”或“激活效果”。如果我们请求一段时间内的任务列表,我们将得到十几个名为“处理故事”的任务,而没有任何其他有用的信息。
我们希望有一个列表视图,该列表分为两列:任务和父任务,以便将来可以按员工对此类列表进行分组。
在我们的项目中,当任务可以有父级时,我们有两种选择:
所以,我们必须:
为了简化信息的感知,我们将为任务类型的文本着色:即“[故事]”或“[史诗]”。
我们将使用什么:
代码示例
if( Parent.Issuetype = "Story"; """{color:green}[${Parent.Issuetype}]{color} ${Parent.Summary}"""; EpicLink; """{color:#713A82}[${EpicLink.Issuetype}]{color} ${EpicLink.EpicName}""" )
如果我们只需要输出一个字符串并在其中插入任务类型和名称,为什么公式以 if 条件开头?没有一些通用的方法来访问任务字段吗?是的,但是对于任务和史诗,这些字段的命名不同,您也需要以不同的方式访问它们,这是 Jira 的一个功能。
差异始于父搜索级别。对于子任务,父任务位于“Parent Issue”Jira 字段中,对于常规任务,史诗将是父任务,位于“Epic Link”字段中。因此,我们必须编写两个不同的选项来访问这些字段。
这就是我们需要 if 条件的地方。 Expr 语言有不同的处理条件的方式。它们之间的选择取决于品味。
有一个“类似excel”的方法:
if (condition1; result1; condition2; result2 … )
或者更“类似代码”的方法:
if condition1 : result1 else if condition2 : result2 else result3
在示例中,我使用了第一个选项;现在让我们以简化的方式看看我们的代码:
if( Parent.Issuetype = "Story"; Some kind of result 1; EpicLink; Some kind of result 2 )
我们看到两个明显的条件:
让我们弄清楚它们是做什么的,从第一个开始,Parent.Issuetype=”Story”。
在本例中,Parent 是一个自动映射到“Parent Issue”字段的变量。正如我们上面所讨论的,这就是子任务的父任务应该存在的地方。使用点符号 (.),我们访问该父级的属性,特别是 Issuetype 属性,它对应于“Issue Type”Jira 字段。事实证明,整个 Parent.Issuetype 行都会返回父任务的类型(如果存在这样的任务)。
此外,我们不必定义或映射任何内容,因为开发人员已经为我们尽力而为。例如,这里是语言中预定义的所有属性(包括 Jira 字段)的链接,在这里您可以看到所有标准变量的列表,也可以安全地访问这些变量而无需额外设置。
因此,第一个条件是查看父任务的类型是否为Story。如果不满足第一个条件,则父任务的类型不是故事,或者根本不存在。这就引出了第二个条件:EpicLink。
事实上,这是我们检查“Epic Link”Jira 字段是否已填写(即检查其存在)的时候。 EpicLink 变量也是标准变量,不需要映射。事实证明,如果任务有史诗链接,我们的条件就满足。
第三种选择是当不满足任何条件时,即该任务既没有父任务也没有 Epic Link。在这种情况下,我们不显示任何内容并将该字段留空。这是自动完成的,因为我们不会得到任何结果。
我们弄清楚了条件,现在让我们看看结果。在这两种情况下,它都是带有文本和特殊格式的字符串。
结果 1(如果父级是 Story):
"""{color:green}[${Parent.Issuetype}]{color} ${Parent.Summary}"""
结果 2(如果有 Epic Link):
"""{color:#713A82}[${EpicLink.Issuetype}]{color} ${EpicLink.EpicName}"""
两个结果在结构上相似:它们都由输出字符串开头和结尾的三引号“””、开头 {color: COLOR} 和结尾 {color} 块中的颜色规范以及通过$ 符号。三引号告诉结构内部将有变量、操作或格式化块(例如颜色)。
对于第一个条件的结果,我们:
因此,我们得到字符串“[Story] Some task name”。正如您可能已经猜到的,Summary 也是一个标准变量。为了使构造此类字符串的方案更加清晰,让我分享一张官方文档中的图片。
以类似的方式,我们收集第二个结果的字符串,但通过十六进制代码设置颜色。我发现史诗的颜色是“#713A82”(顺便说一句,您可以在评论中为史诗建议更准确的颜色)。不要忘记 Epic 发生变化的字段(属性)。使用“EpicName”代替“Summary”,使用“EpicLink”代替“Parent”。
因此,我们的公式方案可以表示为条件表。
条件:父任务存在,且其类型为Story。
结果:带有绿色类型的父任务及其名称的行。
条件:史诗链接字段已填写。
结果:与类型的史诗颜色及其名称相符。
默认情况下,该字段中选择“常规”显示选项,如果不更改它,结果将看起来像纯文本,而不更改颜色和识别块。如果将显示格式更改为“Wiki 标记”,文本将被转换。
现在,让我们来熟悉一下与 Jira 字段无关的变量——局部变量。
从前面的示例中,您了解到我们正在处理具有子任务的故事类型的任务。这产生了估计的特殊情况。为了获得 Story 分数,我们总结其子任务的分数,这些分数以抽象 Story 点的形式进行估计。
这种方法很不寻常,但对我们有用。因此,当故事没有估计但子任务有时,没有问题,但当故事和子任务都有估计时,结构中的标准选项“Σ 故事点”将无法正常工作。
这是因为 Story 的估计被添加到子任务的总和中。因此,故事中显示的金额错误。我们希望避免这种情况,并添加与 Story 中既定估计和子任务总和不一致的指示。
我们需要几个条件,因为这完全取决于 Story 中是否设置了估计。
所以条件是:
当 Story 没有估计值时,我们将子任务估计值的总和显示为橙色,以表明该值尚未在 Story 中设置
如果 Story 有一个估计,则检查它是否对应于子任务估计的总和:
这些条件的措辞可能会令人困惑,所以让我们用一个方案来表达它们。
使用的结构特点
代码示例
with isEstimated = storypoints != undefined: with childrenSum = sum#children{storypoints}: with isStory = issueType = "Story": with isErr = isStory AND childrenSum != storypoints: with color = if isStory : if isEstimated : if isErr : "red" else "green" else "orange": if isEstimated : """{color:$color}$storypoints{color} ${if isErr :""" ($childrenSum)"""}""" else """{color:$color}$childrenSum{color}"""
在深入研究代码之前,让我们将我们的方案转换为更“类似代码”的方式,以了解我们需要哪些变量。
从这个方案中我们看到我们需要:
条件变量:
文本颜色的一个变量- color
两个估计变量:
此外,颜色变量还取决于许多条件,例如,估计的可用性以及行中任务的类型(参见下面的方案)。
因此,为了确定颜色,我们需要另一个条件变量 isStory,它指示任务类型是否为 Story。
sp 变量(故事点)将是标准变量,这意味着它将自动映射到适当的 Jira 字段。我们应该自己定义其余的变量,它们对我们来说是本地的。
现在让我们尝试用代码实现这些方案。首先,让我们定义所有变量。
with isEstimated = storypoints != undefined: with childrenSum = sum#children{storypoints}: with isStory = issueType = "Story": with isErr = isStory AND childrenSum != storypoints:
这些行由相同的语法方案联合起来:with 关键字、变量名和行末尾的冒号“:”。
with 关键字用于表示局部变量(和自定义函数,但更多信息在单独的示例中)。它告诉公式接下来是一个不需要映射的变量。冒号“:”标志着变量定义的结束。
因此,我们创建了 isEstimated 变量(提醒一下,这种情况并不重要)。我们将在其中存储 1 或 0,具体取决于故事点字段是否已填充。 Storypoints 变量会自动映射,因为我们之前没有创建同名的局部变量(例如,使用 Storypoints = ... :)。
未定义变量表示某事物不存在(如其他语言中的 null、NaN 等)。因此,表达式 Storypoints != undefined 可以理解为一个问题:“故事点字段是否已填写?”。
接下来,我们应该确定所有子任务的故事点的总和。为此,我们创建一个局部变量:childrenSum。
with childrenSum = sum#children{storypoints}:
该总和是通过聚合函数计算的。 (您可以在官方文档中阅读类似的函数。)简而言之,Structure 可以通过任务执行各种操作,同时考虑到当前视图的层次结构。
我们使用 sum 函数,除此之外,我们还使用“#”符号传递澄清子任务,这将求和的计算限制为仅适用于当前行的任何子任务。在大括号中,我们表明我们想要总结哪个领域——我们需要对故事点进行估计。
下一个局部变量 isStory 存储一个条件:当前行中的任务类型是否为 Story。
with isStory = issueType = "Story":
我们转向问题类型变量,它与过去的示例很相似,即映射到所需字段的任务类型。我们这样做是因为它是一个标准变量,并且我们之前没有通过 with 定义它。
现在让我们定义 isErr 变量 - 它表示子任务总和与 Story 估计之间存在差异。
with isErr = isStory AND childrenSum != storypoints:
在这里,我们使用之前创建的 isStory 和 ChildrenSum 局部变量。要发出错误信号,我们需要同时满足两个条件:问题类型为 Story (isStory) 且 (AND) 子节点总和 (childrenSum) 不等于 (!=) 任务中的集合估计 (storypoints) )。就像在 JQL 中一样,我们可以在创建条件时使用链接词,例如 AND 或 OR。
请注意,对于每个局部变量,行末尾都有一个“:”符号。它应该位于最后,在定义变量的所有操作之后。例如,如果我们需要将一个变量的定义分成几行,那么冒号“:”只放在最后一个操作之后。就像颜色变量的例子一样——文本的颜色。
with color = if isStory : if isEstimated : if isErr : "red" else "green" else "orange":
这里我们看到了很多“:”,但是它们扮演着不同的角色。 if isStory 后面的冒号是 isStory 条件的结果。让我们回忆一下结构:if 条件:结果。让我们以更复杂的形式呈现这个结构,它定义了一个变量。
with variable = (if condition: (if condition2 : result2 else result3) ):
原来 if condition2 : result2 else result3 可以说是第一个条件的结果,最后有一个冒号“:”,就完成了变量的定义。
乍一看,颜色的定义可能看起来很复杂,但事实上,我们已经在示例开头描述了颜色定义方案。只是第一个条件的结果是,另一个条件开始——一个嵌套条件,以及其中的另一个条件。
但最终的结果与之前提出的方案略有不同。
if isEstimated : """{color:$color}$storypoints{color} ${if isErr :""" ($childrenSum)"""}""" else """{color:$color}$childrenSum{color}"""
我们不必像方案中那样在代码中写入两次“{color}$sp”;我们会变得更聪明。在分支中,如果任务有估计,我们将始终显示 {color: $color}$storypoints{color} (即,仅以所需颜色对故事点进行估计),如果有错误,则在空格之后,我们将用子任务估计的总和来补充该行:($childrenSum)。
如果没有错误,则不会添加。我还请大家注意,这里没有“:”符号,因为我们没有定义变量,而是通过条件显示最终结果。
我们可以在下图中的“ΣSP(mod)”字段中评估我们的工作。屏幕截图具体显示了两个附加字段:
在这些示例的帮助下,我们分析了结构语言的主要特征,这将帮助您解决大多数问题。现在让我们看一下两个更有用的功能:函数和数组。我们将了解如何创建我们自己的自定义函数。
有时,一个冲刺中有很多任务,我们可能会错过其中的微小变化。例如,我们可能会错过一个新的子任务或其中一个故事已进入下一阶段的事实。如果有一个工具可以通知我们任务中最新的重要变化,那就太好了。
我们对昨天以来发生的三种类型的任务状态变化感兴趣:我们开始处理任务、出现新任务、任务关闭。此外,看到任务以“不会做”的决议结束也会很有用。
为此,我们将创建一个包含一串表情符号的字段,这些表情符号负责最新的更改。例如,如果昨天创建了一个任务并且我们开始处理它,那么它将标有两个表情符号:“正在进行”和“新任务”。
如果可以显示多个附加字段(例如,转换到“进行中”状态的日期或单独的“解决方案”字段),为什么我们需要这样的自定义字段?答案很简单——人们感知表情符号比文本更容易、更快,而文本位于不同的领域,需要进行分析。该公式会将所有内容收集到一个地方并为我们进行分析,这将节省我们的精力和时间来处理更有用的事情。
让我们确定不同的表情符号将负责什么:
使用的结构特点
代码示例
if defined(issueType): with now = now(): with daysScope = 1.3: with workDaysBetween(today, from)= ( with weekends = (Weeknum(today) - Weeknum(from)) * 2: HOURS_BETWEEN(from;today)/24 - weekends ): with daysAfterCreated = workDaysBetween(now,created): with daysAfterStart = workDaysBetween(now,latestTransitionToProgress): with daysAfterDone = workDaysBetween(now, resolutionDate): with isWontDo = resolution = "Won't Do": with isRecentCreated = daysAfterCreated >= 0 and daysAfterCreated <= daysScope and not(resolution): with isRecentWork = daysAfterStart >= 0 and daysAfterStart <= daysScope : with isRecentDone = daysAfterDone >= 0 and daysAfterDone <= daysScope : concat( if isRecentCreated : "*️⃣", if isRecentWork : "🚀", if isRecentDone : "✅", if isWontDo : "❌")
解决方案分析
首先,让我们考虑一下确定我们感兴趣的事件所需的全局变量。我们需要知道,从昨天开始:
将现有变量与新映射变量一起使用将帮助我们检查所有这些条件。
让我们继续看代码。第一行以检查任务类型是否存在的条件开头。
if defined(issueType):
这是通过内置定义的函数完成的,该函数检查指定字段是否存在。进行检查以优化公式的计算。
如果该行不是任务,我们不会加载带有无用计算的结构。事实证明,if 之后的所有代码都是结果,我的意思是 if (condition : result) 构造的第二部分。如果不满足条件,则代码也将无法工作。
还需要下一行 now = now(): 来优化计算。在代码中,我们必须将不同的日期与当前日期进行多次比较。为了避免多次执行相同的计算,我们将计算该日期一次并将其设为局部变量。
如果能把我们的“昨天”分开保存也不错。方便的“昨天”凭经验变成了1.3天。让我们把它变成一个变量:with daysScope = 1.3:。
现在我们需要多次计算两个日期之间的天数。例如,当前日期和工作开始日期之间。当然,有一个内置的 DAYS_BETWEEN 函数,它似乎适合我们。但是,例如,如果任务是在周五创建的,那么周一我们将不会看到新任务的通知,因为实际上已经过去了 1.3 天以上。另外,DAYS_BETWEEN函数只统计总天数(即0.5天会变成0天),这也不适合我们。
我们提出了一个要求——我们需要计算这些日期之间的确切工作日数;自定义函数将帮助我们做到这一点。
它的定义语法与定义局部变量的语法非常相似。唯一的区别和唯一的补充是第一个括号中参数的可选枚举。第二个括号包含调用函数时将执行的操作。该函数的定义并不是唯一可能的定义,但我们将使用这个(其他可以在官方文档中找到)。
with workDaysBetween(today, from)= ( with weekends = (Weeknum(today) - Weeknum(from)) * 2: HOURS_BETWEEN(from;today)/24 - weekends ):
我们的自定义 workDaysBetween 函数将计算今天和起始日期之间的工作日,这些日期作为参数传递。该函数的逻辑非常简单:我们计算休假天数,然后从日期之间的总天数中减去它们。
要计算休息天数,我们需要找出从今天到今天已经过去了多少周。为此,我们计算每周数字之间的差异。我们将从 Weeknum 函数获取此数字,该函数为我们提供从年初开始的周数。将这个差值乘以二,我们就得到了休假天数。
接下来,HOURS_BETWEEN 函数计算日期之间的小时数。我们将结果除以 24 以获得天数,然后从该数字中减去休息天数,从而获得日期之间的工作日。
使用我们的新函数,让我们定义一堆辅助变量。请注意,定义中的某些日期是全局变量,我们在示例开头讨论过。
with daysAfterCreated = workDaysBetween(now,created): with daysAfterStart = workDaysBetween(now,latestTransitionToProgress): with daysAfterDone = workDaysBetween(now, resolutionDate):
为了使代码方便阅读,我们定义存储条件结果的变量。
with isWontDo = resolution = "Won't Do": with isRecentCreated = daysAfterCreated >= 0 and daysAfterCreated <= daysScope and not(resolution): with isRecentWork = daysAfterStart >= 0 and daysAfterStart <= daysScope : with isRecentDone = daysAfterDone >= 0 and daysAfterDone <= daysScope :
对于 isRecentCreated 变量,我添加了一个可选条件和 not(分辨率),这有助于我简化未来的行,因为如果任务已经关闭,那么我对有关其最近创建的信息不感兴趣。
最终结果是通过 concat 函数连接各行来构造的。
concat( if isRecentCreated : "*️⃣", if isRecentWork : "🚀", if isRecentDone : "✅", if isWontDo : "❌")
事实证明,只有当条件中的变量等于 1 时,表情符号才会出现在行中。因此,我们的行可以同时显示任务的独立更改。
我们已经谈到了计算不带休息日的工作日的主题。还有一个与此相关的问题,我们将在上一个示例中对其进行分析,同时熟悉数组。
有时我们想知道任务已经运行了多长时间,不包括休息日。例如,为了分析已发布的版本,这是必要的。了解为什么我们需要休息日。除了一个是从周一到周四运行,另一个是从周五到周一运行。在这种情况下,我们不能说任务是相同的,尽管日历天数的差异告诉我们相反的情况。
不幸的是,“开箱即用”的结构不知道如何忽略休息日,并且无论 Jira 设置如何,具有“状态时间...”选项的字段都会生成结果 - 即使周六和周日被指定为休息日。
因此,我们的目标是计算准确的工作日数,忽略休息日,并考虑状态转换对此时间的影响。
地位与它有什么关系?让我来回答一下。假设我们计算出3月10日到3月20日期间,该任务工作了三天。但这三天里,暂停了一天,审稿了一天半。原来这个任务只工作了半天。
由于状态之间切换的问题,上一个示例的解决方案不适合我们,因为自定义 workDaysBetween 函数仅考虑两个选定日期之间的时间。
这个问题可以通过不同的方式解决。示例中的方法在性能方面是最昂贵的,但在计算休息日和状态方面是最准确的。请注意,其实现仅适用于 7.4(2021 年 12 月)之前的 Structure 版本。
因此,该公式背后的想法如下:
因此,我们将获得任务的准确工作时间,忽略休息日和额外状态之间的转换。
使用的结构特点
代码示例
if defined(issueType) : if status != "Open" : with finishDate = if toQA != Undefined : toQA else if toDone != Undefined : toDone else now(): with startDate = DEFAULT(toProgress, toDone): with statusWeekendsCount(dates, status) = ( dates.filter(x -> weekday(x) > 5 and historical_value(this,"status",x)=status).size() ): with overallDays = round(hours_between(startDate,finishDate)/24): with sequenceArray = SEQUENCE(0,overallDays): with datesArray = sequenceArray.map(DATE_ADD(startDate,$,"day")): with progressWeekends = statusWeekendsCount(datesArray, "in Progress"): with progressDays = (timeInProgress/86400000 - progressWeekends).round(1): with color = if( progressDays = 0 ; "gray" ; progressDays > 0 and progressDays <= 2.5; "green" ; progressDays > 2.5 and progressDays <= 4; "orange" ; progressDays > 4; "red" ): """{color:$color}$progressDays d{color}"""
解决方案分析
在将我们的算法转移到代码之前,让我们方便地计算结构。
if defined(issueType) : if status != "Open" :
如果该行不是任务或其状态为“打开”,那么我们将跳过这些行。我们只对已经启动的任务感兴趣。
要计算日期之间的天数,我们必须首先确定这些日期:完成日期和开始日期。
with finishDate = if toQA != Undefined : toQA else if toDone != Undefined : toDone else now(): with startDate = DEFAULT(toProgress, toDone):
我们假设任务完成日期 (finishDate) 为:
工作开始日期 startDate 由转换到“进行中”状态的日期确定。在某些情况下,任务会在没有进入工作阶段的情况下关闭。在这种情况下,我们将结束日期视为开始日期,因此结果为 0 天。
您可能已经猜到,toQA、toDone 和 toProgress 是需要映射到适当状态的变量,如第一个和前面的示例中所示。
我们还看到了新的 DEFAULT(toProgress, toDone) 函数。它检查 toProgress 是否有值,如果没有,则使用 toDone 变量的值。
接下来是 statusWeekendsCount 自定义函数的定义,但我们稍后会返回它,因为它与日期列表密切相关。最好直接看这个列表的定义,以便稍后我们可以理解如何将我们的函数应用于它。
我们想要获取以下形式的日期列表:[startDate (假设 11.03), 12.03, 13.03, 14.03 … finishDate]。在 Structure 中没有一个简单的函数可以为我们完成所有工作。那么让我们使用一个技巧:
现在,让我们看看如何在代码中实现它。我们将使用数组。
with overallDays = round(hours_between(startDate,finishDate)/24): with sequenceArray = SEQUENCE(0,overallDays): with datesArray = sequenceArray.map(DATE_ADD(startDate,$,"day")):
我们计算完成一项任务需要多少天。如前面的示例所示,通过除以 24 和 hours_ Between(startDate,finishDate) 函数。结果写入overallDays 变量中。
我们以sequenceArray 变量的形式创建一个数字序列数组。该数组是通过 SEQUENCE(0,overallDays) 函数构造的,该函数简单地使用从 0 到overallDays 的序列创建所需大小的数组。
接下来是魔术。数组函数之一是map。它将指定的操作应用于数组的每个元素。
我们的任务是为每个数字添加开始日期(即当天的数字)。 DATE_ADD 函数可以做到这一点,它在指定的日期上添加一定数量的天数、月数或年数。
知道了这一点,让我们解密该字符串:
with datesArray = sequenceArray.map(DATE_ADD(startDate, $,"day"))
对于sequenceArray中的每个元素,.map()函数应用DATE_ADD(startDate, $, “day”)。
让我们看看 DATE_ADD 的参数中传递了什么。第一个是 startDate,即添加所需数字的日期。这个数字是由第二个参数指定的,但我们看到$。
$符号表示数组元素。该结构了解 DATE_ADD 函数应用于数组,因此将有所需的数组元素(即 0、1、2 …)而不是 $。
最后一个参数“day”表示我们添加一天,因为该函数可以根据我们指定的内容添加日期、月份和年份。
因此,datesArray 变量将存储从工作开始到完成的日期数组。
让我们回到我们错过的自定义函数。它将过滤掉额外的天数并计算剩余的天数。在分析代码之前,我们在示例的一开始就描述了该算法,即在第 3 段和第 4 段中关于过滤休息日和状态的内容。
with statusWeekendsCount(dates, status) = ( dates.filter(x -> weekday(x) > 5 and historical_value(this,"status",x)=status).size() ):
我们将向自定义函数传递两个参数:日期数组(我们称之为日期)和所需的状态 - status。我们将 .filter() 函数应用于传输的日期数组,该数组仅保留数组中通过过滤条件的记录。在我们的例子中,有两个,它们通过 and 组合起来。在过滤器之后,我们看到.size(),它在完成所有操作后返回数组的大小。
如果我们简化表达式,我们会得到这样的结果:array.filter(condition1 and condition2).size()。那么,结果,我们就得到了适合我们的休息天数,也就是那些通过条件的休息天数。
让我们仔细看看这两个条件:
x -> weekday(x) > 5 and historical_value(this,"status",x)=status
表达式 x -> 只是过滤器语法的一部分,表示我们将调用数组 x 的元素。因此,x 出现在每个条件中(类似于 $ 的情况)。事实证明,x 是传输的日期数组中的每个日期。
第一个条件 weekday(x) > 5 要求日期 x(即每个元素)的工作日大于 5 — 要么是星期六 (6),要么是星期日 (7)。
第二个条件使用历史值。
historical_value(this,"status",x) = status
这是Structure 7.4 版本的一个功能。
该函数访问任务的历史记录并在指定字段中搜索特定日期。在本例中,我们在“状态”字段中搜索日期 x。 this 变量只是函数语法的一部分,它会自动映射并代表该行中的当前任务。
因此,在条件中,我们将传输的状态参数和“status”字段进行比较,该字段由history_value 函数为数组中的每个日期x 返回。如果它们匹配,则该条目保留在列表中。
最后一步是使用我们的函数来计算处于所需状态的天数:
with progressWeekends = statusWeekendsCount(datesArray, "in Progress"): with progressDays = (timeInProgress/86400000 - progressWeekends).round(1):
首先,让我们看看我们的datesArray 中有多少天处于“进行中”状态的休假。也就是说,我们将日期列表和所需的状态传递给自定义函数 statusWeekendsCount。该函数会删除任务状态与“进行中”状态不同的所有工作日和所有休息日,并返回列表中剩余的天数。
然后我们从 timeInProgress 变量中减去这个金额,我们通过“Time in status ...”选项映射该变量。
数字 86400000 是将毫秒变为天的除数。需要使用 .round(1) 函数将结果四舍五入到十分位,例如“4.1”,否则您可能会得到这种类型的条目:“4.0999999 …”。
为了指示任务的长度,我们引入了颜色变量。我们将根据任务花费的天数来更改它。
with color = if( progressDays = 0 ; "gray" ; progressDays > 0 and progressDays <= 2.5; "green" ; progressDays > 2.5 and progressDays <= 4; "orange" ; progressDays > 4; "red" ):
最后一行包含计算天数的结果:
"""{color:$color}$progressDays d{color}"""
我们的结果如下图所示。
顺便说一下,在同一个公式中,您可以显示任何状态的时间。例如,如果我们将“Pause”状态传递给自定义函数,并通过“Time in ... — Pause”映射 timeInProgress 变量,那么我们将计算暂停的确切时间。
您可以组合状态并创建一个条目,例如“wip: 3.2d | rev: 12d”,即计算工作时间和复习时间。您仅受您的想象力和工作流程的限制。
我们介绍了这种公式语言的详尽功能,这些功能将帮助您做类似的事情或编写一些全新且有趣的东西来分析 Jira 任务。
我希望这篇文章可以帮助您找出公式,或者至少让您对这个主题感兴趣。我并不声称我拥有“最好的代码和算法”,所以如果您有关于如何改进示例的想法,我将很高兴您分享它们!
当然,您需要了解,没有人比 ALM Works 开发人员更能告诉您公式。因此,我附上了他们的文档和网络研讨会的链接。如果您开始使用自定义字段,请经常查看它们以了解您可以使用哪些其他功能。