</> Cómo funciona

Caza el infinitivo de una forma verbal (inventada o real, regular)

GET
/v1/cazar
★★☆{ }w1📅

Dada una forma verbal, caza el infinitivo o infinitivos REGULARES de los que puede ser forma —aunque el verbo no exista en el diccionario— con su análisis (modo, tiempo, persona). Método algorítmico: genera candidatos invirtiendo el paradigma y deshaciendo las mutaciones ortográficas, y verifica cada uno conjugándolo (el conjugador es el juez). Una forma puede corresponder a varios infinitivos: se devuelven todos y se marca 'ambiguo'. El análisis viene estructurado en dos caminos por vocal temática: A (-ar) y E/I (-er/-ir). A diferencia de /v1/analizar_verbo (que busca entre los verbos reales), este caza también lo inventado: la parte generativa de la lengua.

Cómo funciona (por dentro)

Mapa de funciones

flowchart TD
  _es_vocal["_es_vocal()"]
  _ajustar_raiz["_ajustar_raiz()"]
  _ajustar_terminacion["_ajustar_terminacion()"]
  _unir["_unir()"]
  todas_las_formas["todas_las_formas()"]
  _terminaciones_superficie["_terminaciones_superficie()"]
  _deshacer_mutacion["_deshacer_mutacion()"]
  generar_candidatos["generar_candidatos()"]
  cazar["cazar()"]
  fila["fila()"]
  fila_inf["fila_inf()"]
  _ajustar_terminacion --> _es_vocal
  _unir --> _ajustar_raiz
  _unir --> _ajustar_terminacion
  cazar --> generar_candidatos
  cazar --> todas_las_formas
  fila --> _unir
  generar_candidatos --> _deshacer_mutacion
  generar_candidatos --> _terminaciones_superficie
  todas_las_formas --> _unir
  todas_las_formas --> fila
  todas_las_formas --> fila_inf

El código

# -*- coding: utf-8 -*-
"""cazar — dada una FORMA verbal, ¿de qué infinitivo(s) regular(es) es forma?

Es el reverso del conjugador, y con una gracia: caza también verbos INVENTADOS
(sirve cualquier forma bien construida, exista el verbo o no). No hay base de
datos: es álgebra pura, así que aquí está TODO el algoritmo a la vista — no hay
tesoro que esconder, solo lógica.

Idea central: GENERAR y VERIFICAR.
  1) Generar candidatos: se le quita a la palabra cada terminación posible del
     paradigma y se deshace la mutación ortográfica hacia atrás. Sale un conjunto
     de infinitivos "sospechosos" (puede sobrar, nunca falta el verdadero).
  2) Verificar: cada sospechoso se CONJUGA entero y se comprueba si la palabra
     aparece en su paradigma. El conjugador es el juez. Si aparece, es válido; y
     de regalo sabemos en qué casilla (modo, tiempo, persona).

Propiedad "ningún fallo": como la generación solo tiene que SOBRAR y la
verificación es exacta, el infinitivo verdadero está SIEMPRE en el resultado, y
no se cuela ni un falso positivo. No es una apuesta: es la preimagen de una
función determinista.
"""
import re

VOCALES = set("aeiouáéíóú")
FUERTES = set("aeoáéó")          # vocales que forman hiato con la i (leído, caído)


def _es_vocal(c):
    return c in VOCALES


# ---------------------------------------------------------------------------
# EL ORÁCULO: conjugador regular con las reglas ORTOGRÁFICAS del español.
# Estas reglas conservan la FONÉTICA de la raíz al cambiar la terminación.
# ---------------------------------------------------------------------------
def _ajustar_raiz(raiz, terminacion, clase):
    """Muta la última consonante de la raíz para que el SONIDO no cambie.

    En -ar solo muta ante terminación que empieza por e/i; en -er/-ir solo ante
    terminación que empieza por o/a. (Por eso 'sacar' -> 'saqué' pero 'plingar'
    -> 'plingo', no 'plinjo': un -ar con terminación en o/a no muta.)
    """
    f = terminacion[0]
    frontal = f in ("e", "é", "i", "í")
    posterior = f in ("a", "á", "o", "ó")
    if clase == "ar":
        if frontal:
            if raiz.endswith("gu"): return raiz[:-2] + "gü"   # averiguar -> averigüé
            if raiz.endswith("c"):  return raiz[:-1] + "qu"   # sacar     -> saqué
            if raiz.endswith("g"):  return raiz[:-1] + "gu"   # pagar     -> pagué
            if raiz.endswith("z"):  return raiz[:-1] + "c"    # cazar     -> cacé
    else:  # er / ir
        if posterior:
            if raiz.endswith("gu"): return raiz[:-2] + "g"    # distinguir -> distingo
            if raiz.endswith("qu"): return raiz[:-2] + "c"    # delinquir  -> delinco
            if raiz.endswith("c"):  return raiz[:-1] + "z"    # vencer     -> venzo
            if raiz.endswith("g"):  return raiz[:-1] + "j"    # coger      -> cojo
    return raiz


