AWS Step Functionsの組み込み関数をフル活用して置換箇所が不特定多数の場合の文字列置換をやってみた(AWS CDK)

2022.09.03

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、CX事業本部 IoT事業部の若槻です。

前回のエントリではStep Functionsでの組み込み関数による文字列置換を実装しましたが、その実装は文字列内に予め決まった個数の置換対象文字列が含まれている場合にのみ対応したものでした。よって置換箇所が複数の場合には対応できません。

例えば、数字の区切り記号は国によってカンマ,だったりピリオド.だったりするのですが、ピリオド区切りの数字文字列をカンマ区切りに置換したい場合、その数字の中の置換箇所の数というのは桁数によって当然変わってきます。(不特定多数となる)

そこで今回は、AWS Step Functionsの組み込み関数をフル活用することにより、置換箇所が不特定多数の場合の文字列置換を実装(AWS CDK)することができたので紹介します。

やってみた

次の5つの組み込み関数を活用して、実現することができました。

  • States.StringSplit
  • States.ArrayLength
  • States.ArrayGetItem
  • States.MathAdd
  • States.Format

実装

次のようなCDK Stack定義による実装を行いました。

lib/aws-cdk-app-stack.ts

import { aws_stepfunctions, Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';

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

    // States.StringSplitで文字列分割
    const getSplittedList = new aws_stepfunctions.Pass(
      this,
      'getSplittedListPass',
      {
        parameters: {
          'val.$': "States.StringSplit($.inputString, '.')",
        },
        resultPath: '$.splittedListOutput',
      }
    );

    // States.ArrayLengthで文字列分割の配列長を取得
    const getSplittedListLength = new aws_stepfunctions.Pass(
      this,
      'getSplittedListLength',
      {
        parameters: { 'val.$': 'States.ArrayLength($.splittedListOutput.val)' },
        resultPath: '$.getSplittedListLengthOutput',
      }
    );

    // States.ArrayGetItemで配列の0番目の要素を取得(concatinatedStringに要素の文字列をループ内で連結していく)
    const initLoopArgs = new aws_stepfunctions.Pass(
      this,
      'initReplacementLoop',
      {
        parameters: {
          currentIndex: 1,
          'concatinatedString.$':
            'States.ArrayGetItem($.splittedListOutput.val, 0)',
        },
        resultPath: '$.loopArgs',
      }
    );

    // ループ処理開始
    // States.ArrayGetItemで現在のIndex番号の要素を取得する
    const getCurrentValue = new aws_stepfunctions.Pass(
      this,
      'getCurrentValue',
      {
        parameters: {
          'val.$':
            'States.ArrayGetItem($.splittedListOutput.val, $.loopArgs.currentIndex)',
        },
        resultPath: '$.getCurrentValueOutput',
      }
    );

    // States.MathAddでIndexをインクリし、States.Formatで置換文字列(,)t現在のIndex番号の要素を前回のconcatinatedStringの末尾に文字列結合する
    const concatinateString = new aws_stepfunctions.Pass(
      this,
      'concatinateString',
      {
        parameters: {
          'currentIndex.$': 'States.MathAdd($.loopArgs.currentIndex, 1)',
          concatinatedString: aws_stepfunctions.JsonPath.format(
            '{}{}{}',
            aws_stepfunctions.JsonPath.stringAt(
              '$.loopArgs.concatinatedString'
            ),
            ',',
            aws_stepfunctions.JsonPath.stringAt('$.getCurrentValueOutput.val')
          ),
        },
        resultPath: '$.loopArgs',
      }
    );

    // 全ての配列要素の処理が完了したらSucceedへ進んで終了、未完了なら次の要素を処理する
    const isLoopFinishedChoice = new aws_stepfunctions.Choice(
      this,
      'isLoopFinishedChoice'
    )
      .when(
        aws_stepfunctions.Condition.numberEqualsJsonPath(
          '$.getSplittedListLengthOutput.val',
          '$.loopArgs.currentIndex'
        ),
        // ループ処理終了
        new aws_stepfunctions.Succeed(this, 'succeed')
      )
      .otherwise(getCurrentValue);

    // State Machine
    new aws_stepfunctions.StateMachine(this, 'myStateMachine', {
      stateMachineName: 'myStateMachine',
      definition: getSplittedList
        .next(getSplittedListLength)
        .next(initLoopArgs)
        .next(getCurrentValue)
        .next(concatinateString)
        .next(isLoopFinishedChoice),
    });
  }
}

上記をCDK Deployしてスタックをデプロイすると、次の定義のState Machineが作成されます。

Definition

{
  "StartAt": "getSplittedListPass",
  "States": {
    "getSplittedListPass": {
      "Type": "Pass",
      "ResultPath": "$.splittedListOutput",
      "Parameters": {
        "val.$": "States.StringSplit($.inputString, '.')"
      },
      "Next": "getSplittedListLength"
    },
    "getSplittedListLength": {
      "Type": "Pass",
      "ResultPath": "$.getSplittedListLengthOutput",
      "Parameters": {
        "val.$": "States.ArrayLength($.splittedListOutput.val)"
      },
      "Next": "initReplacementLoop"
    },
    "initReplacementLoop": {
      "Type": "Pass",
      "ResultPath": "$.loopArgs",
      "Parameters": {
        "currentIndex": 1,
        "concatinatedString.$": "States.ArrayGetItem($.splittedListOutput.val, 0)"
      },
      "Next": "getCurrentValue"
    },
    "getCurrentValue": {
      "Type": "Pass",
      "ResultPath": "$.getCurrentValueOutput",
      "Parameters": {
        "val.$": "States.ArrayGetItem($.splittedListOutput.val, $.loopArgs.currentIndex)"
      },
      "Next": "concatinateString"
    },
    "concatinateString": {
      "Type": "Pass",
      "ResultPath": "$.loopArgs",
      "Parameters": {
        "currentIndex.$": "States.MathAdd($.loopArgs.currentIndex, 1)",
        "concatinatedString.$": "States.Format('{}{}{}', $.loopArgs.concatinatedString, ',', $.getCurrentValueOutput.val)"
      },
      "Next": "isLoopFinishedChoice"
    },
    "isLoopFinishedChoice": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.getSplittedListLengthOutput.val",
          "NumericEqualsPath": "$.loopArgs.currentIndex",
          "Next": "succeed"
        }
      ],
      "Default": "getCurrentValue"
    },
    "succeed": {
      "Type": "Succeed"
    }
  }
}

State Machine Graphは次のようになります。

動作確認

次のInputを指定してState Machineを実行します。

Input

{
  "inputString": "123.456.789"
}

実行結果を見ると、succeedStateで123.456.789が文字列置換された123,456,789が取得できていますね!

注意点

States.StringSplitの仕様として、配列への空文字列の追加はスキップされるようです。よってSplitter文字列が元の文字列の冒頭や末尾にあったり連続していたりすると、今回のState Machineの実装では置換されずに削除される動作となります。

よって.123.456123,456123.456.789.123.456.123,456,789,123,456123..456123,456となります。

これで不都合がある場合は実装を改める必要がありそうです。

おわりに

AWS Step Functionsの組み込み関数をフル活用して、置換箇所が不特定多数の場合の文字列置換をやってみました。

Step Functionsはアイデア次第でこんなこともできるんだぞというのを示せたのではないかと思います。

参考

以上