何を間詰めするか

正規化対象のスペースは2種類ある。

  • 先頭のインデント — Area A / Area B のインデントは正規化後は不要
  • トークン間の連続スペースMOVE A TO BMOVE A TO B

一方、文字列リテラル内のスペース は変更してはいけない。 MOVE ' HELLO ' TO WS-MSG.' HELLO ' はスペースが意味を持つ文字列データだ。

単純実装とその問題点

re.sub(r'\s+', ' ', line).strip() を使えばよい」と思うかもしれないが、 これは文字列リテラル内のスペースも潰してしまう。

Python — 単純実装(NG例)
import re
line = "     MOVE '  HELLO  '   TO   WS-MSG."
bad = re.sub(r'\s+', ' ', line).strip()
print(bad)
# → "MOVE ' HELLO ' TO WS-MSG."   ← リテラル内のスペースが潰れた!

文字列リテラル保護付き実装

文字を1文字ずつ走査し、引用符の中にいるかどうかを in_string フラグで管理する。 リテラル外ではスペースの連続を1つにまとめ、リテラル内ではそのまま出力する。

Python — スペース正規化(文字列保護あり)
def normalize_spaces(lines: list[str]) -> list[str]:
    """
    各行の連続スペースを1つに間詰めし、先頭・末尾の空白を除去する。
    文字列リテラル(シングル/ダブルクォート)内のスペースは変更しない。
    Step1〜4(継続行結合まで)適用後に呼ぶ。
    """
    result = []
    for line in lines:
        normalized = _squeeze_spaces(line)
        if normalized:  # 空行は除外
            result.append(normalized)
    return result


def _squeeze_spaces(line: str) -> str:
    """
    1行のスペースを間詰めする内部関数。
    文字列リテラル内のスペースは保護する。
    """
    output = []
    in_string = False
    quote_char = ''
    prev_was_space = False  # 直前がスペースだったかフラグ

    for ch in line:
        if in_string:
            # リテラル内: そのまま出力
            output.append(ch)
            if ch == quote_char:
                in_string = False
            prev_was_space = False
        else:
            # リテラル外
            if ch in ("'", '"'):
                # リテラル開始
                in_string = True
                quote_char = ch
                output.append(ch)
                prev_was_space = False
            elif ch == ' ':
                # スペース: 直前がスペースでなければ出力
                if not prev_was_space:
                    output.append(ch)
                prev_was_space = True
            else:
                output.append(ch)
                prev_was_space = False

    return ''.join(output).strip()

💡 標識列(インデックス0)の扱い

Step 1〜4 後の行ではインデックス0が元の列7(標識列)だが、通常行(空白)・コメント行(既に削除済み)・継続行(既に結合済み)なので、 このステップでは単純に strip() で先頭の空白インデントをまとめて除去できる。

動作確認

Python — 動作確認
lines = [
    "     MOVE  'HELLO   WORLD'   TO   WS-MSG.",
    "     MOVE  A   TO  B.",
    " IDENTIFICATION DIVISION.",
    "     IF  WS-FLG  =  '  Y  '",
]

result = normalize_spaces(lines)
for line in result:
    print(repr(line))
実行結果
"MOVE 'HELLO   WORLD' TO WS-MSG."
"MOVE A TO B."
"IDENTIFICATION DIVISION."
"IF WS-FLG = '  Y  '"

'HELLO WORLD' 内の複数スペースは保護され、 リテラル外の MOVE A TO B は正しく間詰めされている。

⚠️ PICTURE句の文字 X() はスペースを含まない

PICTURE 句(例: PIC X(30))の括弧内は文字数定義であり文字列リテラルではない。 X(30) のような記述にスペースが入ることは通常ないため特別処理は不要だが、 万が一 PIC X( 30 ) のように書かれていた場合は間詰めされて PIC X( 30 )PIC X( 30 ) と1スペースになる。 これはコンパイラによっては問題になりうるが、実際のソースでは極めてまれだ。

次の章では…

PART 06 では正規化の最難関である 1行1命令への分割 を実装する。 ピリオドが文字列中・PIC句・数値リテラルにも現れること、 END-IF などのスコープ終端がピリオドなしで命令を区切る場合があることを丁寧に解説する。

→ PART 06 — 1行1命令への分割へ