Partager via


Qu’est-ce que les expressions jq dans azure IoT Data Processor Preview ?

Important

Opérations Azure IoT (préversion) – activé parc Azure Arc est actuellement en PRÉVERSION. Vous ne devez pas utiliser ce logiciel en préversion dans des environnements de production.

Pour connaître les conditions juridiques qui s’appliquent aux fonctionnalités Azure en version bêta, en préversion ou plus généralement non encore en disponibilité générale, consultez l’Avenant aux conditions d’utilisation des préversions de Microsoft Azure.

Les expressions jq constituent un moyen puissant d’effectuer des calculs et des manipulations sur des messages de pipeline de données. Ce guide montre des modèles de langage et des approches pour des besoins courants de calcul et de traitement dans vos pipelines de données.

Conseil

Pour essayer les exemples de ce guide, vous pouvez utiliser le terrain de jeu jq et coller les exemples d’entrées et d’expressions dans l’éditeur.

Principes de base du langage

Si vous n’êtes pas familiarisé avec le langage jq, cette section de principes de base du langage offre des informations d’arrière-plan.

Programmation fonctionnelle

Le langage jq est un langage de programmation fonctionnel. Chaque opération utilise une entrée et produit une sortie. Plusieurs opérations sont combinées pour former une logique complexe. Par exemple, soit l’entrée suivante :

{
  "payload": {
    "temperature": 25
  }
}

Voici une expression jq simple qui spécifie un chemin d’accès à récupérer :

.payload.temperature

Ce chemin d’accès est une opération qui utilise une valeur en entrée et génère une autre valeur en sortie. Dans cet exemple, la valeur de sortie est 25.

Quelques considérations importantes sont à prendre en compte lorsque vous utilisez des opérations complexes et chaînées dans jq :

  • Toutes les données non retournées par une opération ne sont plus disponibles dans le reste de l’expression. Il existe certaines façons de contourner cette contrainte, mais en général, vous devez réfléchir aux données dont vous avez besoin plus loin dans l’expression et empêcher leur annulation des opérations précédentes.
  • Les expressions sont considérées comme une série de transformations de données plutôt qu’un ensemble de calculs à effectuer. Même les opérations telles que les affectations ne sont qu’une transformation de la valeur globale dans laquelle un champ a changé.

Toutes les opérations sont des expressions

Dans la plupart des langages non fonctionnels, il existe une distinction entre deux types d’opérations :

  • Les Expressions produisent une valeur qui peut être utilisée dans le contexte d’une autre expression.
  • Les Instructions créent une forme d’effet secondaire plutôt que de manipuler directement une entrée et une sortie.

Dans jq, à part quelques exceptions, toutes les opérations sont des expressions. Les boucles, les opérations if/else et même les affectations sont des expressions qui produisent une nouvelle valeur, plutôt que de créer un effet secondaire dans le système. Par exemple, soit l’entrée suivante :

{
  "temperature": 21,
  "humidity": 65
}

Si vous voulez modifier le champ humidity en 63, vous pouvez utiliser une expression d’affectation :

.humidity = 63

Bien que cette expression semble modifier l’objet en entrée, dans jq, elle produit un nouvel objet ayant une nouvelle valeur pour humidity :

{
  "temperature": 21,
  "humidity": 63
}

Cette différence semble subtile, mais cela signifie que vous pouvez chaîner le résultat de cette opération avec d’autres opérations à l’aide de |, comme décrit plus loin.

Chaîner des opérations avec un canal : |

L’exécution de calculs et de manipulations de données dans jq vous oblige souvent à combiner plusieurs opérations. Vous chaînez des opérations en plaçant un | entre elles. Par exemple, pour calculer la longueur d’un tableau de données dans un message :

{
  "data": [5, 2, 4, 1]
}

Tout d’abord, isolez la partie du message qui contient le tableau :

.data

Cette expression vous donne uniquement le tableau :

[5, 2, 4, 1]

Ensuite, utilisez l’opération length pour calculer la longueur de ce tableau :

length

Cette expression vous donne votre réponse :

4

Utilisez l’opérateur | comme séparateur entre les étapes. Ainsi, en tant qu’expression jq unique, le calcul devient :

.data | length

Si vous essayez d’effectuer une transformation complexe et que vous ne voyez pas d’exemple correspondant exactement à votre problème, il est probable que vous puissiez résoudre votre problème en chaînant plusieurs solutions de ce guide avec le symbole |.

Entrées et arguments d’une fonction

Dans jq, une des principales opérations est l’appel d’une fonction. Dans jq, les fonctions sont fournies sous de nombreuses formes et peuvent utiliser un nombre variable d’entrées. Les entrées d’une fonction peuvent prendre deux formes :

  • Contexte de données : données automatiquement transmises à la fonction par jq. Généralement, les données produites par l’opération avant le dernier symbole |.
  • Arguments d’une fonction : autres expressions et valeurs à fournir pour configurer le comportement d’une fonction.

De nombreuses fonctions n’ont pas d’argument et font leur travail en utilisant le contexte de données fourni par jq. La fonction length en est un exemple :

["a", "b", "c"] | length

Dans l’exemple précédent, l’entrée de length est le tableau créé à gauche du symbole |. La fonction n’a pas besoin d’autres entrées pour calculer la longueur du tableau d’entrée. Vous appelez des fonctions sans argument en utilisant leur nom uniquement. En d’autres termes, utilisez length, et non length().

Certaines fonctions combinent le contexte de données avec un argument unique pour définir leur comportement. Par exemple, la fonction map :

[1, 2, 3] | map(. * 2)

Dans l’exemple précédent, l’entrée de map est le tableau de nombres créé à gauche du symbole |. La fonction map exécute une expression sur chaque élément du tableau d’entrée. Vous fournissez l’expression en tant qu’argument à map, dans ce cas . * 2, pour multiplier la valeur de chaque entrée du tableau par 2 afin de générer le tableau [2, 4, 6]. Vous pouvez configurer n’importe quel comportement interne souhaité avec la fonction de mappage.

Certaines fonctions utilisent plusieurs arguments. Ces fonctions fonctionnent de la même façon que les fonctions à argument unique et utilisent le symbole ; pour séparer les arguments. Par exemple, la fonction sub :

"Hello World" | sub("World"; "jq!")

Dans l’exemple précédent, la fonction sub reçoit « Hello World » en contexte de données d’entrée, puis utilise deux arguments :

  • Expression régulière à rechercher dans la chaîne.
  • Chaîne pour remplacer toute substring correspondante. Séparez les arguments par le symbole ;. Le même modèle s’applique aux fonctions utilisant plus de deux arguments.

Important

Veillez à utiliser ; comme séparateur d’arguments et non ,.

Utiliser des objets

Il existe de nombreuses façons d’extraire des données d’objets, de manipuler et de construire des objets dans jq. Les sections suivantes décrivent certains des modèles les plus courants :

Extraire des valeurs d’un objet

Pour récupérer des clés, vous utilisez généralement une expression de chemin d’accès. Cette opération est souvent combinée avec d’autres opérations pour obtenir des résultats plus complexes.

Il est facile de récupérer des données à partir d’objets. Lorsque vous devez récupérer de nombreux éléments de données dans des structures non-objet, il est courant de convertir les structures non-objet en objets. Étant donné l’entrée suivante :

{
  "payload": {
    "values": {
      "temperature": 45,
      "humidity": 67
    }
  }
}

Utilisez l’expression suivante pour récupérer la valeur d’humidité :

.payload.values.humidity

Cette fonction génère la sortie suivante :

67

Modifier des clés dans un objet

Pour renommer ou modifier des clés dans un objet, vous pouvez utiliser la fonction with_entries. Cette fonction utilise une expression qui fonctionne sur les paires clé/valeur d’un objet et retourne un nouvel objet contenant les résultats de l’expression.

L’exemple suivant vous montre comment renommer le champ temp en temperature pour s’aligner sur un schéma en aval. Étant donné l’entrée suivante :

{
  "payload": {
    "temp": 45,
    "humidity": 67
  }
}

Utilisez l’expression suivante pour renommer le champ temp en temperature :

.payload |= with_entries(if .key == "temp" then .key = "temperature" else . end)

Dans l’expression jq précédente :

  • .payload |= <expression> utilise |= pour mettre à jour la valeur de .payload avec le résultat de l’exécution de l’<expression>. L’utilisation de |= au lieu de = définit le contexte de données de <expression> sur .payload plutôt que sur ..
  • with_entries(<expression>) est un raccourci permettant d’exécuter plusieurs opérations ensemble. Elle effectue les opérations suivantes :
    • Prend un objet en entrée et convertit chaque paire clé/valeur en entrée de structure {"key": <key>, "value": <value>}.
    • Exécute <expression> sur chaque entrée générée à partir de l’objet, en remplaçant la valeur d’entrée de cette entrée par le résultat de l’exécution de <expression>.
    • Convertit l’ensemble d’entrées transformé en objet, en utilisant key comme clé dans la paire clé/valeur et value comme valeur de la clé.
  • if .key == "temp" then .key = "temperature" else . end applique une logique conditionnelle sur la clé de l’entrée. Si la clé est temp, elle est convertie en temperature, en laissant la valeur inchangée. Si la clé n’est pas temp, l’entrée reste inchangée, en retournant . depuis l’expression.

Le code JSON suivant présente la sortie de l’expression précédente :

{
  "payload": {
    "temperature": 45,
    "humidity": 67
  }
}

Convertir un objet en tableau

Bien que les objets soient utiles pour accéder aux données, les tableaux sont souvent plus utiles lorsque vous souhaitez fractionner des messages ou combiner dynamiquement des informations. Utiliser to_entries pour convertir un objet en tableau.

L’exemple suivant montre comment convertir le champ payload en tableau. Étant donné l’entrée suivante :

{
  "id": "abc",
  "payload": {
    "temperature": 45,
    "humidity": 67
  }
}

Utilisez l’expression suivante pour convertir le champ charge utile en tableau :

.payload | to_entries

Le JSON suivant représente la sortie de l’expression jq précédente :

[
  {
    "key": "temperature",
    "value": 45
  },
  {
    "key": "humidity",
    "value": 67
  }
]

Conseil

Cet exemple extrait simplement le tableau et abandonne toutes les autres informations du message. Pour conserver le message global, mais échanger la structure de .payload en tableau, utilisez plutôt .payload |= to_entries.

Créer des objets

Vous construisez des objets en utilisant une syntaxe similaire à celle de JSON, à laquelle vous pouvez fournir un mélange d’informations statiques et dynamiques.

L’exemple suivant montre comment restructurer complètement un objet en créant un objet dont les champs sont renommés et la structure est mise à jour. Étant donné l’entrée suivante :

{
  "payload": {
    "Timestamp": 1681926048,
    "Payload": {
      "dtmi:com:prod1:slicer3345:humidity": {
        "SourceTimestamp": 1681926048,
        "Value": 10
      },
      "dtmi:com:prod1:slicer3345:lineStatus": {
        "SourceTimestamp": 1681926048,
        "Value": [1, 5, 2]
      },
      "dtmi:com:prod1:slicer3345:speed": {
        "SourceTimestamp": 1681926048,
        "Value": 85
      },
      "dtmi:com:prod1:slicer3345:temperature": {
        "SourceTimestamp": 1681926048,
        "Value": 46
      }
    },
    "DataSetWriterName": "slicer-3345",
    "SequenceNumber": 461092
  }
}

Utilisez l’expression jq suivante pour créer un objet contenant la nouvelle structure :

{
  payload: {
    humidity: .payload.Payload["dtmi:com:prod1:slicer3345:humidity"].Value,
    lineStatus: .payload.Payload["dtmi:com:prod1:slicer3345:lineStatus"].Value,
    temperature: .payload.Payload["dtmi:com:prod1:slicer3345:temperature"].Value
  },
  (.payload.DataSetWriterName): "active"
}

Dans l’expression jq précédente :

  • {payload: {<fields>}} crée un objet contenant un champ littéral nommé payload qui est lui-même un objet littéral contenant d’autres champs. Cette approche est la façon la plus simple de construire des objets.
  • humidity: .payload.Payload["dtmi:com:prod1:slicer3345:humidity"].Value, crée un nom de clé statique dont la valeur est calculée dynamiquement. Le contexte de données de toutes les expressions de la construction d’objet est l’entrée complète de l’expression de construction d’objet, dans ce cas le message complet.
  • (.payload.DataSetWriterName): "active" est un exemple de clé d’objet dynamique. Dans cet exemple, la valeur de .payload.DataSetWriterName est mappée à une valeur statique. Utilisez des clés et valeurs statiques et dynamiques dans des combinaisons lorsque vous créez un objet.

Le JSON suivant présente la sortie de l’expression jq précédente :

