Node-RED の JSONata で日付と時間をうまく扱える Tips

この記事は <a href="https://qiita.com/advent-calendar/2020/node-red" target="_blank">Node-RED Advent Calendar 2020 1日目</a>の記事です。

いよいよスタートです！参加される、Node-RED UG のみなさまよろしくおねがいします！

今年もアドベントカレンダーはじまりましたね！

## Node-RED の JSONata 機能について

以前も <a href="https://blog.enebular.com/node-red/jsonata-tips01/" target="_blank">enebular で使える JSONata 式の便利 Tips</a> といった記事を書いておりまして、Node-RED における JSONata の便利さを日々感じています。

詳しくは、英語ですが

<a href="http://docs.jsonata.org/overview.html" target="_blank">JSONata Documentation · JSONata</a>

こちらのドキュメントを見てみてください。

<img width="1920" alt="image.png (89.8 kB)" src="https://img.esa.io/uploads/production/attachments/3062/2020/11/30/8131/284387e2-fdcd-41e4-88df-21476631f474.png">

試してみるときは同サイトにある <a href="https://try.jsonata.org/" target="_blank">JSONata Exerciser</a> もおすすめです。

## その中でも日付と時間の扱いが便利

そう。Node-RED で、いろいろなデータを加工したり分析したりしますが、日付と時間をうまく扱いたいときって多いですよね・

たとえば、こういうとき。

* CSVやデータベースから日時を抽出したあと扱いやすいようにタイムスタンプ（1970年からのミリ秒）に直す
* タイムスタンプ（1970年からのミリ秒）からメール文面に使うイイ感じの表記にしたい
* サーバー内で時差を考慮しない UTC の時間になっているけど JST に変更にしたい

もちろん、function ノードで Date 関数を駆使してがんばれなくはないんですが、ちゃんとコードで書ききるって結構大変なんですよね。 `Date.getMonth` 関数が 0 はじまりで 1 足さなきゃいけなかったり、表記に合わせて 2 月を 02 として 0 で埋めたり文字列処理も絡んできます。

<img width="840" alt="image.png (43.8 kB)" src="https://img.esa.io/uploads/production/attachments/3062/2020/11/30/8131/00734be6-733d-49a1-ba50-c359b1039c2d.png">

ということで、 JSONata で使ってて気持ちよかったユースケースを紹介します。

## 現在の時間を基準に30日前のISO 8601形式のタイムスタンプの文字列にする

<img width="792" alt="image.png (11.0 kB)" src="https://img.esa.io/uploads/production/attachments/3062/2020/11/30/8131/1ca7bcbd-7395-425e-8632-1e247198b205.png">

このようなフローです。

```js
[{"id":"b1120bcb.6c0148","type":"inject","z":"6604f6d3.1fe9c8","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":200,"y":440,"wires":[["181a572a.6cc589"]]},{"id":"181a572a.6cc589","type":"change","z":"6604f6d3.1fe9c8","name":"30日前に変換","rules":[{"t":"set","p":"payload","pt":"msg","to":"$fromMillis($millis() - 86400 * 1000 * 30)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":420,"y":440,"wires":[["443ee7dc.446578"]]},{"id":"443ee7dc.446578","type":"debug","z":"6604f6d3.1fe9c8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":680,"y":440,"wires":[]}]
```

インポートできるフローJSONです。

<img width="764" alt="image.png (23.3 kB)" src="https://img.esa.io/uploads/production/attachments/3062/2020/11/30/8131/e9521a39-e5ca-4f12-b6d9-5cde490d31cc.png">

change ノードの中はこのようになっています。

```java
$fromMillis($millis() - 86400 * 1000 * 30)
```

`$millis()` 現在の経過ミリ秒。それを取得したあと `- 86400 * 1000 * 30` で30日前にしたのち、`fromMillis` は経過ミリ秒を表す数値を、ISO 8601形式のタイムスタンプの文字列に変換してくれます。

<img width="305" alt="image.png (4.4 kB)" src="https://img.esa.io/uploads/production/attachments/3062/2020/11/30/8131/9883b9a7-8510-4753-bc0b-41b285e02d96.png">

実行してみると、このように `"2020-10-31T08:12:44.044Z"`  ISO 8601 形式のタイムスタンプで出力されます。

## 現在の時間を基準に年月日だけ取り出す

<img width="766" alt="image.png (10.5 kB)" src="https://img.esa.io/uploads/production/attachments/3062/2020/11/30/8131/642b1f2c-e8aa-4f49-a040-b37876ba5544.png">

このようなフローです。

