Excelの数式が複雑すぎてインデントしたいなと思ったときにやったこと

# 概要

Excelの数式欄にかなりの長さの数式が埋まっており、読み解くにも修正するにもかなりの苦労が必要になってしまったので、ひとまず抜き出して解析を試みた話。

データの抜き出しには Openpyxl を利用しました。

# こんな状況でした

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13879/fd673e6a-e536-ad9c-7c65-ba3e9f49a7fd.png)


Excelの数式に入れていい文字列は75文字までとか誰か決めてくれ・・・ｗ
（計算結果の途中をどっかのセルに吐き出すとか・・・ね。）

# 特に Excel の IF は読みにくい

```excel
=IF(A1="これこれ", "xxxx", IF(B1="あれこれ", "yyyy", "zzzz"))
```

こんな書き方、良くしますよね・・・。

```
if (A1 === "これこれ") {
   cell = "xxxx";
} elseif (B1 === "あれこれ") {
   cell = "yyyy";
} else {
   cell = "zzzz";
}
```

という感じなんですけど、独自関数定義するとマクロが・・・って話になりますし、リファクタリングも難しいです。なので、インデントしたりしてどうにか可読性をあげるしかないですね。

ちなみに Excel数式におけるインデントに、N（）関数を加えてコメントっぽく書くことができたり、配列数式を使ってコメントっぽく書く方法もあるようです・・・

→ [数式作成に便利なテクニック](https://www.excel-chunchun.com/entry/super_function_1#%E6%95%B0%E5%BC%8F%E4%BD%9C%E6%88%90%E3%81%AB%E4%BE%BF%E5%88%A9%E3%81%AA%E3%83%86%E3%82%AF%E3%83%8B%E3%83%83%E3%82%AF)

# 今回は値の取り出し〜インデントを加えるところをやってみます

もちろんマクロを使って解決する方法もありますね。
→ [ExcelでネストしたIf関数をVBAでインデントして分析しやすくする](https://thom.hateblo.jp/entry/2017/08/20/122525)

## Python でやってみる

今回、Openpyxlを使ってやってみます。

ずばり、数式の解析は [Parsing Formula](https://openpyxl.readthedocs.io/en/stable/formula.html) を使ってできるようです。

とりあえず書いてあるのを試してみましょう。

```python3
>>> from openpyxl.formula import Tokenizer
>>> tok = Tokenizer("""=IF(A1="これこれ", "xxxx", IF(B1="あれこれ", "yyyy", "zzzz"))""")
>>> print("\n".join("%12s%11s%9s" % (t.value, t.type, t.subtype) for t in tok.items))
         IF(       FUNC     OPEN
          A1    OPERAND    RANGE
           =OPERATOR-INFIX
      "これこれ"    OPERAND     TEXT
           ,        SEP      ARG
            WHITE-SPACE
      "xxxx"    OPERAND     TEXT
           ,        SEP      ARG
            WHITE-SPACE
         IF(       FUNC     OPEN
          B1    OPERAND    RANGE
           =OPERATOR-INFIX
      "あれこれ"    OPERAND     TEXT
           ,        SEP      ARG
            WHITE-SPACE
      "yyyy"    OPERAND     TEXT
           ,        SEP      ARG
            WHITE-SPACE
      "zzzz"    OPERAND     TEXT
           )       FUNC    CLOSE
           )       FUNC    CLOSE
```

なるほど。先頭からそれぞれのパーツが関数（開始・終了）なのか、変数なのか等々に分解してくれるようだ。

ということは関数が開始したら改行して、次の行にはインデントをつけ、関数が終了するときにインデントを減らしてから改行していけばよいということですね。

# わかりやすさのためにインデントを可視化して書いてみる

```python3
from openpyxl.formula import Tokenizer
# あとでindent_charはもちろんスペースに直すよ
def parse(formula, indent_char = "_"):
    # 最終的に文字列として結合して返す
    result = ""
    tok = Tokenizer(formula)
    # インデント数
    n = 0
    # 直前で処理したトークン
    last = None

    for t in tok.items:
        if t.type == "FUNC":
            if t.subtype == "OPEN":
                if last is not None and (last.type == "FUNC" and last.subtype == "OPEN"):
                    n += 2
                    result += indent_char * n + t.value
                else:
                    result += t.value

                result += "\n"
            else:
                # function close。インデント数を戻す
                n -= 2
                result += "\n" + indent_char * n + t.value

        else:
            # それ以外は単純結合。直前が関数の場合はインデント
            if last is not None and (last.type == "FUNC" and last.subtype == "OPEN"):
                n += 2
                result += indent_char * n + t.value
            else:
                result += t.value

        last = t

    return result

s = """=IF(A1="これこれ", "xxxx", IF(B1="あれこれ", "yyyy", "zzzz"))"""

print(parse(s))
```

```
IF(
__A1="これこれ", "xxxx", IF(
____B1="あれこれ", "yyyy", "zzzz"
__)
)
```

うん。これで良さそう。

# あとはExcelの指定したセル範囲とかを渡して

```python3
wb = load_workbook(filename, data_only = False)
sheet_name = "Sheet1"
cell_range = "A2:C2"
for line in wb[sheet_name][cell_range]:
    for cell in line:
        # formula のみを抽出
        if cell.data_type == "f":
            f = parse(cell.value)
            # cell.coordinateには「A1」などの表現が入る
            with open(cell.coordinate + ".txt", mode='w') as fl:
                fl.write(f)
```

こんな感じにすれば、数式が埋まっているセルだけ、A2.txt, C2.txtのようにインデント済のテキストとして吐き出せます。