"""E8 · SEGUNDA IMPLEMENTACIÓN de las reglas (escrita aparte, a partir de DEFINICIONES_PREVIAS_E8.md §3; no importa reglas_e8 ni reglas_v2 de A1). Python puro. Misma interfaz pública que reglas_e8 para poder pasar las mismas pruebas: producto_carb, cantidad_carb, energia_elec, banda_eurostat, papel.""" import re, unicodedata def _n(t): return unicodedata.normalize('NFD', (t or '').lower()).encode('ascii', 'ignore').decode('ascii') if t else '' def _base(t): # quita acentos conservando el resto de caracteres no ASCII que no tienen descomposición (p. ej. «€», «—») out = [] for ch in unicodedata.normalize('NFKD', (t or '').lower()): if unicodedata.combining(ch): continue out.append(' ' if ch == '_' else ch) return ''.join(out) def _valor(txt): """Número escrito → float. Admite millares con punto o espacio, decimal con coma, millares ingleses con coma.""" t = txt.strip() partes_esp = t.split(' ') if len(partes_esp) > 1 and all(len(p) == 3 for p in partes_esp[1:]) and 1 <= len(partes_esp[0]) <= 3: return float(''.join(partes_esp)) if '.' in t: ent, _, dec = t.partition(',') grupos = ent.split('.') if len(grupos) > 1 and all(len(g) == 3 for g in grupos[1:]) and 1 <= len(grupos[0]) <= 3: return float(''.join(grupos) + ('.' + dec if dec else '')) return float(t.replace(',', '.')) if ',' in t: g = t.split(',') if len(g) >= 3 and all(len(x) == 3 for x in g[1:]): return float(''.join(g)) if len(g) == 2 and len(g[1]) == 3 and len(g[0]) <= 3: return float(g[0] + g[1]) if float(g[0] + '.' + g[1]) < 10 else float(g[0] + '.' + g[1]) return float(t.replace(',', '.')) return float(t) # ------------------------------------------------------------------------------------------- carburantes _NUMERO = r"\d{1,3}(?:[.\s]\d{3})+(?:,\d+)?|\d{1,3}(?:,\d{3}){2,}|\d+(?:[.,]\d+)?" _LITROS = re.compile(r"(?P" + _NUMERO + r")\s*(?Plitros|litro|litres|litre|ltrs\.?|ltr\.?|lts\.?|lt\.?|l\.?)(?=[^a-z0-9]|$)") _CAPAC = ('carrillo', 'deposito', 'diposit', 'bidon', 'petaca', 'garrafa', 'capacidad', 'maletero', 'congelador') def cantidad_carb(objeto): o = _base(objeto) vals = [] for m in _LITROS.finditer(o): ini = m.start('n') if ini > 0 and (o[ini - 1].isalnum() or o[ini - 1] in '.,'): continue try: v = _valor(m.group('n')) except ValueError: continue lit = m.group(0).rstrip(' .,;:)(-/') if re.search(r"\d\s*l\.?$", lit) and v < 1: continue antes = o[max(0, ini - 40):ini] cap = any(w in antes for w in _CAPAC) or bool(re.search(r"\blata", antes)) or bool(re.match(r"\s*/\s*100\s*km", o[ini + len(lit):])) if not cap: vals.append((v, lit)) if not vals: return None return vals[0][0], vals[0][1], len({v for v, _ in vals}) def producto_carb(objeto): o = _base(objeto) gasoleo = r"(?:gasoleo|gasoil|gas-oil|gas oil|gasoli(?![a-z]))" def letra(x): return re.search(gasoleo + r"\s*(?:tipo\s*|clase\s*)?[\"“'«]?" + x + r"(?![a-z0-9])", o) is not None tiene_b, tiene_c = letra('b'), letra('c') a_fuerte = re.search(gasoleo + r"\s*(?:tipo\s*|clase\s*)?[\"“'«]?a(?![a-z0-9])\s*(?:$|[,.;:)(\"”»/\-]|y(?![a-z])|e(?![a-z])|para(?![a-z])|de automocion|automocion|b7|\d)", o) is not None hay_gasolina = re.search(r"gasolina|sin plomo|(?\d{1,3}(?:[.\s]\d{3})+(?:,\d+)?|\d{1,3}(?:,\d{3})+(?![\d,])|\d+(?:[.,]\d+)?)\s*" r"(?Pgwh|mwh|kwh|kw\s?/\s?h|kw\s?h|kilovatios?\s?hora|megavatios?\s?hora|gigavatios?\s?hora)(?![a-z])") _UMBRALES = ('superior', 'inferior', 'mayor', 'menor', 'igual', 'hasta', 'mas de', 'menos de', '>', '<', 'limite', 'capacidad', 'almacenamiento', 'bateria', 'maximo', 'minimo', 'entre') def energia_elec(objeto): o = _base(objeto) lote = o.split(' — ', 1)[1] if ' — ' in o else '' if 'gas natural' in lote: return dict(motivo='lote_gas') if ('gas natural' in o or re.search(r"(? 0 and (o[i - 1].isalnum() or o[i - 1] in '.,'): continue if any(u in o[max(0, i - 45):i] for u in _UMBRALES): continue try: v = _valor(m.group('n')) except ValueError: continue f = {'g': 1e6, 'm': 1e3}.get(m.group('u')[0], 1.0) detras = o[m.end():m.end() + 20] anual = re.match(r"\s*(?:/\s*(?:ano|any|a)(?![a-z])|al ano|al any|anual|anuales|anuals|ano(?![a-z])|any(?![a-z])|por ano)", detras) is not None hall.append((v * f, anual, m.group(0))) if not hall: return dict(motivo='sin_cantidad') if len({round(h[0], 3) for h in hall}) > 1: return dict(motivo='varias_cantidades', n_distintas=len({round(h[0], 3) for h in hall})) return dict(motivo='ok', kwh=hall[0][0], anual=hall[0][1], literal=hall[0][2], n_distintas=1) def banda_eurostat(mwh): for lim, b in ((20, 'MWH_LT20'), (500, 'MWH20-499'), (2000, 'MWH500-1999'), (20000, 'MWH2000-19999'), (70000, 'MWH20000-69999'), (150000, 'MWH70000-149999')): if mwh < lim: return b return 'MWH_GE150000' # ------------------------------------------------------------------------------------------- papel _W_CAJA = ('cajas', 'caja', 'caixes', 'caixa', 'capses', 'capsa') _W_PAQ = ('paquetes', 'paquete', 'paquets', 'paquet', 'paq', 'paq.', 'pqt', 'pqt.', 'resmas', 'resma', 'packs', 'pack') _W_HOJA = ('folios', 'folio', 'folis', 'hojas', 'hoja', 'fulls', 'full', 'follas', 'folla') _W_UD = ('unidades', 'unidad', 'uds', 'uds.', 'ud', 'ud.', 'u.') _OK = set(_W_CAJA + _W_PAQ + _W_HOJA) | {'unidades', 'unidad', 'uds', 'ud', 'u', 'h', 'g', 'gr', 'grs', 'gramos', 'grams', 'grm', 'gsm', 'x', 'por', 'per', 'cada', 'paquetito', 'paquetitos', 'de', 'y', 'i', 'e', 'o', 'mm', 'cm', 'en', 'total', 'para', 'papel', 'paper', 'din', 'a', 'lote', 'meses', 'mes', 'dias', 'cajita', 'cajitas', 'pales', 'palet', 'palets', 'paq', 'pqt'} _EXCLUSION = re.compile( r"sobres?(?![a-z])|toner|cartucho|carpeta|archivador|boligraf|rotulador|lapic|grapa|clips?(?![a-z])|cinta|celo(?![a-z])|pegamento|tijera|" r"etiqueta|cartulina|(? 1 or (g and not g <= {75, 80}): return dict(motivo='gramaje_especial') gram = next(iter(g)) if g else 80 rec = re.search(r"recicl|recycl", o) is not None vir = re.search(r"fibra virgen|(?= 7 hall = [] for m in re.finditer(n_rx + r"(cajas?|caixes|caixa|capses|capsa|paquetes?|paquets?|paq\.?|pqt\.?|resmas?|packs?)(?![a-z])", o): if m.start(1) in tam or codigo(m.group(1)): continue hall.append((_valor(m.group(1)) * (hc if m.group(2).startswith(('caj', 'caix', 'caps')) else hp), m.group(0))) for m in re.finditer(n_rx + r"(?:unidades|unidad|uds?\.?|u\.)\s*(?:de\s+)?(cajas?|caixes|paquetes?|paquets?|resmas?)(?![a-z])", o): if codigo(m.group(1)): continue hall.append((_valor(m.group(1)) * (hc if m.group(2).startswith(('caj', 'caix')) else hp), m.group(0))) if not hall: for m in re.finditer(n_rx + r"(?:folios?|folis|hojas?|fulls?|follas?)(?![a-z])", o): if m.start(1) in tam or codigo(m.group(1)): continue if _valor(m.group(1)) >= 1000: hall.append((_valor(m.group(1)), m.group(0))) if not hall: uds = [m for m in re.finditer(n_rx + r"(?:unidades|unidad|uds?\.?|u\.)(?![a-z])", o) if m.start(1) not in tam and not codigo(m.group(1))] env = re.search(r"(paquete|paquet|resma|pq|caja|caixa)?\s*(?:de\s*)?(? 1: return dict(motivo='varias_cantidades') hojas = hall[0][0] if hojas <= 0: return dict(motivo='sin_cantidad') a4 = re.search(r"(?