【Lambda不要】Step FunctionsのMapステートでymd文字列でループする【CDKコード付き】

# 本記事の要約

StepFunctionsでこれを入力すると
```json
{
  "start": "20250401",
  "end": "20250403"
}
```

本記事に記載の処理を実装すれば、`20250401`、`20250402`、`20250403`からなるリストが生成され、
**日付でのループ処理を実装できる**。

もちろんLambda関数を使いません。

# Step Functionsのコード

### Step Functions ステートマシン定義

```json
{
  "Comment": "An example of using JSONata expression to create date array from ymd string to loop with date range on Map state",
  "StartAt": "parse arguments / ymd to millis",
  "QueryLanguage": "JSONata",
  "States": {
    "parse arguments / ymd to millis": {
      "Type": "Pass",
      "Next": "parse arguments / millis to ymd array",
      "Output": {
        "start": "{% $toMillis($substring($states.input.start, 0, 4) & \"-\" & $substring($states.input.start, 4, 2) & \"-\" & $substring($states.input.start, 6, 2) & \"T09:00:00.000Z\") %}",
        "end": "{% $toMillis($substring($states.input.end, 0, 4) & \"-\" & $substring($states.input.end, 4, 2) & \"-\" & $substring($states.input.end, 6, 2) & \"T09:00:00.000Z\") %}"
      }
    },
    "parse arguments / millis to ymd array": {
      "Type": "Pass",
      "Next": "ymd de loop",
      "Output": {
        "dates": "{% $map($range($states.input.start, $states.input.end, 86400000), function ($v) { $replace($substring($fromMillis($v), 0, 10), '-', '') } ) %}"
      }
    },
    "ymd de loop": {
      "Type": "Map",
      "ItemProcessor": {
        "ProcessorConfig": {
          "Mode": "INLINE"
        },
        "StartAt": "some process",
        "States": {
          "some process": {
            "Type": "Pass",
            "End": true,
            "Output": {
              "message": "{% \"you can get a date as $states.input.date, the date value is :\" & $states.input.date %}"
            }
          }
        }
      },
      "Items": "{% $states.input.dates %}",
      "ItemSelector": {
        "date": "{% $states.context.Map.Item.Value %}"
      },
      "MaxConcurrency": 2,
      "End": true
    }
  }
}
```

### ポイント

ステートマシン定義（コード）のJSONのトップレベルに、QueryLanguage=JSONataを指定することによって、ステートの出力の記述でJSONataを利用することが可能となる。
```
"QueryLanguage": "JSONata"
```
ビジュアルエディタ上の表示で言えば、以下添付画像のように「ステートマシンクエリ言語」の欄が「JSONata」であること。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653543/b52a07e4-b541-446a-ab1a-a5d830f27790.png)

**言い換えると、クエリ言語としてJSONataが指定されていないと本記事に記載の方法は使えない。**
そのような場合は、マネジメントコンソール上で編集していると以下のようなエラーが表示されるはず。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653543/fe40eec6-082e-4619-b8f6-dbbaa42cbfb9.png)

※上記エラーを`"QueryLanguage": "JSONata"`を指定せずに修正して無理やり実行しても、JSONataが評価されずステートで以下のような出力が得られる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653543/0a6cfd5b-cccd-4e90-a8fe-992f842d0542.png)

# JSONataとは

