fix(normalizer): правило тысяч только при скобках на всю строку, спека и README (AURORA-1405, #35)
I-1: «(4,200) руб», «1,234(5,678)» и т. п. разбираются как прежде; I-2/I-3: спека и README; M-1..M-3: docstring, комментарий регулярки, тесты крайних случаев. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1f308ddfe3
commit
9dd568ce44
@ -169,7 +169,7 @@ ack (с `attempt` повторного запроса); файл в ИС вто
|
|||||||
`string_value` — текст ячейки, только когда сумма не разобрана; `is_recognized = confidence == 100 и разбор не
|
`string_value` — текст ячейки, только когда сумма не разобрана; `is_recognized = confidence == 100 и разбор не
|
||||||
отбрасывал символов`.
|
отбрасывал символов`.
|
||||||
- **`Money` (решение №30).** Контракт допускает не больше двух знаков после запятой. Сумма с тремя и более знаками
|
- **`Money` (решение №30).** Контракт допускает не больше двух знаков после запятой. Сумма с тремя и более знаками
|
||||||
(`1,234`, `0,125`: пробел тысяч, распознанный как запятая) не принимается: `amount = null`, `string_value` — текст
|
(`0,125`, `12,3456`; запятая-разделитель тысяч `4,200`/`1,234` — это 4200/1234, AURORA-1405, #35) не принимается: `amount = null`, `string_value` — текст
|
||||||
ячейки, `is_recognized = false` (таблица получает `needs_check`, остальные ячейки и документ целы). Сумма вида
|
ячейки, `is_recognized = false` (таблица получает `needs_check`, остальные ячейки и документ целы). Сумма вида
|
||||||
`1.234,56` (несколько точек) разбирается как `123456` — все точки считаются разделителями тысяч (дословный перенос) —
|
`1.234,56` (несколько точек) разбирается как `123456` — все точки считаются разделителями тысяч (дословный перенос) —
|
||||||
с `is_recognized = false`.
|
с `is_recognized = false`.
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
"""Суммы из OCR-ячеек (§11.1 п. 7). Перенос `_parse_amount`, `_is_blank` и регулярок старого ядра с одним отличием —
|
"""Суммы из OCR-ячеек (§11.1 п. 7). Перенос `_parse_amount`, `_is_blank` и регулярок старого ядра с одним отличием —
|
||||||
порядком вычисления `verified` (решение №13 design.md, см. комментарий в `parse_amount`).
|
порядком вычисления `verified` (решение №13 design.md, см. комментарий в `parse_amount`).
|
||||||
|
|
||||||
Что делает разбор: если текст целиком (после strip, без пробелов, с учётом скобок — берётся содержимое первой пары — и
|
Что делает разбор: пробелы долой; если текст целиком (а при скобках на всю строку — их содержимое) имеет вид
|
||||||
знака) имеет вид `[+-]?[1-9]\\d{0,2}(,\\d{3})+`, запятые — разделители тысяч и удаляются (`4,200` → `4200`,
|
`[+-]?[1-9]\\d{0,2}(,\\d{3})+`, запятые — разделители тысяч и удаляются (`4,200` → `4200`, `(4,200)` → `-4200`,
|
||||||
`(4,200)` → `-4200`, `1,234,567` → `1234567`); это не отбрасывание символов, `verified` не страдает. Правило тысяч —
|
`1,234,567` → `1234567`); это не отбрасывание символов, `verified` не страдает. Правило тысяч — решение владельца
|
||||||
решение владельца 15.10.2026 (AURORA-1405, scan-gateway#35), см. docs/context/05-domain-rules.md §11.1 п. 7.
|
15.10.2026 (AURORA-1405, scan-gateway#35), см. docs/context/05-domain-rules.md §11.1 п. 7. Затем запятая → точка;
|
||||||
Затем запятая → точка, пробелы долой; если есть круглые скобки — берётся содержимое первой пары, и сумма
|
если есть круглые скобки — берётся содержимое первой пары, и сумма
|
||||||
отрицательная; всё, кроме цифр, `+`, `-` и `.`, отбрасывается; если точек больше одной, удаляются ВСЕ точки
|
отрицательная; всё, кроме цифр, `+`, `-` и `.`, отбрасывается; если точек больше одной, удаляются ВСЕ точки
|
||||||
(`1.234,56` → `123456`, а не `1234.56`: последняя точка тоже теряется, такая сумма всегда `verified = False`).
|
(`1.234,56` → `123456`, а не `1234.56`: последняя точка тоже теряется, такая сумма всегда `verified = False`).
|
||||||
`verified` — разбор не отбрасывал символов (скобки в счёт не идут). Побочный эффект №13: скобки без содержимого
|
`verified` — разбор не отбрасывал символов (скобки в счёт не идут). Побочный эффект №13: скобки без содержимого
|
||||||
@ -23,7 +23,8 @@ __all__ = ["ParsedAmount", "is_blank", "parse_amount"]
|
|||||||
_BLANK_CHARS_RE = re.compile(r"[\s\-–—]")
|
_BLANK_CHARS_RE = re.compile(r"[\s\-–—]")
|
||||||
_ALLOWED_RE = re.compile(r"[^\d+\-.]")
|
_ALLOWED_RE = re.compile(r"[^\d+\-.]")
|
||||||
_BRACKET_RE = re.compile(r"\(([^()]*)\)")
|
_BRACKET_RE = re.compile(r"\(([^()]*)\)")
|
||||||
# запятая-разделитель тысяч (AURORA-1405, scan-gateway#35): «4,200», «-1,234,567»; проверяется на всей строке
|
# запятая-разделитель тысяч (AURORA-1405, scan-gateway#35): «4,200», «-1,234,567»; только если строка целиком
|
||||||
|
# (или содержимое пары скобок, занимающей всю строку) совпала; «(4,200) руб», «1,234(5,678)» — прежний разбор
|
||||||
_THOUSANDS_RE = re.compile(r"[+-]?[1-9]\d{0,2}(?:,\d{3})+")
|
_THOUSANDS_RE = re.compile(r"[+-]?[1-9]\d{0,2}(?:,\d{3})+")
|
||||||
|
|
||||||
|
|
||||||
@ -36,9 +37,9 @@ class ParsedAmount:
|
|||||||
def parse_amount(raw: str) -> ParsedAmount:
|
def parse_amount(raw: str) -> ParsedAmount:
|
||||||
"""Парсит сумму из распознанного текста OCR-ячейки.
|
"""Парсит сумму из распознанного текста OCR-ячейки.
|
||||||
|
|
||||||
Если содержимое (без пробелов, с учётом скобок) целиком вида `[+-]?[1-9]\\d{0,2}(,\\d{3})+` — удаляет запятые
|
Убирает пробелы; если строка целиком (или пара скобок на всю строку — её содержимое) вида
|
||||||
(разделители тысяч: `4,200` → 4200) без потери verified. Затем заменяет запятую на точку, удаляет пробелы,
|
`[+-]?[1-9]\\d{0,2}(,\\d{3})+` — удаляет запятые (разделители тысяч: `4,200` → 4200) без потери verified.
|
||||||
извлекает содержимое круглых скобок
|
Затем заменяет запятую на точку, извлекает содержимое круглых скобок
|
||||||
(означает отрицательное число), удаляет непотребные символы; если точек больше одной — считает их разделителями
|
(означает отрицательное число), удаляет непотребные символы; если точек больше одной — считает их разделителями
|
||||||
тысяч и удаляет все (десятичная часть при этом не сохраняется: `1.234,56` → 123456).
|
тысяч и удаляет все (десятичная часть при этом не сохраняется: `1.234,56` → 123456).
|
||||||
|
|
||||||
@ -47,7 +48,7 @@ def parse_amount(raw: str) -> ParsedAmount:
|
|||||||
"""
|
"""
|
||||||
s = raw.strip().replace(" ", "")
|
s = raw.strip().replace(" ", "")
|
||||||
inner = _BRACKET_RE.search(s)
|
inner = _BRACKET_RE.search(s)
|
||||||
if _THOUSANDS_RE.fullmatch(inner.group(1) if inner else s):
|
if _THOUSANDS_RE.fullmatch(inner.group(1) if inner and inner.group(0) == s else s):
|
||||||
s = s.replace(",", "")
|
s = s.replace(",", "")
|
||||||
s = s.replace(",", ".")
|
s = s.replace(",", ".")
|
||||||
stripped_raw = s
|
stripped_raw = s
|
||||||
|
|||||||
@ -40,6 +40,16 @@ from src.normalizer.ocr.amounts import ParsedAmount, is_blank, parse_amount
|
|||||||
("12,3456", Decimal("12.3456"), True),
|
("12,3456", Decimal("12.3456"), True),
|
||||||
("4,200,", Decimal("4200"), False), # хвостовая запятая: правило не применяется (две точки удалены, как раньше)
|
("4,200,", Decimal("4200"), False), # хвостовая запятая: правило не применяется (две точки удалены, как раньше)
|
||||||
("(4,200}", Decimal("4.200"), False), # нет пары скобок: правило не применяется, минус не ставится
|
("(4,200}", Decimal("4.200"), False), # нет пары скобок: правило не применяется, минус не ставится
|
||||||
|
# скобки не на всю строку — правило тысяч не применяется, прежний разбор (запятая → точка)
|
||||||
|
("(4,200) руб", Decimal("-4.200"), False),
|
||||||
|
("abc(4,200)", Decimal("-4.200"), False),
|
||||||
|
("(4,200)x", Decimal("-4.200"), False),
|
||||||
|
("(4,200)(1,000)", Decimal("-4.200"), False),
|
||||||
|
("1,234(5,678)", Decimal("-5.678"), False),
|
||||||
|
("((4,200))", Decimal("-4.200"), True),
|
||||||
|
("01,234", Decimal("1.234"), True), # ведущий ноль — не тысячи
|
||||||
|
("4,2000", Decimal("4.2000"), True),
|
||||||
|
("1, 234", Decimal("1234"), True), # пробелы убираются первыми
|
||||||
# №13: скобки без содержимого — не сумма, но и не «неразобранный текст»
|
# №13: скобки без содержимого — не сумма, но и не «неразобранный текст»
|
||||||
("()", None, True),
|
("()", None, True),
|
||||||
("( )", None, True),
|
("( )", None, True),
|
||||||
|
|||||||
@ -238,8 +238,8 @@ SHALL отражать состояние.
|
|||||||
имени, иначе `confidence` ячейки наименования.
|
имени, иначе `confidence` ячейки наименования.
|
||||||
- `values` — по одному на колонку документа (по позиции; если раскладка строки после повторной шапки даёт иное число колонок —
|
- `values` — по одному на колонку документа (по позиции; если раскладка строки после повторной шапки даёт иное число колонок —
|
||||||
недостающие значения как «ячейки нет», лишние отбрасываются, одно предупреждение на таблицу «повторная шапка изменила
|
недостающие значения как «ячейки нет», лишние отбрасываются, одно предупреждение на таблицу «повторная шапка изменила
|
||||||
число колонок значений», решение №21; таблица без колонок значений — предупреждение «нет колонок значений», №25): `amount` — `parse_amount` (запятая → точка, скобки → минус, пробелы и прочие символы
|
число колонок значений», решение №21; таблица без колонок значений — предупреждение «нет колонок значений», №25): `amount` — `parse_amount` (запятая-разделитель тысяч (`4,200`, `1,234` → 4200, 1234; AURORA-1405, #35) удаляется, запятая → точка, скобки → минус, пробелы и прочие символы
|
||||||
отбрасываются, несколько точек — разделители тысяч; прочерк/пусто → `null`; сумма с более чем двумя знаками после запятой (`1,234`) не помещается в `Money` контракта — `amount = null`,
|
отбрасываются, несколько точек — разделители тысяч; прочерк/пусто → `null`; сумма с более чем двумя знаками после запятой (`0,125`) не помещается в `Money` контракта — `amount = null`,
|
||||||
`string_value` — текст ячейки, `is_recognized = false`, решение №30); `string_value` — текст ячейки как
|
`string_value` — текст ячейки, `is_recognized = false`, решение №30); `string_value` — текст ячейки как
|
||||||
распознан (`\n`/`\t` → пробел, края обрезаны) **только если `amount = null`** и текст не прочерк (DBML: «как распознано,
|
распознан (`\n`/`\t` → пробел, края обрезаны) **только если `amount = null`** и текст не прочерк (DBML: «как распознано,
|
||||||
если не число»), иначе `null`; `confidence` — ячейки (значения вне 0–100 и NaN приводятся к диапазону, №30); `is_recognized = confidence == 100 и разбор не отбрасывал символов`;
|
если не число»), иначе `null`; `confidence` — ячейки (значения вне 0–100 и NaN приводятся к диапазону, №30); `is_recognized = confidence == 100 и разбор не отбрасывал символов`;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user