Download t2s/trnum.py from SevdanurGenc/Text-to-SQL: direct link, hf CLI and curl.
- Browser
- Download file 7.23 kB
-
https://huggingface.co/SevdanurGenc/Text-to-SQL/resolve/main/t2s/trnum.py
- Command line
-
hf download hf://SevdanurGenc/Text-to-SQL/t2s/trnum.py
-
curl -L -o trnum.py https://huggingface.co/SevdanurGenc/Text-to-SQL/resolve/main/t2s/trnum.py
7.23 kB
| """Türkçeye özgü metin, sayı, tarih ve birim normalizasyonu. | |
| Üç sayı ayrıştırma kipi desteklenir (ablasyon deneyi için): | |
| - "tr" : doğru Türkçe yorum -> '1.234,56' = 1234.56, '%4,5' = 4.5, '(1.200)' = -1200 | |
| - "en" : yaygın naif yaklaşım (İngilizce yerel ayar) -> '.' ondalık, ',' binlik kabul edilir; | |
| '1.234' = 1.234 (1000 kat hata), '4,5' = 45 (10 kat hata), '1.234,56' -> ayrıştırılamaz | |
| - "text" : hiç ayrıştırma yok; hücreler metin olarak saklanır (SQLite SUM('1.234,56') = 1.234 !) | |
| """ | |
| from __future__ import annotations | |
| import re | |
| import unicodedata | |
| from dataclasses import dataclass | |
| from datetime import date | |
| # ------------------------------------------------------------------ metin | |
| _TR_ASCII = str.maketrans("çğıöşüÇĞİÖŞÜâîûÂÎÛ", "cgiosuCGIOSUaiuAIU") | |
| def tr_lower(s: str) -> str: | |
| """Türkçe kurallarına uygun küçük harf: 'İ'->'i', 'I'->'ı'.""" | |
| if s is None: | |
| return s | |
| return str(s).replace("I", "ı").replace("İ", "i").lower() | |
| def tr_upper(s: str) -> str: | |
| return str(s).replace("i", "İ").replace("ı", "I").upper() | |
| def fold(s: str) -> str: | |
| """Karşılaştırma için: Türkçe küçük harf + aksan/şapka kaldırma + ASCII'ye katlama. | |
| 'İSTANBUL', 'Istanbul', 'istanbul', 'ıstanbul' -> 'istanbul'.""" | |
| if s is None: | |
| return "" | |
| s = tr_lower(str(s)).translate(_TR_ASCII) | |
| s = unicodedata.normalize("NFKD", s) | |
| s = "".join(ch for ch in s if not unicodedata.combining(ch)) | |
| return re.sub(r"\s+", " ", s).strip() | |
| def slugify(s: str, maxlen: int = 48) -> str: | |
| s = fold(s) | |
| s = s.replace("%", " yuzde ").replace("²", "2").replace("³", "3") | |
| s = re.sub(r"[^a-z0-9]+", "_", s).strip("_") | |
| return s[:maxlen].strip("_") or "sutun" | |
| def clean_cell(s) -> str: | |
| if s is None: | |
| return "" | |
| s = str(s).replace(" ", " ").replace("−", "-").replace("–", "-") | |
| return re.sub(r"\s+", " ", s).strip() | |
| # ------------------------------------------------------------------ sayılar | |
| NULL_TOKENS = {"", "-", "--", "—", "...", "…", "n/a", "na", "yok", "x", ".."} | |
| _RE_TR_THOUSANDS = re.compile(r"^\d{1,3}(\.\d{3})+(,\d+)?$") # 1.234 | 1.234,56 | |
| _RE_TR_PLAIN = re.compile(r"^\d+(,\d+)?$") # 1234 | 1234,56 | |
| _RE_EN_THOUSANDS = re.compile(r"^\d{1,3}(,\d{3})+(\.\d+)?$") # 1,234.56 | |
| _RE_EN_PLAIN = re.compile(r"^\d+\.\d+$") # 3.14 | |
| _CURRENCY = re.compile(r"(₺|\bTL\b|\bTRY\b|\bUSD\b|\$|€|\bEUR\b)", re.I) | |
| class ParsedNumber: | |
| value: float | None | |
| is_percent: bool = False | |
| ok: bool = True | |
| def parse_number(raw, mode: str = "tr") -> ParsedNumber: | |
| """Bir hücreyi sayıya çevirmeyi dener. ok=False ise hücre sayı değildir.""" | |
| s = clean_cell(raw) | |
| if fold(s) in NULL_TOKENS: | |
| return ParsedNumber(None, ok=True) | |
| if mode == "text": | |
| return ParsedNumber(None, ok=False) | |
| is_pct = "%" in s | |
| s = s.replace("%", "") | |
| s = _CURRENCY.sub("", s).strip() | |
| neg = False | |
| if s.startswith("(") and s.endswith(")"): | |
| neg, s = True, s[1:-1].strip() | |
| if s.startswith("-") or s.startswith("+"): | |
| neg, s = s.startswith("-"), s[1:].strip() | |
| s = s.replace(" ", "") | |
| if not s or not re.fullmatch(r"[\d.,]+", s): | |
| return ParsedNumber(None, ok=False) | |
| val = None | |
| if mode == "tr": | |
| if _RE_TR_THOUSANDS.match(s) or _RE_TR_PLAIN.match(s): | |
| val = float(s.replace(".", "").replace(",", ".")) | |
| elif _RE_EN_THOUSANDS.match(s): # belgede İngilizce biçim varsa yine de tanı | |
| val = float(s.replace(",", "")) | |
| elif _RE_EN_PLAIN.match(s): # '3.5' gibi tek nokta, 3 hane değil -> ondalık | |
| val = float(s) | |
| elif mode == "en": # naif: Python float() mantığı | |
| try: | |
| val = float(s.replace(",", "")) | |
| except ValueError: | |
| val = None | |
| if val is None: | |
| return ParsedNumber(None, ok=False) | |
| return ParsedNumber(-val if neg else val, is_percent=is_pct) | |
| def format_tr(x, dec: int | None = None) -> str: | |
| """Sonuçları kullanıcıya Türkçe biçimde gösterir.""" | |
| if x is None: | |
| return "—" | |
| if isinstance(x, bool): | |
| return str(x) | |
| if isinstance(x, int) or (isinstance(x, float) and x.is_integer() and dec is None): | |
| s = f"{int(x):,}" | |
| return s.replace(",", ".") | |
| if isinstance(x, float): | |
| d = 2 if dec is None else dec | |
| s = f"{x:,.{d}f}" | |
| return s.replace(",", "§").replace(".", ",").replace("§", ".") | |
| return str(x) | |
| # ------------------------------------------------------------------ tarihler | |
| AYLAR = {"ocak": 1, "subat": 2, "mart": 3, "nisan": 4, "mayis": 5, "haziran": 6, "temmuz": 7, | |
| "agustos": 8, "eylul": 9, "ekim": 10, "kasim": 11, "aralik": 12, | |
| "oca": 1, "sub": 2, "mar": 3, "nis": 4, "may": 5, "haz": 6, "tem": 7, "agu": 8, | |
| "eyl": 9, "eki": 10, "kas": 11, "ara": 12} | |
| _RE_DMY = re.compile(r"^(\d{1,2})[./-](\d{1,2})[./-](\d{4})$") | |
| _RE_YMD = re.compile(r"^(\d{4})-(\d{2})-(\d{2})$") | |
| _RE_MON_Y = re.compile(r"^([a-z]+)\s*(\d{4})$") | |
| _RE_Y_Q = re.compile(r"^(\d{4})\s*[/-]?\s*(q|ç|c)\s*([1-4])$") | |
| def parse_date(raw) -> str | None: | |
| """'31.12.2024' -> '2024-12-31', 'Ocak 2024' -> '2024-01', '2024/Ç1' -> '2024-Q1'.""" | |
| s = fold(clean_cell(raw)) | |
| if not s: | |
| return None | |
| m = _RE_DMY.match(s) | |
| if m: | |
| d, mo, y = map(int, m.groups()) | |
| try: | |
| return date(y, mo, d).isoformat() | |
| except ValueError: | |
| return None | |
| m = _RE_YMD.match(s) | |
| if m: | |
| return s | |
| m = _RE_MON_Y.match(s) | |
| if m and m.group(1) in AYLAR: | |
| return f"{m.group(2)}-{AYLAR[m.group(1)]:02d}" | |
| m = _RE_Y_Q.match(s) | |
| if m: | |
| return f"{m.group(1)}-Q{m.group(3)}" | |
| return None | |
| _RE_DATE_RANGE = re.compile(r"(\d{1,2}[./]\d{1,2}[./]?(\d{4})?)\s*[-–]\s*(\d{1,2}[./]\d{1,2}[./](\d{4}))") | |
| _RE_YEAR = re.compile(r"\b(19\d{2}|20\d{2})\b") | |
| # ------------------------------------------------------------------ birimler | |
| UNIT_PATTERNS = [ | |
| (r"\bmilyar\s*(tl|₺)\b", "milyar TL", 1e9), | |
| (r"\bmilyon\s*(tl|₺)\b", "milyon TL", 1e6), | |
| (r"\bbin\s*(tl|₺)\b", "bin TL", 1e3), | |
| (r"\b(tl|₺|türk lirası)\b", "TL", 1.0), | |
| (r"\bbin\s*mwh\b", "bin MWh", 1e3), | |
| (r"\bmwh\b", "MWh", 1.0), | |
| (r"\bgwh\b", "GWh", 1e3), | |
| (r"%|\byuzde\b", "%", 1.0), | |
| (r"\bkisi\b", "kişi", 1.0), | |
| (r"\bton\b", "ton", 1.0), | |
| (r"\bm2\b|m²", "m²", 1.0), | |
| (r"\bm3\b|m³", "m³", 1.0), | |
| ] | |
| def detect_unit(text: str) -> tuple[str | None, float]: | |
| """Başlık veya tablo adı içindeki parantezli birimi bulur. ('bin TL', 1000.0)""" | |
| if not text: | |
| return None, 1.0 | |
| parts = re.findall(r"\(([^()]*)\)", text) or [text] | |
| for part in parts: | |
| f = fold(part) | |
| for pat, name, mult in UNIT_PATTERNS: | |
| if re.search(pat, f): | |
| return name, mult | |
| return None, 1.0 | |
| def strip_units(text: str) -> str: | |
| """'Ücret (TL)' -> 'Ücret'; '(-)' gibi işaretleri de kaldırır.""" | |
| out = re.sub(r"\((?:[^()]*?)\)", " ", text) | |
| return re.sub(r"\s+", " ", out).strip() | |