{
  "payload": {
    "humidity": 10,
    "lineStatus": [1, 5, 2],
    "temperature": 46
  },
  "slicer-3345": "active"
}

Ajouter des champs à un objet

Vous pouvez augmenter un objet en ajoutant des champs pour fournir un contexte supplémentaire aux données. Utilisez une affectation à un champ qui n’existe pas.

L’exemple suivant montre comment ajouter un champ averageVelocity à la charge utile. Étant donné l’entrée suivante :

{
  "payload": {
    "totalDistance": 421,
    "elapsedTime": 1598
  }
}

Utilisez l’expression jq suivante pour ajouter un champ averageVelocity à la charge utile :

.payload.averageVelocity = (.payload.totalDistance / .payload.elapsedTime)

Contrairement à d’autres exemples qui utilisent le symbole |=, cet exemple utilise une affectation standard, =. Par conséquent, il n’étend pas l’expression située à droite du champ sur la gauche. Cette approche est nécessaire pour accéder à d’autres champs de la charge utile.

Le JSON suivant présente la sortie de l’expression jq précédente :

{
  "payload": {
    "totalDistance": 421,
    "elapsedTime": 1598,
    "averageVelocity": 0.2634543178973717
  }
}

Ajouter des champs de façon conditionnelle à un objet

La combinaison d’une logique conditionnelle avec la syntaxe permettant d’ajouter des champs à un objet active des scénarios tels que l’ajout de valeurs par défaut à des champs qui ne sont pas présents.

L’exemple suivant montre comment ajouter une unité à des mesures de température qui n’ont pas d’unité. L’unité par défaut est le Celsius. Étant donné l’entrée suivante :

{
  "payload": [
    {
      "timestamp": 1689712296407,
      "temperature": 59.2,
      "unit": "fahrenheit"
    },
    {
      "timestamp": 1689712399609,
      "temperature": 52.2
    },
    {
      "timestamp": 1689712400342,
      "temperature": 50.8,
      "unit": "celsius"
    }
  ]
}

Utilisez l’expression jq suivante pour ajouter une unité à des mesures de température qui n’ont pas d’unités:

