全角を2文字として文字列をスライスする


**実行環境**
Windows10
Python 3.9.5

## はじめに
固定長のテキストファイルの読み込みの場面で、文字列をスライスすることが多いです。

たとえば5文字間隔で数値が並んでいる場合。

```py:固定長のテキストから指定の位置の数値を取得
text = ' 10.0 20.0 30.0'
sliced_text = text[5:10]  # ' 20.0'
value = float(sliced_text)  # 20.0
```

扱うテキストがすべて半角文字であればこれで事足りますが、全角文字が含まれる場合は面倒です。

python では全角を1文字としてカウントします。

そのため、全角を2文字としてカウントしている全角入りの固定長のテキストの場合は、期待の結果になりません。

```py:全角を含む固定長のテキストをスライス
text = ' 数字 20.0 30.0'
sliced_text = text[5:10]  # '0.0 3'
value = float(sliced_text)  # ValueError
```

解決策をあれこれ探してみましたが、直接的な方法にはたどりつけませんでした。

どこかに便利なライブラリがありそうではありますが、今回は「全角を2文字として文字列をスライスする」関数を自作することにしました。

**※追記**：コメントにて本記事に関するスマートなコードを頂きましたので、併せてご覧ください。

なお半角全角の区別の方法について、こちらのページを参考にさせていただきました。

[Pythonで半角1文字、全角2文字として文字数（幅）カウント](https://note.nkmk.me/python-unicodedata-east-asian-width-count/)

[unicodedata](https://docs.python.org/ja/3.10/library/unicodedata.html) という標準ライブラリの east_asian_width() という関数で、文字（一部の文字を除く）の半角全角を判定します。

## 全角を2文字として文字列をスライスする関数
スライスしたい文字列と2本のスライスのインデクスを引数にとり、スライスされた文字列を返します。

以降、単に「文字数」という場合は全角を2文字としてカウントした値を意味します。

通常のスライスと同様の仕様は以下の通りです。

* 1本目のスライスのインデクスは、省略されると0とみなされる
* 2本目のスライスのインデクスは、省略されると文字列の文字数とみなされる
* 1本目のスライスのインデクスが文字数を超えると空白を返す
* 2本目のスライスが1本目のスライスの前だと空白を返す

なお、こちらの機能は実装しません。

* インデクス指定による文字の取得
* ステップ
* 負のインデクス

負のインデクスが渡されたときは IndexError にしています。

以下コードです。

```py:全角を2文字として文字列をスライスする関数
import unicodedata

def slice_zenkaku(text: str, first_idx=0, second_idx=None) -> str:

    # 負のインデクスはエラー
    if first_idx < 0:
        raise IndexError('Index must be positive')
    if (second_idx is not None) and (second_idx < 0):
        raise IndexError('Index must be positive')

    # 対象文字列を「インデクスをkey、1文字をvalueとした辞書」に格納
    dict_char = {}
    idx = 0
    for c in text:
        dict_char[idx] = c
        if unicodedata.east_asian_width(c) in 'FWA':
            # 全角文字は2文字としてカウント
            idx += 2
        else:
            idx += 1

    # 2つめのインデクスの指定がなければ文字数+1
    if second_idx is None:
        second_idx = max(dict_char.keys()) + 1

    # 結果の文字列を取得
    sliced_text = ''
    for idx, char in dict_char.items():
        if idx < first_idx:
            continue
        elif first_idx <= idx < second_idx:
            sliced_text += char
        else:
            break
    return sliced_text
```

## 関数の挙動
「aあbい」という文字列を使って、関数 slice_zenkaku の挙動を確認します。
![aあbい.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1409923/bf42b128-9721-b52d-016c-ba40003e6efe.png)


```py:確認用コード
word = 'aあbい'
result = slice_zenkaku(word)
print(f'{word}[:] -> {result}')

for i in range(7):
    for j in range(7):
        if i < j:
            result = slice_zenkaku(word, i, j)
            print(f'{word}[{i}:{j}] -> {result}')
```
```:結果
aあbい[:] -> aあbい
aあbい[0:1] -> a
aあbい[0:2] -> aあ
aあbい[0:3] -> aあ
aあbい[0:4] -> aあb
aあbい[0:5] -> aあbい
aあbい[0:6] -> aあbい
aあbい[1:2] -> あ
aあbい[1:3] -> あ
aあbい[1:4] -> あb
aあbい[1:5] -> あbい
aあbい[1:6] -> あbい
aあbい[2:3] ->
aあbい[2:4] -> b
aあbい[2:5] -> bい
aあbい[2:6] -> bい
aあbい[3:4] -> b
aあbい[3:5] -> bい
aあbい[3:6] -> bい
aあbい[4:5] -> い
aあbい[4:6] -> い
aあbい[5:6] ->
```
違和感なくスライスできたイメージです。

全角文字の真ん中にスライスが入るときはアタマ側で判定されます。

## さいごに
全角を2文字分として文字列をスライスする関数を定義しました。

固定長のテキストファイルに全角を入れてくれるなというのが本音ですが、日本人の宿命として受け入れました。

なお特殊な文字についてはテストをしていないので、文字によっては想定通りの結果とならない場合があるかもしれません。