libs.utils.textutil

libs/utils/textutil.py

  1"""
  2libs/utils/textutil.py
  3"""
  4
  5import os
  6import unicodedata
  7from math import ceil, floor
  8from typing import TYPE_CHECKING, Literal
  9
 10import libs.global_value as g
 11
 12if TYPE_CHECKING:
 13    from pathlib import Path
 14
 15
 16def len_count(text: str) -> int:
 17    """文字数をカウント(全角文字は2)
 18
 19    Args:
 20        text (str): 判定文字列
 21
 22    Returns:
 23        int: 文字数
 24    """
 25
 26    count = 0
 27    for c in text:
 28        if unicodedata.east_asian_width(c) in "FWA":
 29            count += 2
 30        else:
 31            count += 1
 32
 33    return count
 34
 35
 36def str_conv(text: str, kind: Literal["h2z", "z2h", "h2k", "k2h"]) -> str:
 37    """文字列変換
 38
 39    Args:
 40        text (str): 変換対象文字列
 41        kind (str): 変換種類
 42        - h2z: 半角文字を全角文字に変換(数字のみ)
 43        - z2h: 全角文字を半角文字に変換(数字のみ)
 44        - h2k: ひらがなをカタカナに変換
 45        - k2h: カタカナをひらがなに変換
 46
 47    Returns:
 48        str: 変換後の文字列
 49    """
 50
 51    zen = "".join(chr(0xff10 + i) for i in range(10))
 52    han = "".join(chr(0x30 + i) for i in range(10))
 53    hira = "".join(chr(0x3041 + i) for i in range(86))
 54    kana = "".join(chr(0x30a1 + i) for i in range(86))
 55
 56    match kind:
 57        case "h2z":  # 半角文字を全角文字に変換(数字のみ)
 58            trans_table = str.maketrans(han, zen)
 59        case "z2h":  # 全角文字を半角文字に変換(数字のみ)
 60            trans_table = str.maketrans(zen, han)
 61        case "h2k":  # ひらがなをカタカナに変換
 62            trans_table = str.maketrans(hira, kana)
 63        case "k2h":  # カタカナをひらがなに変換
 64            trans_table = str.maketrans(kana, hira)
 65        case _:
 66            return text
 67
 68    return text.translate(trans_table)
 69
 70
 71def count_padding(data):
 72    """プレイヤー名一覧の中の最も長い名前の文字数を返す
 73
 74    Args:
 75        data (list, dict): 対象プレイヤー名の一覧
 76
 77    Returns:
 78        int: 文字数
 79    """
 80
 81    name_list = []
 82
 83    if isinstance(data, list):
 84        name_list = data
 85
 86    if isinstance(data, dict):
 87        for i in data.keys():
 88            for name in [data[i][x]["name"] for x in ("東家", "南家", "西家", "北家")]:
 89                if name not in name_list:
 90                    name_list.append(name)
 91
 92    if name_list:
 93        return max(len_count(x) for x in name_list)
 94    return 0
 95
 96
 97def save_file_path(filename: str, delete: bool = False) -> "Path":
 98    """保存ファイルのフルパスを取得
 99
100    Args:
101        filename (str): デフォルトファイル名
102        delete (bool, optional): 生成済みファイルを削除. Defaults to False.
103
104    Returns:
105        Path: 保存ファイルパス
106    """
107
108    _, file_ext = os.path.splitext(filename)
109    file_name = f"{g.params["filename"]}{file_ext}" if g.params.get("filename") else f"{filename}"
110    file_path = g.cfg.setting.work_dir / file_name
111
112    if file_path.exists() and delete:
113        os.remove(file_path)
114
115    return file_path
116
117
118def split_balanced(data: list, target_size: int, tolerance: float = 0.15) -> list:
119    """リストデータを指定個数で分割
120
121    Args:
122        data (list): 対象データ
123        target_size (int): 分割サイズ
124        tolerance (float, optional): 個数誤差. Defaults to 0.15.
125
126    Returns:
127        list: 分割したリスト
128    """
129
130    # 分割サイズに0が指定されている場合は何もしない
131    if not target_size:
132        return data
133
134    n = len(data)
135    if n == 0:
136        return []
137
138    min_size = int(target_size * (1 - tolerance))
139    max_size = int(target_size * (1 + tolerance))
140
141    # 最小ブロック数の候補を計算
142    min_blocks = ceil(n / max_size)
143    max_blocks = floor(n / min_size)
144
145    # 許容範囲内でブロック数を決める(なるべく少ない)
146    for num_blocks in range(min_blocks, max_blocks + 1):
147        size = n / num_blocks
148        if min_size <= size <= max_size:
149            break
150    else:
151        # 条件を満たすブロック数がない場合は単純均等割り
152        num_blocks = ceil(n / target_size)
153
154    # 実際の分割処理
155    base_size = n // num_blocks
156    remainder = n % num_blocks
157
158    result: list = []
159    start = 0
160    for i in range(num_blocks):
161        end = start + base_size + (1 if i < remainder else 0)
162        result.append(data[start:end])
163        start = end
164
165    return result
166
167
168def split_text_blocks(text: str, limit: int = 2000) -> list[str]:
169    """指定文字数でテキストを行単位で分割してリストにする
170
171    Args:
172        text (str): 対象文字列
173        limit (int, optional): 分割文字数. Defaults to 2000.
174
175    Returns:
176        list[str]: 分割リスト
177    """
178
179    blocks = []
180    current_data = ""
181    buffer_data = ""
182    in_code = False
183    min_gap_after_code_start = 10
184    lines_count = 0
185
186    for _, line in enumerate(text.splitlines(keepends=True)):
187        stripped = line.strip()
188        buffer_data += line
189
190        # --- コードブロック開始/終了検出 ---
191        if stripped.startswith("```"):
192            in_code = not in_code
193            if not in_code:
194                current_data += buffer_data
195                buffer_data = ""
196            continue
197
198        lines_count += 1 if in_code else 0
199
200        # --- 文字数チェック ---
201        if len(current_data + buffer_data) > limit:
202            if lines_count > min_gap_after_code_start:
203                if in_code:
204                    blocks.append(current_data + buffer_data + "```\n")
205                    buffer_data = "```\n"
206                else:
207                    blocks.append(current_data + buffer_data)
208                    buffer_data = ""
209            else:
210                blocks.append(current_data)  # 先頭の改行は削除されてしまう
211            current_data = ""
212
213    return blocks
def len_count(text: str) -> int:
17def len_count(text: str) -> int:
18    """文字数をカウント(全角文字は2)
19
20    Args:
21        text (str): 判定文字列
22
23    Returns:
24        int: 文字数
25    """
26
27    count = 0
28    for c in text:
29        if unicodedata.east_asian_width(c) in "FWA":
30            count += 2
31        else:
32            count += 1
33
34    return count

