"""Reglas v1 (sin modelos) de mercado y de cantidad con unidad sobre el objeto (minúsculas, sin acentos). RE2 (DuckDB). Se usan igual en a3_mercados.py (SQL) y en las pruebas (pruebas/test_reglas.py, con re de Python).""" # ---- Mercados: CPV (prefijo/rango de 8 dígitos) o texto del objeto, con exclusiones de texto ------------------------- MERCADOS = { 'carburantes': dict( cpv="(cpv BETWEEN '09130000' AND '09134999')", txt=r"(gasoleo|gasoil|gas-oil|gasolina|carburant|combustible)", excl=r"(biomasa|pellet|astilla|estella|mantenim|manteniment|reparaci|inspecci|analisi|analitic|calibraci|tanque|" r"surtidor|desbrozadora|motosierra|sopladora|cortacesped|generador|caldera de|vehiculos? (de )?gasolina|" r"motor(es)? (de )?gasolina|tarjeta|targeta|nuclear|pila de combustible|hidrogeno|filtro|filtre|aceite|\boli\b|" r"anticongelant|boquerel|alquiler|lloguer|dietes|dietas|viatge|viaje|desp\.? ?avan)"), 'electricidad': dict( cpv="(cpv IN ('09310000','65300000','65310000') AND NOT (regexp_matches(o, 'gas natural|gasoleo|gasoil|propano|combustible') AND NOT regexp_matches(o, 'electric')))", txt=r"(suministro (de )?(la )?(energia )?electric|energia electrica|subministrament (d.)?(energia )?electric|" r"energia electrica|electricidad|subministrament electric)", excl=r"(instalaci|mantenim|manteniment|reparaci|\bobras?\b|fotovolt|autoconsum|recarga|vehicul|bicicleta|" r"patinete|material|herramient|grupo electrogeno|cuadro|baja tension.*(proyecto|redacci)|redacci|asistencia tecnica|auditor|" r"certificad|boletin|legalizaci|contador|comptador)"), 'medicamentos': dict( cpv="(cpv BETWEEN '33600000' AND '33699999')", txt=r"(\bmedicament|\bfarmacos?\b|vacunas?\b)", excl=r"(nevera|frigor|armario|carro|software|aplicaci|transporte|custodia|almacenaje|gestion|destrucci|" r"residuos|dispensador|sistema automatizado|formaci|curso|estudio|ensayo clinico|filtro|jeringa|camara)"), 'gas_natural': dict( cpv="(cpv BETWEEN '09120000' AND '09123999')", txt=r"(suministro (de )?gas natural|subministrament (de )?gas natural|gas natural canalizado|\bgas natural\b)", excl=r"(instalaci|mantenim|manteniment|reparaci|caldera|vehicul|acometida|\bobras?\b|redacci|proyecto|" r"inspecci|revisi|calefaccion y|escomesa|comptador|contador|muntatge|\blinia\b|distribuci|viveres)"), 'papel': dict( cpv="(cpv BETWEEN '30197600' AND '30197699')", txt=r"(\bpapel (de )?(impresi|fotocop|reprograf|oficina|blanco|reciclado|din)|\bpapel a ?[34]\b|\ba ?4\b.*\bpapel|" r"\bpaper (d.)?(impressi|fotocop|reciclat|oficina)|\bresmas?\b|\bfolios\b)", excl=r"(higienic|secamanos|toalla|camilla|pintado|papeleras?|contenedor|recogida|destrucci|reciclaje de|" r"impresion de|imprenta|edicion|libros?\b|revista|cartel|sobres impresos|filtro|filtre|directori|sardina|taller|" r"acreditaci|vidrio)"), 'vehiculos': dict( cpv="(cpv BETWEEN '34110000' AND '34119999' OR cpv BETWEEN '34130000' AND '34136999')", txt=r"((adquisici|suministro|compra|subministrament|adquisicio)[a-z ]{0,25}\b(vehiculos?|turismos?|furgonetas?|" r"todoterrenos?|furgon(es)?|vehicles?)\b)", excl=r"(alquiler|arrendamiento|renting|lloguer|arrendament|reparaci|mantenim|manteniment|seguros?\b|\bitv\b|lavado|" r"neumatic|combustible|recambio|carburant|rotulaci|equipamiento de|carrozado|transformaci|electrico de juguete|" r"bateria|baterias|piezas|material|repuesto|equipament|extintor|recarga|remolque|alternador|panel|fungible|redacci|" r"gasoleo|gasoil|asistencia tecnica|subvenci)"), } # ---- Cantidad con unidad (primer acierto en el objeto) ----------------------------------------------------------------- NUM = r"(\d{1,3}(?:[.\s]\d{3})+(?:,\d+)?|\d+(?:[.,]\d+)?)" FIN = r"(?:[^a-z0-9]|$)" UNIDADES = [ # clase, regex de unidad (tras el número) ('litros', r"(?:litros?|litres?|ltrs?\.?|lts?\.?|l\.?)"), ('energia', r"(?:[kmg]w\.?h|[kmg]wh|kilovatios? ?hora|megavatios? ?hora)"), ('m3', r"(?:m3|m³|nm3|metros? cubicos?)"), ('masa', r"(?:kg|kgs|kilos?|kilogramos?|toneladas?|tn|tm)"), ('envases', r"(?:unidades|unitats|uds?\.?|u\.|envases|cajas|caixes|viales|vials|ampollas|comprimidos|dosis|blisters?|" r"paquetes|paquets|resmas?|cajas)"), ('vehiculos', r"(?:vehiculos?|vehicles?|turismos?|furgonetas?|todoterrenos?|furgon(?:es)?|camiones?)"), ] NUM_PALABRA = r"(un|una|dos|tres|cuatro|cinco|seis|siete|ocho|nueve|diez|doce|quince|veinte)" PRECIO_UNITARIO = r"((eur|€|euros?|cent(imos)?|cts?)\.? ?/ ?(l|litro|kwh|mwh|m3)\b|(precio|preu|import) (unitario |unitari )?(por|per|del|de) (litro|kwh|mwh)|\bc?€/l\b|\beur/l\b)" DESCUENTO = r"(descuento|descompte|bonificaci|rappel|baja (sobre|respecto)|rebaja|cents?\.? ?(de )?(descuento|euro)|cent(imos)? por litro|precio (del )?surtidor|pvp|precio de venta al publico)" import re as _re, unicodedata as _ud _RX = [(c, _re.compile(NUM + r"\s*" + u + FIN)) for c, u in UNIDADES] _RX_VEH_PAL = _re.compile(r"\b" + NUM_PALABRA + r"\s+(?:" + dict(UNIDADES)['vehiculos'] + r")" + FIN) _PAL = dict(un=1, una=1, dos=2, tres=3, cuatro=4, cinco=5, seis=6, siete=7, ocho=8, nueve=9, diez=10, doce=12, quince=15, veinte=20) _RX_PU = _re.compile(PRECIO_UNITARIO) def norm(t): t = (t or '').lower() return ''.join(ch for ch in _ud.normalize('NFKD', t) if not _ud.combining(ch)) def numero(s): """'33.000' -> 33000; '10,53' -> 10.53; '1.000,5' -> 1000.5; '33 000' -> 33000; '1.5' -> 1.5.""" s = s.strip() if _re.fullmatch(r"\d{1,3}(?:[.\s]\d{3})+(?:,\d+)?", s): return float(_re.sub(r"[.\s]", "", s).replace(',', '.')) return float(s.replace(',', '.')) def cantidades(objeto): """Todas las cantidades con unidad del objeto: lista de (clase, valor, literal). Sin modelos.""" o = norm(objeto); out = [] for c, rx in _RX: for m in rx.finditer(o): lit = m.group(0).rstrip(' .,;:)(-/') pre = o[max(0, m.start()-1):m.start()] if pre and (pre.isalnum() or pre in '.,'): # el número debe empezar en límite (no «b7», «e10», «a4») continue try: v = numero(m.group(1)) except ValueError: continue if c == 'litros' and _re.search(r"\d\s*l\.?$", lit) and v < 1: # «0,5 l» casi nunca es una compra continue out.append((c, v, lit)) for m in _RX_VEH_PAL.finditer(o): out.append(('vehiculos', float(_PAL[m.group(1)]), m.group(0).strip())) return out def precio_unitario_literal(objeto): return bool(_RX_PU.search(norm(objeto)))