.payload |= map(.unit //= "celsius")

Dans l’expression jq précédente :

  • .payload |= <expression> utilise |= pour mettre à jour la valeur de .payload avec le résultat de l’exécution de l’<expression>. L’utilisation de |= au lieu de = définit le contexte de données de <expression> sur .payload plutôt que sur ..
  • map(<expression>) exécute <expression> sur chaque entrée du tableau et remplace la valeur en entrée par ce que <expression> produit.
  • .unit //= "celsius" utilise l’affectation spéciale //=. Cette affectation combine (=) avec l’opérateur alternatif (//) pour s’affecter la valeur existante de .unit si elle n’est pas false ou null. Si .unit est false ou null, l’expression affecte "celsius" comme étant la valeur de .unit, créant .unit si nécessaire.

Le JSON suivant présente la sortie de l’expression jq précédente :

{
  "payload": [
    {
      "timestamp": 1689712296407,
      "temperature": 59.2,
      "unit": "fahrenheit"
    },
    {
      "timestamp": 1689712399609,
      "temperature": 52.2,
      "unit": "celsius"
    },
    {
      "timestamp": 1689712400342,
      "temperature": 50.8,
      "unit": "celsius"
    }
  ]
}

Supprimer des champs d’un objet

Utilisez la fonction del pour supprimer les champs inutiles d’un objet.

L’exemple suivant montre comment supprimer le champ timestamp, car il n’est pas pertinent pour le reste du calcul. Étant donné l’entrée suivante :

{
  "payload": {
    "timestamp": "2023-07-18T20:57:23.340Z",
    "temperature": 153,
    "pressure": 923,
    "humidity": 24
  }
}

L’expression jq suivante supprime le champ timestamp :

del(.payload.timestamp)

Le JSON suivant présente la sortie de l’expression jq précédente :

{
  "payload": {
    "temperature": 153,
    "pressure": 923,
    "humidity": 24
  }
}

Utiliser des tableaux

Dans jq, les tableaux constituent le bloc de construction principal d’une itération et d’un fractionnement de message. Les exemples suivants montrent comment manipuler des tableaux.

Extraire des valeurs d’un tableau

Les tableaux sont plus difficiles à inspecter que les objets, car les données peuvent se trouver dans différents index du tableau dans différents messages. Par conséquent, pour extraire des valeurs d’un tableau, vous devez souvent rechercher dans le tableau les données dont vous avez besoin.

L’exemple suivant montre comment extraire quelques valeurs d’un tableau pour créer un objet qui contient les données qui vous intéressent. Étant donné l’entrée suivante :

{
  "payload": {
    "data": [
      {
        "field": "dtmi:com:prod1:slicer3345:humidity",
        "value": 10
      },
      {
        "field": "dtmi:com:prod1:slicer3345:lineStatus",
        "value": [1, 5, 2]
      },
      {
        "field": "dtmi:com:prod1:slicer3345:speed",
        "value": 85
      },
      {
        "field": "dtmi:com:prod1:slicer3345:temperature",
        "value": 46
      }
    ],
    "timestamp": "2023-07-18T20:57:23.340Z"
  }
}

Utilisez l’expression jq suivante pour extraire les valeurs timestamp, temperature, humidity et pressure du tableau pour créer un objet :

.payload |= {
    timestamp,
    temperature: .data | map(select(.field == "dtmi:com:prod1:slicer3345:temperature"))[0]?.value,
    humidity: .data | map(select(.field == "dtmi:com:prod1:slicer3345:humidity"))[0]?.value,
    pressure: .data | map(select(.field == "dtmi:com:prod1:slicer3345:pressure"))[0]?.value,
}

Dans l’expression jq précédente :

  • .payload |= <expression> utilise |= pour mettre à jour la valeur de .payload avec le résultat de l’exécution de l’<expression>. L’utilisation de |= au lieu de = définit le contexte de données de <expression> sur .payload plutôt que sur ..
  • {timestamp, <other-fields>} est le raccourci de timestamp: .timestamp, ce qui ajoute le timestamp en tant que champ à l’objet en utilisant le champ du même nom de l’objet d’origine. <other-fields> ajoute d’autres champs à l’objet.
  • temperature: <expression>, humidity: <expression>, pressure: <expression> définit la température, l’humidité et la pression dans l’objet obtenu en fonction des résultats des trois expressions.
  • .data | <expression> limite le calcul de la valeur au tableau data de la charge utile et exécute <expression> sur le tableau.
  • map(<expression>)[0]?.value effectue plusieurs opérations :
    • map(<expression>) exécute <expression> sur chaque élément du tableau en retournant le résultat de l’exécution de cette expression sur chaque élément.
    • [0] extrait le premier élément du tableau obtenu.
    • ? permet le nouveau chaînage d’un segment de chemin d’accès, même si la valeur précédente est null. Lorsque la valeur précédente est null, le chemin d’accès suivant retourne également null au lieu d’échouer.
    • .value extrait le champ value du résultat.
  • select(.field == "dtmi:com:prod1:slicer3345:temperature") exécute l’expression booléenne à l’intérieur de select() sur l’entrée. Si le résultat est true, l’entrée est transmise. Si le résultat est false, l’entrée est supprimée. map(select(<expression>)) est une combinaison courante utilisée pour filtrer les éléments d’un tableau.

Le JSON suivant présente la sortie de l’expression jq précédente :

{
  "payload": {
    "timestamp": "2023-07-18T20:57:23.340Z",
    "temperature": 46,
    "humidity": 10,
    "pressure": null
  }
}

Modifier les entrées d’un tableau

Modifiez les entrées d’un tableau avec l’expression map(). Utilisez ces expressions pour modifier chaque élément du tableau.

L’exemple suivant montre comment convertir le timestamp de chaque entrée du tableau d’une milliseconde en temps unix en chaîne RFC3339. Étant donné l’entrée suivante :

{
  "payload": [
    {
      "field": "humidity",
      "timestamp": 1689723806615,
      "value": 10
    },
    {
      "field": "lineStatus",
      "timestamp": 1689723849747,
      "value": [1, 5, 2]
    },
    {
      "field": "speed",
      "timestamp": 1689723868830,
      "value": 85
    },
    {
      "field": "temperature",
      "timestamp": 1689723880530,
      "value": 46
    }
  ]
}

Utiliser l’expression jq suivante pour convertir le timestamp de chaque entrée du tableau d’une milliseconde en temps unix en chaîne RFC3339 :

.payload |= map(.timestamp |= (. / 1000 | strftime("%Y-%m-%dT%H:%M:%SZ")))

Dans l’expression jq précédente :

  • .payload |= <expression> utilise |= pour mettre à jour la valeur de .payload avec le résultat de l’exécution de l’<expression>. L’utilisation de |= au lieu de = définit le contexte de données de <expression> sur .payload plutôt que sur ..
  • map(<expression>) exécute <expression> sur chaque élément du tableau, remplaçant chacun par la sortie de l’exécution de <expression>.
  • .timestamp |= <expression> définit le timestamp sur une nouvelle valeur basée sur l’exécution de <expression>, où le contexte de données de <expression> est la valeur de .timestamp.
  • (. / 1000 | strftime("%Y-%m-%dT%H:%M:%SZ")) convertit les millisecondes en secondes et utilise un formateur de chaîne d’heure pour produire un timestamp ISO 8601.

Le JSON suivant présente la sortie de l’expression jq précédente :

{
  "payload": [
    {
      "field": "humidity",
      "timestamp": "2023-07-18T23:43:26Z",
      "value": 10
    },
    {
      "field": "lineStatus",
      "timestamp": "2023-07-18T23:44:09Z",
      "value": [1, 5, 2]
    },
    {
      "field": "speed",
      "timestamp": "2023-07-18T23:44:28Z",
      "value": 85
    },
    {
      "field": "temperature",
      "timestamp": "2023-07-18T23:44:40Z",
      "value": 46
    }
  ]
}

Convertir un tableau en objet

Pour restructurer un tableau en objet afin qu’il soit plus facile d’accès ou conforme à un schéma souhaité, utilisez from_entries. Étant donné l’entrée suivante :

{
  "payload": [
    {
      "field": "humidity",
      "timestamp": 1689723806615,
      "value": 10
    },
    {
      "field": "lineStatus",
      "timestamp": 1689723849747,
      "value": [1, 5, 2]
    },
    {
      "field": "speed",
      "timestamp": 1689723868830,
      "value": 85
    },
    {
      "field": "temperature",
      "timestamp": 1689723880530,
      "value": 46
    }
  ]
}

Utilisez l’expression jq suivante pour convertir le tableau en objet :

.payload |= (
    map({key: .field, value: {timestamp, value}})
    | from_entries
)

Dans l’expression jq précédente :

  • .payload |= <expression> utilise |= pour mettre à jour la valeur de .payload avec le résultat de l’exécution de l’<expression>. L’utilisation de |= au lieu de = définit le contexte de données de <expression> sur .payload plutôt que sur ..
  • map({key: <expression>, value: <expression>}) convertit chaque élément du tableau en objet du formulaire {"key": <data>, "value": <data>}, ce dont a besoin la structure from_entries.
  • {key: .field, value: {timestamp, value}} crée un objet à partir d’une entrée de tableau, en mappant field à la clé et en créant une valeur qui est un objet contenant timestamp et value. {timestamp, value} est un raccourci de {timestamp: .timestamp, value: .value}.
  • <expression> | from_entries convertit une <expression> matricielle en objet, en mappant le champ key de chaque entrée de tableau à la clé de l’objet et le champ value de chaque entrée de tableau à la valeur de cette clé.

Le JSON suivant présente la sortie de l’expression jq précédente :

{
  "payload": {
    "humidity": {
      "timestamp": 1689723806615,
      "value": 10
    },
    "lineStatus": {
      "timestamp": 1689723849747,
      "value": [1, 5, 2]
    },
    "speed": {
      "timestamp": 1689723868830,
      "value": 85
    },
    "temperature": {
      "timestamp": 1689723880530,
      "value": 46
    }
  }
}

Créer des tableaux

La création de littéraux de tableau est similaire à la création de littéraux d’objet. La syntaxe jq d’un littéral de tableau est similaire à celle de JSON et JavaScript.

L’exemple suivant montre comment extraire des valeurs dans un simple tableau pour un traitement ultérieur.

Étant donné l’entrée suivante :

{
  "payload": {
    "temperature": 14,
    "humidity": 56,
    "pressure": 910
  }
}

L’utilisation de l’expression jq suivante crée un tableau à partir des valeurs des champs temperature, humidityet pressure :

.payload |= ([.temperature, .humidity, .pressure])

Le JSON suivant présente la sortie de l’expression jq précédente :

{
  "payload": [14, 56, 910]
}

Ajouter des entrées à un tableau

Vous pouvez ajouter des entrées au début ou à la fin d’un tableau en utilisant l’opérateur + sur le tableau et ses nouvelles entrées. L’opérateur += simplifie cette opération en ajoutant automatiquement les nouvelles entrées à la fin du tableau. Étant donné l’entrée suivante :

{
  "payload": {
    "Timestamp": 1681926048,
    "Payload": {
      "dtmi:com:prod1:slicer3345:humidity": {
        "SourceTimestamp": 1681926048,
        "Value": 10
      },
      "dtmi:com:prod1:slicer3345:lineStatus": {
        "SourceTimestamp": 1681926048,
        "Value": [1, 5, 2]
      },
      "dtmi:com:prod1:slicer3345:speed": {
        "SourceTimestamp": 1681926048,
        "Value": 85
      },
      "dtmi:com:prod1:slicer3345:temperature": {
        "SourceTimestamp": 1681926048,
        "Value": 46
      }
    },
    "DataSetWriterName": "slicer-3345",
    "SequenceNumber": 461092
  }
}

Utilisez l’expression jq suivante pour ajouter les valeurs 12 et 41 à la fin du tableau de valeurs lineStatus :

.payload.Payload["dtmi:com:prod1:slicer3345:lineStatus"].Value += [12, 41]

Le JSON suivant présente la sortie de l’expression jq précédente :

{
  "payload": {
    "Timestamp": 1681926048,
    "Payload": {
      "dtmi:com:prod1:slicer3345:humidity": {
        "SourceTimestamp": 1681926048,
        "Value": 10
      },
      "dtmi:com:prod1:slicer3345:lineStatus": {
        "SourceTimestamp": 1681926048,
        "Value": [1, 5, 2, 12, 41]
      },
      "dtmi:com:prod1:slicer3345:speed": {
        "SourceTimestamp": 1681926048,
        "Value": 85
      },
      "dtmi:com:prod1:slicer3345:temperature": {
        "SourceTimestamp": 1681926048,
        "Value": 46
      }
    },
    "DataSetWriterName": "slicer-3345",
    "SequenceNumber": 461092
  }
}

Supprimer des entrées d’un tableau

Utilisez la fonction del pour supprimer les entrées d’un tableau de la même façon que pour supprimer un objet. Étant donné l’entrée suivante :

{
  "payload": {
    "Timestamp": 1681926048,
    "Payload": {
      "dtmi:com:prod1:slicer3345:humidity": {
        "SourceTimestamp": 1681926048,
        "Value": 10
      },
      "dtmi:com:prod1:slicer3345:lineStatus": {
        "SourceTimestamp": 1681926048,
        "Value": [1, 5, 2]
      },
      "dtmi:com:prod1:slicer3345:speed": {
        "SourceTimestamp": 1681926048,
        "Value": 85
      },
      "dtmi:com:prod1:slicer3345:temperature": {
        "SourceTimestamp": 1681926048,
        "Value": 46
      }
    },
    "DataSetWriterName": "slicer-3345",
    "SequenceNumber": 461092
  }
}

Utilisez l’expression jq suivante pour supprimer la deuxième entrée du tableau de valeurs lineStatus :

del(.payload.Payload["dtmi:com:prod1:slicer3345:lineStatus"].Value[1])

Le JSON suivant présente la sortie de l’expression jq précédente :

{
  "payload": {
    "Timestamp": 1681926048,
    "Payload": {
      "dtmi:com:prod1:slicer3345:humidity": {
        "SourceTimestamp": 1681926048,
        "Value": 10
      },
      "dtmi:com:prod1:slicer3345:lineStatus": {
        "SourceTimestamp": 1681926048,
        "Value": [1, 2]
      },
      "dtmi:com:prod1:slicer3345:speed": {
        "SourceTimestamp": 1681926048,
        "Value": 85
      },
      "dtmi:com:prod1:slicer3345:temperature": {
        "SourceTimestamp": 1681926048,
        "Value": 46
      }
    },
    "DataSetWriterName": "slicer-3345",
    "SequenceNumber": 461092
  }
}

Supprimer les entrées de tableau en double

Si des éléments de tableau se chevauchent, vous pouvez supprimer les entrées en double. Dans la plupart des langages de programmation, vous pouvez supprimer des doublons à l’aide de variables de recherche côte à côte. Dans jq, la meilleure approche consiste à organiser les données selon la façon dont elles doivent être traitées, puis à effectuer toutes les opérations avant de les convertir au format souhaité.

L’exemple suivant montre comment utiliser un message contenant certaines valeurs, puis le filtrer afin de disposer uniquement de la dernière lecture de chaque valeur. Étant donné l’entrée suivante :

{
  "payload": [
    {
      "name": "temperature",
      "value": 12,
      "timestamp": 1689727870701
    },
    {
      "name": "humidity",
      "value": 51,
      "timestamp": 1689727944440
    },
    {
      "name": "temperature",
      "value": 15,
      "timestamp": 1689727994085
    },
    {
      "name": "humidity",
      "value": 25,
      "timestamp": 1689727914558
    },
    {
      "name": "temperature",
      "value": 31,
      "timestamp": 1689727987072
    }
  ]
}

Utilisez l’expression jq suivante pour filtrer l’entrée afin de disposer uniquement de la dernière lecture de chaque valeur :

.payload |= (group_by(.name) | map(sort_by(.timestamp)[-1]))

Conseil

Si vous ne cherchez pas à récupérer la valeur la plus récente de chaque nom, vous pouvez simplifier l’expression à .payload |= unique_by(.name)

Dans l’expression jq précédente :

  • .payload |= <expression> utilise |= pour mettre à jour la valeur de .payload avec le résultat de l’exécution de l’<expression>. L’utilisation de |= au lieu de = définit le contexte de données de <expression> sur .payload plutôt que sur ..
  • group_by(.name) utilise un tableau comme entrée, place des éléments dans des sous-tableaux en fonction de la valeur du .name de chaque élément. Chaque sous-tableau contient les éléments du tableau d’origine avec la même valeur de .name.
  • map(<expression>) utilise le groupe de tableaux généré par group_by et exécute <expression> sur chacun des sous-tableaux.
  • sort_by(.timestamp)[-1] extrait l’élément qui vous intéresse de chaque sous-tableau :
    • sort_by(.timestamp) trie les éléments par la valeur croissante de leur champ .timestamp dans le sous-tableau en cours.
    • [-1] récupère le dernier élément du sous-tableau trié, qui correspond à l’entrée ayant l’heure la plus récente sur chaque nom.

Le JSON suivant présente la sortie de l’expression jq précédente :

{
  "payload": [
    {
      "name": "humidity",
      "value": 51,
      "timestamp": 1689727944440
    },
    {
      "name": "temperature",
      "value": 15,
      "timestamp": 1689727994085
    }
  ]
}

Valeurs de calcul entre les éléments d’un tableau

Vous pouvez combiner les valeurs des éléments d’un tableau pour calculer des valeurs telles que les moyennes des éléments.

Cet exemple montre comment réduire le tableau en récupérant le timestamp le plus élevé et la valeur moyenne des entrées qui partagent le même nom. Étant donné l’entrée suivante :

{
  "payload": [
    {
      "name": "temperature",
      "value": 12,
      "timestamp": 1689727870701
    },
    {
      "name": "humidity",
      "value": 51,
      "timestamp": 1689727944440
    },
    {
      "name": "temperature",
      "value": 15,
      "timestamp": 1689727994085
    },
    {
      "name": "humidity",
      "value": 25,
      "timestamp": 1689727914558
    },
    {
      "name": "temperature",
      "value": 31,
      "timestamp": 1689727987072
    }
  ]
}

Utilisez l’expression jq suivante pour récupérer le timestamp le plus élevé et la valeur moyenne des entrées qui partagent le même nom :

.payload |= (group_by(.name) | map(
  {
    name: .[0].name,
    value: map(.value) | (add / length),
    timestamp: map(.timestamp) | max
  }
))

Dans l’expression jq précédente :

  • .payload |= <expression> utilise |= pour mettre à jour la valeur de .payload avec le résultat de l’exécution de l’<expression>. L’utilisation de |= au lieu de = définit le contexte de données de <expression> sur .payload plutôt que sur ..
  • group_by(.name) prend un tableau comme entrée, et place des éléments dans des sous-tableaux en fonction de la valeur de .name de chaque élément. Chaque sous-tableau contient les éléments du tableau d’origine avec la même valeur de .name.
  • map(<expression>) utilise le groupe de tableaux généré par group_by et exécute <expression> sur chacun des sous-tableaux.
  • {name: <expression>, value: <expression>, timestamp: <expression>} construit un objet hors du sous-tableau d’entrée avec les champs name, valueet timestamp. Chaque <expression> produit la valeur souhaitée de la clé associée.
  • .[0].name récupère le premier élément du sous-tableau et en extrait le champ name. Tous les éléments du sous-tableau ont le même nom. Vous devez donc uniquement récupérer le premier.
  • map(.value) | (add / length) calcule la value moyenne de chaque sous-tableau :
    • map(.value) convertit le sous-tableau en tableau du champ value dans chaque entrée, en retournant dans ce cas un tableau de nombres.
    • add est une fonction jq intégrée qui calcule la somme d’un tableau de nombres.
    • length est une fonction jq intégrée qui calcule le nombre ou la longueur d’un tableau.
    • add / length divise la somme par le nombre pour déterminer la moyenne.
  • map(.timestamp) | max recherche la valeur timestamp maximale de chaque sous-tableau :
    • map(.timestamp) convertit le sous-tableau en tableau des champs timestamp dans chaque entrée, en retournant dans ce cas un tableau de nombres.
    • max est une fonction jq intégrée qui recherche la valeur maximale dans un tableau.

Le JSON suivant présente la sortie de l’expression jq précédente :

{
  "payload": [
    {
      "name": "humidity",
      "value": 38,
      "timestamp": 1689727944440
    },
    {
      "name": "temperature",
      "value": 19.333333333333332,
      "timestamp": 1689727994085
    }
  ]
}

Utiliser des chaînes

jq fournit plusieurs utilitaires qui permettent de manipuler et de construire des chaînes. Les exemples suivants présentent certains cas d’usage courants.

Fractionner des chaînes

Si une chaîne contient plusieurs éléments d’informations séparés par un caractère commun, vous pouvez utiliser la fonction split() pour extraire les éléments individuels.

L’exemple suivant montre comment fractionner une chaîne de rubrique et retourner un segment spécifique de la rubrique. Cette technique est pratique lorsque vous utilisez des expressions de clé de partition. Étant donné l’entrée suivante :

{
  "systemProperties": {
    "timestamp": "2023-01-11T10:02:07Z"
  },
  "qos": 1,
  "topic": "assets/slicer-3345/tags/rpm",
  "properties": {
    "contentType": "application/json"
  },
  "payload": {
    "Timestamp": 1681926048,
    "Value": 142
  }
}

Utilisez l’expression jq suivante pour fractionner la chaîne de rubrique, en utilisant / comme séparateur, et pour renvoyer un segment spécifique de la rubrique :

.topic | split("/")[1]

Dans l’expression jq précédente :

  • .topic | <expression> sélectionne la clé topic de l’objet racine et exécute <expression> sur les données qu’il contient.
  • split("/") décompose la chaîne de la rubrique dans un tableau en fractionnant la chaîne à chaque fois qu’elle trouve le caractère /. Dans ce cas, il produit un ["assets", "slicer-3345", "tags", "rpm"].
  • [1] récupère l’élément à l’index 1 du tableau de l’étape précédente, dans ce cas un slicer-3345.

Le JSON suivant présente la sortie de l’expression jq précédente :

"slicer-3345"

Construire des chaînes dynamiquement

jq vous permet de construire des chaînes à l’aide de modèles de chaînes en utilisant la syntaxe \(<expression>) dans une chaîne. Utilisez ces modèles pour générer des chaînes dynamiquement.

L’exemple suivant montre comment ajouter un préfixe à chaque clé dans un objet en utilisant des modèles de chaîne. Étant donné l’entrée suivante :

{
  "temperature": 123,
  "humidity": 24,
  "pressure": 1021
}

Utilisez l’expression jq suivante pour ajouter un préfixe à chaque clé dans l’objet :

with_entries(.key |= "current-\(.)")

Dans l’expression jq précédente :

  • with_entries(<expression>) convertit l’objet en tableau de paires clé/valeur de structure {key: <key>, value: <value>}, exécute <expression> sur chaque paire clé/valeur et convertit les paires en objet.
  • .key |= <expression> met à jour la valeur de .key dans l’objet paire clé/valeur en fonction du résultat de <expression>. L’utilisation de |= au lieu de = définit le contexte de données de <expression> sur la valeur de .key, plutôt que sur l’objet complet de paire clé/valeur.
  • "current-\(.)" produit une chaîne qui commence par « current- », puis insère la valeur du contexte de données en cours ., dans ce cas la valeur de la clé. La syntaxe \(<expression>) dans la chaîne indique que vous souhaitez remplacer cette partie de la chaîne par le résultat de l’exécution de <expression>.

Le JSON suivant présente la sortie de l’expression jq précédente :

{
  "current-temperature": 123,
  "current-humidity": 24,
  "current-pressure": 1021
}

Utiliser des expressions régulières

jq prend en charge des expressions régulières standard. Vous pouvez utiliser des expressions régulières pour extraire, remplacer et vérifier des modèles dans des chaînes. Les fonctions d’expression courantes de jq incluent test(), match(), split(), capture(), sub() et gsub().

Extraire des valeurs en utilisant des expressions régulières

Si vous ne pouvez pas utiliser le fractionnement de chaîne pour extraire une valeur d’une chaîne, essayez d’utiliser des expressions régulières pour extraire les valeurs dont vous avez besoin.

L’exemple suivant montre comment normaliser des clés d’objet en testant une expression régulière, puis en la remplaçant par un autre format. Étant donné l’entrée suivante :

{
  "payload": {
    "Timestamp": 1681926048,
    "Payload": {
      "dtmi:com:prod1:slicer3345:humidity": {
        "SourceTimestamp": 1681926048,
        "Value": 10
      },
      "dtmi:com:prod1:slicer3345:speed": {
        "SourceTimestamp": 1681926048,
        "Value": 85
      },
      "temperature": {
        "SourceTimestamp": 1681926048,
        "Value": 46
      }
    },
    "DataSetWriterName": "slicer-3345",
    "SequenceNumber": 461092
  }
}

Utilisez l’expression jq suivante pour normaliser les clés d’objet :

.payload.Payload |= with_entries(
    .key |= if test("^dtmi:.*:(?<tag>[^:]+)$") then
        capture("^dtmi:.*:(?<tag>[^:]+)$").tag
    else
        .
    end
)

Dans l’expression jq précédente :

  • .payload |= <expression> utilise |= pour mettre à jour la valeur de .payload avec le résultat de l’exécution de l’<expression>. L’utilisation de |= au lieu de = définit le contexte de données de <expression> sur .payload plutôt que sur ..
  • with_entries(<expression>) convertit l’objet en tableau de paires clé/valeur de structure {key: <key>, value: <value>}, exécute <expression> sur chaque paire clé/valeur et convertit les paires en objet.
  • .key |= <expression> met à jour la valeur de .key dans l’objet paire clé/valeur en fonction du résultat de <expression>. l’utilisation de |= au lieu de = définit le contexte de données de <expression> sur la valeur de .key, plutôt que l’objet complet de paire clé/valeur.
  • if test("^dtmi:.*:(?<tag>[^:]+)$") then capture("^dtmi:.*:(?<tag>[^:]+)$").tag else . end vérifie et met à jour la clé en fonction d’une expression régulière :
    • test("^dtmi:.*:(?<tag>[^:]+)$") vérifie le contexte de données d’entrée, dans ce cas la clé, par rapport à l’expression régulière ^dtmi:.*:(?<tag>[^:]+)$. Si l’expression régulière correspond, elle retourne true. Sinon, elle retourne false.
    • capture("^dtmi:.*:(?<tag>[^:]+)$").tag exécute l’expression régulière ^dtmi:.*:(?<tag>[^:]+)$ sur le contexte de données d’entrée, dans ce cas la clé, et place tous les groupes de capture de l’expression régulière, indiqués par (?<tag>...), dans un objet en tant que sortie. L’expression extrait ensuite .tag de cet objet pour renvoyer les informations extraites par l’expression régulière.
    • . dans la branche else, l’expression transmet les données sans modification.

Le JSON suivant présente la sortie de l’expression jq précédente :

{
  "payload": {
    "Timestamp": 1681926048,
    "Payload": {
      "humidity": {
        "SourceTimestamp": 1681926048,
        "Value": 10
      },
      "speed": {
        "SourceTimestamp": 1681926048,
        "Value": 85
      },
      "temperature": {
        "SourceTimestamp": 1681926048,
        "Value": 46
      }
    },
    "DataSetWriterName": "slicer-3345",
    "SequenceNumber": 461092
  }
}

Fractionner des messages en messages distincts

Une fonctionnalité utile du langage jq réside dans sa capacité à produire plusieurs sorties à partir d’une seule entrée. Cette fonctionnalité vous permet de fractionner les messages en plusieurs messages distincts que le pipeline doit traiter. La clé de cette technique est .[], qui fractionne des tableaux en valeurs distinctes. Les exemples suivants présentent quelques scénarios qui utilisent cette syntaxe.

Nombre dynamique de sorties

En règle générale, lorsque vous souhaitez fractionner un message en plusieurs sorties, le nombre de sorties souhaitées dépend de la structure du message. La syntaxe [] vous permet d’effectuer ce type de fractionnement.

Par exemple, vous avez un message contenant une liste de balises que vous souhaitez placer dans des messages distincts. Étant donné l’entrée suivante :

{
  "systemProperties": {
    "partitionKey": "slicer-3345",
    "partitionId": 5,
    "timestamp": "2023-01-11T10:02:07Z"
  },
  "qos": 1,
  "topic": "assets/slicer-3345",
  "properties": {
    "responseTopic": "assets/slicer-3345/output",
    "contentType": "application/json"
  },
  "payload": {
    "Timestamp": 1681926048,
    "Payload": {
      "dtmi:com:prod1:slicer3345:humidity": {
        "sourceTimestamp": 1681926048,
        "value": 10
      },
      "dtmi:com:prod1:slicer3345:lineStatus": {
        "sourceTimestamp": 1681926048,
        "value": [1, 5, 2]
      },
      "dtmi:com:prod1:slicer3345:speed": {
        "sourceTimestamp": 1681926048,
        "value": 85
      },
      "dtmi:com:prod1:slicer3345:temperature": {
        "sourceTimestamp": 1681926048,
        "value": 46
      }
    },
    "DataSetWriterName": "slicer-3345",
    "SequenceNumber": 461092
  }
}

Utilisez l’expression jq suivante pour fractionner le message en plusieurs messages :

.payload.Payload = (.payload.Payload | to_entries[])
| .payload |= {
  DataSetWriterName,
  SequenceNumber,
  Tag: .Payload.key,
  Value: .Payload.value.value,
  Timestamp: .Payload.value.sourceTimestamp
}

Dans l’expression jq précédente :

  • .payload.Payload = (.payload.Payload | to_entries[]) fractionne le message en plusieurs messages :
    • .payload.Payload = <expression> assigne le résultat de l’exécution de <expression> à .payload.Payload. En règle générale, vous utilisez |=, dans ce cas pour étendre le contexte de <expression> jusqu’à .payload.Payload, mais comme |= ne prend pas en charge le fractionnement de message en messages distincts, utilisez plutôt =.
    • (.payload.Payload | <expression>) limite le côté droit de l’expression d’affectation à .payload.Payload afin que <expression> fonctionne sur la partie correcte du message.
    • to_entries[] correspond à deux opérations et est un raccourci de to_entries | .[] :
      • to_entries convertit l’objet en tableau de paires clé/valeur suivant un schéma {"key": <key>, "value": <value>}. Ces informations représentent ce que vous souhaitez séparer en différents messages.
      • [] effectue le fractionnement du message. Chaque entrée du tableau devient une valeur distincte dans jq. Lors de l’affectation à .payload.Payload, chaque valeur distincte entraîne une copie du message global, avec .payload.Payload défini sur la valeur correspondante produite par le côté droit de l’affectation.
  • .payload |= <expression> remplace la valeur de .payload par le résultat de l’exécution de <expression>. À ce stade, la requête traite un flux de valeurs plutôt qu’une seule valeur en raison du fractionnement de l’opération précédente. Par conséquent, l’affectation est exécutée une fois pour chaque message généré par l’opération précédente plutôt qu’une seule fois sur l’ensemble.
  • {DataSetWriterName, SequenceNumber, ...} construit un nouvel objet correspondant à la valeur de .payload. DataSetWriterName et SequenceNumber ne sont pas modifiés. Vous pouvez donc utiliser la syntaxe raccourcie plutôt que d’écrire DataSetWriterName: .DataSetWriterName et SequenceNumber: .SequenceNumber.
  • Tag: .Payload.key, extrait la clé d’objet d’origine de la Payload interne et la remonte vers l’objet parent. L’opération to_entries exécutée précédemment dans la requête a créé le champ key.
  • Value: .Payload.value.value et Timestamp: .Payload.value.sourceTimestamp effectuent une extraction similaire des données à partir de la charge utile interne. Cette fois à partir de la valeur de la paire clé/valeur d’origine. Le résultat est un objet plat de charge utile que vous pouvez utiliser dans un traitement ultérieur.

Le JSON suivant présente la sortie de l’expression jq précédente. Chaque sortie devient un message autonome pour des étapes de traitement ultérieures du pipeline :

{
  "systemProperties": {
    "partitionKey": "slicer-3345",
    "partitionId": 5,
    "timestamp": "2023-01-11T10:02:07Z"
  },
  "qos": 1,
  "topic": "assets/slicer-3345",
  "properties": {
    "responseTopic": "assets/slicer-3345/output",
    "contentType": "application/json"
  },
  "payload": {
    "DataSetWriterName": "slicer-3345",
    "SequenceNumber": 461092,
    "Tag": "dtmi:com:prod1:slicer3345:humidity",
    "Value": 10,
    "Timestamp": 1681926048
  }
}
{
  "systemProperties": {
    "partitionKey": "slicer-3345",
    "partitionId": 5,
    "timestamp": "2023-01-11T10:02:07Z"
  },
  "qos": 1,
  "topic": "assets/slicer-3345",
  "properties": {
    "responseTopic": "assets/slicer-3345/output",
    "contentType": "application/json"
  },
  "payload": {
    "DataSetWriterName": "slicer-3345",
    "SequenceNumber": 461092,
    "Tag": "dtmi:com:prod1:slicer3345:lineStatus",
    "Value": [1, 5, 2],
    "Timestamp": 1681926048
  }
}
{
  "systemProperties": {
    "partitionKey": "slicer-3345",
    "partitionId": 5,
    "timestamp": "2023-01-11T10:02:07Z"
  },
  "qos": 1,
  "topic": "assets/slicer-3345",
  "properties": {
    "responseTopic": "assets/slicer-3345/output",
    "contentType": "application/json"
  },
  "payload": {
    "DataSetWriterName": "slicer-3345",
    "SequenceNumber": 461092,
    "Tag": "dtmi:com:prod1:slicer3345:speed",
    "Value": 85,
    "Timestamp": 1681926048
  }
}
{
  "systemProperties": {
    "partitionKey": "slicer-3345",
    "partitionId": 5,
    "timestamp": "2023-01-11T10:02:07Z"
  },
  "qos": 1,
  "topic": "assets/slicer-3345",
  "properties": {
    "responseTopic": "assets/slicer-3345/output",
    "contentType": "application/json"
  },
  "payload": {
    "DataSetWriterName": "slicer-3345",
    "SequenceNumber": 461092,
    "Tag": "dtmi:com:prod1:slicer3345:temperature",
    "Value": 46,
    "Timestamp": 1681926048
  }
}

Nombre fixe de sorties

Pour fractionner un message en un nombre fixe de sorties plutôt qu’un nombre dynamique de sorties en fonction de la structure du message, utilisez l’opérateur , plutôt que [].

L’exemple suivant montre comment fractionner les données en deux messages en fonction des noms de champs existants. Étant donné l’entrée suivante :

{
  "topic": "test/topic",
  "payload": {
    "minTemperature": 12,
    "maxTemperature": 23,
    "minHumidity": 52,
    "maxHumidity": 92
  }
}

Utilisez l’expression jq suivante pour fractionner le message en deux messages :

.payload = (
  {
    field: "temperature",
    minimum: .payload.minTemperature,
    maximum: .payload.maxTemperature
  },
  {
    field: "humidity",
    minimum: .payload.minHumidity,
    maximum: .payload.maxHumidity
  }
)

Dans l’expression jq précédente :

  • .payload = ({<fields>},{<fields>}) attribue les deux littéraux d’objet à .payload dans le message. Les objets séparés par des virgules produisent deux valeurs distinctes et les assignent à une .payload, ce qui entraîne le fractionnement du message complet en deux messages. Chaque nouveau message a une .payload définie sur l’une des valeurs.
  • {field: "temperature", minimum: .payload.minTemperature, maximum: .payload.maxTemperature} est un constructeur d’objet littéral qui remplit les champs d’un objet avec une chaîne littérale et d’autres données extraites de l’objet.

Le JSON suivant présente la sortie de l’expression jq précédente. Chaque sortie devient un message autonome pour d’autres étapes de traitement :

{
  "topic": "test/topic",
  "payload": {
    "field": "temperature",
    "minimum": 12,
    "maximum": 23
  }
}
{
  "topic": "test/topic",
  "payload": {
    "field": "humidity",
    "minimum": 52,
    "maximum": 92
  }
}

Opérations mathématiques

jq prend en charge des opérations mathématiques courantes. Certaines opérations sont des opérateurs tels que + et -. Ces opérations incluent des fonctions telles que sin et exp.

Arithmétique

jq prend en charge cinq opérations arithmétiques de base : l’addition (+), la soustraction (-), la multiplication (*), la division (/) et le modulo (%). Contrairement à de nombreuses fonctionnalités de jq, ces opérations sont des opérations infixées qui vous permettent d’écrire l’expression mathématique complète dans une seule expression sans séparateurs |.

L’exemple suivant montre comment convertir une température Fahrenheit en température Celsius et extraire la lecture des secondes en cours d’un timestamp unix en millisecondes. Étant donné l’entrée suivante :

{
  "payload": {
    "temperatureF": 94.2,
    "timestamp": 1689766750628
  }
}

Utilisez l’expression jq suivante pour convertir la température Fahrenheit en température Celsius et extraire la lecture des secondes en cours d’un timestamp unix en millisecondes :

.payload.temperatureC = (5/9) * (.payload.temperatureF - 32)
| .payload.seconds = (.payload.timestamp / 1000) % 60

Dans l’expression jq précédente :

  • .payload.temperatureC = (5/9) * (.payload.temperatureF - 32) crée un champ temperatureC dans la charge utile définie sur la conversion de temperatureF des Fahrenheit en Celsius.
  • .payload.seconds = (.payload.timestamp / 1000) % 60 utilise une durée en millisecondes unix et la convertit en secondes, puis extrait le nombre de secondes dans la minute courante à l’aide d’un calcul modulo.

Le JSON suivant présente la sortie de l’expression jq précédente :

{
  "payload": {
    "temperatureF": 94.2,
    "timestamp": 1689766750628,
    "temperatureC": 34.55555555555556,
    "seconds": 10
  }
}

Fonctions mathématiques

jq inclut plusieurs fonctions qui effectuent des opérations mathématiques. La liste complète est disponible dans le manuel jq.

L’exemple suivant montre comment calculer l’énergie cinétique à partir de champs de masse et de vélocité. Étant donné l’entrée suivante :

{
  "userProperties": [
    { "key": "mass", "value": 512.1 },
    { "key": "productType", "value": "projectile" }
  ],
  "payload": {
    "velocity": 97.2
  }
}

Utilisez l’expression jq suivante pour calculer l’énergie cinétique à partir de champs de masse et de vélocité :

.payload.energy = (0.5 * (.userProperties | from_entries).mass * pow(.payload.velocity; 2) | round)

Dans l’expression jq précédente :

  • .payload.energy = <expression> crée un champ energy dans la charge utile résultant de l’exécution de <expression>.
  • (0.5 * (.userProperties | from_entries).mass * pow(.payload.velocity; 2) | round) est la formule de l’énergie :
    • (.userProperties | from_entries).mass extrait l’entrée mass de la liste userProperties. Les données sont déjà configurées en tant qu’objets avec key et value, de sorte que from_entries peut les convertir directement en objet. L’expression récupère la clé mass de l’objet résultant et retourne sa valeur.
    • pow(.payload.velocity; 2) extrait la vélocité de la charge utile et la met au carré en l’élevant à la puissance 2.
    • <expression> | round arrondit le résultat au nombre entier le plus proche pour éviter une haute précision trompeuse dans le résultat.

Le JSON suivant présente la sortie de l’expression jq précédente :

{
  "userProperties": [
    { "key": "mass", "value": 512.1 },
    { "key": "productType", "value": "projectile" }
  ],
  "payload": {
    "velocity": 97.2,
    "energy": 2419119
  }
}

logique booléenne.

Les pipelines de traitement de données utilisent souvent jq pour filtrer les messages. Le filtrage utilise généralement des expressions et des opérateurs booléens. En outre, la logique booléenne est utile pour effectuer un flux de contrôle dans les transformations et dans des cas d’usage de filtrage plus avancés.

Les exemples suivants montrent certaines des fonctionnalités les plus courantes utilisées dans les expressions booléennes dans jq :

Opérateurs booléens et conditionnels de base

jq fournit les opérateurs logiques booléens de base and, or et not. Les opérateurs and et or sont des opérateurs infixés. not est une fonction que vous appelez en tant que filtre, par exemple <expression> | not.

jq contient les opérateurs conditionnels >, <, ==, !=, >= et <=. Ces opérateurs sont des opérateurs infixés.

L’exemple suivant montre comment effectuer une logique booléenne de base à l’aide de conditions. Étant donné l’entrée suivante :

{
  "payload": {
    "temperature": 50,
    "humidity": 92,
    "site": "Redmond"
  }
}

Utilisez l’expression jq suivante pour vérifier si :

  • La température est comprise entre 30 et 60 degrés, limite supérieure incluse.
  • L’humidité est inférieure à 80 et le site est Redmond.
.payload
| ((.temperature > 30 and .temperature <= 60) or .humidity < 80) and .site == "Redmond"
| not

Dans l’expression jq précédente :

  • .payload | <expression> limite <expression> au contenu de .payload. Cette syntaxe rend le reste de l’expression moins détaillée.
  • ((.temperature > 30 and .temperature <= 60) or .humidity < 80) and .site == "Redmond" retourne true si la température est comprise entre 30 et 60 degrés (limite supérieure incluse) ou si l’humidité est inférieure à 80, puis retourne uniquement true si le site est également Redmond.
  • <expression> | not utilise le résultat de l’expression précédente et lui applique un opérateur logique NOT, inversant le résultat de true à false dans cet exemple.

Le JSON suivant présente la sortie de l’expression jq précédente :

false

Vérifier l’existence d’une clé d’objet

Vous pouvez créer un filtre qui vérifie la structure d’un message plutôt que son contenu. Par exemple, vous pouvez vérifier si une clé particulière est présente dans un objet. Pour effectuer cette vérification, utilisez la fonction has ou une vérification par rapport à null. L’exemple suivant présente ces deux approches. Étant donné l’entrée suivante :

{
  "payload": {
    "temperature": 51,
    "humidity": 41,
    "site": null
  }
}

Utilisez l’expression jq suivante pour vérifier si la charge utile a un champ temperature, si le champ site n’est pas null et effectuer d’autres vérifications :

.payload | {
    hasTemperature: has("temperature"),
    temperatureNotNull: (.temperature != null),
    hasSite: has("site"),
    siteNotNull: (.site != null),
    hasMissing: has("missing"),
    missingNotNull: (.missing != null),
    hasNested: (has("nested") and (.nested | has("inner"))),
    nestedNotNull: (.nested?.inner != null)
}

Dans l’expression jq précédente :

  • .payload | <expression> limite le contexte de données de <expression> à la valeur de .payload pour rendre <expression> moins détaillée.
  • hasTemperature: has("temperature"), et d’autres expressions similaires montrent comment la fonction has se comporte avec un objet d’entrée. La fonction retourne true uniquement si la clé est présente. hasSite est vrai bien que la valeur de site soit null.
  • temperatureNotNull: (.temperature != null), et d’autres expressions similaires montrent comment la vérification != null effectue une vérification similaire à has. Une clé inexistante dans un objet est null si elle est accessible à l’aide de la syntaxe .<key>, ou la clé existe, mais a une valeur null. siteNotNull et missingNotNull ont une valeur false, même si une clé est présente et l’autre est absente.
  • hasNested: (has("nested") and (.nested | has("inner"))) effectue une vérification sur un objet imbriqué avec has, où l’objet parent peut ne pas exister. Le résultat est une cascade de vérifications à chaque niveau pour éviter une erreur.
  • nestedNotNull: (.nested?.inner != null) effectue la même vérification sur un objet imbriqué à l’aide de != null et de ? pour activer le chaînage de chemins sur des champs qui n’existent peut-être pas. Cette approche produit une syntaxe plus propre des chaînes profondément imbriquées qui peuvent exister ou non, mais elles ne peuvent pas différencier les valeurs clés null de celles qui n’existent pas.

Le JSON suivant présente la sortie de l’expression jq précédente :

{
  "hasTemperature": true,
  "temperatureNotNull": true,
  "hasSite": true,
  "siteNotNull": false,
  "hasMissing": false,
  "missingNotNull": false,
  "hasNested": false,
  "nestedNotNull": false
}

Vérifier l’existence d’une entrée de tableau

Utilisez la fonction any pour vérifier l’existence d’une entrée dans un tableau. Étant donné l’entrée suivante :

{
  "userProperties": [
    { "key": "mass", "value": 512.1 },
    { "key": "productType", "value": "projectile" }
  ],
  "payload": {
    "velocity": 97.2,
    "energy": 2419119
  }
}

Utilisez l’expression jq suivante pour vérifier si le tableau userProperties a une entrée contenant une clé de mass et aucune entrée contenant une clé de missing :

.userProperties | any(.key == "mass") and (any(.key == "missing") | not)

Dans l’expression jq précédente :

  • .userProperties | <expression> limite le contexte de données de <expression> à la valeur de userProperties pour rendre le reste de <expression> moins détaillée.
  • any(.key == "mass") exécute l’expression .key == "mass" sur chaque élément du tableau userProperties, retournant true si l’expression prend la valeur true pour au moins un élément du tableau.
  • (any(.key == "missing") | not) exécute .key == "missing" sur chaque élément du tableau userProperties, retournant true si un élément prend la valeur true, puis annule le résultat global avec | not.

Le JSON suivant présente la sortie de l’expression jq précédente :

true

Flux de contrôle

Le flux de contrôle dans jq est différent de la plupart des langages, car la plupart des formes de flux de contrôle sont directement pilotées par les données. Les expressions if/else sont toujours prises en charge avec la sémantique de programmation fonctionnelle traditionnelle, mais vous pouvez obtenir la plupart des structures de boucle en utilisant des combinaisons des fonctions map et reduce.

Les exemples suivants montrent certains scénarios courants de flux de contrôle dans jq.

Instructions if-else

jq prend en charge les conditions à l’aide de if <test-expression> then <true-expression> else <false-expression> end. Vous pouvez insérer d’autres cas en ajoutant elif <test-expression> then <true-expression> au milieu. La principale différence entre jq et de nombreux autres langages réside dans le fait que chaque expression then et else produit un résultat utilisé dans les opérations suivantes de l’expression jq globale.

L’exemple suivant montre comment utiliser des instructions if pour produire des informations conditionnelles. Étant donné l’entrée suivante :

{
  "payload": {
    "temperature": 25,
    "humidity": 52
  }
}

Utilisez l’expression jq suivante pour vérifier si la température est élevée, faible ou normale :

.payload.status = if .payload.temperature > 80 then
  "high"
elif .payload.temperature < 30 then
  "low"
else
  "normal"
end

Dans l’expression jq précédente :

  • .payload.status = <expression> affecte le résultat de l’exécution de <expression> à un nouveau champ status dans la charge utile.
  • if ... end est l’expression if/elif/else principale :
    • if .payload.temperature > 80 then "high" vérifie la température par rapport à une valeur élevée, retournant "high" si la valeur est true, sinon elle continue.
    • elif .payload.temperature < 30 then "low" effectue une deuxième vérification par rapport à une valeur faible de température, définissant le résultat sur "low" si la valeur est true, sinon elle continue.
    • else "normal" end retourne "normal" si aucune des vérifications précédentes n’est vraie et ferme l’expression avec end.

Le JSON suivant présente la sortie de l’expression jq précédente :

{
  "payload": {
    "temperature": 25,
    "humidity": 52,
    "status": "low"
  }
}

Mappage

Dans les langages fonctionnels tels que jq, la méthode la plus courante pour effectuer une logique itérative consiste à créer un tableau, puis à mapper les valeurs de ce tableau aux nouvelles valeurs. Cette technique est obtenue dans jq en utilisant la fonction map, qui apparaît dans la plupart des exemples de ce guide. Si vous souhaitez effectuer une opération sur plusieurs valeurs, map est probablement la solution.

L’exemple suivant montre comment utiliser map pour supprimer un préfixe des clés d’un objet. Cette solution peut être écrite plus succinctement en utilisant with_entries, mais la version plus détaillée présentée ici illustre le mappage réel caché sous l’approche raccourcie. Étant donné l’entrée suivante :

{
  "payload": {
    "rotor_rpm": 150,
    "rotor_temperature": 51,
    "rotor_cycles": 1354
  }
}

Utilisez l’expression jq suivante pour supprimer le préfixe rotor_ des clés de la charge utile :

.payload |= (to_entries | map(.key |= ltrimstr("rotor_")) | from_entries)

Dans l’expression jq précédente :

  • .payload |= <expression> utilise |= pour mettre à jour la valeur de .payload avec le résultat de l’exécution de l’<expression>. L’utilisation de |= au lieu de = définit le contexte de données de <expression> sur .payload plutôt que sur ..
  • (to_entries | map(<expression) | from_entries) effectue une conversion de tableau d’objets et mappe chaque entrée à une nouvelle valeur avec <expression>. Cette approche est sémantiquement équivalente à with_entries(<expression>) :
    • to_entries convertit un objet en tableau, chaque paire clé/valeur devenant un objet distinct de structure {"key": <key>, "value": <value>}.
    • map(<expression>) exécute <expression> sur chaque élément du tableau et produit un tableau de sortie avec les résultats de chaque expression.
    • from_entries est l’inverse de to_entries. La fonction convertit un tableau d’objets de structure {"key": <key>, "value": <value>} en objet avec les champs key et value mappés en paires clé/valeur.
  • .key |= ltrimstr("rotor_") met à jour la valeur de .key dans chaque entrée avec le résultat de ltrimstr("rotor_"). La syntaxe |= étend le contexte de données à droite de la valeur de .key. ltrimstr supprime le préfixe donné de la chaîne s’il est présent.

Le JSON suivant présente la sortie de l’expression jq précédente :

{
  "payload": {
    "rpm": 150,
    "temperature": 51,
    "cycles": 1354
  }
}

Réduire

La réduction est la principale méthode pour effectuer des opérations de boucle ou itératives sur les éléments d’un tableau. L’opération de réduction se compose d’un accumulateur et d’une opération qui utilise l’accumulateur et l’élément en cours du tableau en tant qu’entrées. Chaque itération de la boucle retourne la valeur suivante de l’accumulateur, et la sortie finale de l’opération de réduction est la dernière valeur de l’accumulateur. Réduire est appelé plier dans d’autres langages de programmation fonctionnels.

Utilisez l’opération reduce dans jq pour effectuer une réduction. La plupart des cas d’usage n’ont pas besoin de cette manipulation de bas niveau et peuvent plutôt utiliser des fonctions de niveau supérieur, mais reduce est un outil général pratique.

L’exemple suivant montre comment calculer la variation moyenne de la valeur d’une métrique sur les points de données dont vous disposez. Étant donné l’entrée suivante :

{
  "payload": [
    {
      "value": 65,
      "timestamp": 1689796743559
    },
    {
      "value": 55,
      "timestamp": 1689796771131
    },
    {
      "value": 59,
      "timestamp": 1689796827766
    },
    {
      "value": 62,
      "timestamp": 1689796844883
    },
    {
      "value": 58,
      "timestamp": 1689796864853
    }
  ]
}

Utilisez l’expression jq suivante pour calculer la variation moyenne de la valeur sur les points de données :

.payload |= (
  reduce .[] as $item (
    null;
    if . == null then
      {totalChange: 0, previous: $item.value, count: 0}
    else
      .totalChange += (($item.value - .previous) | length)
      | .previous = $item.value
      | .count += 1
    end
  )
  | .totalChange / .count
)

Dans l’expression jq précédente :

  • .payload |= <expression> utilise |= pour mettre à jour la valeur de .payload avec le résultat de l’exécution de l’<expression>. L’utilisation de |= au lieu de = définit le contexte de données de <expression> sur .payload plutôt que sur ..
  • reduce .[] as $item (<init>; <expression>) est la structure d’une opération de réduction classique contenant les parties suivantes :
    • .[] as $item doit toujours être <expression> as <variable> et est le plus souvent .[] as $item. L’<expression> produit un flux de valeurs, chacune étant enregistrées dans une <variable> pour une itération de l’opération de réduction. Si vous souhaitez effectuer une itération sur un tableau, .[] le fractionne en flux. Cette syntaxe est identique à la celle utilisée pour fractionner des messages en messages distincts, mais l’opération reduce n’utilise pas le flux pour générer plusieurs sorties. reduce ne fractionne pas votre message en messages distincts.
    • <init> dans ce cas null est la valeur initiale de l’accumulateur utilisé dans l’opération de réduction. Cette valeur est souvent définie sur vide ou zéro. Cette valeur devient le contexte de données, . dans cette <expression> de boucle, pour la première itération.
    • <expression> est l’opération effectuée sur chaque itération de l’opération de réduction. Elle a accès à la valeur en cours de l’accumulateur, via ., et la valeur en cours du flux via le <variable> déclaré précédemment, dans ce cas $item.
  • if . == null then {totalChange: 0, previous: $item.value, count: 0} est un conditionnel qui permet de gérer la première itération de réduction. Il configure la structure de l’accumulateur pour l’itération suivante. Étant donné que l’expression calcule les différences entre les entrées, la première entrée configure les données utilisées pour calculer une différence sur la deuxième itération de réduction. Les champs totalChange, previous et count servent de variables de boucle, et sont mis à jour à chaque itération.
  • .totalChange += (($item.value - .previous) | length) | .previous = $item.value | .count += 1 est l’expression dans le cas de else. Cette expression définit chaque champ de l’objet accumulateur sur une nouvelle valeur basée sur un calcul. Pour totalChange, il trouve la différence entre les valeurs en cours et précédentes et obtient la valeur absolue. De façon contre-intuitive, il utilise la fonction length pour obtenir la valeur absolue. previous est réglé sur le $item du value en cours à utiliser dans l’itération suivante, et count est incrémenté.
  • .totalChange / .count calcule la variation moyenne entre les points de données une fois l’opération de réduction terminée et vous avez la valeur finale de l’accumulateur.

Le JSON suivant présente la sortie de l’expression jq précédente :

{
  "payload": 5.25
}

Boucles

Les boucles dans jq sont généralement réservées aux cas d’usage avancés. Étant donné que dans jq chaque opération est une expression qui produit une valeur, la sémantique basée sur des instructions des boucles dans la plupart des langages n’est pas un ajustement naturel dans jq. Envisagez d’utiliser map ou reduce pour répondre à vos besoins.

Il existe deux principaux types de boucles traditionnelles dans jq. D’autres types de boucles existent, mais sont destinés à des cas d’usage plus spécialisés :

  • while applique une opération à plusieurs reprises sur le contexte de données d’entrée, mettant à jour la valeur du contexte de données à utiliser dans l’itération suivante et produisant cette valeur en tant que sortie. La sortie d’une boucle while est un tableau contenant les valeurs produites à chaque itération de la boucle.
  • until, comme while, applique une opération à plusieurs reprises sur le contexte de données d’entrée, mettant à jour la valeur du contexte de données à utiliser dans l’itération suivante. Contrairement à while, la boucle until génère la valeur produite à la dernière itération de la boucle.

L’exemple suivant montre comment utiliser une boucle until pour éliminer progressivement les points de données de valeurs hors norme d’une liste de lectures jusqu’à ce que l’écart type soit inférieur à une valeur prédéfinie. Étant donné l’entrée suivante :

{
  "payload": [
    {
      "value": 65,
      "timestamp": 1689796743559
    },
    {
      "value": 55,
      "timestamp": 1689796771131
    },
    {
      "value": 59,
      "timestamp": 1689796827766
    },
    {
      "value": 62,
      "timestamp": 1689796844883
    },
    {
      "value": 58,
      "timestamp": 1689796864853
    }
  ]
}

Utilisez l’expression jq suivante pour éliminer progressivement les points de données de valeurs hors norme d’une liste de lectures jusqu’à ce que l’écart type soit inférieur à 2 :

def avg: add / length;
def stdev: avg as $mean | (map(. - $mean | . * .) | add) / (length - 1) | sqrt;
.payload |= (
  sort_by(.value)
  | until(
    (map(.value) | stdev) < 2 or length == 0;
    (map(.value) | avg) as $avg
    | if ((.[0].value - $avg) | length) > ((.[-1].value - $avg) | length) then
      del(.[0])
    else
      del(.[-1])
    end
  )
)

Dans l’expression jq précédente :

  • def avg: add / length; définit une nouvelle fonction appelée avg qui est utilisée pour calculer des moyennes plus loin dans l’expression. L’expression à droite de : est l’expression logique utilisée chaque fois que vous utilisez avg. L’expression <expression> | avg équivaut à <expression> | add / length
  • def stdev: avg as $mean | (map(. - $mean | . * .) | add) / (length - 1) | sqrt; définit une nouvelle fonction appelée stdev. La fonction calcule l’exemple d’écart type d’un tableau en utilisant une version modifiée de la réponse de la communauté sur StackOverflow.
  • .payload |= <expression> les deux premières def sont simplement des déclarations et démarrent l’expression réelle. L’expression exécute <expression> avec un objet de données d’entrée de .payload et réaffecte le résultat à .payload.
  • sort_by(.value) trie le tableau des entrées de tableau par leur champ value. Cette solution vous oblige à identifier et à manipuler les valeurs les plus élevées et les plus faibles d’un tableau, de sorte que le tri préalable des données réduit le calcul et simplifie le code.
  • until(<condition>; <expression>) exécute <expression> sur l’entrée jusqu’à ce que <condition> retourne true. L’entrée de chaque exécution de <expression> et de <condition> est la sortie de l’exécution précédente de <expression>. Le résultat de la dernière exécution de <expression> est retourné depuis la boucle.
  • (map(.value) | stdev) < 2 or length == 0 est la condition de la boucle :
    • map(.value) convertit le tableau en liste de simples nombres à utiliser dans le calcul suivant.
    • (<expression> | stdev) < 2 calcule l’écart type du tableau et retourne true si l’écart type est inférieur à 2.
    • length == 0 récupère la longueur du tableau d’entrée et retourne true si elle est égale à 0. Pour vous protéger du cas où toutes les entrées sont supprimées, le résultat est or-ed avec l’expression globale.
  • (map(.value) | avg) as $avg convertit le tableau en tableau de nombres et calcule leur moyenne, puis enregistre le résultat dans une variable $avg. Cette approche permet d’économiser les coûts de calcul, car vous réutilisez la moyenne plusieurs fois dans l’itération de boucle. Les expressions d’affectation de variable ne modifient pas le contexte de données de l’expression suivante après |. Le reste du calcul a donc toujours accès au tableau complet.
  • if <condition> then <expression> else <expression> end est la principale logique de l’itération de boucle. Il utilise une <condition> pour déterminer l’<expression> à exécuter et à retourner.
  • ((.[0].value - $avg) | length) > ((.[-1].value - $avg) | length) est la condition if qui compare les valeurs les plus élevées et les plus faibles par rapport à la valeur moyenne, puis compare ces différences :
    • (.[0].value - $avg) | length récupère le champ value de la première entrée de tableau et obtient la différence entre celui-ci et la moyenne globale. La première entrée de tableau est la plus faible en raison du tri précédent. Cette valeur peut être négative, de sorte que le résultat est redirigé vers length, qui retourne la valeur absolue lorsqu’un nombre est donné en tant qu’entrée.
    • (.[-1].value - $avg) | length effectue la même opération sur la dernière entrée de tableau et calcule la valeur absolue pour des raisons de sécurité. La dernière entrée de tableau est la plus élevée en raison du tri précédent. Les valeurs absolues sont ensuite comparées dans la condition globale à l’aide de >.
  • del(.[0]) est l’expression then qui s’exécute lorsque la première entrée du tableau est la plus grande valeur hors norme. L’expression supprime l’élément à .[0] du tableau. L’expression retourne les données restantes dans le tableau après l’opération.
  • del(.[-1]) est l’expression else qui s’exécute lorsque la dernière entrée du tableau est la plus grande valeur hors norme. L’expression supprime l’élément à .[-1], la dernière entrée, du tableau. L’expression retourne les données restantes dans le tableau après l’opération.

Le JSON suivant présente la sortie de l’expression jq précédente :

{
  "payload": [
    {
      "value": 58,
      "timestamp": 1689796864853
    },
    {
      "value": 59,
      "timestamp": 1689796827766
    },
    {
      "value": 60,
      "timestamp": 1689796844883
    }
  ]
}

Annuler des messages

Lorsque vous écrivez une expression de filtre, vous pouvez demander au système d’annuler les messages que vous ne voulez pas en retournant false. Ce comportement est le comportement de base des expressions conditionnelles dans jq. Toutefois, il y a des moments où vous transformez des messages ou effectuez des filtres plus avancés lorsque le système doit annuler explicitement ou implicitement des messages pour vous. Les exemples suivants montrent comment implémenter ce comportement.

Annulation explicite

Pour annuler explicitement un message dans une expression de filtre, retournez false depuis l’expression.

Vous pouvez également annuler un message à partir d’une transformation à l’aide de la fonction intégrée empty dans jq.

L’exemple suivant montre comment calculer une moyenne de points de données dans le message et annuler tous les messages ayant une moyenne inférieure à une valeur fixée. Il est possible et pertinent d’obtenir ce comportement avec la combinaison d’un index de transformation et d’un index de filtre. Utilisez l’approche qui convient le mieux à votre situation. Étant donné les entrées suivantes :

Message 1

{
  "payload": {
    "temperature": [23, 42, 63, 61],
    "humidity": [64, 36, 78, 33]
  }
}

Message 2

{
  "payload": {
    "temperature": [42, 12, 32, 21],
    "humidity": [92, 63, 57, 88]
  }
}

Utilisez l’expression jq suivante pour calculer la moyenne des points de données et annuler les messages ayant une température moyenne inférieure à 30 ou une humidité moyenne supérieure à 90 :

.payload |= map_values(add / length)
| if .payload.temperature > 30 and .payload.humidity < 90 then . else empty end

Dans l’expression jq précédente :

  • .payload |= <expression> utilise |= pour mettre à jour la valeur de .payload avec le résultat de l’exécution de l’<expression>. L’utilisation de |= au lieu de = définit le contexte de données de <expression> sur .payload plutôt que sur ..
  • map_values(add / length) exécute add / length pour chaque valeur du sous-objet .payload. L’expression additionne les éléments du tableau de valeurs, puis divise par la longueur du tableau pour calculer la moyenne.
  • if .payload.temperature > 30 and .payload.humidity < 90 then . else empty end vérifie deux conditions par rapport au message résultant. Si le filtre prend la valeur true, comme dans la première entrée, le message complet est généré en tant que sortie. Si le filtre prend la valeur false, comme dans la deuxième entrée, il retourne empty, ce qui entraîne un flux vide ne contenant aucune valeur. Ce résultat entraîne l’annulation de l’expression du message correspondant.

Sortie 1

{
  "payload": {
    "temperature": 47.25,
    "humidity": 52.75
  }
}

Sortie 2

(aucune sortie)

Annulation implicite à l’aide d’erreurs

Les expressions de filtre et de transformation peuvent annuler implicitement des messages en faisant en sorte que jq produise une erreur. Bien que cette approche ne soit pas une bonne pratique, car le pipeline ne peut pas différencier une erreur que vous avez intentionnellement provoquée d’une erreur causée par une entrée inattendue de votre expression. Le système gère actuellement une erreur de runtime dans le filtre ou la transformation en annulant le message et en enregistrant l’erreur.

Un scénario courant qui utilise cette approche est lorsqu’une entrée d’un pipeline peut avoir des messages disjoints structurellement. L’exemple suivant montre comment recevoir deux types de messages, l’un étant correctement évalué par rapport au filtre, l’autre étant structurellement incompatible avec l’expression. Étant donné les entrées suivantes :

Message 1

{
  "payload": {
    "sensorData": {
      "temperature": 15,
      "humidity": 62
    }
  }
}

Message 2

{
  "payload": [
    {
      "rpm": 12,
      "timestamp": 1689816609514
    },
    {
      "rpm": 52,
      "timestamp": 1689816628580
    }
  ]
}

Utilisez l’expression jq suivante pour filtrer des messages avec une température inférieure à 10 et une humidité supérieure à 80 :

.payload.sensorData.temperature > 10 and .payload.sensorData.humidity < 80

Dans l’exemple précédent, l’expression elle-même est une expression booléenne composée simple. L’expression est conçue pour fonctionner avec la structure du premier des messages d’entrée affichés précédemment. Lorsque l’expression reçoit le deuxième message, la structure de tableau de la .payload est incompatible avec l’accès à l’objet dans l’expression et génère une erreur. Si vous souhaitez filtrer en fonction des valeurs de température/d’humidité et supprimer des messages ayant une structure incompatible, cette expression fonctionne. Une autre approche qui ne génère aucune erreur consiste à ajouter (.payload | type) == "object" and au début de l’expression.

Sortie 1

true

Sortie 2

(erreur)

Utilitaires temporels

jq ne prend pas en charge l’horaire en tant que type natif. Toutefois, certains formats acceptés et émis par le processeur de données prennent en charge l’horaire en tant que type natif. Ces types sont généralement représentés à l’aide du type Go time.Time .

Pour vous permettre d’interagir avec ces valeurs à partir de jq, le processeur de données fournit un module avec un ensemble de fonctions qui vous permettent de :

  • Convertir entre l’heure native, les chaînes ISO 8601 et les timestamps Unix numériques.
  • Effectuez différentes opérations spécifiques à l’heure sur tous ces types.

Module time

Toutes les fonctions spécifiques à l’heure spéciales sont spécifiées dans un module time que vous pouvez importer dans une requête.

Importez le module au début de votre requête avec l’une des deux méthodes :

  • import" "time" as time;
  • include "time"

La première méthode place toutes les fonctions dans le module sous un espace de noms, par exemple time::totime. La deuxième méthode place simplement toutes les fonctions temporelles au premier niveau, par exemple totime. Les deux syntaxes sont pertinentes et sont fonctionnellement équivalentes.

Formats et conversion

Le module horaire fonctionne avec trois formats horaires :

  • time est une valeur de temps native. Vous ne pouvez l’utiliser qu’avec les fonctions du module horaire. Reconnu comme un type de données time lors de la sérialisation.
  • unix est un timestamp Unix numérique qui représente le temps sous forme de secondes depuis l’époque Unix. Il peut s’agir d’un nombre entier ou à virgule flottante. Reconnu comme type numérique correspondant lors de la sérialisation.
  • iso est une représentation au format de chaîne ISO 8601 de l’heure. Reconnu comme une chaîne lors de la sérialisation.

Le module horaire fournit les fonctions suivantes pour vérifier et manipuler ces types :

  • time::totime convertit l’un des trois types en time.
  • time::tounix convertit l’un des trois types en unix.
  • time::toiso convertit l’un des trois types en iso.
  • time::istime retourne true si les données sont au format time.

Opérations temporelles

Le module horaire fournit différentes opérations spécifiques au temps qui fonctionnent sur tous les types. Les fonctions suivantes peuvent prendre n’importe quel type pris en charge comme entrée et retourner le même type que leur sortie. Les timestamp entiers peuvent être convertis en horodatages à virgule flottante si la précision est nécessaire.

  • time::utc convertit l’heure au format UTC.
  • time::zone(zone) convertit l’heure dans la zone fournie. zone est une chaîne de zone ISO 8601. Par exemple : time::zone("-07").
  • time::local convertit l’heure en heure locale.
  • time::offset(duration) décale l’heure de la durée fournie. duration utilise la syntaxe de chaîne de durée de Go. Par exemple : time::offset("1m2s").
  • time::offset(value;unit) décale l’heure de la durée fournie. Cette fonction utilise un nombre et une chaîne d’unité. Par exemple : time::offset(2;"s"). Cette fonction est utile lorsque la durée provient d’une autre propriété.

Remarque

Les trois fonctions de fuseau horaire n’ont aucun effet significatif sur les timestamps Unix.

Utilitaires divers

Le module util est une collection d’utilitaires qui étend les fonctionnalités du runtime jq.

Module util

Tous les utilitaires divers sont spécifiés dans un module util que vous pouvez importer dans une requête.

Importez le module au début de votre requête avec l’une des deux méthodes :

  • import" "util" as util;
  • include "util"

La première méthode place toutes les fonctions dans le module sous un espace de noms, par exemple util::uuid. La deuxième méthode place simplement toutes les autres fonctions au premier niveau, par exemple uuid. Les deux syntaxes sont pertinentes et sont fonctionnellement équivalentes.

Le module util inclut actuellement la fonction uuid qui retourne un nouvel UUID aléatoire au format de chaîne standard.

Manipulation binaire

jq est conçu pour fonctionner avec des données qui peuvent être représentées en tant que JSON. Toutefois, les pipelines Azure IoT Data Processor Preview prennent également en charge un format de données brute qui contient des données binaires nonparées. Pour utiliser des données binaires, la version de jq fournie avec le processeur de données contient un package conçu pour vous aider à traiter des données binaires. Il vous permet d’effectuer les opérations suivantes :

  • Convertissez dans les deux sens les formats binaires et d’autres formats tels que les tableaux de base64 et d’entiers.
  • Utilisez des fonctions intégrées pour lire des valeurs numériques et de chaîne à partir d’un message binaire.
  • Effectuez des modifications de point de données binaires tout en conservant leur format.

Important

Vous ne pouvez pas utiliser de fonctions ou d’opérateurs jq intégrés qui modifient une valeur binaire. Cela signifie aucune concaténation avec +, aucun map fonctionnant sur les octets et aucune affectation mixte avec des valeurs binaires telles que |=, +=, //=. Vous pouvez utiliser l’affectation standard (==). Si vous essayez d’utiliser des données binaires avec une opération non prise en charge, le système génère une erreur jqImproperBinaryUsage. Si vous avez besoin de manipuler vos données binaires de manière personnalisée, envisagez d’utiliser l’une des fonctions suivantes pour les convertir en base64 ou un tableau d’entiers pour votre calcul, puis en les convertissant en binaire.

Les sections suivantes décrivent la prise en charge binaire dans le moteur jq du processeur de données.

Module binary

La prise en charge binaire dans le moteur jq du processeur de données est spécifiée dans un module binary que vous pouvez importer.

Importez le module au début de votre requête avec l’une des deux méthodes :

  • import "binary" as binary;
  • include "binary"

La première méthode place toutes les fonctions dans le module sous un espace de noms, par exemple binary::tobase64. La deuxième méthode place simplement toutes les fonctions binaires au premier niveau, par exemple tobase64. Les deux syntaxes sont pertinentes et sont fonctionnellement équivalentes.

Formats et conversion

Le module binaire fonctionne avec trois types :

  • binaire : valeur binaire, utilisable uniquement directement avec les fonctions du module binaire. Reconnu par un pipeline comme type de données binaire lors de la sérialisation. Utilisez ce type pour une sérialisation brute.
  • tableau : format qui transforme le binaire en tableau de nombres pour vous permettre d’effectuer votre propre traitement. Reconnu par un pipeline comme tableau d’entiers lors d’une sérialisation.
  • base64 : représentation de binaires sous un format de chaîne. Principalement utile si vous souhaitez convertir entre les binaires et les chaînes. Reconnu par un pipeline comme une chaîne lors d’une sérialisation.

Vous pouvez convertir entre les trois types dans vos requêtes jq en fonction de vos besoins. Par exemple, vous pouvez convertir du binaire en tableau, effectuer une manipulation personnalisée, puis revenir finalement en binaire pour conserver les informations de type.

Functions

Les fonctions suivantes sont fournies pour la vérification de ces types et leur manipulation :

  • binary::tobinary convertit l’un des trois types en binaire.
  • binary::toarray convertit l’un des trois types en tableau.
  • binary::tobase64 convertit l’un des trois types en base64.
  • binary::isbinary retourne true si les données sont au format binaire.
  • binary::isarray retourne true si les données sont au format tableau.
  • binary::isbase64 retourne true si les données sont au format base64.

Le module fournit également la fonction binary::edit(f) pour modifier rapidement des données binaires. La fonction convertit l’entrée au format tableau, lui applique la fonction, puis convertit le résultat en binaire.

Extraire des données du binaire

Le module binaire vous permet d’extraire des valeurs des données binaires à utiliser dans la décompression des charges utiles binaires personnalisées. En général, cette fonctionnalité suit celle des autres bibliothèques de décompression binaires et suit une dénomination similaire. Les types suivants peuvent être décompressés :

  • Entiers (int8, int16, int32, int64, uint8, uint16, uint32, uint64)
  • Flottants (flottant, double)
  • Chaînes (utf8)

Le module permet également de spécifier des décalages et des endianness, le cas échéant.

Fonctions permettant de lire des données binaires

Le module binaire fournit les fonctions suivantes pour extraire des données de valeurs binaires. Vous pouvez utiliser toutes les fonctions avec l’un des trois types que le package peut convertir.

Les paramètres de fonction sont facultatifs, offset a la valeur par défaut 0 et length a la valeur par défaut du reste des données.

  • binary::read_int8(offset) lit un int8 à partir d’une valeur binaire.
  • binary::read_int16_be(offset) lit un int16 à partir d’une valeur binaire dans l’ordre big-endian.
  • binary::read_int16_le(offset) lit un int16 à partir d’une valeur binaire dans l’ordre little-endian.
  • binary::read_int32_be(offset) lit un int32 à partir d’une valeur binaire dans l’ordre big-endian.
  • binary::read_int32_le(offset) lit un int32 à partir d’une valeur binaire dans l’ordre little-endian.
  • binary::read_int64_be(offset) lit un int64 à partir d’une valeur binaire dans l’ordre big-endian.
  • binary::read_int64_le(offset) lit un int64 à partir d’une valeur binaire dans l’ordre little-endian.
  • binary::read_uint8(offset) lit un uint8 à partir d’une valeur binaire.
  • binary::read_uint16_be(offset) lit un uint16 à partir d’une valeur binaire dans l’ordre big-endian.
  • binary::read_uint16_le(offset) lit un uint16 à partir d’une valeur binaire dans l’ordre little-endian.
  • binary::read_uint32_be(offset) lit un uint32 à partir d’une valeur binaire dans l’ordre big-endian.
  • binary::read_uint32_le(offset) lit un uint32 à partir d’une valeur binaire dans l’ordre little-endian.
  • binary::read_uint64_be(offset) lit un uint64 à partir d’une valeur binaire dans l’ordre big-endian.
  • binary::read_uint64_le(offset) lit un uint64 à partir d’une valeur binaire dans l’ordre little-endian.
  • binary::read_float_be(offset) lit un flottant à partir d’une valeur binaire dans l’ordre big-endian.
  • binary::read_float_le(offset) lit un flottant à partir d’une valeur binaire dans l’ordre little-endian.
  • binary::read_double_be(offset) lit un double à partir d’une valeur binaire dans l’ordre big-endian.
  • binary::read_double_le(offset) lit un double à partir d’une valeur binaire dans l’ordre little-endian.
  • binary::read_bool(offset; bit) lit un booléen à partir d’une valeur binaire, en vérifiant le bit donné de la valeur.
  • binary::read_bit(offset; bit) lit un bit à partir d’une valeur binaire, en utilisant l’index de bits donné.
  • binary::read_utf8(offset; length) lit une chaîne UTF-8 à partir d’une valeur binaire.

Écrire des données binaires

Le module binaire vous permet d’encoder et d’écrire des valeurs binaires. Cette fonctionnalité vous permet de construire ou d’apporter des modifications aux charges utiles binaires directement dans jq. L’écriture de données prend en charge le même ensemble de types de données que l’extraction de données et vous permet également de spécifier l’endianness à utiliser.

L’écriture de données se présente sous deux formes :

  • write_* fonctions mettent à jour les données en place dans une valeur binaire, utilisée pour mettre à jour ou manipuler des valeurs existantes.
  • append_* fonctions ajoutent des données à la fin d’une valeur binaire, utilisée pour ajouter ou construire de nouvelles valeurs binaires.

Fonctions permettant d’écrire des données binaires

Le module binaire fournit les fonctions suivantes pour écrire des données en valeurs binaires. Toutes les fonctions peuvent être exécutées sur l’un des trois types valides que ce package peut convertir.

Le paramètre value est requis pour toutes les fonctions, mais offset est facultatif, si les valeurs valides et par défaut sont 0.

Fonctions d’écriture :

  • binary::write_int8(value; offset) écrit un int8 en valeur binaire.
  • binary::write_int16_be(value; offset) écrit un int16 en valeur binaire dans l’ordre big-endian.
  • binary::write_int16_le(value; offset) écrit un int16 en valeur binaire dans l’ordre little-endian.
  • binary::write_int32_be(value; offset) écrit un int32 en valeur binaire dans l’ordre big-endian.
  • binary::write_int32_le(value; offset) écrit un int32 en valeur binaire dans l’ordre little-endian.
  • binary::write_int64_be(value; offset) écrit un int64 en valeur binaire dans l’ordre big-endian.
  • binary::write_int64_le(value; offset) écrit un int64 en valeur binaire dans l’ordre little-endian.
  • binary::write_uint8(value; offset) écrit un uint8 en valeur binaire.
  • binary::write_uint16_be(value; offset) écrit un uint16 en valeur binaire dans l’ordre big-endian.
  • binary::write_uint16_le(value; offset) écrit un uint16 en valeur binaire dans l’ordre little-endian.
  • binary::write_uint32_be(value; offset) écrit un uint32 en valeur binaire dans l’ordre big-endian.
  • binary::write_uint32_le(value; offset) écrit un uint32 en valeur binaire dans l’ordre little-endian.
  • binary::write_uint64_be(value; offset) écrit un uint64 en valeur binaire dans l’ordre big-endian.
  • binary::write_uint64_le(value; offset) écrit un uint64 en valeur binaire dans l’ordre little-endian.
  • binary::write_float_be(value; offset) écrit un flottant en valeur binaire dans l’ordre big-endian.
  • binary::write_float_le(value; offset) écrit un flottant en valeur binaire dans l’ordre little-endian.
  • binary::write_double_be(value; offset) écrit un double en valeur binaire dans l’ordre big-endian.
  • binary::write_double_le(value; offset) écrit un double en valeur binaire dans l’ordre little-endian.
  • binary::write_bool(value; offset; bit) écrit un booléen en octet unique dans une valeur binaire, en définissant le bit donné sur la valeur booléenne.
  • binary::write_bit(value; offset; bit) écrit un bit unique en valeur binaire, en laissant les autres bits de l’octet tel qu’ils sont.
  • binary::write_utf8(value; offset) écrit une chaîne UTF-8 en valeur binaire.

Fonction ajouter :

  • binary::append_int8(value) ajoute un int8 à une valeur binaire.
  • binary::append_int16_be(value) ajoute un int16 à une valeur binaire dans l’ordre big-endian.
  • binary::append_int16_le(value) ajoute un int16 à une valeur binaire dans l’ordre little-endian.
  • binary::append_int32_be(value) ajoute un int32 à une valeur binaire dans l’ordre big-endian.
  • binary::append_int32_le(value) ajoute un int32 à une valeur binaire dans l’ordre little-endian.
  • binary::append_int64_be(value) ajoute un int64 à une valeur binaire dans l’ordre big-endian.
  • binary::append_int64_le(value) ajoute un int64 à une valeur binaire dans l’ordre little-endian.
  • binary::append_uint8(value) ajoute un uint8 à une valeur binaire.
  • binary::append_uint16_be(value) ajoute un uint16 à une valeur binaire dans l’ordre big-endian.
  • binary::append_uint16_le(value) ajoute un uint16 à une valeur binaire dans l’ordre little-endian.
  • binary::append_uint32_be(value) ajoute un uint32 à une valeur binaire dans l’ordre big-endian.
  • binary::append_uint32_le(value) ajoute un uint32 à une valeur binaire dans l’ordre little-endian.
  • binary::append_uint64_be(value) ajoute un uint64 à une valeur binaire dans l’ordre big-endian.
  • binary::append_uint64_le(value) ajoute un uint64 à une valeur binaire dans l’ordre little-endian.
  • binary::append_float_be(value) ajoute un flottant à une valeur binaire dans l’ordre big-endian.
  • binary::append_float_le(value) ajoute un flottant à une valeur binaire dans l’ordre little-endian.
  • binary::append_double_be(value) ajoute un double à une valeur binaire dans l’ordre big-endian.
  • binary::append_double_le(value) ajoute un double à une valeur binaire dans l’ordre little-endian.
  • binary::append_bool(value; bit) ajoute un booléen à un octet unique en valeur binaire, en définissant le bit donné sur la valeur booléenne.
  • binary::append_utf8(value) ajoute une chaîne UTF-8 à une valeur binaire.

Exemples de binaires

Cette section présente certains cas d’usage courants d’utilisation de données binaires. Les exemples utilisent un message d’entrée courant.

Supposons que vous ayez un message avec une charge utile en format binaire personnalisé contenant plusieurs sections. Chaque section contient les données suivantes dans l’ordre des octets big-endian :

  • Uint32 qui contient la longueur du nom du champ en octets.
  • Chaîne UTF-8 qui contient le nom du champ dont la longueur est spécifiée par l’uint32 précédent.
  • Un double qui contient la valeur du champ.

Pour cet exemple, vous disposez de trois de ces sections, contenant :

  • 11 (uint32)

  • température (UTF-8)

  • 86.0 (double)

  • 8 (uint32)

  • humidité (UTF-8)

  • 51.290 (double)

  • 8 (uint32)

  • pression (UTF-8)

  • 346.23 (double)

Ces données ressemblent à ceci lorsqu’elles sont imprimées dans la section payload d’un message :

{
  "payload": "base64::AAAAC3RlbXBlcmF0dXJlQFWAAAAAAAAAAAAIaHVtaWRpdHlASaUeuFHrhQAAAAhwcmVzc3VyZUB1o64UeuFI"
}

Remarque

La représentation base64::<string> de données binaires sert uniquement à faciliter la différenciation des autres types et n’est pas représentative du format de données physiques pendant le traitement.

Extraire des valeurs directement

Si vous connaissez la structure exacte du message, vous pouvez récupérer les valeurs de celui-ci à l’aide des décalages appropriés.

Utilisez l’expression jq suivante pour extraire les valeurs :

import "binary" as binary;
.payload | {
  temperature: binary::read_double_be(15),
  humidity: binary::read_double_be(35),
  pressure: binary::read_double_be(55)
}

Le JSON suivant présente la sortie de l’expression jq précédente :

{
  "humidity": 51.29,
  "pressure": 346.23,
  "temperature": 86
}

Extraire des valeurs dynamiquement

Si le message peut contenir des champs dans n’importe quel ordre, vous pouvez extraire dynamiquement le message complet :

Utilisez l’expression jq suivante pour extraire les valeurs :

import "binary" as binary;
.payload
| {
    parts: {},
    rest: binary::toarray
}
|
until(
    (.rest | length) == 0;
    (.rest | binary::read_uint32_be) as $length
    | {
        parts: (
            .parts +
            {
                (.rest | binary::read_utf8(4; $length)): (.rest | binary::read_double_be(4 + $length))
            }
        ),
        rest: .rest[(12 + $length):]
    }
)
| .parts

Le JSON suivant présente la sortie de l’expression jq précédente :

{
  "humidity": 51.29,
  "pressure": 346.23,
  "temperature": 86
}

Modifier des valeurs directement

Cet exemple montre comment modifier l’une des valeurs. Comme dans le cas de l’extraction, c’est plus facile si vous savez où se trouve la valeur que vous souhaitez modifier dans les données binaires. Cet exemple montre comment convertir la température de Fahrenheit en Celsius.

Utilisez l’expression jq suivante pour convertir la température de Fahrenheit en Celsius dans le message binaire :

import "binary" as binary;
15 as $index
| .payload
| binary::write_double_be(
    ((5 / 9) * (binary::read_double_be($index) - 32));
    $index
)

Le JSON suivant présente la sortie de l’expression jq précédente :

"base64::AAAAC3RlbXBlcmF0dXJlQD4AAAAAAAAAAAAIaHVtaWRpdHlASaUeuFHrhQAAAAhwcmVzc3VyZUB1o64UeuFI"

Si vous appliquez la logique d’extraction indiquée précédemment, vous obtenez la sortie suivante :

{
  "humidity": 51.29,
  "pressure": 346.23,
  "temperature": 30
}

Modifier des valeurs dynamiquement

Cet exemple montre comment obtenir le même résultat que l’exemple précédent en localisant dynamiquement la valeur souhaitée dans la requête.

Utilisez l’expression jq suivante pour convertir la température de Fahrenheit en Celsius dans le message binaire, en localisant dynamiquement les données à modifier :

import "binary" as binary;
.payload
| binary::edit(
    {
        index: 0,
        data: .
    }
    | until(
        (.data | length) <= .index;
        .index as $index
        | (.data | binary::read_uint32_be($index)) as $length
        | if (.data | binary::read_utf8($index + 4; $length)) == "temperature" then
            (
                (.index + 4 + $length) as $index
                | .data |= binary::write_double_be(((5 / 9) * (binary::read_double_be($index) - 32)); $index)
            )
        end
        | .index += $length + 12
    )
    | .data
)

Le JSON suivant présente la sortie de l’expression jq précédente :

"base64::AAAAC3RlbXBlcmF0dXJlQD4AAAAAAAAAAAAIaHVtaWRpdHlASaUeuFHrhQAAAAhwcmVzc3VyZUB1o64UeuFI"

Insérer de nouvelles valeurs

Ajoutez de nouvelles valeurs à l’aide des fonctions d’ajout du package. Par exemple, pour ajouter un champ windSpeed ayant une valeur de 31.678 à l’entrée tout en préservant le format binaire entrant, utilisez l’expression jq suivante :

import "binary" as binary;
"windSpeed" as $key
| 31.678 as $value
| .payload
| binary::append_uint32_be($key | length)
| binary::append_utf8($key)
| binary::append_double_be($value)

Le JSON suivant présente la sortie de l’expression jq précédente :

"base64:AAAAC3RlbXBlcmF0dXJlQFWAAAAAAAAAAAAIaHVtaWRpdHlASaUeuFHrhQAAAAhwcmVzc3VyZUB1o64UeuFIAAAACXdpbmRTcGVlZEA/rZFocrAh"