def _ajustar_terminacion(raiz, terminacion, clase):
    """La 'i' átona entre vocales se vuelve 'y' (leyó, leyendo); tras ll/ñ se
    absorbe (bulló, tañendo)."""
    if clase == "ar":
        return terminacion
    if not re.match(r"^i[aeoáéó]", terminacion):
        return terminacion
    if raiz.endswith("ll") or raiz.endswith("ñ"):
        return terminacion[1:]                 # bull + ió -> bulló
    if raiz.endswith("gu") or raiz.endswith("qu"):
        return terminacion                     # la u de gu/qu es muda: distinguieron, no distinguyeron
    if raiz and _es_vocal(raiz[-1]):
        return "y" + terminacion[1:]           # le + ió -> leyó
    return terminacion


def _unir(raiz, terminacion, clase):
    """Pega raíz + terminación aplicando las dos reglas ortográficas."""
    r = _ajustar_raiz(raiz, terminacion, clase)
    t = _ajustar_terminacion(r, terminacion, clase)
    return r + t


PERSONAS = ["yo", "tú", "él/ella", "nosotros", "vosotros", "ellos/ellas"]

# Las terminaciones del paradigma regular, por conjugación.
TERMINACIONES = {
    "ar": {"presente": ["o", "as", "a", "amos", "áis", "an"],
           "imperfecto": ["aba", "abas", "aba", "ábamos", "abais", "aban"],
           "preterito": ["é", "aste", "ó", "amos", "asteis", "aron"],
           "presubj": ["e", "es", "e", "emos", "éis", "en"],
           "imsubjRa": ["ara", "aras", "ara", "áramos", "arais", "aran"],
           "imsubjSe": ["ase", "ases", "ase", "ásemos", "aseis", "asen"]},
    "er": {"presente": ["o", "es", "e", "emos", "éis", "en"],
           "imperfecto": ["ía", "ías", "ía", "íamos", "íais", "ían"],
           "preterito": ["í", "iste", "ió", "imos", "isteis", "ieron"],
           "presubj": ["a", "as", "a", "amos", "áis", "an"],
           "imsubjRa": ["iera", "ieras", "iera", "iéramos", "ierais", "ieran"],
           "imsubjSe": ["iese", "ieses", "iese", "iésemos", "ieseis", "iesen"]},
    "ir": {"presente": ["o", "es", "e", "imos", "ís", "en"],
           "imperfecto": ["ía", "ías", "ía", "íamos", "íais", "ían"],
           "preterito": ["í", "iste", "ió", "imos", "isteis", "ieron"],
           "presubj": ["a", "as", "a", "amos", "áis", "an"],
           "imsubjRa": ["iera", "ieras", "iera", "iéramos", "ierais", "ieran"],
           "imsubjSe": ["iese", "ieses", "iese", "iésemos", "ieseis", "iesen"]},
}
# Futuro y condicional se pegan al INFINITIVO entero (no a la raíz).
FUTURO = ["é", "ás", "á", "emos", "éis", "án"]
CONDIC = ["ía", "ías", "ía", "íamos", "íais", "ían"]


def todas_las_formas(infinitivo):
    """Conjuga el infinitivo entero y devuelve la lista plana de sus formas, cada
    una etiquetada con (modo, tiempo, persona). Es el paradigma completo que usa
    el cazador como 'verdad' contra la que verificar."""
    inf = infinitivo.strip().lower()
    clase = inf[-2:]
    raiz = inf[:-2]
    E = TERMINACIONES[clase]
    formas = []

    def fila(clave, modo, tiempo):
        for i, t in enumerate(E[clave]):
            formas.append((_unir(raiz, t, clase), modo, tiempo, PERSONAS[i]))

    def fila_inf(terms, tiempo):
        for i, t in enumerate(terms):
            formas.append((inf + t, "indicativo", tiempo, PERSONAS[i]))

    formas.append((_unir(raiz, "ando" if clase == "ar" else "iendo", clase), "no personal", "gerundio", "—"))
    fila("presente", "indicativo", "presente")
    fila("imperfecto", "indicativo", "pretérito imperfecto")
    fila("preterito", "indicativo", "pretérito perfecto simple")
    fila_inf(FUTURO, "futuro")
    fila_inf(CONDIC, "condicional")
    fila("presubj", "subjuntivo", "presente")
    fila("imsubjRa", "subjuntivo", "pretérito imperfecto")
    fila("imsubjSe", "subjuntivo", "pretérito imperfecto (-se)")
    return formas