文字数をカウント(全角文字は2)

Arguments:
  • text (str): 判定文字列
Returns:

int: 文字数

def str_conv(text: str, kind: Literal['h2z', 'z2h', 'h2k', 'k2h']) -> str:
37def str_conv(text: str, kind: Literal["h2z", "z2h", "h2k", "k2h"]) -> str:
38    """文字列変換
39
40    Args:
41        text (str): 変換対象文字列
42        kind (str): 変換種類
43        - h2z: 半角文字を全角文字に変換(数字のみ)
44        - z2h: 全角文字を半角文字に変換(数字のみ)
45        - h2k: ひらがなをカタカナに変換
46        - k2h: カタカナをひらがなに変換
47
48    Returns:
49        str: 変換後の文字列
50    """
51
52    zen = "".join(chr(0xff10 + i) for i in range(10))
53    han = "".join(chr(0x30 + i) for i in range(10))
54    hira = "".join(chr(0x3041 + i) for i in range(86))
55    kana = "".join(chr(0x30a1 + i) for i in range(86))
56
57    match kind:
58        case "h2z":  # 半角文字を全角文字に変換(数字のみ)
59            trans_table = str.maketrans(han, zen)
60        case "z2h":  # 全角文字を半角文字に変換(数字のみ)
61            trans_table = str.maketrans(zen, han)
62        case "h2k":  # ひらがなをカタカナに変換
63            trans_table = str.maketrans(hira, kana)
64        case "k2h":  # カタカナをひらがなに変換
65            trans_table = str.maketrans(kana, hira)
66        case _:
67            return text
68
69    return text.translate(trans_table)

文字列変換

Arguments:
  • text (str): 変換対象文字列
  • kind (str): 変換種類
  • - h2z: 半角文字を全角文字に変換(数字のみ)
  • - z2h: 全角文字を半角文字に変換(数字のみ)
  • - h2k: ひらがなをカタカナに変換
  • - k2h: カタカナをひらがなに変換
Returns:

str: 変換後の文字列

def count_padding(data):
72def count_padding(data):
73    """プレイヤー名一覧の中の最も長い名前の文字数を返す
74
75    Args:
76        data (list, dict): 対象プレイヤー名の一覧
77
78    Returns:
79        int: 文字数
80    """
81
82    name_list = []
83
84    if isinstance(data, list):
85        name_list = data
86
87    if isinstance(data, dict):
88        for i in data.keys():
89            for name in [data[i][x]["name"] for x in ("東家", "南家", "西家", "北家")]:
90                if name not in name_list:
91                    name_list.append(name)
92
93    if name_list:
94        return max(len_count(x) for x in name_list)
95    return 0