JSONに対するクエリと変換を行う言語。
[JSONataの公式ホームページ](https://jsonata.org/)

超ざっくり言えば文字列のなかでプログラミングを行うことで、JSONデータの中から特定のデータを取り出したり、計算や変換処理をしながら特定のデータを取り出したり出来る、おもろいやつ。

参考
[公式ドキュメント](https://docs.jsonata.org/overview)
[JSONata の言語ガイドを訳してみた #javascript - Qiita](https://qiita.com/yamachan360/items/91805334890d18ef6e23)

### AWS Step FunctionsにおけるJSONata

2024年11月からStep Functionsに追加された新機能。
→　[公式からのお知らせ](https://aws.amazon.com/jp/blogs/compute/simplifying-developer-experience-with-variables-and-jsonata-in-aws-step-functions/)

超ざっくり言うと；
Step Functionsに入力や出力、引数、項目などの項目はJSONで書くけれど、
そのJSONの値を`{%  %}`で囲まれたexpressionにすると、数式っぽいものが埋め込めるって訳。（※JSONata言語は`{%  %}`で囲まれた中の文字列であって、`{%%}`自体はStep Functions特有の表現である点に注意）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653543/daf08697-22e7-4333-b02e-dc8bed01b585.png)

それはStep Functionsサービスの中のプログラムの中の処理がJSONata言語を実装して、入力／出力／引数／項目記述内のJSONの中のJSONata表現を評価するように実装されているから（たぶん）。

なので、Step FunctionsにおけるJSONataは通常のJSONata言語の仕様に加え、Step Functions独自の変数や関数が実装されている。

→　[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/transforming-data.html)

その中で特によく使うのが、Step Functions独自のJSONata予約変数`$states`
- `$states.input`：ステートの入力のJSONが格納されている
- `$states.result`：AWS SDKのアクションを実行した際にAPIから返された結果が格納される
- `$states.context`：当該executionに関する情報が格納される　→　[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/input-output-contextobject.html)
- `$states.errorOutput`：APIまたはサブワークフローが失敗した場合のエラー出力が格納される

そしてこの`$states.input`をからめたJSONata expressionを（Passステートでもよいので）ステートのJSONに書けるよー！ってのが本記事のきも

### 今回使うJSONata関数の公式ドキュメント

[$toMillis()](https://docs.jsonata.org/date-time-functions#tomillis)　[$substring()](https://docs.jsonata.org/string-functions#substring)　[$replace()](https://docs.jsonata.org/string-functions#replace)　[$map()](https://docs.jsonata.org/higher-order-functions#map)　[$fromMillis()](https://docs.jsonata.org/date-time-functions#frommillis)
[$range()](https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/transforming-data.html#:~:text=%24range%20%2D%20%E5%80%A4%E3%81%AE%E9%85%8D%E5%88%97%E3%82%92%E7%94%9F%E6%88%90%E3%81%99%E3%82%8BStates.ArrayRange%E7%B5%84%E3%81%BF%E8%BE%BC%E3%81%BF%E9%96%A2%E6%95%B0%E3%81%AB%E7%9B%B8%E5%BD%93%E3%81%99%E3%82%8B%20JSONata%E3%80%82)

# 実行結果およびインラインでの解説

ステートマシンへの入力
```
{
  "start": "20250401",
  "end": "20250403"
}
```

ステートマシンからの出力

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653543/8047d8ee-d7fe-4c2a-9c94-c9ee0333d1fd.png)

Mapステート内Passステートの出力のリストがMapステートの出力となり、Mapステートの直後がステートマシンの終わりならば、これがステートマシンの出力になる。

当該Passステートにおいて入力で受けたdateを出力に含めていることから、startからendまでの範囲内の各日付のymd文字列を当該Passステートで受け取れていることが分かる。

以下、各ステートの実行結果。

###### 1つめのステート
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653543/4af68252-a4a5-4d82-820b-e7c4e71fdd20.png)

入力されたymd文字列`start`および`end`をJSONataでUnix Epoch Time（ミリ秒）の整数に変換するステートである。
そのJSONata表現はステートマシン定義を参照のこと。見やすいためにインデントすると以下の通り。
```JSONata
{%
    $toMillis(
        $substring(
            $states.input.start, 0, 4
        ) & \"-\" &
        $substring(
            $states.input.start, 4, 2
        ) & \"-\" &
        $substring(
            $states.input.start, 6, 2
        )
        & \"T09:00:00.000Z\"
    )
%}
```
`$states.input.start`：AWS Step Functions JSONata特有の表現と考えて差し支えない。`$states.input`はステートの入力を表し、ここにドット(`.`)をつけたのち入力JSONにおけるカラム名を指定することで、ステートの入力の値として評価される。すなわちステートの実行時に入力の`start`の値が代入される。
`$toMillis()`: [JSONataの関数](https://docs.jsonata.org/date-time-functions#frommillis)。ISO 8601日付形式の文字列を、Unix Epoch Time（ミリ秒）の整数値に変換した値を返す。
`$substring()`:[JSONataの関数](https://docs.jsonata.org/string-functions#substring)。第一引数の文字列の部分文字列を得る。[第二引数(zero origin)]文字目から[第三引数]文字取り出す。
`&`: ここでは文字列を結合する演算子として作用する。
`"T09:00:00.000Z"`：文字列リテラル。y-m-d形式文字列をISO8601に無理やり変えるためにタイムゾーンを表す文字列を追記。

このJSONata expressionにより、`start`の値は以下の流れで変換される。
`20250401` → `2025`と`04`と`01` → `2025-04-01` → `2025-04-01T09:00:00.000Z` → `1743498000000`

endも同様。
この操作を行う記述をステートの出力として記述したので、このPassステートの出力はstart, endのUnix Epoch Time（ミリ秒）の整数値それぞれがstart, endカラムとしてトップレベルに格納されたオブジェクトとなる。

###### 2つめのステート

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653543/b92039fb-595d-4aee-b7bb-5d57b23f0979.png)

入力されたUnix Epoch Time（ミリ秒）`start`と`end`について、`start`から`end`まで`86400000`ずつ増加する等差数列からなるリストを生成し、各要素をymd文字列へそれぞれ変換したリストを`dates`として出力する。

JSONata表現をインデントすると以下の通り。
```
{%
    $map(
        $range(
            $states.input.start,
            $states.input.end,
            86400000
        ),
        function ($v) {
            $replace(
                $substring(
                    $fromMillis($v),
                    0,
                    10
                ),
                '-',
                ''
            )
        }
    )
%}
```

`$states.input.start`および`$states.input.end`：[1つめのステート](#1つめのステート)の解説を参照
`86400000`: Unix Epoch Time（ミリ秒）で1日間を表す整数リテラル。
`$map()`: [JSONataの関数](https://docs.jsonata.org/higher-order-functions#map)。[第一引数]のリストの各要素に対して[第二引数]の関数でループ処理し、**[第二引数]の関数の戻り値のリストを返す**。
`$range()`：[Step FunctionsにおけるJSONataの関数](https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/transforming-data.html#:~:text=%24range%20%2D%20%E5%80%A4%E3%81%AE%E9%85%8D%E5%88%97%E3%82%92%E7%94%9F%E6%88%90%E3%81%99%E3%82%8BStates.ArrayRange%E7%B5%84%E3%81%BF%E8%BE%BC%E3%81%BF%E9%96%A2%E6%95%B0%E3%81%AB%E7%9B%B8%E5%BD%93%E3%81%99%E3%82%8B%20JSONata%E3%80%82)。[第一引数]から[第二引数]までで[第三引数]ずつ増加する等差数列のリストを返す。
`$replace()`：[JSONataの関数](https://docs.jsonata.org/string-functions#replace)。[第一引数]の文字列のうち[第二引数]で指定された正規表現に該当する文字を[第三引数]の文字に置き換えた文字列を返す。
`function () {}`：[JSONataにおける関数型プログラミングの文法](https://docs.jsonata.org/programming#defining-a-function)。`$map()`関数の第二引数に入れた場合、この関数は以下の引数をとる。
・第一引数：ループで処理中の要素
・第二引数（省略可能）：ループで処理中の要素が何要素目か
・第三引数（省略可能）：もとのリスト
`function ($v) { $replace($substring($fromMillis($v), 0, 10), '-', '') }`：Unix Epoch Time（ミリ秒）をymd文字列に変換する関数として作用する。

このJSONata expressionにより、start、endの値は以下の流れで変換される。
「`1743498000000`から`1743670800000`まで`86400000`ごとにループ」 → 「`1743498000000`、
`1743584400000`、`1743670800000`それぞれをymd文字列に変換しそのリストを返す」→「`20250401`、`20250402`、`20250403`のリストを返す」

ステートの出力記述により、ymdの連番のリストがdatesという名前をつけられステートから出力される。

###### 3つめのステート

＜概要＞
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653543/17ee8042-189d-4d45-9243-f652f9131531.png)

＜1ループ目＞
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653543/50632a94-8ff8-4e1e-95e0-e1837f1a2b78.png)

☆Mapステートの定義を書く時のポイント；
- 「`項目を提供`」の欄に、「どの変数でループするか」を書く。
ここにはJSONata式を記入する。$states.inputはMapステートの入力を表し、その直下の`dates`というカラムを指定する。
`dates`にはymd文字列のリストが前のステートから入力されるため、このMapステートはymdごとにループする。
- 「`[ItemSelector]で項目を変更`」の欄に、「ループの中の処理（ステート）に何の変数を渡すか」を指定する。「処理中の要素」は`$states.context.Map.Item.Value`で得られるので、これを記入する。本記事の例のようにすると、中のステートでは`date`というカラムに処理中の1つのymd文字列が記入されたものが入力される。
（「`[ItemSelector]で項目を変更`」の欄に`$status.input`以下の変数を記入することで、「処理中の要素」のほかにもMapステートの入力の値を渡すことも可能）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653543/d54228eb-8c46-42c5-977b-debfe0c5a8c5.png)

☆中のPassステート

親のMapステートで「`[ItemSelector]で項目を変更`」の欄に記入された「処理中の要素」はステートの入力として得られる。
つまり`$states.input.date`が処理中のymd文字列の値となる。
これにより、本記事の例ではymd文字列を適当な文字列を結合して出力している。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653543/5783dfdd-f121-4710-a440-ef355210a7b7.png)

もちろん、`$states.input`はAWS SDKアクションの引数として与えることが出来る。
`Lambda:invoke`アクションのペイロードにymd文字列を入れた例を以下画像に示す。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653543/07902ec1-9e79-4560-a50b-bef1d43064fa.png)

# Step FunctionsのCDKコード

```typescript: StudySfnJSONataStack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import {
  DefinitionBody,
  Pass,
  Map,
  ProvideItems,
  StateMachine,
} from "aws-cdk-lib/aws-stepfunctions";

export class StudySfnJSONataStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const state1 = Pass.jsonata(this, "State1", {
      stateName: "parse arguments / ymd to millis",
      outputs: {
        "start": "{% $toMillis($substring($states.input.start, 0, 4) & \"-\" & $substring($states.input.start, 4, 2) & \"-\" & $substring($states.input.start, 6, 2) & \"T09:00:00.000Z\") %}",
        "end": "{% $toMillis($substring($states.input.end, 0, 4) & \"-\" & $substring($states.input.end, 4, 2) & \"-\" & $substring($states.input.end, 6, 2) & \"T09:00:00.000Z\") %}"
      },
    });

    const state2 = Pass.jsonata(this, "State2", {
      stateName: "parse arguments / millis to ymd array",
      outputs: {
        "dates": "{% $map($range($states.input.start, $states.input.end, 86400000), function ($v) { $replace($substring($fromMillis($v), 0, 10), '-', '') } ) %}"
      },
    });
    state1.next(state2);

    const mapState = Map.jsonata(this, "MapState", {
      stateName: "",
      items: ProvideItems.jsonata("{% $states.input.dates %}"),
      itemSelector: {
        "date": "{% $states.context.Map.Item.Value %}"
      },
      maxConcurrency: 2,
    });
    state2.next(mapState);
 
    const stateInside = Pass.jsonata(this, "StateInside", {
      stateName: "some process",
      outputs:{
        "message": "{% \"you can get a date as $states.input.date, the date value is :\" & $states.input.date %}"
      },
    });
    mapState.itemProcessor(stateInside);

    const stateMachine = new StateMachine(
      this,
      "StateMachine",
      {
        stateMachineName: `StudySfnJSONataStateMachine`,
        definitionBody: DefinitionBody.fromChainable(
          state1
        ),
      }
    );

  }
}

```

# コメント

15:34
いま、ねこが膝の上にいます。
感触がすっっっっっごくやわらかくて、あったかいってだけでも昇天確実なんですが。
小刻みに動いて、ちょこちょこ喋ってるのが、明らかにそばにいてくれていて
一生懸命寝てるので、かわいすぎてどうしようもありません。

15:38
かわいすぎて私が落ち着かないので私の体が揺れたり小刻みに動いてしまって、とらちゃん的にはこういうの好きじゃなくてすぐ降りちゃうもので、いま起きてるってのが分かっているのですが、それでも目を閉じています。
ぱっと見ではそっと目を閉じているように見えるので、サイゼリヤの壁画の天使みたいなんですが:天使_猫_angel-cat:
よく見ると寝ようとして一生懸命目を閉じているんですね。