# ---------------------------------------------------------------------------
# EL CAZADOR: de la forma al infinitivo, por generar-y-verificar.
# ---------------------------------------------------------------------------
CLASES = ["ar", "er", "ir"]


def _terminaciones_superficie(clase):
    """Todas las terminaciones que pueden aparecer en SUPERFICIE (incluidas las
    variantes con 'y' y las de la i absorbida). Generoso a propósito: puede
    sobrar; lo que sobra lo poda la verificación."""
    E = TERMINACIONES[clase]
    lst = [t for clave in E for t in E[clave]]
    lst.append("ando" if clase == "ar" else "iendo")
    if clase != "ar":
        for t in list(lst):
            if re.match(r"^i[aeoáéó]", t):
                lst.append("y" + t[1:])   # variante leyó / leyendo
                lst.append(t[1:])         # variante bulló / tañó
    return list(dict.fromkeys(lst))


def _deshacer_mutacion(raiz, terminacion, clase):
    """Inverso de _ajustar_raiz: dada la raíz de superficie, ¿de qué raíz(es)
    subyacentes pudo salir? (qu <- c, gü <- gu, gu <- g, c <- z; y en -er/-ir:
    z <- c, j <- g, g <- gu, c <- qu)."""
    f = terminacion[0]
    out = []
    frontal = f in ("e", "é", "i", "í")
    posterior = f in ("a", "á", "o", "ó")
    if clase == "ar" and frontal:
        if raiz.endswith("qu"): out.append(raiz[:-2] + "c")
        if raiz.endswith("gü"): out.append(raiz[:-2] + "gu")
        elif raiz.endswith("gu"): out.append(raiz[:-2] + "g")
        if raiz.endswith("c"):  out.append(raiz[:-1] + "z")
    if clase in ("er", "ir") and posterior:
        if raiz.endswith("z"): out.append(raiz[:-1] + "c")
        if raiz.endswith("j"): out.append(raiz[:-1] + "g")
        if raiz.endswith("g"): out.append(raiz[:-1] + "gu")
        if raiz.endswith("c"): out.append(raiz[:-1] + "qu")
    return out


def generar_candidatos(palabra):
    """Superconjunto de infinitivos que PODRÍAN producir la palabra. Para cada
    clase y cada terminación de superficie que encaje, se propone la raíz por
    identidad y las raíces de la de-mutación."""
    cands = set()
    for clase in CLASES:
        for t in _terminaciones_superficie(clase):
            if len(palabra) > len(t) and palabra.endswith(t):
                raiz = palabra[: -len(t)]
                cands.add(raiz + clase)
                for r in _deshacer_mutacion(raiz, t, clase):
                    cands.add(r + clase)
    return cands


def cazar(palabra):
    """Caza el/los infinitivo(s) del que la palabra es forma, con su análisis.

    Recorre los DOS CAMINOS de vocal temática (A = -ar, E/I = -er/-ir): dentro de
    E/I, -er y -ir solo divergen en cuatro puntos; en el resto son el mismo verbo,
    así que se verifican las dos raíces. Nada se poda: se devuelven TODOS los
    infinitivos que de verdad la producen (una forma puede tener varios).
    """
    w = palabra.strip().lower()
    resultado = []
    for inf in generar_candidatos(w):
        if len(inf) < 3:
            continue
        # VERIFICACIÓN: conjugar el candidato y ver si la palabra está.
        casillas = [
            {"modo": m, "tiempo": ti, "persona": p}
            for (forma, m, ti, p) in todas_las_formas(inf) if forma == w
        ]
        if casillas:
            resultado.append({"infinitivo": inf, "conjugacion": inf[-2:], "casillas": casillas})
    resultado.sort(key=lambda a: a["infinitivo"])
    infinitivos = [a["infinitivo"] for a in resultado]
    return {"forma": w, "es_forma": bool(infinitivos),
            "ambiguo": len(infinitivos) > 1, "infinitivos": infinitivos, "analisis": resultado}

Pruébalo

Llámala con el código a la vista.

Ejemplos con el SDK

curl "https://api.wordkers.com/v1/cazar?forma=casa" \
  -H "X-API-Key: TU_CLAVE"

Cómo empezar con el SDK →

¿Quieres que cambie algo, o copiar la lógica para lo tuyo? Sin problema — escríbeme. Lo que ves es el algoritmo; los datos y las claves se quedan en casa.

Wordkers · Catálogo · Empezar · Tu cuenta