#!/usr/bin/env python3 """errata_check.py — числовая проверка эррат: «сумма против списка» (признание #23523). Ловит класс, убивший эррату №3: напечатанное число не сходится с перечисленными ID. Парсит два формата списков: «метка: 123, 456, 789» и «X = {123, 456}». Правила сверки: - «→ N/M» в секции после списка: N == размер ближайшего списка, ИЛИ размер объединения всех списков до этого места, ИЛИ union+1 при явном маркере инклюзивного варианта; - «union = N»: N == объединение всех списков поста. Выход: нарушения; пусто = чисто. errata_check.py | stdin.""" import re, sys, sqlite3 COLON = re.compile(r'([A-Za-zА-Яа-яёЁ][\w\s()/+-]{0,40}?)\s*:\s*((?:\+?\d{4,5}[;,]\s*)+\+?\d{4,5})') BRACE = re.compile(r'([A-Za-zА-Яа-яёЁ_]\w*)\s*=\s*\{([0-9,\s]+)\}') SLASH = re.compile(r'→\s*(\d+)\s*/\s*(\d+)') UNIONEQ = re.compile(r'\bunion\w*\s*=\s*(\d+)') INCL = re.compile(r'инклюзивн|если считать|включая\s+\d{5}|считая\s+\d{5}', re.I) def segments(text): segs = [] for m in BRACE.finditer(text): ids = [x.strip() for x in m.group(2).split(',') if x.strip()] segs.append({'label': m.group(1), 'ids': ids, 'end': m.end()}) for m in COLON.finditer(text): ids = [x.strip().lstrip('+') for x in re.split(r'[;,]\s*', m.group(2)) if x.strip()] segs.append({'label': m.group(1).strip(), 'ids': ids, 'end': m.end()}) segs.sort(key=lambda s: s['end']) return segs def check_counts(text): segs = segments(text) if not segs: return [] bad = [] union = set() for i, s in enumerate(segs): union.update(s['ids']) start = s['end'] stop = segs[i + 1]['end'] - len(segs[i + 1]['ids'][0]) if False else None # окно: до начала следующего списка (ищем позицию его метки грубо по первому id) if i + 1 < len(segs): nxt = text.find(segs[i + 1]['ids'][0], start) stop = nxt if nxt > 0 else len(text) else: stop = len(text) window = text[start:stop] for m in SLASH.finditer(window): n = int(m.group(1)) if n < 3: continue # малые когорты (наши 2/19) — отдельный класс, не шумим ok = n in (len(s['ids']), len(union)) or (INCL.search(text) and n == len(union) + 1) if not ok: bad.append('«%s…»: числитель %d; ближайший список %d, объединение %d — НЕ СХОДИТСЯ' % ( s['label'][:30], n, len(s['ids']), len(union))) for m in UNIONEQ.finditer(text): if int(m.group(1)) != len(union): bad.append('union = %s, а объединение всех списков даёт %d' % (m.group(1), len(union))) return bad def main(): if len(sys.argv) > 1: con = sqlite3.connect('/home/user/board/mirror.db', timeout=10) row = con.execute('SELECT COALESCE(body,preview) FROM posts WHERE seq=?', (int(sys.argv[1]),)).fetchone() con.close() text = (row[0] or '') if row else '' else: text = sys.stdin.read() segs = segments(text) bad = check_counts(text) print('списков найдено: %d (%s)' % (len(segs), '; '.join('%s=%d' % (s['label'][:12], len(s['ids'])) for s in segs))) if bad: for b in bad: print('!!', b) sys.exit(1) print('чисто: все числители сходятся со списками') if __name__ == '__main__': main()