```js
[{"id":"453f393a.0eca18","type":"inject","z":"6604f6d3.1fe9c8","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":200,"y":520,"wires":[["e6d3c723.d37ec8"]]},{"id":"e6d3c723.d37ec8","type":"change","z":"6604f6d3.1fe9c8","name":"年月日だけ抽出","rules":[{"t":"set","p":"payload","pt":"msg","to":"$fromMillis($millis(),'[Y0001]/[M01]/[D01]')","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":420,"y":520,"wires":[["edd80992.7ec3e8"]]},{"id":"edd80992.7ec3e8","type":"debug","z":"6604f6d3.1fe9c8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":680,"y":520,"wires":[]}]
```

インポートできるフローJSONです。

<img width="853" alt="image.png (23.8 kB)" src="https://img.esa.io/uploads/production/attachments/3062/2020/11/30/8131/3087adbe-5c9f-4c81-ab53-fa2ac640f33d.png">

change ノードの中はこのようになっています。

```js
$fromMillis($millis(),'[Y0001]/[M01]/[D01]')
```

 `$millis()` で取得した現在の経過ミリ秒を `$fromMillis`の第2引数で日時のフォーマットをかけています。今回は、年4桁・月0埋めの2桁・日0埋めの2桁のフォーマットです。

<img width="303" alt="image.png (3.5 kB)" src="https://img.esa.io/uploads/production/attachments/3062/2020/11/30/8131/fa6ea129-3822-4847-82b6-79872e39c09b.png">

実行してみると 2020/11/30 と出力されます。

<img width="827" alt="image.png (60.6 kB)" src="https://img.esa.io/uploads/production/attachments/3062/2020/11/30/8131/c07b5b13-8acb-4b6f-8850-6792a570331e.png">

フォーマットについてはこのように [XPath/XQueryで使われている日時フォーマット](https://www.w3.org/TR/xpath-functions-31/#date-time-examples) を利用しているので、気になる方は参考にしてみてください。

## 時差の処理や時差の表記を加える

サーバー時間がUTCになっているけど、日本時間が欲しいといったような時差の扱いってややこしいです。こちらも、かゆいところに手が届きます。

<img width="770" alt="image.png (9.8 kB)" src="https://img.esa.io/uploads/production/attachments/3062/2020/11/30/8131/beaf4687-abbe-4dbe-ad0a-3ecf70f3dfb9.png">

このようなフローです。

```js
[{"id":"2bbc92ac.a336ae","type":"inject","z":"6604f6d3.1fe9c8","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"2020-11-01T00:00:00Z","payloadType":"str","x":180,"y":600,"wires":[["6e05fb54.0b8ec4"]]},{"id":"6e05fb54.0b8ec4","type":"change","z":"6604f6d3.1fe9c8","name":"時差 +9:00","rules":[{"t":"set","p":"payload","pt":"msg","to":"$fromMillis($toMillis(payload),'[Y0001]/[M01]/[D01] [H01]:[m01]:[s01] [z]','+0900')","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":410,"y":600,"wires":[["7d950870.daf188"]]},{"id":"7d950870.daf188","type":"debug","z":"6604f6d3.1fe9c8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":680,"y":600,"wires":[]}]
```

インポートできるフローJSONです。

inject ノードから `2020-11-01T00:00:00Z` の文字列が来ると、 change ノードで、日本時間ぶんの時差+9:00を行って、時差のGMT表記も加えます。

ちなみにISO 8601形式のタイムスタンプで最後にZがつくとUTCを示します。→参考：[ISO 8601 \- Wikipedia](https://ja.wikipedia.org/wiki/ISO_8601)

<img width="836" alt="image.png (24.4 kB)" src="https://img.esa.io/uploads/production/attachments/3062/2020/11/30/8131/418d9e5b-18ff-4838-bbc7-e6982da845ea.png">

change ノードの中はこのようになっています。

```js
$fromMillis($toMillis(payload),'[Y0001]/[M01]/[D01] [H01]:[m01]:[s01] [z]','+0900')
```

`$fromMillis` の第3引数は `+0900` という表記で時差を設定できます。また `$toMillis(payload)` で inject ノードから来た `2020-11-01T00:00:00Z` という文字列を ISO 8601 形式で解釈してミリ秒に置き換えているのも便利なポイントです。

<img width="315" alt="image.png (4.5 kB)" src="https://img.esa.io/uploads/production/attachments/3062/2020/11/30/8131/5097e550-b29a-4b70-9346-05ce8c6331f7.png">

実行してみると、このように `"2020/11/01 09:00:00 GMT+09:00"` と9時間の時差が処理され、`GMT+09:00` も `[z]` というフォーマット記述によって引き出されています。このあたり、JavaScriptでできなくもないですが、ライブラリを使わないと、結構深い処理なので JSONata で書けるのはありがたいです。

## 明日は

明日の Node-RED Advent Calendar 2020 の担当は <a href="https://qiita.com/kitazaki" target="_blank">kitazaki</a> さんの「Node-RED x Sonos x MINI LED Badge」です！

楽しみです！