O plugin Structure para Jira é muito útil para o trabalho diário com tarefas e sua análise; ele leva a visualização e a estruturação dos tickets do Jira a um novo nível e faz tudo isso imediatamente.
E nem todo mundo sabe disso, mas a funcionalidade das fórmulas de estrutura pode simplesmente te surpreender. Usando fórmulas, você pode criar tabelas extremamente úteis que podem simplificar muito o trabalho com tarefas e, o mais importante, são úteis para realizar uma análise mais profunda de lançamentos, épicos e projetos.
Neste artigo você verá como criar suas próprias fórmulas, começando pelos exemplos mais simples e terminando com casos complexos, mas bastante úteis.
Então, para quem é este texto? Alguém pode se perguntar: por que escrever um artigo quando a documentação oficial no site do ALM Works está ali, esperando que os leitores se aprofundem. Isso é verdade. Porém, sou uma daquelas pessoas que não tinha a menor noção de que o Structure escondia funcionalidades tão amplas: “Espere, isso sempre foi uma opção?!” Essa constatação me fez pensar: pode haver outras pessoas que ainda não sabem o tipo de coisas que podem fazer com fórmulas e estrutura.
Este artigo também será útil para quem já está familiarizado com fórmulas. Você aprenderá algumas opções práticas interessantes para usar campos personalizados e, talvez, pegar emprestado alguns deles para seus projetos . A propósito, se você tiver algum exemplo interessante de sua autoria, ficarei feliz se você compartilhá-lo nos comentários .
Cada exemplo é analisado detalhadamente, desde a descrição do problema até a explicação do código, com profundidade suficiente para que não restem dúvidas. É claro que, junto com as explicações, cada exemplo é ilustrado por um código que você pode experimentar por si mesmo, sem se aprofundar na análise.
Se você não tem vontade de ler, mas tem interesse em fórmulas, confira os webinars do ALM Works . Explicam o básico em 40 minutos; as informações são apresentadas ali de forma muito compactada.
Você não precisa de nenhum conhecimento adicional para entender os exemplos, então quem já trabalhou com Jira e Structure poderá repetir os exemplos em suas tabelas sem problemas.
Os desenvolvedores forneceram uma sintaxe bastante flexível com sua linguagem Expr. Basicamente, a filosofia aqui é “escreva como quiser e vai dar certo”.
Então vamos começar!
Então, por que iríamos querer usar fórmulas? Bem, às vezes acontece que não temos campos padrão Jira suficientes, como “Destinatário”, “Pontos de história” e assim por diante. Ou precisamos calcular algum valor para determinados campos, exibir a capacidade restante por versão e descobrir quantas vezes a tarefa mudou de status. Talvez até queiramos mesclar vários campos em um para facilitar a leitura da nossa Estrutura.
Para resolver esses problemas, precisamos de fórmulas e as usaremos para criar campos personalizados.
A primeira coisa que precisamos fazer é entender como funciona uma fórmula. Isso nos permite aplicar algum tipo de operação a uma string. Como estamos carregando muitas tarefas na estrutura, a fórmula é aplicada a cada linha da tabela inteira. Normalmente, todas as suas operações visam trabalhar com tarefas nessas linhas.
Assim, se solicitarmos que a fórmula exiba algum campo do Jira, por exemplo, “Cessionário”, então a fórmula será aplicada para cada tarefa, e teremos outra coluna “Cessionário”.
As fórmulas consistem em várias entidades básicas:
Conheceremos melhor as fórmulas e sua sintaxe através de alguns exemplos, e passaremos por seis casos práticos.
Antes de ver cada exemplo, indicaremos quais recursos de Estrutura estamos usando; novos recursos que ainda não foram explicados estarão em negrito. Cada um dos exemplos a seguir terá um nível crescente de complexidade. Eles estão organizados para apresentar gradualmente os recursos importantes da fórmula.
Aqui está a estrutura básica que você verá sempre:
Estes exemplos cobrem tópicos que vão desde mapeamento de variáveis até arrays complexos:
Primeiro, vamos descobrir como criar campos personalizados com fórmulas. Na parte superior direita da Estrutura, ao final de todas as colunas, existe um ícone “+” — clique nele. No campo que aparece, escreva “Fórmula…” e selecione o item apropriado.
Vamos discutir como salvar uma fórmula. Infelizmente, ainda não é possível salvar uma fórmula específica separadamente em algum lugar (apenas no seu caderno, como eu). No webinar do ALM Works, a equipe mencionou que está trabalhando em um banco de fórmulas, mas por enquanto a única maneira de salvá-las é salvar a visualização inteira junto com a fórmula.
Quando terminarmos de trabalhar em uma fórmula, precisamos clicar na visualização da nossa estrutura (ela provavelmente estará marcada com um asterisco azul) e clicar em “Salvar” para substituir a visualização atual. Ou você pode clicar em “Salvar como…” para criar uma nova visualização. (Não se esqueça de disponibilizá-lo para outros usuários do Jira, pois as novas visualizações são privadas por padrão.)
A fórmula será salva no restante dos campos em uma visualização específica, e você poderá vê-la na guia “Avançado” do menu “Visualizar detalhes”.
A partir da versão 8.2, o Structure agora tem a capacidade de salvar fórmulas em 3 cliques rápidos.
A caixa de diálogo salvar está disponível na janela de edição de fórmulas. Caso esta janela não esteja aberta, basta clicar no ícone do triângulo ▼ na coluna desejada.
Na janela de edição vemos o campo “Coluna Salva”, à direita há um ícone com uma notificação azul, o que significa que as alterações na fórmula não foram salvas. Clique neste ícone e selecione a opção “Salvar como…”.
Em seguida, insira os nomes da nossa coluna (fórmula) e escolha em qual espaço salvá-la. “Minhas Colunas” se quisermos salvá-lo em uma lista pessoal. “Global”, para que a fórmula seja salva na lista geral, onde poderá ser editada por todos os usuários da sua Estrutura. Clique em “Salvar”.
Agora nossa fórmula está salva. Podemos carregá-lo em qualquer estrutura ou salvá-lo novamente em qualquer lugar. Ao salvar novamente a fórmula, ela será atualizada em todas as estruturas em que for utilizada.
O mapeamento de variáveis também é salvo com a fórmula, mas falaremos sobre mapeamento mais tarde.
Agora, vamos aos nossos exemplos!
Precisamos de uma tabela com uma lista de tarefas, bem como as datas de início e término para trabalhar nessas tarefas. Também precisamos da tabela para exportá-la para um Excel separado. Infelizmente, Jira e Structure não sabem como fornecer essas datas imediatamente.
As datas de início e término são as datas de transição para status específicos, no nosso caso são “Em Andamento” e “Fechado”. Precisamos pegar essas datas e exibir cada uma delas em um campo separado (isso é necessário para posterior exportação para Gantt). Então, teremos dois campos (duas fórmulas).
Os recursos de estrutura usados
Um exemplo de código
Campo para a data de início:
firstTransitionToStart
Campo para a data final:
latestTransitionToDone
Nesse caso, o código é uma variável única, firstTransitionToStart, para o campo de data de início, e lastTransitionToDone para o segundo campo.
Vamos nos concentrar no campo de data de início por enquanto. Nosso objetivo é obter a data em que a tarefa passou para o status “Em andamento” (isso corresponde ao início lógico da tarefa), de modo que a variável seja nomeada, de forma bastante explícita, para evitar a necessidade de adivinhações posteriores, como “primeira transição para começar".
Para transformar uma data em uma variável, recorremos ao mapeamento de variáveis. Vamos salvar nossa fórmula clicando no botão “Salvar”.
Nossa variável apareceu na seção “Variáveis”, com um ponto de exclamação ao lado. A estrutura indica que ela não pode vincular uma variável a um campo no Jira e teremos que fazer isso nós mesmos (ou seja, mapeá-la).
Clique na variável e vá para a interface de mapeamento. Selecione o campo ou operação necessária — procure a operação “Data de Transição…”. Para fazer isso, escreva “transição” no campo de seleção. Serão oferecidas várias opções ao mesmo tempo, e uma delas nos convém: “Primeira Transição para Em Andamento”. Mas para demonstrar como funciona o mapeamento, vamos escolher a opção “Data de Transição…”.
Depois disso, você precisa escolher o status em que ocorreu a transição e a ordem dessa transição - a primeira ou a última.
Selecione ou entre em “Status” — “Status: Em Andamento” (ou o status correspondente em seu Workflow), e em “Transição” — “Primeira transição para status”, já que o início do trabalho em uma tarefa é a primeira transição para o status correspondente.
Se em vez de “Data de Transição…” escolhêssemos a opção inicialmente proposta “Primeira Transição para Em Andamento”, então o resultado seria quase o mesmo — a Estrutura escolheria os parâmetros necessários para nós. A única coisa é que, em vez de “Status: Em Andamento”, teríamos “Categoria: Em Andamento”.
Deixe-me observar uma característica importante: um status e uma categoria são duas coisas diferentes. Um status é um status específico, não é ambíguo, mas uma categoria pode incluir vários status. Existem apenas três categorias: “A fazer”, “Em andamento” e “Concluído”. No Jira, eles geralmente são marcados com as cores cinza, azul e verde, respectivamente. O status deve pertencer a uma dessas categorias.
Recomendo indicar um status específico em casos como este para evitar confusão com status da mesma categoria. Por exemplo, temos dois status da categoria “To Do” no projeto, “Aberto” e “Fila de controle de qualidade”.
Voltemos ao nosso exemplo.
Depois de selecionar as opções necessárias, podemos clicar em “<Voltar à lista de variáveis” para completar as opções de mapeamento da variável firstTransitionToStart. Se fizermos tudo certo, veremos uma marca de seleção verde.
Ao mesmo tempo, em nosso campo personalizado, vemos alguns números estranhos que não se parecem em nada com uma data. No nosso caso, o resultado da fórmula será o valor da variável firstTransitionToStart, e seu valor é em milissegundos desde janeiro de 1970. Para obter a data correta, precisamos escolher um formato específico de exibição da fórmula.
A seleção do formato está localizada na parte inferior da janela de edição. “Geral” está selecionado lá por padrão. Precisamos de “Data/Hora” para exibir a data corretamente.
Para o segundo campo, lastTransitionToDone, faremos o mesmo. A única diferença é que no mapeamento já podemos selecionar a categoria “Concluído”, e não o status (já que normalmente há apenas um status inequívoco de conclusão da tarefa). Selecionamos “Latest Transition” como parâmetro de transição, pois estamos interessados na transição mais recente para a categoria “Done”.
O resultado final para os dois campos ficará assim.
Agora vamos ver como conseguir o mesmo resultado, mas com nosso próprio formato de exibição.
Não estamos satisfeitos com o formato de exibição da data do exemplo anterior, pois precisamos de um formato especial para a tabela Gantt — “01.01.2022”.
Vamos exibir as datas utilizando as funções integradas na Estrutura, especificando o formato que nos convém.
Recursos de estrutura usados
Um exemplo de código
FORMAT_DATETIME(firstTransitionToStart;"dd.MM.yyyy")
Os desenvolvedores forneceram muitas funções diferentes, incluindo uma função separada para exibir a data em nosso próprio formato: FORMAT_DATETIME; é isso que vamos usar. A função usa dois argumentos: uma data e uma string no formato desejado.
Configuramos a variável firstTransitionToStart (primeiro argumento) usando as mesmas regras de mapeamento do exemplo anterior. O segundo argumento é uma string que especifica o formato, e nós o definimos assim: “dd.MM.yyyy”. Isso corresponde ao formulário que desejamos, “01.01.2022”.
Assim, nossa fórmula dará imediatamente o resultado na forma desejada. Assim, podemos manter a opção “Geral” nas configurações do campo.
O segundo campo com a data de término da obra é feito da mesma forma. Como resultado, a estrutura deve ficar como na imagem abaixo.
Em princípio, não há dificuldades significativas em trabalhar com a sintaxe das fórmulas. Se precisar de uma variável, escreva seu nome; se você precisar de uma função, novamente, basta escrever seu nome e passar os argumentos (se forem necessários).
Quando o Structure encontra um nome desconhecido, ele assume que é uma variável e tenta mapeá-lo sozinho ou nos pede ajuda.
A propósito, uma observação importante: a estrutura não diferencia maiúsculas de minúsculas, então firstTransitionToStart, firsttransitiontostart e firSttrAnsItiontOStarT são a mesma variável. A mesma regra se aplica às funções. Para obter um estilo de código inequívoco, nos exemplos tentaremos aderir às regras das Convenções de Capitalização do MSDN.
Agora vamos nos aprofundar na sintaxe e examinar um formato especial para exibir o resultado.
Trabalhamos com tarefas regulares (Tarefa, Bug, etc.) e com tarefas do tipo História que possuem subtarefas. Em algum momento, precisamos descobrir em quais tarefas e subtarefas o funcionário trabalhou durante determinado período.
O problema é que muitas subtarefas não fornecem informações sobre a história em si, pois são chamadas de “trabalhar na história”, “montar” ou, por exemplo, “ativar o efeito”. E se solicitarmos uma lista de tarefas para um determinado período, receberemos uma dezena de tarefas com o nome “trabalhar na história” sem qualquer outra informação útil.
Gostaríamos de ter uma visão com uma lista dividida em duas colunas: uma tarefa e uma tarefa pai, para que futuramente fosse possível agrupar tal lista por funcionários.
Em nosso projeto, temos duas opções quando uma tarefa pode ter um pai:
Então, devemos:
Para simplificar a percepção da informação, iremos colorir o texto do tipo de tarefa: ou seja, “[História]” ou “[Épico]”.
O que usaremos:
Um exemplo de código
if( Parent.Issuetype = "Story"; """{color:green}[${Parent.Issuetype}]{color} ${Parent.Summary}"""; EpicLink; """{color:#713A82}[${EpicLink.Issuetype}]{color} ${EpicLink.EpicName}""" )
Por que a fórmula começa com uma condição if, se precisamos apenas gerar uma string e inserir o tipo e o nome da tarefa lá? Não existe uma maneira universal de acessar campos de tarefas? Sim, mas para tarefas e épicos, esses campos têm nomes diferentes e você também precisa acessá-los de forma diferente, esse é um recurso do Jira.
As diferenças começam no nível da pesquisa pai. Para uma subtarefa, o pai fica no campo “Problema pai” do Jira, e para uma tarefa normal, o épico será o pai, localizado no campo “Link do épico”. Assim, teremos que escrever duas opções diferentes para acessar esses campos.
É aqui que precisamos de uma condição if. A linguagem Expr possui diferentes maneiras de lidar com condições. A escolha entre eles é uma questão de gosto.
Existe um método “semelhante ao Excel”:
if (condition1; result1; condition2; result2 … )
Ou um método mais “parecido com código”:
if condition1 : result1 else if condition2 : result2 else result3
No exemplo, usei a primeira opção; agora vamos analisar nosso código de forma simplificada:
if( Parent.Issuetype = "Story"; Some kind of result 1; EpicLink; Some kind of result 2 )
Vemos duas condições óbvias:
Vamos descobrir o que eles fazem e começar com o primeiro, Parent.Issuetype=”Story”.
Neste caso, Pai é uma variável que é mapeada automaticamente para o campo “Problema Pai”. É aqui que, como discutimos acima, o pai da subtarefa deve residir. Utilizando a notação de ponto (.), acessamos a propriedade deste pai, em particular, a propriedade Issuetype, que corresponde ao campo “Tipo de Problema” do Jira. Acontece que toda a linha Parent.Issuetype nos retorna o tipo da tarefa pai, se tal tarefa existir.
Além disso, não tivemos que definir ou mapear nada, pois os desenvolvedores já fizeram o melhor por nós. Aqui, por exemplo, está um link para todas as propriedades (incluindo campos Jira) que são predefinidas na linguagem, e aqui você pode ver uma lista de todas as variáveis padrão, que também podem ser acessadas com segurança sem configurações adicionais.
Assim, a primeira condição é verificar se o tipo da tarefa pai é História. Se a primeira condição não for satisfeita, então o tipo da tarefa pai não é História ou não existe. E isso nos leva à segunda condição: EpicLink.
Na verdade, é nesse momento que verificamos se o campo “Epic Link” do Jira está preenchido (ou seja, verificamos sua existência). A variável EpicLink também é padrão e não precisa ser mapeada. Acontece que nossa condição será satisfeita se a tarefa tiver Epic Link.
E a terceira opção é quando nenhuma das condições é atendida, ou seja, a tarefa não tem pai nem Epic Link. Neste caso, não exibimos nada e deixamos o campo vazio. Isso é feito automaticamente, pois não obteremos nenhum dos resultados.
Descobrimos as condições, agora vamos aos resultados. Em ambos os casos, é uma string com texto e formatação especial.
Resultado 1 (se o pai for Story):
"""{color:green}[${Parent.Issuetype}]{color} ${Parent.Summary}"""
Resultado 2 (se houver Epic Link):
"""{color:#713A82}[${EpicLink.Issuetype}]{color} ${EpicLink.EpicName}"""
Ambos os resultados são semelhantes em estrutura: ambos consistem em aspas triplas “”” no início e no final da string de saída, especificação de cor nos blocos de abertura {color: COLOR} e fechamento {color}, bem como operações feitas através do Símbolo $. As aspas triplas informam à estrutura que dentro dela haverá variáveis, operações ou blocos de formatação (como cores).
Para o resultado da primeira condição, temos:
Assim, obtemos a string “[Story] Algum nome de tarefa”. Como você deve ter adivinhado, Resumo também é uma variável padrão. Para tornar mais claro o esquema de construção de tais strings, deixe-me compartilhar uma imagem da documentação oficial.
De forma semelhante, coletamos a string para o segundo resultado, mas definimos a cor por meio do código hexadecimal. Descobri que a cor do épico era “#713A82” (nos comentários, aliás, você pode sugerir uma cor mais precisa para o épico). Não se esqueça dos campos (propriedades) que mudam para o Epic. Em vez de “Resumo”, use “EpicName”, em vez de “Parent”, use “EpicLink”.
Como resultado, o esquema da nossa fórmula pode ser representado como uma tabela de condições.
Condição: a tarefa pai existe e seu tipo é História.
Resultado: Linha com tipo verde de tarefa pai e seu nome.
Condição: O campo Epic Link está preenchido.
Resultado: Linha com a cor épica do tipo e seu nome.
Por padrão, a opção de exibição “Geral” está selecionada no campo e, se não for alterada, o resultado ficará como texto simples, sem alteração de cor e identificação dos blocos. Se você alterar o formato de exibição para “Wiki Markup”, o texto será transformado.
Agora, vamos nos familiarizar com variáveis que não estão relacionadas aos campos do Jira — variáveis locais.
No exemplo anterior, você aprendeu que estamos trabalhando com tarefas do tipo História, que possuem subtarefas. Isto dá origem a um caso especial com estimativas. Para obter uma pontuação de história, resumimos as pontuações de suas subtarefas, que são estimadas em pontos abstratos de história.
A abordagem é incomum, mas funciona para nós. Portanto, quando a História não tem estimativa, mas as subtarefas têm, não há problema, mas quando tanto a História quanto as subtarefas têm estimativa, a opção padrão da Estrutura, “Σ Pontos da História”, funciona incorretamente.
Isso ocorre porque a estimativa da História é somada à soma das subtarefas. Como resultado, o valor errado é exibido na história. Gostaríamos de evitar isso e adicionar uma indicação de inconsistência com a estimativa estabelecida no Story e na soma das subtarefas.
Precisamos de várias condições, pois tudo depende se a estimativa está definida no Story.
Então as condições são:
Quando o Story não possui estimativa , exibimos a soma das estimativas das subtarefas em laranja para indicar que esse valor ainda não foi definido no Story
Se Story tiver uma estimativa , verifique se ela corresponde à estimativa da soma das subtarefas:
A formulação destas condições pode ser confusa, por isso vamos expressá-las num esquema.
Recursos de estrutura usados
Um exemplo de código
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}"""
Antes de mergulhar no código, vamos transformar nosso esquema em uma forma mais “parecida com código” para entender quais variáveis precisamos.
A partir deste esquema, vemos que precisaremos de:
Variáveis de condição:
Uma variável de cor do texto – cor
Duas variáveis de estimativa:
Além disso, a variável cor também depende de uma série de condições, por exemplo, da disponibilidade de um orçamento e do tipo de tarefa na linha (ver esquema abaixo).
Portanto, para determinar a cor, precisaremos de outra variável de condição, isStory, que indica se o tipo de tarefa é Story.
A variável sp (storypoints) será padrão, o que significa que será mapeada automaticamente para o campo apropriado do Jira. Devemos definir o resto das variáveis por nós mesmos e elas serão locais para nós.
Agora vamos tentar implementar os esquemas em código. Primeiro, vamos definir todas as variáveis.
with isEstimated = storypoints != undefined: with childrenSum = sum#children{storypoints}: with isStory = issueType = "Story": with isErr = isStory AND childrenSum != storypoints:
As linhas são unidas pelo mesmo esquema de sintaxe: a palavra-chave with, o nome da variável e o símbolo de dois pontos “:” no final da linha.
A palavra-chave with é usada para denotar variáveis locais (e funções personalizadas, mas falaremos mais sobre isso em um exemplo separado). Diz à fórmula que a seguir vai uma variável que não precisa ser mapeada. Os dois pontos “:” sinalizam o final da definição da variável.
Assim, criamos a variável isEstimated (lembrete, esse caso não é importante). Armazenaremos 1 ou 0 nele, dependendo se o campo de pontos da história estiver preenchido. A variável storypoints é mapeada automaticamente porque não criamos uma variável local com o mesmo nome antes (por exemplo, com storypoints =… :).
A variável indefinida denota a inexistência de algo (como null, NaN e similares em outras linguagens). Portanto, a expressão storypoints != undefined pode ser lida como uma pergunta: “O campo story points está preenchido?”.
A seguir, devemos determinar a soma dos pontos da história de todas as tarefas filhas. Para fazer isso, criamos uma variável local: childrenSum.
with childrenSum = sum#children{storypoints}:
Esta soma é calculada através da função de agregação. (Você pode ler sobre funções como esta na documentação oficial .) Resumindo, o Structure pode realizar diversas operações com tarefas, levando em consideração a hierarquia da visualização atual.
Utilizamos a função soma e, além dela, através do símbolo “#”, passamos os filhos de esclarecimento, o que limita o cálculo da soma apenas a quaisquer tarefas filhas da linha atual. Entre chaves, indicamos qual campo queremos resumir — precisamos de uma estimativa em storypoints.
A próxima variável local, isStory, armazena uma condição: se o tipo de tarefa na linha atual é uma Story.
with isStory = issueType = "Story":
Voltamo-nos para a variável issueType, familiar do exemplo anterior, ou seja, o tipo de tarefa que mapeia sozinha para o campo desejado. Estamos fazendo isso porque é uma variável padrão e não a definimos anteriormente.
Agora vamos definir a variável isErr — ela sinaliza uma discrepância entre a soma da subtarefa e a estimativa da história.
with isErr = isStory AND childrenSum != storypoints:
Aqui estamos usando as variáveis locais isStory e childrenSum que criamos anteriormente. Para sinalizar um erro, precisamos que duas condições sejam atendidas simultaneamente: o tipo de problema é Story (isStory) e (AND) a soma dos pontos filhos (childrenSum) não é igual (!=) à estimativa definida na tarefa (storypoints ). Assim como em JQL, podemos usar palavras de ligação ao criar condições, como AND ou OR.
Observe que para cada uma das variáveis locais existe um símbolo “:” no final da linha. Deve estar no final, após todas as operações que definem a variável. Por exemplo, se precisarmos dividir a definição de uma variável em várias linhas, os dois pontos “:” serão colocados somente após a última operação. Como no exemplo da variável color — a cor do texto.
with color = if isStory : if isEstimated : if isErr : "red" else "green" else "orange":
Aqui vemos muitos “:”, mas eles desempenham papéis diferentes. Os dois pontos após if isStory são o resultado da condição isStory. Vamos relembrar a construção: if condição: resultado. Vamos apresentar esta construção de uma forma mais complexa, que define uma variável.
with variable = (if condition: (if condition2 : result2 else result3) ):
Acontece que if condição2 : resultado2 else resultado3 é, por assim dizer, o resultado da primeira condição, e no final há dois pontos “:”, que completa a definição da variável.
À primeira vista, a definição de cor pode parecer complicada, embora, na verdade, tenhamos descrito aqui o esquema de definição de cores apresentado no início do exemplo. Acontece que, como resultado da primeira condição, outra condição começa - uma condição aninhada e outra nela.
Mas o resultado final é um pouco diferente do esquema apresentado anteriormente.
if isEstimated : """{color:$color}$storypoints{color} ${if isErr :""" ($childrenSum)"""}""" else """{color:$color}$childrenSum{color}"""
Não precisamos escrever “{color}$sp'' duas vezes no código, como estava no esquema; seremos mais espertos sobre as coisas. No branch, se a tarefa tiver uma estimativa, sempre exibiremos {color: $color}$storypoints{color} (ou seja, apenas uma estimativa em story points na cor necessária), e se houver um erro, então após um espaço, complementaremos a linha com a soma da estimativa das subtarefas: ($childrenSum).
Se não houver erro, ele não será adicionado. Chamo também a atenção para o fato de não existir o símbolo “:”, pois não definimos uma variável, mas exibimos o resultado final através de uma condição.
Podemos avaliar nosso trabalho na imagem abaixo no campo “∑SP (mod)”. A captura de tela mostra especificamente dois campos adicionais:
Com a ajuda desses exemplos, analisamos os principais recursos da linguagem de estrutura que o ajudarão a resolver a maioria dos problemas. Vejamos agora mais dois recursos úteis, nossas funções e arrays. Veremos como criar nossa própria função personalizada.
Às vezes, há muitas tarefas em um sprint e podemos perder pequenas alterações nelas. Por exemplo, podemos perder uma nova subtarefa ou o fato de uma das histórias ter passado para o próximo estágio. Seria bom ter uma ferramenta que nos notificasse sobre as últimas mudanças importantes nas tarefas.
Estamos interessados em três tipos de alterações de status de tarefas que ocorreram desde ontem: começamos a trabalhar na tarefa, uma nova tarefa apareceu, a tarefa foi encerrada. Além disso, será útil ver se a tarefa foi encerrada com a resolução “Não vou fazer”.
Para isso, criaremos um campo com uma sequência de emojis responsáveis pelas últimas alterações. Por exemplo, se uma tarefa foi criada ontem e começamos a trabalhar nela, ela será marcada com dois emojis: “Em andamento” e “Nova tarefa”.
Por que precisamos de tal campo personalizado se vários campos adicionais podem ser exibidos, por exemplo, a data de transição para o status “Em Andamento” ou um campo “Resolução” separado? A resposta é simples: as pessoas percebem os emojis com mais facilidade e rapidez do que o texto, que está localizado em campos diferentes e precisa ser analisado. A fórmula irá reunir tudo em um só lugar e analisar para nós, o que nos poupará esforço e tempo para coisas mais úteis.
Vamos determinar pelo que os diferentes emojis serão responsáveis:
Recursos de estrutura usados
Um exemplo de código
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 : "❌")
Uma análise da solução
Para começar, vamos pensar nas variáveis globais que precisamos para determinar os eventos que nos interessam. Precisamos saber, se desde ontem:
Usar variáveis já existentes junto com novas variáveis de mapeamento nos ajudará a verificar todas essas condições.
Vamos passar para o código. A primeira linha começa com uma condição que verifica se o tipo de tarefa existe.
if defined(issueType):
Isso é feito por meio da função definida integrada, que verifica a existência do campo especificado. A verificação é feita para otimizar o cálculo da fórmula.
Não carregaremos a Estrutura com cálculos inúteis, se a linha não for uma tarefa. Acontece que todo o código depois do if é o resultado, ou seja, a segunda parte da construção do if (condição: resultado). E se a condição não for atendida, o código também não funcionará.
A próxima linha com now = now(): também é necessária para otimizar os cálculos. Mais adiante no código, teremos que comparar datas diferentes com a data atual várias vezes. Para não fazermos o mesmo cálculo várias vezes, vamos calcular esta data uma vez e torná-la uma variável local agora.
Também seria bom manter o nosso “ontem” separadamente. O conveniente “ontem” empiricamente se transformou em 1,3 dias. Vamos transformar isso em uma variável: com daysScope = 1.3:.
Agora precisamos calcular o número de dias entre duas datas várias vezes. Por exemplo, entre a data atual e a data de início do trabalho. Claro, existe uma função interna DAYS_BETWEEN, que parece adequada para nós. Mas, se a tarefa, por exemplo, foi criada na sexta-feira, então na segunda-feira não veremos o aviso de uma nova tarefa, pois na verdade já se passaram mais de 1,3 dias. Além disso, a função DAYS_BETWEEN conta apenas o número total de dias (ou seja, 0,5 dias se transformará em 0 dias), o que também não nos convém.
Estabelecemos um requisito: precisamos calcular o número exato de dias úteis entre essas datas; e uma função personalizada nos ajudará com isso.
Sua sintaxe de definição é muito semelhante à sintaxe de definição de uma variável local. A única diferença e a única adição é a enumeração opcional de argumentos nos primeiros colchetes. Os segundos colchetes contêm as operações que serão executadas quando nossa função for chamada. Esta definição da função não é a única possível, mas utilizaremos esta (outras podem ser encontradas na documentação oficial ).
with workDaysBetween(today, from)= ( with weekends = (Weeknum(today) - Weeknum(from)) * 2: HOURS_BETWEEN(from;today)/24 - weekends ):
Nossa função customizada workDaysBetween calculará os dias úteis entre hoje e as datas a partir, que são passadas como argumentos. A lógica da função é muito simples: contamos o número de dias de folga e subtraímos do total de dias entre as datas.
Para calcular o número de dias de folga, precisamos descobrir quantas semanas se passaram entre hoje e a partir de. Para fazer isso, calculamos a diferença entre os números de cada uma das semanas. Obteremos esse número da função Weeknum, que nos fornece o número da semana desde o início do ano. Multiplicando essa diferença por dois, obtemos o número de dias de folga passados.
A seguir, a função HOURS_BETWEEN conta o número de horas entre nossas datas. Dividimos o resultado por 24 para obter o número de dias e subtraímos os dias de folga desse número, obtendo os dias úteis entre as datas.
Usando nossa nova função, vamos definir um monte de variáveis auxiliares. Observe que algumas das datas nas definições são variáveis globais, das quais falamos no início do exemplo.
with daysAfterCreated = workDaysBetween(now,created): with daysAfterStart = workDaysBetween(now,latestTransitionToProgress): with daysAfterDone = workDaysBetween(now, resolutionDate):
Para tornar o código fácil de ler, vamos definir variáveis que armazenam os resultados das condições.
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 :
Para a variável isRecentCreated, adicionei uma condição opcional e not(resolução), o que me ajuda a simplificar a linha futura, pois se a tarefa já estiver fechada, não estou interessado em informações sobre sua criação recente.
O resultado final é construído através da função concat, concatenando as linhas.
concat( if isRecentCreated : "*️⃣", if isRecentWork : "🚀", if isRecentDone : "✅", if isWontDo : "❌")
Acontece que o emoji estará na linha somente quando a variável na condição for igual a 1. Assim, nossa linha pode exibir simultaneamente alterações independentes na tarefa.
Já tocamos no tema da contagem de dias úteis sem folgas. Há outro problema relacionado a isso, que analisaremos em nosso último exemplo e ao mesmo tempo nos familiarizaremos com arrays.
Às vezes queremos saber há quanto tempo uma tarefa está em execução, excluindo os dias de folga. Isto é necessário, por exemplo, para analisar a versão lançada. Para entender por que precisamos de dias de folga. Só que um funcionava de segunda a quinta e o outro, de sexta a segunda. Nesta situação, não podemos afirmar que as tarefas são equivalentes, embora a diferença em dias de calendário nos diga o contrário.
Infelizmente, a Estrutura “pronta para uso” não sabe como ignorar dias de folga, e o campo com a opção “Tempo em status…” produz um resultado independentemente das configurações do Jira – mesmo que sábado e domingo sejam especificados como dias de folga.
Como resultado, nosso objetivo é calcular o número exato de dias úteis, ignorando os dias de folga, e levar em consideração o impacto das transições de status nesse horário.
E o que os status têm a ver com isso? Deixe-me responder. Suponha que calculamos que entre 10 e 20 de março a tarefa funcionou por três dias. Mas desses 3 dias, ficou um dia em pausa e um dia e meio em revisão. Acontece que a tarefa durou apenas meio dia.
A solução do exemplo anterior não nos convém devido ao problema de alternância entre status, pois a função customizada workDaysBetween leva em consideração apenas o tempo entre duas datas selecionadas.
Este problema pode ser resolvido de diferentes maneiras. O método do exemplo é o mais caro em termos de desempenho, mas o mais preciso em termos de contagem de dias de folga e status. Observe que sua implementação só funciona na versão Structure anterior a 7.4 (dezembro de 2021).
Portanto, a ideia por trás da fórmula é a seguinte:
Assim, obteremos o tempo exato de trabalho na tarefa, ignorando dias de folga e transições entre status extras.
Recursos de estrutura usados
Um exemplo de código
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}"""
Uma análise da solução
Antes de transferir nosso algoritmo para o código, vamos facilitar os cálculos da Estrutura.
if defined(issueType) : if status != "Open" :
Se a linha não for uma tarefa ou seu status for “Aberto”, pularemos essas linhas. Estamos interessados apenas nas tarefas que foram lançadas para funcionar.
Para calcular o número de dias entre as datas, devemos primeiro determinar estas datas: finishDate e startDate.
with finishDate = if toQA != Undefined : toQA else if toDone != Undefined : toDone else now(): with startDate = DEFAULT(toProgress, toDone):
Assumiremos que a data de conclusão da tarefa (finishDate) é:
Data de início do trabalho startDate é determinada pela data de transição para o status “Em Andamento”. Há casos em que a tarefa é encerrada sem passar para a fase de trabalho. Nesses casos, consideramos a data de fechamento como data de início, portanto o resultado é 0 dias.
Como você deve ter adivinhado, toQA, toDone e toProgress são variáveis que precisam ser mapeadas para os status apropriados, como no primeiro exemplo e nos exemplos anteriores.
Também vemos a nova função DEFAULT(toProgress, toDone). Ele verifica se toProgress possui um valor e, caso contrário, utiliza o valor da variável toDone.
A seguir vem a definição da função personalizada statusWeekendsCount, mas retornaremos a ela mais tarde, pois está intimamente relacionada a listas de datas. É melhor ir direto à definição desta lista, para depois entendermos como aplicar nossa função a ela.
Queremos obter uma lista de datas no seguinte formato: [startDate (digamos 11.03), 12.03, 13.03, 14.03… finishDate]. Não existe uma função simples que faça todo o trabalho para nós na Estrutura. Então vamos recorrer a um truque:
Agora, vamos ver como podemos implementá-lo no código. Estaremos trabalhando com arrays.
with overallDays = round(hours_between(startDate,finishDate)/24): with sequenceArray = SEQUENCE(0,overallDays): with datesArray = sequenceArray.map(DATE_ADD(startDate,$,"day")):
Contamos quantos dias levará o trabalho em uma tarefa. Como no exemplo anterior, através da divisão por 24 e da função hours_between(startDate,finishDate). O resultado é escrito na variável globalDays.
Criamos um array da sequência numérica na forma da variável sequenciaArray. Esse array é construído por meio da função SEQUENCE(0,overallDays), que simplesmente cria um array do tamanho desejado com uma sequência de 0 a globalDays.
Em seguida vem a magia. Uma das funções do array é map. Aplica a operação especificada a cada elemento da matriz.
Nossa tarefa é adicionar a data de início a cada número (ou seja, o número do dia). A função DATE_ADD pode fazer isso, ela adiciona um certo número de dias, meses ou anos à data especificada.
Sabendo disso, vamos descriptografar a string:
with datesArray = sequenceArray.map(DATE_ADD(startDate, $,"day"))
Para cada elemento no sequenciaArray, a função .map() aplica DATE_ADD(startDate, $, “day”).
Vamos ver o que é passado nos argumentos de DATE_ADD. A primeira coisa é startDate, a data à qual o número desejado será adicionado. Este número é especificado pelo segundo argumento, mas vemos $.
O símbolo $ denota um elemento de array. A estrutura entende que a função DATE_ADD é aplicada a um array, e portanto ao invés de $ haverá o elemento desejado do array (ou seja, 0, 1, 2…).
O último argumento “dia” é uma indicação de que adicionamos um dia, pois a função pode adicionar um dia, mês e ano, dependendo do que especificarmos.
Assim, a variável datasArray armazenará um array de datas desde o início do trabalho até o seu término.
Voltemos à função personalizada que perdemos. Ele filtrará os dias extras e calculará o restante. Descrevemos este algoritmo logo no início do exemplo, antes de analisar o código, nomeadamente nos parágrafos 3 e 4 sobre filtragem de dias de folga e status.
with statusWeekendsCount(dates, status) = ( dates.filter(x -> weekday(x) > 5 and historical_value(this,"status",x)=status).size() ):
Passaremos dois argumentos para a função personalizada: um array de datas, vamos chamá-lo de datas, e o status necessário — status. Aplicamos a função .filter() ao array de datas transferidas, que mantém apenas os registros do array que passaram pela condição de filtro. No nosso caso, existem dois deles e são combinados por meio de e. Após o filtro, vemos .size(), ele retorna o tamanho do array após todas as operações nele serem realizadas.
Se simplificarmos a expressão, obteremos algo assim: array.filter(condição1 e condição2).size(). Então, como resultado, obtivemos o número de dias de folga que nos convinha, ou seja, aqueles dias de folga que ultrapassaram as condições.
Vamos dar uma olhada mais de perto em ambas as condições:
x -> weekday(x) > 5 and historical_value(this,"status",x)=status
A expressão x -> é apenas parte da sintaxe do filtro, indicando que chamaremos o elemento do array x . Portanto, x aparece em cada condição (semelhante ao que aconteceu com $). Acontece que x é cada data da matriz de datas transferidas.
A primeira condição, weekday(x) > 5, exige que o dia da semana da data x (ou seja, cada elemento) seja maior que 5 — seja sábado (6) ou domingo (7).
A segunda condição usa valor_histórico.
historical_value(this,"status",x) = status
Esse é um recurso do Structure da versão 7.4.
A função acessa o histórico da tarefa e busca uma data específica no campo especificado. Neste caso, procuramos a data x no campo “status”. A variável this é apenas parte da sintaxe da função, é mapeada automaticamente e representa a tarefa atual na linha.
Assim, na condição, comparamos o argumento do status transferido e o campo “status”, que é retornado pela função valor_histórico para cada data x no array. Se corresponderem, a entrada permanecerá na lista.
O toque final é a utilização da nossa função para contar a quantidade de dias no status desejado:
with progressWeekends = statusWeekendsCount(datesArray, "in Progress"): with progressDays = (timeInProgress/86400000 - progressWeekends).round(1):
Primeiro, vamos descobrir quantos dias de folga com status “em andamento” estão em nosso DateArray. Ou seja, passamos nossa lista de datas e o status desejado para a função customizada statusWeekendsCount. A função retira todos os dias da semana e todos os dias de folga em que o status da tarefa difere do status “em andamento” e retorna o número de dias restantes na lista.
Depois subtraímos esse valor da variável timeInProgress, que mapeamos através da opção “Tempo em status…”.
O número 86400000 é o divisor que transformará milissegundos em dias. A função .round(1) é necessária para arredondar o resultado para décimos, por exemplo para “4.1”, caso contrário você pode obter este tipo de entrada: “4.0999999…”.
Para indicar a duração da tarefa, introduzimos a variável color. Vamos alterá-lo dependendo do número de dias gastos na tarefa.
with color = if( progressDays = 0 ; "gray" ; progressDays > 0 and progressDays <= 2.5; "green" ; progressDays > 2.5 and progressDays <= 4; "orange" ; progressDays > 4; "red" ):
E a linha final com o resultado dos dias calculados:
"""{color:$color}$progressDays d{color}"""
Nosso resultado ficará como na imagem abaixo.
Aliás, na mesma fórmula você pode exibir a hora de qualquer status. Se, por exemplo, passarmos o status “Pause” para nossa função customizada, e mapearmos a variável timeInProgress através de “Time in… — Pause”, então calcularemos o tempo exato da pausa.
Você pode combinar status e fazer uma entrada como “wip: 3.2d | rev: 12d”, ou seja, calcule o tempo em trabalho e o tempo em revisão. Você está limitado apenas pela sua imaginação e pelo seu fluxo de trabalho.
Apresentamos um número exaustivo de recursos desta linguagem de fórmula que o ajudarão a fazer algo semelhante ou escrever algo completamente novo e interessante para analisar tarefas do Jira.
Espero que o artigo tenha ajudado você a descobrir as fórmulas, ou pelo menos tenha despertado seu interesse neste tópico. Não afirmo ter “o melhor código e algoritmo”, então se você tiver ideias sobre como melhorar os exemplos, ficaria feliz se você os compartilhasse!
Claro, você precisa entender que ninguém falará melhor sobre fórmulas do que os desenvolvedores do ALM Works. Portanto, estou anexando links para sua documentação e webinars. E se você começar a trabalhar com campos personalizados, verifique-os com frequência para ver quais outros recursos você pode usar.