El complemento Structure para Jira es muy útil para el trabajo diario con tareas y su análisis; lleva la visualización y estructuración de los tickets de Jira a un nuevo nivel y lo hace desde el primer momento.
Y no todo el mundo lo sabe, pero la funcionalidad de las fórmulas de estructura puede dejarte boquiabierto. Usando fórmulas, puedes crear tablas extremadamente útiles que pueden simplificar enormemente el trabajo con tareas y, lo más importante, son útiles para realizar un análisis más profundo de lanzamientos, epopeyas y proyectos.
En este artículo, verá cómo crear sus propias fórmulas, desde los ejemplos más simples hasta casos complejos, pero bastante útiles.
Entonces, ¿para quién es este texto? Uno podría preguntarse por qué escribir un artículo cuando la documentación oficial en el sitio web de ALM Works está ahí esperando a que los lectores profundicen. Eso es cierto. Sin embargo, soy una de esas personas que ni siquiera tenía la más mínima idea de que Structure ocultaba una funcionalidad tan amplia: "Espera, ¡¿esta fue una opción todo el tiempo?!" Esa comprensión me hizo pensar que puede haber otras personas que tampoco sepan todavía el tipo de cosas que pueden hacer con fórmulas y estructura.
Este artículo también será útil para quienes ya estén familiarizados con las fórmulas. Aprenderá algunas opciones prácticas interesantes para usar campos personalizados y, tal vez, tomará prestados algunos de ellos para sus proyectos . Por cierto, si tiene algún ejemplo interesante propio, estaré encantado de que lo comparta en los comentarios. .
Cada ejemplo se analiza en detalle, desde la descripción del problema hasta la explicación del código, lo suficientemente a fondo como para que no queden dudas. Por supuesto, además de las explicaciones, cada ejemplo está ilustrado con un código que puedes probar tú mismo sin tener que profundizar en el análisis.
Si no tiene ganas de leer, pero le interesan las fórmulas, consulte los seminarios web de ALM Works . Estos explican los conceptos básicos en 40 minutos; la información se presenta allí de una manera muy comprimida.
No necesitas ningún conocimiento adicional para entender los ejemplos, por lo que cualquiera que haya trabajado con Jira y Structure podrá repetir los ejemplos en sus tablas sin ningún problema.
Los desarrolladores proporcionaron una sintaxis bastante flexible con su lenguaje Expr. Básicamente, la filosofía aquí es "escribe como quieras y funcionará".
¡Entonces empecemos!
Entonces, ¿por qué querríamos utilizar fórmulas? Bueno, a veces resulta que no tenemos suficientes campos estándar de Jira, como "Asignado", "Puntos de historia", etc. O necesitamos calcular una cantidad para ciertos campos, mostrar la capacidad restante por versión y averiguar cuántas veces la tarea ha cambiado de estado. Tal vez incluso queramos fusionar varios campos en uno para que nuestra Estructura sea más fácil de leer.
Para resolver estos problemas, necesitamos fórmulas y las usaremos para crear campos personalizados.
Lo primero que debemos hacer es entender cómo funciona una fórmula. Nos permite aplicar algún tipo de operación a una cadena. Debido a que cargamos muchas tareas en la estructura, la fórmula se aplica a cada línea de toda la tabla. Por lo general, todas sus operaciones están dirigidas a trabajar con tareas de estas líneas.
Entonces, si le pedimos a la fórmula que muestre algún campo de Jira, por ejemplo, "Asignado", entonces la fórmula se aplicará para cada tarea y tendremos otra columna "Asignado".
Las fórmulas constan de varias entidades básicas:
Nos familiarizaremos con las fórmulas y su sintaxis a través de algunos ejemplos y veremos seis casos prácticos.
Antes de ver cada ejemplo, indicaremos qué características de Estructura estamos usando; Las nuevas funciones que aún no se han explicado estarán en negrita. Cada uno de los siguientes ejemplos tendrá un nivel creciente de complejidad. Están organizados para presentarle gradualmente las características importantes de la fórmula.
Esta es la estructura básica que verá cada vez:
Estos ejemplos cubren temas que van desde el mapeo de variables hasta matrices complejas:
Primero, descubramos cómo crear campos personalizados con fórmulas. En la parte superior derecha de Estructura, al final de todas las columnas, hay un ícono “+”; haga clic en él. En el campo que aparece, escriba “Fórmula…” y seleccione el elemento apropiado.
Analicemos cómo guardar una fórmula. Desafortunadamente, todavía no es posible guardar una fórmula específica por separado en algún lugar (sólo en tu cuaderno, como hago yo). En el seminario web de ALM Works, el equipo mencionó que están trabajando en un banco de fórmulas, pero por ahora la única forma de guardarlas es guardar la vista completa junto con la fórmula.
Cuando terminemos de trabajar en una fórmula, debemos hacer clic en la vista de nuestra estructura (lo más probable es que esté marcada con un asterisco azul) y hacer clic en "Guardar" para sobrescribir la vista actual. O puede hacer clic en "Guardar como..." para crear una nueva vista. (No olvides ponerlo a disposición de otros usuarios de Jira, ya que las nuevas vistas son privadas de forma predeterminada).
La fórmula se guardará en el resto de los campos en una vista particular y podrá verla en la pestaña "Avanzado" del menú "Ver detalles".
A partir de la versión 8.2, Structure ahora tiene la capacidad de guardar fórmulas con 3 clics rápidos.
El cuadro de diálogo para guardar está disponible desde la ventana de edición de fórmulas. Si esta ventana no está abierta, simplemente haga clic en el icono del triángulo ▼ en la columna deseada.
En la ventana de edición vemos el campo “Columna guardada”, a la derecha hay un ícono con una notificación azul, lo que significa que los cambios en la fórmula no se han guardado. Haga clic en este icono y seleccione la opción “Guardar como…”.
Luego ingresa nombres para nuestra columna (fórmula) y elige en qué espacio guardarla. “Mis Columnas” si queremos guardarla en una lista personal. “Global”, de modo que la fórmula se guardará en la lista general, donde podrá ser editada por todos los usuarios de tu Estructura. Clic en Guardar".
Ahora nuestra fórmula está guardada. Podemos cargarlo en cualquier estructura o volver a guardarlo desde cualquier lugar. Al volver a guardar la fórmula, se actualizará en todas las estructuras en las que se utilice.
El mapeo de variables también se guarda con la fórmula, pero hablaremos sobre el mapeo más adelante.
¡Ahora, pasemos a nuestros ejemplos!
Necesitamos una tabla con una lista de tareas, así como las fechas de inicio y finalización para trabajar en esas tareas. También necesitamos la tabla para exportarla a un Excel-Gantt separado. Desafortunadamente, Jira y Structure no saben cómo proporcionar dichas fechas de forma inmediata.
Las fechas de inicio y finalización son las fechas de transición a estados específicos, en nuestro caso son “En curso” y “Cerrado”. Necesitamos tomar estas fechas y mostrar cada una de ellas en un campo separado (esto es necesario para exportar más a Gantt). Entonces tendremos dos campos (dos fórmulas).
Las características de la estructura utilizadas.
Un ejemplo de código
Campo para la fecha de inicio:
firstTransitionToStart
Campo para la fecha de finalización:
latestTransitionToDone
En este caso, el código es una única variable, firstTransitionToStart, para el campo de fecha de inicio, y lastTransitionToDone para el segundo campo.
Centrémonos en el campo de fecha de inicio por ahora. Nuestro objetivo es obtener la fecha en que la tarea pasó al estado "En progreso" (esto corresponde al inicio lógico de la tarea), por lo que la variable se nombra, de manera bastante explícita para evitar la necesidad de adivinar más adelante, como "primera transición a comenzar".
Para convertir una fecha en una variable, recurrimos al mapeo de variables. Guardemos nuestra fórmula haciendo clic en el botón "Guardar".
Nuestra variable apareció en la sección "Variables", con un signo de exclamación al lado. La estructura indica que no puede vincular una variable a un campo en Jira y tendremos que hacerlo nosotros mismos (es decir, mapearlo).
Haga clic en la variable y vaya a la interfaz de mapeo. Seleccione el campo o la operación necesaria; busque la operación “Fecha de transición…”. Para ello, escriba “transición” en el campo de selección. Se le ofrecerán varias opciones a la vez y una de ellas nos conviene: "Primera transición a En curso". Pero para demostrar cómo funciona el mapeo, elijamos la opción "Fecha de transición ...".
Después de eso, debe elegir el estado en el que se produjo la transición y el orden de esta transición: la primera o la última.
Seleccione o ingrese en "Estado" - "Estado: En progreso" (o el estado correspondiente en su flujo de trabajo), y en "Transición" - "Primera transición al estado", ya que el comienzo del trabajo en una tarea es la primera transición al estatus correspondiente.
Si en lugar de “Fecha de transición…” elegimos la opción inicialmente propuesta “Primera transición a En curso”, entonces el resultado sería casi el mismo: la Estructura elegiría los parámetros necesarios por nosotros. Lo único es que, en lugar de "Estado: En curso", tendríamos "Categoría: En curso".
Permítanme señalar una característica importante: un estado y una categoría son dos cosas diferentes. Un estado es un estado específico, no es ambiguo, pero una categoría puede incluir varios estados. Sólo hay tres categorías: "Por hacer", "En curso" y "Listo". En Jira, suelen estar marcados con colores gris, azul y verde respectivamente. El estado debe pertenecer a una de estas categorías.
Recomiendo indicar un estado específico en casos como este para evitar confusiones con estados de la misma categoría. Por ejemplo, tenemos dos estados de la categoría "Por hacer" en el proyecto, "Abierto" y "Cola de control de calidad".
Volvamos a nuestro ejemplo.
Una vez que hayamos seleccionado las opciones necesarias, podemos hacer clic en “<Volver a la lista de variables” para completar las opciones de mapeo para la variable firstTransitionToStart. Si hacemos todo bien, veremos una marca de verificación verde.
Al mismo tiempo, en nuestro campo personalizado, vemos algunos números extraños que no parecen una fecha en absoluto. En nuestro caso, el resultado de la fórmula será el valor de la variable firstTransitionToStart, y su valor es milisegundos desde enero de 1970. Para obtener la fecha correcta, debemos elegir un formato de visualización de fórmula específico.
La selección de formato se encuentra en la parte inferior de la ventana de edición. Allí está seleccionado "General" de forma predeterminada. Necesitamos “Fecha/Hora” para mostrar la fecha correctamente.
Para el segundo campo, LatestTransitionToDone, haremos lo mismo. La única diferencia es que al mapear ya podemos seleccionar la categoría "Listo" y no el estado (ya que generalmente solo hay un estado de finalización de tarea inequívoco). Seleccionamos “Última transición” como parámetro de transición, ya que estamos interesados en la transición más reciente a la categoría “Listo”.
El resultado final para los dos campos se verá así.
Ahora veamos cómo lograr el mismo resultado, pero con nuestro propio formato de visualización.
No estamos satisfechos con el formato de visualización de la fecha del ejemplo anterior, ya que necesitamos uno especial para la tabla de Gantt: “01.01.2022”.
Visualicemos las fechas usando las funciones integradas en Estructura, especificando el formato que más nos convenga.
Características estructurales utilizadas
Un ejemplo de código
FORMAT_DATETIME(firstTransitionToStart;"dd.MM.yyyy")
Los desarrolladores han proporcionado muchas funciones diferentes, incluida una separada para mostrar la fecha en nuestro propio formato: FORMAT_DATETIME; eso es lo que vamos a usar. La función utiliza dos argumentos: una fecha y una cadena del formato deseado.
Configuramos la variable firstTransitionToStart (primer argumento) usando las mismas reglas de mapeo que en el ejemplo anterior. El segundo argumento es una cadena que especifica el formato y lo definimos así: “dd.MM.aaaa”. Esto corresponde al formulario que queremos, “01.01.2022”.
Por lo tanto, nuestra fórmula dará inmediatamente un resultado en la forma deseada. Entonces, podemos mantener la opción “General” en la configuración del campo.
El segundo campo con la fecha de finalización del trabajo se realiza de la misma forma. Como resultado, la estructura debería verse como en la imagen de abajo.
En principio, no existen dificultades importantes al trabajar con la sintaxis de fórmulas. Si necesitas una variable, escribe su nombre; Si necesita una función, nuevamente, simplemente escriba su nombre y pase los argumentos (si son necesarios).
Cuando Structure encuentra un nombre desconocido, asume que es una variable e intenta mapearlo por sí mismo o nos pide ayuda.
Por cierto, una nota importante: la estructura no distingue entre mayúsculas y minúsculas, por lo que firstTransitionToStart, firsttransitiontostart y firSttrAnsItiontOStarT son la misma variable. La misma regla se aplica a las funciones. Para lograr un estilo de código inequívoco, en los ejemplos intentaremos cumplir con las reglas de las Convenciones de capitalización de MSDN.
Ahora profundicemos en la sintaxis y veamos un formato especial para mostrar el resultado.
Trabajamos con tareas regulares (Tarea, Bug, etc.) y con tareas tipo Historia que tienen subtareas. En algún momento, necesitamos saber en qué tareas y subtareas trabajó el empleado durante un período determinado.
El problema es que muchas subtareas no proporcionan información sobre la historia en sí, como se les llama "trabajar en la historia", "preparar" o, por ejemplo, "activar el efecto". Y si solicitamos una lista de tareas para un período determinado, obtendremos una docena de tareas con el nombre "trabajar en la historia" sin ninguna otra información útil.
Nos gustaría tener una vista con una lista dividida en dos columnas: una tarea y una tarea principal, para que en el futuro sea posible agrupar dicha lista por empleados.
En nuestro proyecto, tenemos dos opciones cuando una tarea puede tener un padre:
Entonces, debemos:
Para simplificar la percepción de la información, colorearemos el texto del tipo de tarea: es decir, “[Historia]” o “[Épica]”.
Qué usaremos:
Un ejemplo de código
if( Parent.Issuetype = "Story"; """{color:green}[${Parent.Issuetype}]{color} ${Parent.Summary}"""; EpicLink; """{color:#713A82}[${EpicLink.Issuetype}]{color} ${EpicLink.EpicName}""" )
¿Por qué la fórmula comienza con una condición if, si solo necesitamos generar una cadena e insertar allí el tipo y el nombre de la tarea? ¿No existe alguna forma universal de acceder a los campos de tareas? Sí, pero para tareas y epopeyas, estos campos tienen nombres diferentes y también debes acceder a ellos de manera diferente; esta es una característica de Jira.
Las diferencias comienzan en el nivel de la búsqueda de los padres. Para una subtarea, el padre vive en el campo Jira "Problema principal", y para una tarea normal, el epic será el padre, ubicado en el campo "Enlace épico". En consecuencia, tendremos que escribir dos opciones diferentes para acceder a estos campos.
Aquí es donde necesitamos una condición if. El lenguaje Expr tiene diferentes formas de lidiar con las condiciones. La elección entre ellos es cuestión de gustos.
Existe un método "similar a Excel":
if (condition1; result1; condition2; result2 … )
O un método más "similar a un código":
if condition1 : result1 else if condition2 : result2 else result3
En el ejemplo, utilicé la primera opción; Ahora veamos nuestro código de forma simplificada:
if( Parent.Issuetype = "Story"; Some kind of result 1; EpicLink; Some kind of result 2 )
Vemos dos condiciones obvias:
Averigüemos qué hacen y comencemos con el primero, Parent.Issuetype=”Story”.
En este caso, Parent es una variable que se asigna automáticamente al campo "Problema principal". Aquí es donde, como comentamos anteriormente, debe vivir el padre de la subtarea. Usando la notación de puntos (.), accedemos a la propiedad de este padre, en particular, a la propiedad Issuetype, que corresponde al campo "Tipo de problema" de Jira. Resulta que toda la línea Parent.Issuetype nos devuelve el tipo de tarea principal, si dicha tarea existe.
Además, no tuvimos que definir ni mapear nada, ya que los desarrolladores ya hicieron todo lo posible por nosotros. Aquí, por ejemplo, hay un enlace a todas las propiedades (incluidos los campos de Jira) que están predefinidas en el idioma, y aquí puede ver una lista de todas las variables estándar, a las que también se puede acceder de forma segura sin configuraciones adicionales.
Por lo tanto, la primera condición es ver si el tipo de tarea principal es Historia. Si no se cumple la primera condición, entonces el tipo de tarea principal no es Historia o no existe en absoluto. Y esto nos lleva a la segunda condición: EpicLink.
De hecho, es aquí cuando comprobamos si el campo “Epic Link” de Jira está rellenado (es decir, comprobamos su existencia). La variable EpicLink también es estándar y no es necesario asignarla. Resulta que nuestra condición se cumple si la tarea tiene Epic Link.
Y la tercera opción es cuando no se cumple ninguna de las condiciones, es decir, la tarea no tiene padre ni Epic Link. En este caso, no mostramos nada y dejamos el campo vacío. Esto se hace automáticamente ya que no obtendremos ninguno de los resultados.
Descubrimos las condiciones, ahora pasemos a los resultados. En ambos casos, es una cadena con texto y formato especial.
Resultado 1 (si el padre es Story):
"""{color:green}[${Parent.Issuetype}]{color} ${Parent.Summary}"""
Resultado 2 (si hay Epic Link):
"""{color:#713A82}[${EpicLink.Issuetype}]{color} ${EpicLink.EpicName}"""
Ambos resultados son similares en estructura: ambos constan de comillas triples “”” al principio y al final de la cadena de salida, especificación de color en los bloques de apertura {color: COLOR} y de cierre {color}, así como operaciones realizadas a través del Símbolo $. Las comillas triples le dicen a la estructura que dentro habrá variables, operaciones o bloques de formato (como colores).
Para el resultado de la primera condición, hacemos:
Por lo tanto, obtenemos la cadena "[Historia] Nombre de alguna tarea". Como habrás adivinado, Resumen también es una variable estándar. Para aclarar el esquema para construir dichas cadenas, permítanme compartir una imagen de la documentación oficial.
De manera similar, recopilamos la cadena para el segundo resultado, pero configuramos el color mediante el código hexadecimal. Descubrí que el color de Epic era “#713A82” (en los comentarios, por cierto, puedes sugerir un color más preciso para Epic). No te olvides de los campos (propiedades) que cambian para Epic. En lugar de "Resumen", utilice "EpicName", en lugar de "Parent", utilice "EpicLink".
Como resultado, el esquema de nuestra fórmula se puede representar como una tabla de condiciones.
Condición: la tarea principal existe y su tipo es Historia.
Resultado: Línea con el tipo verde de tarea principal y su nombre.
Condición: El campo Enlace épico está completo.
Resultado: Línea con el color épico del tipo y su nombre.
De forma predeterminada, la opción de visualización "General" está seleccionada en el campo y, si no la cambia, el resultado se verá como texto sin formato sin cambiar el color ni identificar los bloques. Si cambia el formato de visualización a "Marcado Wiki", el texto se transformará.
Ahora, familiaricémonos con las variables que no están relacionadas con los campos de Jira: las variables locales.
En el ejemplo anterior, aprendiste que estamos trabajando con tareas del tipo Historia, que tienen subtareas. Esto da lugar a un caso especial con las estimaciones. Para obtener una puntuación de Historia, resumimos las puntuaciones de sus subtareas, que se estiman en puntos abstractos de Historia.
El enfoque es inusual, pero funciona para nosotros. Entonces, cuando la Historia no tiene una estimación, pero las subtareas sí, no hay problema, pero cuando tanto la Historia como las subtareas tienen una estimación, la opción estándar de Estructura, “Σ Puntos de la Historia”, funciona incorrectamente.
Esto se debe a que la estimación de Story se suma a la suma de las subtareas. Como resultado, se muestra una cantidad incorrecta en Story. Nos gustaría evitar esto y agregar una indicación de inconsistencia con la estimación establecida en Story y la suma de subtareas.
Necesitamos varias condiciones, ya que todo depende de si la estimación está configurada en Story.
Entonces las condiciones son:
Cuando Story no tiene estimación , mostramos la suma de la estimación de subtareas en naranja para indicar que este valor aún no se ha establecido en Story
Si Story tiene una estimación , verifique si corresponde a la suma de la estimación de las subtareas:
La redacción de estas condiciones puede resultar confusa, así que expresémoslas en un esquema.
Características estructurales utilizadas
Un ejemplo 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 sumergirnos en el código, transformemos nuestro esquema en una forma más "similar a un código" para comprender qué variables necesitamos.
De este esquema vemos que necesitaremos:
Variables de condición:
Una variable del color del texto : color
Dos variables de estimación:
Además, la variable de color también depende de una serie de condiciones, por ejemplo, de la disponibilidad de una estimación y del tipo de tarea en la línea (consulte el esquema a continuación).
Entonces, para determinar el color, necesitaremos otra variable de condición, isStory, que indica si el tipo de tarea es Story.
La variable sp (storypoints) será estándar, lo que significa que se asignará automáticamente al campo Jira apropiado. El resto de variables las debemos definir nosotros mismos y serán locales para nosotros.
Ahora intentemos implementar los esquemas en código. Primero, definamos todas las variables.
with isEstimated = storypoints != undefined: with childrenSum = sum#children{storypoints}: with isStory = issueType = "Story": with isErr = isStory AND childrenSum != storypoints:
Las líneas están unidas por el mismo esquema de sintaxis: la palabra clave with, el nombre de la variable y el símbolo de dos puntos “:” al final de la línea.
La palabra clave with se usa para indicar variables locales (y funciones personalizadas, pero hablaremos más sobre eso en un ejemplo separado). Le dice a la fórmula que a continuación va una variable que no necesita ser asignada. Los dos puntos “:” marcan el final de la definición de variable.
Así, creamos la variable isEstimated (recordatorio, ese caso no es importante). Almacenaremos 1 o 0 en él, dependiendo de si el campo de puntos de la historia está lleno. La variable storypoints se asigna automáticamente porque no hemos creado antes una variable local con el mismo nombre (por ejemplo, con storypoints =... :).
La variable indefinida denota la inexistencia de algo (como nulo, NaN y similares en otros idiomas). Por lo tanto, la expresión storypoints != indefinido puede leerse como una pregunta: “¿Está completo el campo storypoints?”.
A continuación, debemos determinar la suma de los puntos de la historia de todas las tareas infantiles. Para ello, creamos una variable local: ChildrenSum.
with childrenSum = sum#children{storypoints}:
Esta suma se calcula mediante la función de agregación. (Puede leer sobre funciones como esta en la documentación oficial ). En pocas palabras, Structure puede realizar varias operaciones con tareas, teniendo en cuenta la jerarquía de la vista actual.
Usamos la función de suma y, además, usando el símbolo "#", pasamos los hijos de aclaración, lo que limita el cálculo de la suma solo a cualquier tarea secundaria de la línea actual. Entre llaves, indicamos qué campo queremos resumir; necesitamos una estimación en puntos de historia.
La siguiente variable local, isStory, almacena una condición: si el tipo de tarea en la línea actual es una Historia.
with isStory = issueType = "Story":
Pasamos a la variable issuesType, familiar del ejemplo anterior, es decir, el tipo de tarea que se asigna por sí sola al campo deseado. Estamos haciendo esto porque es una variable estándar y no la hemos definido previamente.
Ahora definamos la variable isErr: señala una discrepancia entre la suma de la subtarea y la estimación de la Historia.
with isErr = isStory AND childrenSum != storypoints:
Aquí estamos usando las variables locales isStory y ChildrenSum que creamos anteriormente. Para señalar un error, necesitamos que se cumplan dos condiciones simultáneamente: el tipo de problema es Historia (isStory) y (Y) la suma de puntos de los niños (childrenSum) no es igual (!=) a la estimación establecida en la tarea (storypoints ). Al igual que en JQL, podemos usar palabras de enlace al crear condiciones, como AND u OR.
Tenga en cuenta que para cada una de las variables locales hay un símbolo ":" al final de la línea. Debe estar al final, después de todas las operaciones que definen la variable. Por ejemplo, si necesitamos dividir la definición de una variable en varias líneas, entonces los dos puntos ":" se colocan solo después de la última operación. Como en el ejemplo con la variable color: el color del texto.
with color = if isStory : if isEstimated : if isErr : "red" else "green" else "orange":
Aquí vemos muchos “:”, pero desempeñan funciones diferentes. Los dos puntos después de if isStory son el resultado de la condición isStory. Recordemos la construcción: si condición: resultado. Presentemos esta construcción en una forma más compleja, que define una variable.
with variable = (if condition: (if condition2 : result2 else result3) ):
Resulta que si condición2: resultado2, de lo contrario resultado3 es, por así decirlo, el resultado de la primera condición, y al final hay dos puntos ":", que completan la definición de la variable.
A primera vista, la definición de color puede parecer complicada, aunque, de hecho, hemos descrito aquí el esquema de definición de color presentado al principio del ejemplo. Simplemente, como resultado de la primera condición, comienza otra condición: una condición anidada y otra en ella.
Pero el resultado final es ligeramente diferente al esquema presentado anteriormente.
if isEstimated : """{color:$color}$storypoints{color} ${if isErr :""" ($childrenSum)"""}""" else """{color:$color}$childrenSum{color}"""
No tenemos que escribir “{color}$sp'' dos veces en el código, como estaba en el esquema; Seremos más inteligentes con las cosas. En la rama, si la tarea tiene una estimación, siempre mostraremos {color: $color}$storypoints{color} (es decir, solo una estimación en puntos de historia en el color necesario), y si hay un error, entonces después de un espacio, complementaremos la línea con la suma de la estimación de las subtareas: ($childrenSum).
Si no hay ningún error, no se agregará. También llamo su atención sobre el hecho de que no existe el símbolo “:”, ya que no definimos una variable, sino que mostramos el resultado final a través de una condición.
Podemos evaluar nuestro trabajo en la imagen de abajo en el campo “∑SP (mod)”. La captura de pantalla muestra específicamente dos campos adicionales:
Con la ayuda de estos ejemplos, hemos analizado las características principales del lenguaje estructural que le ayudarán a resolver la mayoría de los problemas. Veamos ahora dos características más útiles, nuestras funciones y matrices. Veremos cómo crear nuestra propia función personalizada.
A veces hay muchas tareas en un sprint y es posible que nos perdamos pequeños cambios en ellas. Por ejemplo, podemos perdernos una nueva subtarea o el hecho de que una de las historias haya pasado a la siguiente etapa. Sería bueno tener una herramienta que nos notifique sobre los últimos cambios importantes en las tareas.
Nos interesan tres tipos de cambios de estado de tareas que han ocurrido desde ayer: comenzamos a trabajar en la tarea, apareció una nueva tarea y la tarea se cerró. Además, será útil ver que la tarea se cierra con la resolución "No funciona".
Para ello crearemos un campo con una cadena de emojis que son responsables de los últimos cambios. Por ejemplo, si ayer se creó una tarea y comenzamos a trabajar en ella, entonces se marcará con dos emojis: “En progreso” y “Nueva tarea”.
¿Por qué necesitamos un campo personalizado de este tipo si se pueden mostrar varios campos adicionales, por ejemplo, la fecha de transición al estado "En curso" o un campo de "Resolución" separado? La respuesta es simple: las personas perciben los emojis más fácil y rápidamente que el texto, que se encuentra en diferentes campos y necesita ser analizado. La fórmula recopilará todo en un solo lugar y lo analizará por nosotros, lo que nos ahorrará esfuerzo y tiempo para cosas más útiles.
Determinemos de qué se encargarán los diferentes emoji:
Características estructurales utilizadas
Un ejemplo 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 : "❌")
Un análisis de la solución.
Para empezar, pensemos en las variables globales que necesitamos para determinar los eventos que nos interesan. Necesitamos saber si desde ayer:
El uso de variables ya existentes junto con nuevas variables de mapeo nos ayudará a verificar todas estas condiciones.
Pasemos al código. La primera línea comienza con una condición que verifica si el tipo de tarea existe.
if defined(issueType):
Esto se hace a través de la función definida incorporada, que verifica la existencia del campo especificado. La verificación se realiza para optimizar el cálculo de la fórmula.
No cargaremos Estructura con cálculos inútiles, si la línea no es una tarea. Resulta que todo el código después de if es el resultado, es decir, la segunda parte de la construcción if (condición: resultado). Y si no se cumple la condición, el código tampoco funcionará.
La siguiente línea con now = now(): también es necesaria para optimizar los cálculos. Más adelante en el código, tendremos que comparar diferentes fechas con la fecha actual varias veces. Para no hacer el mismo cálculo varias veces, calcularemos esta fecha una vez y ahora la convertiremos en una variable local.
También sería bueno mantener nuestro “ayer” por separado. El conveniente "ayer" empíricamente se convirtió en 1,3 días. Convirtamos esto en una variable: con daysScope = 1.3:.
Ahora necesitamos calcular varias veces el número de días entre dos fechas. Por ejemplo, entre la fecha actual y la fecha de inicio del trabajo. Por supuesto, hay una función DAYS_BETWEEN incorporada, que parece adecuada para nosotros. Pero si la tarea, por ejemplo, se creó el viernes, el lunes no veremos ningún aviso de nueva tarea, ya que en realidad han pasado más de 1,3 días. Además, la función DAYS_BETWEEN solo cuenta el número total de días (es decir, 0,5 días se convertirán en 0 días), lo que tampoco nos conviene.
Hemos formulado un requisito: debemos calcular el número exacto de días hábiles entre estas fechas; y una función personalizada nos ayudará con esto.
Su sintaxis de definición es muy similar a la sintaxis para definir una variable local. La única diferencia y la única adición es la enumeración opcional de argumentos entre los primeros corchetes. Los segundos corchetes contienen las operaciones que se realizarán cuando se llame a nuestra función. Esta definición de la función no es la única posible, pero usaremos ésta (se pueden encontrar otras en la documentación oficial ).
with workDaysBetween(today, from)= ( with weekends = (Weeknum(today) - Weeknum(from)) * 2: HOURS_BETWEEN(from;today)/24 - weekends ):
Nuestra función personalizada workDaysBetween calculará los días laborables entre hoy y desde las fechas, que se pasan como argumentos. La lógica de la función es muy sencilla: contamos el número de días libres y los restamos del número total de días entre las fechas.
Para calcular el número de días libres, necesitamos saber cuántas semanas han pasado entre hoy y desde. Para ello, calculamos la diferencia entre los números de cada una de las semanas. Este número lo obtendremos de la función Weeknum, que nos proporciona el número de semana del inicio del año. Multiplicando esta diferencia por dos obtenemos el número de días libres transcurridos.
A continuación, la función HOURS_BETWEEN cuenta el número de horas entre nuestras fechas. Dividimos el resultado por 24 para obtener el número de días y restamos los días libres de este número, para obtener los días laborables entre las fechas.
Usando nuestra nueva función, definamos un montón de variables auxiliares. Tenga en cuenta que algunas de las fechas en las definiciones son variables globales, de las que hablamos al principio del ejemplo.
with daysAfterCreated = workDaysBetween(now,created): with daysAfterStart = workDaysBetween(now,latestTransitionToProgress): with daysAfterDone = workDaysBetween(now, resolutionDate):
Para que el código sea fácil de leer, definamos variables que almacenen los resultados de las condiciones.
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 la variable isRecentCreated, agregué una condición opcional y not(resolución), lo que me ayuda a simplificar la línea futura, porque si la tarea ya está cerrada, entonces no me interesa información sobre su creación reciente.
El resultado final se construye mediante la función concat, concatenando las líneas.
concat( if isRecentCreated : "*️⃣", if isRecentWork : "🚀", if isRecentDone : "✅", if isWontDo : "❌")
Resulta que el emoji estará en la línea solo cuando la variable en la condición sea igual a 1. Por lo tanto, nuestra línea puede mostrar simultáneamente cambios independientes en la tarea.
Hemos tocado el tema de contar los días laborables sin días libres. Hay otro problema relacionado con esto, que analizaremos en nuestro último ejemplo y al mismo tiempo nos familiarizaremos con las matrices.
A veces queremos saber cuánto tiempo lleva ejecutándose una tarea, excluyendo los días libres. Esto es necesario, por ejemplo, para analizar la versión publicada. Para entender por qué necesitamos días libres. Excepto que uno funcionaba de lunes a jueves y el otro, de viernes a lunes. En tal situación, no podemos afirmar que las tareas sean equivalentes, aunque la diferencia de días naturales nos dice lo contrario.
Desafortunadamente, la estructura "lista para usar" no sabe cómo ignorar los días libres, y el campo con la opción "Tiempo en estado..." produce un resultado independientemente de la configuración de Jira, incluso si el sábado y el domingo se especifican como días libres.
Como resultado, nuestro objetivo es calcular el número exacto de días laborables, ignorando los días libres y tener en cuenta el impacto de las transiciones de estado en este tiempo.
¿Y qué tienen que ver los estatus con esto? Déjame responder. Supongamos que calculamos que entre el 10 y el 20 de marzo, la tarea estuvo funcionando durante tres días. Pero de estos 3 días estuvo en pausa un día y en revisión día y medio. Resulta que la tarea estuvo en el trabajo solo medio día.
La solución del ejemplo anterior no nos conviene debido al problema de cambiar entre estados, porque la función personalizada workDaysBetween solo tiene en cuenta el tiempo entre dos fechas seleccionadas.
Este problema se puede solucionar de diferentes formas. El método del ejemplo es el más caro en términos de rendimiento, pero el más preciso en cuanto a contar los días libres y los estados. Tenga en cuenta que su implementación solo funciona en la versión de Structure anterior a 7.4 (diciembre de 2021).
Entonces, la idea detrás de la fórmula es la siguiente:
Así, obtendremos el tiempo exacto de trabajo en la tarea, ignorando los días libres y las transiciones entre estados adicionales.
Características estructurales utilizadas
Un ejemplo 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}"""
Un análisis de la solución.
Antes de transferir nuestro algoritmo al código, facilitemos los cálculos de Estructura.
if defined(issueType) : if status != "Open" :
Si la línea no es una tarea o su estado es "Abierto", omitiremos esas líneas. Sólo nos interesan las tareas que se han lanzado a funcionar.
Para calcular el número de días entre fechas, primero debemos determinar estas fechas: FinishDate y StartDate.
with finishDate = if toQA != Undefined : toQA else if toDone != Undefined : toDone else now(): with startDate = DEFAULT(toProgress, toDone):
Supondremos que la fecha de finalización de la tarea (finishDate) es:
Fecha de inicio del trabajo startDate está determinada por la fecha de transición al estado "En curso". Hay casos en los que la tarea se cierra sin pasar a la etapa de trabajo. En tales casos, consideramos la fecha de cierre como fecha de inicio, por lo que el resultado es 0 días.
Como habrás adivinado, toQA, toDone y toProgress son variables que deben asignarse a los estados apropiados como en el primer ejemplo y en el anterior.
También vemos la nueva función DEFAULT(toProgress, toDone). Comprueba si toProgress tiene un valor y, si no, utiliza el valor de la variable toDone.
Luego viene la definición de la función personalizada statusWeekendsCount, pero volveremos a ella más adelante, ya que está estrechamente relacionada con las listas de fechas. Es mejor ir directamente a la definición de esta lista, para que luego podamos entender cómo aplicarle nuestra función.
Queremos obtener una lista de fechas de la siguiente forma: [fecha de inicio (digamos 11.03), 12.03, 13.03, 14.03… fecha de finalización]. No existe una función simple que haga todo el trabajo por nosotros en Structure. Entonces recurramos a un truco:
Ahora, veamos cómo podemos implementarlo en el código. Trabajaremos con matrices.
with overallDays = round(hours_between(startDate,finishDate)/24): with sequenceArray = SEQUENCE(0,overallDays): with datesArray = sequenceArray.map(DATE_ADD(startDate,$,"day")):
Contamos cuántos días llevará trabajar en una tarea. Como en el ejemplo anterior, mediante la división por 24 y la función horas_entre(fechainicial,fechafinalizada). El resultado se escribe en la variable globalDays.
Creamos una matriz de la secuencia de números en forma de variable secuenciaArray. Esta matriz se construye mediante la función SEQUENCE(0,overallDays), que simplemente crea una matriz del tamaño deseado con una secuencia de 0 a globalDays.
Luego viene la magia. Una de las funciones de la matriz es map. Aplica la operación especificada a cada elemento de la matriz.
Nuestra tarea es sumar la fecha de inicio a cada número (es decir, el número del día). La función DATE_ADD puede hacer esto, agrega una cierta cantidad de días, meses o años a la fecha especificada.
Sabiendo esto, desciframos la cadena:
with datesArray = sequenceArray.map(DATE_ADD(startDate, $,"day"))
A cada elemento de la secuenciaArray, se aplica la función .map() DATE_ADD(startDate, $, “día”).
Veamos qué se pasa en los argumentos de DATE_ADD. Lo primero es startDate, la fecha a la que se sumará el número deseado. Este número está especificado por el segundo argumento, pero vemos $.
El símbolo $ indica un elemento de matriz. La estructura entiende que la función DATE_ADD se aplica a un array, y por tanto en lugar de $ estará el elemento del array deseado (es decir, 0, 1, 2…).
El último argumento “día” es una indicación de que agregamos un día, ya que la función puede agregar un día, mes y año, dependiendo de lo que especifiquemos.
Por lo tanto, la variable dateArray almacenará una serie de fechas desde el inicio del trabajo hasta su finalización.
Volvamos a la función personalizada que nos perdimos. Filtrará los días adicionales y calculará el resto. Describimos este algoritmo al principio del ejemplo, antes de analizar el código, concretamente en los párrafos 3 y 4 sobre el filtrado de días libres y estados.
with statusWeekendsCount(dates, status) = ( dates.filter(x -> weekday(x) > 5 and historical_value(this,"status",x)=status).size() ):
Pasaremos dos argumentos a la función personalizada: una matriz de fechas, llamémosla fechas, y el estado requerido: estado. Aplicamos la función .filter() a la matriz de fechas transferidas, que mantiene solo aquellos registros en la matriz que han pasado por la condición de filtro. En nuestro caso, son dos y se combinan mediante y. Después del filtro, vemos .size(), devuelve el tamaño de la matriz después de realizar todas las operaciones en ella.
Si simplificamos la expresión, obtenemos algo como esto: array.filter(condición1 y condición2).size(). Entonces, como resultado, obtuvimos el número de días libres que nos convenían, es decir, aquellos días libres que cumplían las condiciones.
Echemos un vistazo más de cerca a ambas condiciones:
x -> weekday(x) > 5 and historical_value(this,"status",x)=status
La expresión x -> es solo parte de la sintaxis del filtro, lo que indica que llamaremos al elemento de la matriz x. Por lo tanto, x aparece en cada condición (similar a como ocurría con $). Resulta que x es cada fecha de la matriz de fechas transferidas.
La primera condición, día de la semana(x) > 5, requiere que el día de la semana de la fecha x (es decir, cada elemento) sea mayor que 5: es sábado (6) o domingo (7).
La segunda condición usa valor_histórico.
historical_value(this,"status",x) = status
Esa es una característica de Structure de la versión 7.4.
La función accede al historial de la tarea y busca una fecha específica en el campo especificado. En este caso, buscamos la fecha x en el campo "estado". Esta variable es solo parte de la sintaxis de la función, se asigna automáticamente y representa la tarea actual en la línea.
Por lo tanto, en la condición, comparamos el argumento de estado transferido y el campo "estado", que devuelve la función valor_histórico para cada fecha x en la matriz. Si coinciden, la entrada permanece en la lista.
El toque final es el uso de nuestra función para contar el número de días en el estado deseado:
with progressWeekends = statusWeekendsCount(datesArray, "in Progress"): with progressDays = (timeInProgress/86400000 - progressWeekends).round(1):
Primero, averigüemos cuántos días libres con el estado "en progreso" hay en nuestro conjunto de fechas. Es decir, pasamos nuestra lista de fechas y el estado deseado a la función personalizada statusWeekendsCount. La función elimina todos los días de la semana y todos los días libres en los que el estado de la tarea difiere del estado "en progreso" y devuelve el número de días restantes en la lista.
Luego restamos esta cantidad a la variable timeInProgress, que mapeamos mediante la opción “Tiempo en estado…”.
El número 86400000 es el divisor que convertirá milisegundos en días. La función .round(1) es necesaria para redondear el resultado a décimas, por ejemplo a “4.1”, de lo contrario puede obtener este tipo de entrada: “4.0999999…”.
Para indicar la duración de la tarea, introducimos la variable color. Lo cambiaremos dependiendo de la cantidad de días dedicados a la tarea.
with color = if( progressDays = 0 ; "gray" ; progressDays > 0 and progressDays <= 2.5; "green" ; progressDays > 2.5 and progressDays <= 4; "orange" ; progressDays > 4; "red" ):
Y la línea final con el resultado de los días calculados:
"""{color:$color}$progressDays d{color}"""
Nuestro resultado se verá como en la imagen de abajo.
Por cierto, en la misma fórmula, puede mostrar la hora de cualquier estado. Si, por ejemplo, pasamos el estado "Pausa" a nuestra función personalizada y asignamos la variable timeInProgress a través de "Tiempo en... - Pausa", entonces calcularemos el tiempo exacto en la pausa.
Puede combinar estados y realizar una entrada como “wip: 3.2d | rev: 12d”, es decir calcular el tiempo en trabajo y el tiempo en revisión. Sólo estás limitado por tu imaginación y tu flujo de trabajo.
Presentamos una serie exhaustiva de características de este lenguaje de fórmulas que lo ayudarán a hacer algo similar o escribir algo completamente nuevo e interesante para analizar las tareas de Jira.
Espero que el artículo te haya ayudado a descubrir las fórmulas, o al menos te haya interesado en este tema. No pretendo tener “el mejor código y algoritmo”, así que si tienes ideas sobre cómo mejorar los ejemplos, ¡me encantaría que las compartas!
Por supuesto, debe comprender que nadie le informará mejor sobre las fórmulas que los desarrolladores de ALM Works. Por lo tanto, adjunto enlaces a su documentación y seminarios web. Y si comienza a trabajar con campos personalizados, revíselos con frecuencia para ver qué otras funciones puede utilizar.