プレイヤー名一覧の中の最も長い名前の文字数を返す

Arguments:
  • data (list, dict): 対象プレイヤー名の一覧
Returns:

int: 文字数

def save_file_path(filename: str, delete: bool = False) -> pathlib.Path:
 98def save_file_path(filename: str, delete: bool = False) -> "Path":
 99    """保存ファイルのフルパスを取得
100
101    Args:
102        filename (str): デフォルトファイル名
103        delete (bool, optional): 生成済みファイルを削除. Defaults to False.
104
105    Returns:
106        Path: 保存ファイルパス
107    """
108
109    _, file_ext = os.path.splitext(filename)
110    file_name = f"{g.params["filename"]}{file_ext}" if g.params.get("filename") else f"{filename}"
111    file_path = g.cfg.setting.work_dir / file_name
112
113    if file_path.exists() and delete:
114        os.remove(file_path)
115
116    return file_path

保存ファイルのフルパスを取得

Arguments:
  • filename (str): デフォルトファイル名
  • delete (bool, optional): 生成済みファイルを削除. Defaults to False.
Returns:

Path: 保存ファイルパス

def split_balanced(data: list, target_size: int, tolerance: float = 0.15) -> list:
119def split_balanced(data: list, target_size: int, tolerance: float = 0.15) -> list:
120    """リストデータを指定個数で分割
121
122    Args:
123        data (list): 対象データ
124        target_size (int): 分割サイズ
125        tolerance (float, optional): 個数誤差. Defaults to 0.15.
126
127    Returns:
128        list: 分割したリスト
129    """
130
131    # 分割サイズに0が指定されている場合は何もしない
132    if not target_size:
133        return data
134
135    n = len(data)
136    if n == 0:
137        return []
138
139    min_size = int(target_size * (1 - tolerance))
140    max_size = int(target_size * (1 + tolerance))
141
142    # 最小ブロック数の候補を計算
143    min_blocks = ceil(n / max_size)
144    max_blocks = floor(n / min_size)
145
146    # 許容範囲内でブロック数を決める(なるべく少ない)
147    for num_blocks in range(min_blocks, max_blocks + 1):
148        size = n / num_blocks
149        if min_size <= size <= max_size:
150            break
151    else:
152        # 条件を満たすブロック数がない場合は単純均等割り
153        num_blocks = ceil(n / target_size)
154
155    # 実際の分割処理
156    base_size = n // num_blocks
157    remainder = n % num_blocks
158
159    result: list = []
160    start = 0
161    for i in range(num_blocks):
162        end = start + base_size + (1 if i < remainder else 0)
163        result.append(data[start:end])
164        start = end
165
166    return result

リストデータを指定個数で分割

Arguments:
  • data (list): 対象データ
  • target_size (int): 分割サイズ
  • tolerance (float, optional): 個数誤差. Defaults to 0.15.
Returns:

list: 分割したリスト

def split_text_blocks(text: str, limit: int = 2000) -> list[str]:
169def split_text_blocks(text: str, limit: int = 2000) -> list[str]:
170    """指定文字数でテキストを行単位で分割してリストにする
171
172    Args:
173        text (str): 対象文字列
174        limit (int, optional): 分割文字数. Defaults to 2000.
175
176    Returns:
177        list[str]: 分割リスト
178    """
179
180    blocks = []
181    current_data = ""
182    buffer_data = ""
183    in_code = False
184    min_gap_after_code_start = 10
185    lines_count = 0
186
187    for _, line in enumerate(text.splitlines(keepends=True)):
188        stripped = line.strip()
189        buffer_data += line
190
191        # --- コードブロック開始/終了検出 ---
192        if stripped.startswith("```"):
193            in_code = not in_code
194            if not in_code:
195                current_data += buffer_data
196                buffer_data = ""
197            continue
198
199        lines_count += 1 if in_code else 0
200
201        # --- 文字数チェック ---
202        if len(current_data + buffer_data) > limit:
203            if lines_count > min_gap_after_code_start:
204                if in_code:
205                    blocks.append(current_data + buffer_data + "```\n")
206                    buffer_data = "```\n"
207                else:
208                    blocks.append(current_data + buffer_data)
209                    buffer_data = ""
210            else:
211                blocks.append(current_data)  # 先頭の改行は削除されてしまう
212            current_data = ""
213
214    return blocks

指定文字数でテキストを行単位で分割してリストにする

Arguments:
  • text (str): 対象文字列
  • limit (int, optional): 分割文字数. Defaults to 2000.
Returns:

list[str]: 分割リスト