COBOLで湯婆婆を実装してみる~2バイト文字判定の仕方~

## はじめに

@Nemesisさんの「[Javaで湯婆婆を実装してみる](https://qiita.com/Nemesis/items/c7192a7c510788d2cba2)」が話題なので、流行りに乗っかろうと思います。
まだ書かれていなそうな言語、ということで昔使っていたCOBOLでやってみることにしました。

### 必要な処理と対応するCOBOL構文
湯婆婆の処理の流れは、以下の4工程です。
①コンソールに規定のメッセージを出力
②コンソールに入力された名前を受け取る
③ランダムで何文字目かを決める
④決めた文字を規定のメッセージに含めて出力

各処理に対応するCOBOL構文は以下の通りです。

- コンソール出力  
```DISPLAY 変数.```
- コンソール入力  
```ACCEPT 変数 FROM CONSOLE.```
- ランダム  
```FUNCTION RANDOM().```  
- 値の代入  
```MOVE 1 TO 変数.```
- 部分文字列の取得  
```文字型変数(開始Index:取得バイト数)```  
- 変数宣言  
```01 変数 PIC X(40).```   
- コメントアウト  
```7バイト目に*を付けた行```   

## コード(その1)
<details><summary>長いので畳んでます</summary><div>

```
       IDENTIFICATION DIVISION.
       PROGRAM-ID. coba-ba.
       AUTHOR. Keita INAGAKI.
       ENVIRONMENT DIVISION.
       CONFIGURATION SECTION.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
      *
       01  IN-NAME         PIC X(40).
       01  RAND-VAL        PIC V9(10).
       01  INPUT-SIZE      PIC 9(5).
       01  TMP-IDX         PIC 9(5).
       01  RESULT-INDEX    PIC 9(5).
       01  SEED-VAL        PIC 9(5).
      * 乱数のシードに渡せるのは0~32767のため、分秒4桁を渡す
       01  DATE-NOW.
           03  DATE-NOW-YY        PIC X(04).
           03  DATE-NOW-MM        PIC X(02).
           03  DATE-NOW-DD        PIC X(02).
           03  DATE-NOW-HH        PIC X(02).
           03  DATE-NOW-FFSS      PIC X(04).
      *
      ******************************************************************
      *  ＰＲＯＣＥＤＵＲＥ　ＤＩＶＩＳＩＯＮ
      ******************************************************************
       PROCEDURE DIVISION.
       CNTL SECTION.
      *    メッセージ出力
           DISPLAY "契約書だよ。そこに名前を書きな。".
      *    コンソール入力を受け取る
           ACCEPT IN-NAME      FROM CONSOLE.
      *    入力文字長の取得
           PERFORM GET-INPUT-SIZE.
           DISPLAY "フン。"
                   IN-NAME(1:INPUT-SIZE)
                   "というのかい。贅沢な名"
                   "だねぇ。".
      *    文字を奪う
           PERFORM TAKE-AWAY-STRING.
      *     DISPLAY RESULT-INDEX.
           DISPLAY "今からお前の名前は" IN-NAME(RESULT-INDEX:1)
            "だ。いいかい、" IN-NAME(RESULT-INDEX:1)
            "だよ。分かったら返事をするんだ、" IN-NAME(RESULT-INDEX:1)
            "!!".
           STOP RUN.
      *
      **********************************************************
      * N文字目を決める(in:IN-NAME out:RESULT-INDEX)
      **********************************************************
       TAKE-AWAY-STRING      SECTION.
       TAKE-AWAY-STRING-RTN.
      *    乱数シードのために現在時刻を取る
           MOVE  FUNCTION CURRENT-DATE          TO  DATE-NOW.
           MOVE  FUNCTION NUMVAL(DATE-NOW-FFSS) TO  SEED-VAL.
      *    乱数は0以上1未満で作成される
           MOVE  FUNCTION RANDOM(SEED-VAL)  TO  RAND-VAL.
      *     DISPLAY RAND-VAL.
      *    乱数と文字長を掛けて何文字目かを決定する
           MULTIPLY RAND-VAL BY INPUT-SIZE GIVING RESULT-INDEX.
           IF RESULT-INDEX = 0 THEN
               MOVE 1 TO  RESULT-INDEX
           END-IF.
       TAKE-AWAY-STRING-EXT.
           EXIT.
      **********************************************************
      * 入力文字数カウント(in:IN-NAME out:INPUT-SIZE)
      **********************************************************
       GET-INPUT-SIZE      SECTION.
       GET-INPUT-SIZE-RTN.
           INITIALIZE INPUT-SIZE.
           PERFORM VARYING TMP-IDX FROM 1 BY 1
                              UNTIL TMP-IDX  > 40
      *       DISPLAY "char: " IN-NAME(TMP-IDX:1)
             IF IN-NAME(TMP-IDX:1) = SPACE THEN
               EXIT PERFORM
             END-IF
             ADD 1 TO INPUT-SIZE
           END-PERFORM.
       GET-INPUT-SIZE-EXT.
           EXIT.
```
</div></details>
苦戦はしましたが、一通りの処理が書けました。早速実行してみます。
![hankaku.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/858001/f92b990e-ed7f-c7b6-d8f4-2d68e2cc964c.png)
良い感じに文字を奪って出力できています。が、
![2baito.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/858001/3633b171-02c0-8091-85f6-82527a50405e.png)
**\ 2バイト文字だー！！ /**
COBOLの部分参照はバイト単位なので、**2バイト文字の考慮が必要**でした。
ググれば何とかなる、と思ったら同じような書き込み多数で、どうやら便利な関数は無いようです。
覚悟を決めて[sjisの文字コード表](https://ja.wikipedia.org/wiki/Shift_JIS)を確認します。
1バイト目を確認すれば、それが1バイトの文字か2バイトの文字かが判断できるようなので、
COBOLの制御に組み込んでみます。半角カナの範囲も忘れずに判定します。

```
      * 全角半角判定
             IF (IN-NAME(TMP-IDX:1) >= X"00" AND
                 IN-NAME(TMP-IDX:1)  <= X"7F") OR -- ascii
                (IN-NAME(TMP-IDX:1) >= X"A1" AND
                 IN-NAME(TMP-IDX:1)  <= X"DF") THEN -- 半角カナ
      *          DISPLAY "半角：" IN-NAME(TMP-IDX:1)
             ELSE
      *          DISPLAY "全角：" IN-NAME(TMP-IDX:2)
                ADD 1 TO TMP-IDX                 -- 次の文字までIndexをずらす
             END-IF
```
1バイト目の文字範囲をチェックし、2バイト文字であれば2バイトの塊として扱う形で記述しています。
(定義外のエリアをどう扱うべきか、ちょっとわからなかったので、とりあえず2バイト扱いしています。。)

## コード(最終版)
<details><summary>長いので畳んでます</summary>
<div>

```
       IDENTIFICATION DIVISION.
       PROGRAM-ID. coba-ba.
       AUTHOR. Keita INAGAKI.
       ENVIRONMENT DIVISION.
       CONFIGURATION SECTION.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
      *
       01  IN-NAME         PIC X(40).
       01  RAND-VAL        PIC V9(10).
       01  INPUT-BYTES     PIC 9(5).
       01  INPUT-SIZE      PIC 9(5).
       01  TMP-IDX         PIC 9(5).
       01  TMP-SIZE        PIC 9(5).
       01  RESULT-INDEX    PIC 9(5).
       01  RESULT-STR      PIC X(10).
       01  RESULT-BYTES    PIC 9(1).
       01  SEED-VAL        PIC 9(5).
      * 乱数のシードに渡すのは0~32767のため、分秒4桁を渡す
       01  DATE-NOW.
           03  DATE-NOW-YY        PIC X(04).
           03  DATE-NOW-MM        PIC X(02).
           03  DATE-NOW-DD        PIC X(02).
           03  DATE-NOW-HH        PIC X(02).
           03  DATE-NOW-FFSS      PIC X(04).
      *
      ******************************************************************
      *  ＰＲＯＣＥＤＵＲＥ　ＤＩＶＩＳＩＯＮ
      ******************************************************************
       PROCEDURE DIVISION.
       CNTL SECTION.
      *    メッセージ出力
           DISPLAY "契約書だよ。そこに名前を書きな。".
      *    コンソール入力を受け取る
           ACCEPT IN-NAME      FROM CONSOLE.
      *    入力文字長の取得
           PERFORM GET-INPUT-BYTES.
      *    受け取った名前を出力
           DISPLAY "フン。"
                   IN-NAME(1:INPUT-BYTES)
                   "というのかい。贅沢な名"
                   "だねぇ。".
      *    文字を奪う
           PERFORM TAKE-AWAY-STRING.
      *     DISPLAY RESULT-INDEX.
      *     全角半角で取得バイト数を変えるため、処理変更
      *     DISPLAY "今からお前の名前は" IN-NAME(RESULT-INDEX:1)
      *      "だ。いいかい、" IN-NAME(RESULT-INDEX:1)
      *      "だよ。分かったら返事をするんだ、" IN-NAME(RESULT-INDEX:1)
      *      "!!".
           PERFORM GET-STR-BY-INDEX.
           DISPLAY "今からお前の名前は"
                  RESULT-STR(1:RESULT-BYTES)
                  "だ。いいかい、"
                  RESULT-STR(1:RESULT-BYTES)
                  "だよ。分かったら返事をするんだ、"
                  RESULT-STR(1:RESULT-BYTES)
                  "!!".
           STOP RUN.
      *
      **********************************************************
      * N文字目を決める(in:IN-NAME out:RESULT-INDEX)
      **********************************************************
       TAKE-AWAY-STRING      SECTION.
       TAKE-AWAY-STRING-RTN.
      *    乱数シードのために現在時刻を取る
           MOVE  FUNCTION CURRENT-DATE          TO  DATE-NOW.
           MOVE  FUNCTION NUMVAL(DATE-NOW-FFSS) TO  SEED-VAL.
      *    乱数は0以上1未満で作成される
           MOVE  FUNCTION RANDOM(SEED-VAL)  TO  RAND-VAL.
      *     DISPLAY RAND-VAL.
      *    入力文字長を取得する
           PERFORM GET-INPUT-SIZE.
      *     DISPLAY INPUT-SIZE.
      *    乱数と文字長を掛けて何文字目かを決定する
           MULTIPLY RAND-VAL BY INPUT-SIZE GIVING RESULT-INDEX.
           IF RESULT-INDEX = 0 THEN
               MOVE 1 TO  RESULT-INDEX
           END-IF.
       TAKE-AWAY-STRING-EXT.
           EXIT.
      **********************************************************
      * 入力バイト数カウント
      * in : IN-NAME
      * out: INPUT-BYTES
      **********************************************************
       GET-INPUT-BYTES      SECTION.
       GET-INPUT-BYTES-RTN.
           INITIALIZE INPUT-BYTES.
           PERFORM VARYING TMP-IDX FROM 1 BY 1
                              UNTIL TMP-IDX  > 40
             IF IN-NAME(TMP-IDX:1) = SPACE THEN
               EXIT PERFORM
             END-IF
             ADD 1 TO INPUT-BYTES
           END-PERFORM.
       GET-INPUT-BYTES-EXT.
           EXIT.
      **********************************************************
      * 入力文字数カウント
      * in : IN-NAME
      * out: INPUT-SIZE
      **********************************************************
       GET-INPUT-SIZE      SECTION.
       GET-INPUT-SIZE-RTN.
           INITIALIZE INPUT-SIZE.
           PERFORM VARYING TMP-IDX FROM 1 BY 1
                              UNTIL TMP-IDX  > 40
      *       DISPLAY "char: " IN-NAME(TMP-IDX:1)
             IF IN-NAME(TMP-IDX:1) = SPACE THEN
               EXIT PERFORM
             END-IF
             ADD 1 TO INPUT-SIZE
      * 全角半角判定
             IF (IN-NAME(TMP-IDX:1) >= X"00" AND
                 IN-NAME(TMP-IDX:1)  <= X"7F") OR
                (IN-NAME(TMP-IDX:1) >= X"A1" AND
                 IN-NAME(TMP-IDX:1)  <= X"DF") THEN
      *          DISPLAY "半角：" IN-NAME(TMP-IDX:1)
             ELSE
      *          DISPLAY "全角：" IN-NAME(TMP-IDX:2)
                ADD 1 TO TMP-IDX
             END-IF
           END-PERFORM.
       GET-INPUT-SIZE-EXT.
           EXIT.
      **********************************************************
      * 入力文字数カウント
      * in : RESULT-INDEX
      * out: RESULT-STR
      **********************************************************
       GET-STR-BY-INDEX    SECTION.
       GET-STR-BY-INDEX-RTN.
           INITIALIZE TMP-SIZE.
           PERFORM VARYING TMP-IDX FROM 1 BY 1
                              UNTIL TMP-IDX  > 40
             IF IN-NAME(TMP-IDX:1) = SPACE THEN
               EXIT PERFORM
             END-IF
      * 全角半角判定
             ADD 1 TO TMP-SIZE
      *       DISPLAY "char: " IN-NAME(TMP-IDX:1)
             IF TMP-SIZE = RESULT-INDEX THEN
               IF (IN-NAME(TMP-IDX:1) >= X"00" AND
                   IN-NAME(TMP-IDX:1)  <= X"7F") OR
                  (IN-NAME(TMP-IDX:1) >= X"A1" AND
                   IN-NAME(TMP-IDX:1)  <= X"DF") THEN
                  MOVE IN-NAME(TMP-IDX:1) TO RESULT-STR
                  MOVE 1                  TO RESULT-BYTES
               ELSE
                  MOVE IN-NAME(TMP-IDX:2) TO RESULT-STR
                  MOVE 2                  TO RESULT-BYTES
               END-IF
      *         DISPLAY "RESULT BECOME " RESULT-STR
               EXIT PERFORM
             END-IF
             IF (IN-NAME(TMP-IDX:1) >= X"00" AND
                 IN-NAME(TMP-IDX:1)  <= X"7F") OR
                (IN-NAME(TMP-IDX:1) >= X"A1" AND
                 IN-NAME(TMP-IDX:1)  <= X"DF") THEN
      *
             ELSE
                ADD 1 TO TMP-IDX
             END-IF
           END-PERFORM.
       GET-STR-BY-INDEX-EXT.
           EXIT.
```
</div></details>
実行結果はこちら！
![perfect.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/858001/61f6abf9-f087-a6a3-29c9-78549b346f56.png)
無事に全角/半角/半角カナで文字を奪うことに成功しました！
もちろん、入力文字が空の際はちゃんとエラーになります。
![error2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/858001/15074284-836b-50eb-21fd-e176fabae6ac.png)
これをもって、COBOL版湯婆婆とさせていただきます。

## おわりに
高級言語のありがたみを実感しました。
ただ今回のおかげで、今まで敬遠していた文字コードと向かい合うことができ、理解が進んだので、
湯婆婆に取り組んで良かったな、と思います。ありがとうございました。