Metadata-Version: 2.4
Name: fastcorex
Version: 0.4.0
Summary: Extension en C para acelerar loops y estructuras de datos comunes
License: MIT
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: C
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: license
Dynamic: license-file
Dynamic: requires-python
Dynamic: summary

# fastcorex

Extensión en C para acelerar loops y estructuras de datos comunes en Python. No es un reemplazo de NumPy ni Pandas — es una capa delgada que elimina las partes tediosas y repetitivas (agrupar, deduplicar, aplanar, contar, filtrar, particionar, fusionar dicts, generar slugs, acceso anidado seguro, rellenar texto, ventanas deslizantes) que normalmente se reescriben a mano en cada proyecto.

## Instalación

```
pip install fastcorex
```

Requiere Python 3.9 o superior y un compilador de C (se compila al instalar, como cualquier extensión nativa).

## Uso rápido

```python
import fastcorex as fx

fx.fast_sum([1, 2, 3])                      # 6.0
fx.count_freq(["a", "b", "a"])              # {'a': 2, 'b': 1}
fx.unique([3, 1, 2, 1, 3])                  # [3, 1, 2]
fx.filter_gt([1, 5, 10, 3], 4.0)            # [5.0, 10.0]
fx.groupby(lista_de_dicts, "categoria")     # dict agrupado
fx.flatten([1, [2, [3, 4]], 5])             # [1, 2, 3, 4, 5]
fx.unique_by(lista_de_dicts, "id")          # FastList dedup por campo (encadenable)
fx.chunk([1, 2, 3, 4, 5], 2)                # [[1, 2], [3, 4], [5]]
fx.safe_get(d, "a.b.c", default=None)       # acceso anidado sin try/except
fx.clamp(15.0, 0.0, 10.0)                   # 10.0
fx.pick({"a": 1, "b": 2, "c": 3}, ["a"])    # {'a': 1}
fx.omit({"a": 1, "b": 2, "c": 3}, ["a"])    # {'b': 2, 'c': 3}
fx.deep_merge(config_base, config_local)    # dict fusionado recursivamente
fx.slugify("Título con Ñandú")              # "titulo-con-nandu"
fx.partition(numeros, lambda n: n > 0)      # (positivos, no_positivos)
fx.dedupe_consecutive([1, 1, 2, 2, 1])      # [1, 2, 1]
fx.flatten_dict({"a": {"b": 1}})            # {'a.b': 1}
fx.invert_dict({"a": 1, "b": 2})            # {1: 'a', 2: 'b'}
fx.pad("hi", 6, mode="center")              # "  hi  "
fx.clip_outliers([1, -50, 100], 0, 10)      # [1.0, 0.0, 10.0]
fx.rolling_window([1, 2, 3, 4], 2)          # [[1, 2], [2, 3], [3, 4]]
fx.filter_range(numeros, 5, 15)             # sin callback, más rápido que filter()
fx.partition_gt(numeros, 10)                # sin callback, más rápido que partition()
fx.ensure_list(5)                           # [5]
fx.first_or_default(lista, predicado)       # primer match o default

# Texto (0.4.0)
fx.word_count("hola mundo")                 # 2
fx.find_all("abcabc", "abc")                # [0, 3]
fx.truncate("un texto largo", 10)           # "un texto..."

# Números (0.4.0)
fx.round_to(3.14159, 2)                     # 3.14
fx.random_int_fast(1, 100, 5)               # [23, 87, 4, 56, 12] (no criptográfico)
fx.lerp(0, 100, 0.5)                        # 50.0

# Fechas (0.4.0)
fx.is_weekend(fecha)                        # True/False
fx.business_days_between(f1, f2)            # días hábiles entre dos fechas
fx.format_relative(fecha)                   # "hace 3 días", "en 2 horas"

# Geometría 2D (0.4.0)
fx.distance((0, 0), (3, 4))                 # 5.0
fx.midpoint((0, 0), (10, 10))               # (5.0, 5.0)
fx.point_in_rect((5, 5), (0, 0, 10, 10))    # True
fx.point_in_circle((1, 1), (0, 0), 5)       # True

# Estadística básica (0.4.0)
fx.mean(numeros)                            # promedio
fx.median(numeros)                          # mediana
fx.stdev(numeros)                           # desviación estándar
fx.percentile(numeros, 95)                  # percentil 95
fx.mode(numeros)                            # valor más frecuente
```

## Documentación de funciones

### Funciones de la versión 0.1.x

**fast_sum(lista)** — Suma todos los elementos numéricos de una lista.
```python
fx.fast_sum([1, 2, 3, 4.5])  # 10.5
```

**count_freq(lista)** — Cuenta cuántas veces aparece cada elemento. Reemplaza un loop de 4 líneas con `dict.get()` por una sola llamada.
```python
fx.count_freq(["a", "b", "a", "c", "b", "a"])  # {'a': 3, 'b': 2, 'c': 1}
```

**unique(lista)** — Elimina duplicados manteniendo el orden original. Reemplaza el patrón de `set()` + loop + `append`.
```python
fx.unique([3, 1, 2, 1, 3, 4])  # [3, 1, 2, 4]
```

**filter_gt(lista, umbral)** — Devuelve solo los elementos mayores al umbral dado.
```python
fx.filter_gt([1, 5, 10, 3, 8], 4.0)  # [5.0, 10.0, 8.0]
```

**groupby(lista_de_dicts, clave)** — Agrupa una lista de diccionarios según el valor de una clave. Devuelve un dict normal. Reemplaza el patrón de dict + `setdefault` manual.
```python
fx.groupby(ventas, "categoria")  # {'ropa': [...], 'comida': [...]}
```

**flatten(lista_anidada)** — Aplana listas anidadas de cualquier profundidad. Reemplaza una función recursiva escrita a mano. Ante una entrada con anidamiento extremo (más de ~10,000 niveles) lanza `RecursionError` de forma segura en vez de arriesgar un desbordamiento de pila.
```python
fx.flatten([1, [2, 3, [4, [5, 6]], 7], 8])  # [1, 2, 3, 4, 5, 6, 7, 8]
```

**unique_by(lista_de_dicts, clave)** — Deduplica diccionarios según el valor de un campo específico, no el objeto completo. Devuelve un `FastList` (ver sección de encadenamiento).
```python
fx.unique_by(ventas, "id")  # FastList sin ids repetidos
```

**chunk(lista, tamaño)** — Parte una lista en sublistas de tamaño fijo, sin solaparse; el último chunk puede quedar más corto. Reemplaza el slicing manual con `range(0, len(lista), tamaño)`.
```python
fx.chunk([1, 2, 3, 4, 5, 6, 7], 3)  # [[1, 2, 3], [4, 5, 6], [7]]
```

**safe_get(dict, "a.b.c", default=None)** — Acceso anidado seguro a diccionarios usando un path con puntos. Reemplaza el `try/except (KeyError, TypeError)` que normalmente envuelve un acceso encadenado. `default` puede pasarse posicional o como keyword.
```python
fx.safe_get({"a": {"b": {"c": 42}}}, "a.b.c")            # 42
fx.safe_get({"a": {"b": {}}}, "a.b.c", default="N/A")    # "N/A"
```
Un path vacío, o con un segmento vacío (`"a..b"`, `".a"`, `"a."`), lanza `ValueError` en vez de devolver silenciosamente el default — un path malformado suele ser un bug en quien llama, no un caso de "no encontrado".

**clamp(valor, minimo, maximo)** — Acota un número al rango `[minimo, maximo]`. Reemplaza `max(minimo, min(valor, maximo))` o un if/elif/else.
```python
fx.clamp(15.0, 0.0, 10.0)  # 10.0
fx.clamp(-5.0, 0.0, 10.0)  # 0.0
```

### Funciones de la versión 0.2.0

**pick(dict, claves)** — Devuelve un nuevo dict con solo las claves indicadas que existan en el original; las ausentes se ignoran sin error. Reemplaza `{k: d[k] for k in claves if k in d}`.
```python
fx.pick({"nombre": "Ana", "edad": 30, "email": "a@x.com"}, ["nombre", "email"])
# {'nombre': 'Ana', 'email': 'a@x.com'}
```

**omit(dict, claves)** — Lo inverso de `pick`: devuelve un nuevo dict sin las claves indicadas. Reemplaza `{k: v for k, v in d.items() if k not in claves}`.
```python
fx.omit({"nombre": "Ana", "password": "secreta", "edad": 30}, ["password"])
# {'nombre': 'Ana', 'edad': 30}
```

**deep_merge(base, override)** — Fusiona `override` sobre `base` recursivamente: cuando ambos tienen un dict en la misma clave, se fusionan sus contenidos en vez de que uno reemplace al otro; en cualquier otro caso, `override` gana. Ninguno de los dos argumentos originales se modifica. Ante un anidamiento extremo en cualquiera de los dos argumentos, lanza `RecursionError` de forma segura en vez de arriesgar un desbordamiento de pila.
```python
config_base = {"db": {"host": "localhost", "port": 5432}, "debug": False}
config_local = {"db": {"port": 5433}}
fx.deep_merge(config_base, config_local)
# {'db': {'host': 'localhost', 'port': 5433}, 'debug': False}
```

**slugify(texto)** — Normaliza un string a minúsculas, sin acentos (cubre á é í ó ú ü ñ ç y sus mayúsculas), con guiones en vez de espacios o símbolos, sin guiones duplicados ni al inicio/final.
```python
fx.slugify("Título de Sección: ¡Importante!")  # "titulo-de-seccion-importante"
```
**Atención con texto no latino:** el conjunto de acentos que transcribe es acotado (español/portugués/francés básico). Texto completamente en otro alfabeto (cirílico, chino, árabe, etc.) no tiene ninguna transliteración definida, y cada carácter termina colapsando a separador — el resultado final es una cadena **vacía**, no un error ni una excepción.
```python
fx.slugify("Привет мир")   # '' (cadena vacía, no un error)
fx.slugify("你好世界")      # '' (cadena vacía, no un error)
```
Si el texto de entrada puede no ser latino (por ejemplo, viene de un formulario de usuario sin restricción de idioma), conviene verificar el resultado antes de usarlo como identificador: `fx.slugify(texto) or "sin-titulo"`.

**partition(lista, predicado)** — Recorre la lista una sola vez y la separa en `(cumplen, no_cumplen)` según `predicado(item)`, en vez de hacer dos pasadas. Devuelve una tupla de dos `FastList`. **Nota de rendimiento:** ver la sección de benchmarks — para el caso de "separar por un umbral numérico", `partition_gt` es 4x-6x más rápida.
```python
pares, impares = fx.partition(range(10), lambda n: n % 2 == 0)
# ([0, 2, 4, 6, 8], [1, 3, 5, 7, 9])
```

**dedupe_consecutive(lista)** — Colapsa elementos repetidos que aparecen uno justo después del otro. A diferencia de `unique()`, no deduplica globalmente: `dedupe_consecutive([1, 2, 1])` deja `[1, 2, 1]` intacto, porque el segundo `1` no es consecutivo con el primero.
```python
fx.dedupe_consecutive([1, 1, 2, 2, 2, 1, 3, 3])  # [1, 2, 1, 3]
```

### Funciones nuevas (0.3.0)

**flatten_dict(dict, sep=".")** — Aplana un dict anidado a un solo nivel, generando claves tipo `"a.b.c"` para cada valor no-dict encontrado en profundidad. Es el inverso conceptual de `safe_get`: en vez de bajar por un path con puntos, genera todos los paths posibles de una vez. Los valores que son listas **no** se aplanan, se conservan tal cual. Ante un anidamiento extremo, lanza `RecursionError` de forma segura en vez de arriesgar un desbordamiento de pila.
```python
fx.flatten_dict({"usuario": {"nombre": "Ana", "direccion": {"ciudad": "Lima"}}})
# {'usuario.nombre': 'Ana', 'usuario.direccion.ciudad': 'Lima'}
fx.flatten_dict({"a": {"b": 1}}, sep="/")  # {'a/b': 1}
```
Útil para "achatar" una respuesta de API anidada antes de escribirla a una fila de CSV o de base de datos plana.

**invert_dict(dict)** — Devuelve un nuevo dict con claves y valores intercambiados. Si hay valores duplicados, el último gana (mismo comportamiento que reconstruirlo a mano con un loop). Los valores deben ser hasheables, igual que exige Python al usarlos como clave.
```python
fx.invert_dict({"rojo": "#FF0000", "verde": "#00FF00"})
# {'#FF0000': 'rojo', '#00FF00': 'verde'}
```
Reemplaza: `{v: k for k, v in d.items()}`.

**pad(texto, ancho, fill=" ", mode="right")** — Rellena un string a un ancho mínimo. `mode` puede ser `"right"` (rellena a la derecha, texto alineado a la izquierda), `"left"` (rellena a la izquierda, texto alineado a la derecha) o `"center"`. Si el texto ya mide `ancho` o más, se devuelve sin cambios.
```python
fx.pad("hi", 6)                      # "hi    "
fx.pad("hi", 6, mode="left")         # "    hi"
fx.pad("hi", 6, mode="center")       # "  hi  "
fx.pad("hi", 6, fill="*")            # "hi****"
```
Es equivalente a `str.ljust`/`str.rjust`/`str.center`, pero unificados bajo un solo nombre de parámetro (`mode`) y con validación explícita del carácter de relleno (debe ser exactamente uno) y del valor de `mode`, en vez de tener que recordar cuál de los tres métodos usar y qué pasa si `fill` mide más de un carácter (con los métodos nativos, silenciosamente solo se usa mal). Ver la sección de benchmarks para las dos comparaciones de rendimiento distintas que aplican aquí.

**clip_outliers(lista, minimo, maximo)** — Devuelve una nueva lista con cada número acotado al rango `[minimo, maximo]`. Es `clamp()` aplicado a una lista completa de una vez, en vez de un loop + `clamp()` por elemento.
```python
fx.clip_outliers([1.0, -50.0, 100.0, 5.0], 0.0, 10.0)  # [1.0, 0.0, 10.0, 5.0]
```
Útil para descartar valores atípicos de sensores, precios o mediciones antes de graficarlos o promediarlos.

**rolling_window(lista, tamaño)** — Genera una lista de sublistas, cada una una "ventana" de `tamaño` elementos consecutivos que se desliza de a uno. A diferencia de `chunk()` (que particiona sin solapamiento), `rolling_window` sí solapa: con tamaño 2, `[1,2,3,4]` da `[[1,2],[2,3],[3,4]]`, no `[[1,2],[3,4]]`. Si `tamaño` es mayor que la longitud de la lista, devuelve una lista vacía.
```python
fx.rolling_window([1, 2, 3, 4, 5], 3)  # [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
```
Pensado para promedios móviles, detección de tendencias, o comparar cada elemento contra su vecindario inmediato — ver el ejemplo de encadenamiento con `.map()` más abajo para un promedio móvil real.

### Especializadas de rendimiento (0.3.0)

Estas dos funciones resuelven el mismo problema que `partition()`/`.filter()` con una lambda de comparación numérica, pero **sin invocar ningún callback de Python** — ver la sección de benchmarks para el porqué y la magnitud real de la mejora (4x-7x según el caso).

**filter_range(lista, minimo, maximo, inclusive=True)** — Devuelve los elementos dentro de `[minimo, maximo]` (con `inclusive=True`, el valor por defecto) o `(minimo, maximo)` exclusivo en ambos extremos (`inclusive=False`). Generaliza `filter_gt` a un rango completo.
```python
fx.filter_range([1, 5, 10, 15, 20], 5, 15)                    # [5, 10, 15]
fx.filter_range([1, 5, 10, 15, 20], 5, 15, inclusive=False)   # [10]
```

**partition_gt(lista, umbral)** — Especialización de `partition()` para separar por un único umbral numérico: devuelve `(mayores, resto)`, igual que `partition(lista, lambda x: x > umbral)` pero sin el costo de invocar Python en cada elemento.
```python
fx.partition_gt([1, 5, 10, 15, 20], 10)  # ([15, 20], [1, 5, 10])
```

### Utilidades en Python puro (0.2.0)

Estas dos funciones se implementaron directamente en Python, no en C, porque su costo ya es mínimo en el intérprete y una extensión de C no traería ninguna ganancia medible.

**ensure_list(valor)** — Envuelve `valor` en una lista si no es ya una lista o tupla.
```python
fx.ensure_list(5)          # [5]
fx.ensure_list([1, 2, 3])  # [1, 2, 3]
```

**first_or_default(iterable, predicado=None, default=None)** — Devuelve el primer elemento que cumple `predicado`, o `default` si ninguno cumple, sin lanzar `StopIteration`. Funciona con cualquier iterable, incluidos generadores.
```python
fx.first_or_default([1, 2, 3, 4], lambda x: x > 2)  # 3
fx.first_or_default([], default="vacío")             # "vacío"
```

### Texto (0.4.0)

Utilidades sobre strings que no tienen que ver con listas/dicts: útiles procesando logs, archivos, entrada de usuario, o cualquier texto en general.

**word_count(texto)** — Cuenta palabras separadas por espacio en blanco (tab, salto de línea, y separadores Unicode menos comunes incluidos), sin construir la lista intermedia que produciría `len(texto.split())`.
```python
fx.word_count("El veloz murciélago hindú")  # 4
```

**find_all(texto, patron)** — Todas las posiciones (índices base 0) donde `patron` aparece en `texto`, incluyendo coincidencias solapadas (`find_all("aaaa", "aa")` da `[0, 1, 2]`, no solo `[0, 2]`).
```python
fx.find_all("abcabcabc", "abc")  # [0, 3, 6]
```

**truncate(texto, largo, suffix="...")** — Corta `texto` a lo sumo a `largo` caracteres (incluyendo el sufijo en esa cuenta), buscando el último espacio antes del límite para no partir una palabra a la mitad. Si el texto ya mide `largo` o menos, se devuelve sin cambios.
```python
fx.truncate("Este es un texto muy largo para mostrar", 20)  # "Este es un texto..."
```

### Números (0.4.0)

**round_to(numero, decimales)** — Redondea a `decimales` posiciones, usando round-half-up (2.5 siempre redondea a 3) en vez del "banker's rounding" que usa `round()` nativo de Python (que redondea 2.5 a 2, al par más cercano).
```python
fx.round_to(2.5, 0)  # 3.0 (round() nativo daría 2.0)
```

**random_int_fast(minimo, maximo, cantidad=1)** — Uno o varios enteros aleatorios en `[minimo, maximo]` (ambos inclusive). **No es criptográficamente seguro** (usa `rand()` de C, sembrado una vez al cargar el módulo) — para eso está el módulo `secrets` de la librería estándar. Pensado para juegos, simulaciones, generación de datos de prueba, donde importa el volumen y la velocidad, no la impredecibilidad a prueba de ataques.
```python
fx.random_int_fast(1, 6)        # 4 (una tirada de dado)
fx.random_int_fast(1, 100, 1000)  # 1000 enteros aleatorios de una vez
```

**lerp(a, b, t)** — Interpolación lineal: `t=0.0` da `a`, `t=1.0` da `b`, valores intermedios dan el punto proporcional. `t` fuera de `[0, 1]` extrapola en vez de fallar (común en motores de animación). **Nota de rendimiento:** ver la sección de benchmarks — es una de las funciones donde Python puro gana.
```python
fx.lerp(0, 100, 0.5)  # 50.0
```

### Fechas (0.4.0)

Trabajan sobre objetos `date`/`datetime` de la librería estándar, usando la C API oficial de `datetime` en vez de reimplementar aritmética de calendario a mano.

**is_weekend(fecha)** — `True` si `fecha` (un `date` o `datetime`) cae en sábado o domingo. **Nota de rendimiento:** ver la sección de benchmarks — Python puro gana en este caso.
```python
fx.is_weekend(date(2026, 7, 18))  # True (sábado)
```

**business_days_between(fecha1, fecha2)** — Número de días hábiles (lunes a viernes, sin considerar feriados) entre dos fechas, inclusive en ambos extremos si caen en día hábil. Negativo si `fecha2` es anterior a `fecha1`.
```python
fx.business_days_between(date(2026, 7, 20), date(2026, 7, 24))  # 5 (lunes a viernes)
```

**format_relative(fecha)** — Descripción relativa en español de qué tan lejos está `fecha` de `datetime.now()`: `"hace 3 días"`, `"en 2 horas"`, `"justo ahora"`. Acepta `date` o `datetime`.
```python
fx.format_relative(datetime.now() - timedelta(days=3))  # "hace 3 días"
```

### Geometría 2D (0.4.0)

Convención: un punto es una tupla `(x, y)`; un rectángulo es `(x, y, ancho, alto)` con `(x, y)` como esquina superior izquierda. **Nota de rendimiento:** ver la sección de benchmarks — las cuatro funciones de esta área son casos donde Python puro gana.

**distance(p1, p2)** — Distancia euclidiana entre dos puntos.
```python
fx.distance((0, 0), (3, 4))  # 5.0
```

**midpoint(p1, p2)** — Punto medio entre dos puntos, como tupla `(x, y)`.
```python
fx.midpoint((0, 0), (10, 10))  # (5.0, 5.0)
```

**point_in_rect(punto, rect)** — `True` si `punto` cae dentro de `rect`, bordes inclusive.
```python
fx.point_in_rect((5, 5), (0, 0, 10, 10))  # True
```

**point_in_circle(punto, centro, radio)** — `True` si `punto` cae dentro (o justo en el borde) del círculo de `centro` y `radio`.
```python
fx.point_in_circle((1, 1), (0, 0), 5)  # True
```

### Estadística básica (0.4.0)

Medidas de tendencia central y dispersión sobre listas de números. **Nota de rendimiento:** el resultado varía según la función y la distribución de los datos — ver la sección de benchmarks para el detalle completo antes de asumir que todas ganan por igual.

**mean(lista)** — Promedio aritmético.
```python
fx.mean([1, 2, 3, 4, 5])  # 3.0
```

**median(lista)** — Mediana; con cantidad par de elementos, promedio de los dos valores centrales.
```python
fx.median([1, 2, 3, 4, 5])  # 3.0
```

**stdev(lista)** — Desviación estándar **poblacional** (divide por `n`, no por `n-1`). Para la varianza muestral, usar `statistics.stdev()` de la librería estándar.
```python
fx.stdev([2, 4, 4, 4, 5, 5, 7, 9])  # 2.0
```

**percentile(lista, p)** — Valor del percentil `p` (0-100), con interpolación lineal entre los dos valores más cercanos (el mismo método que usa `numpy.percentile` por defecto).
```python
fx.percentile([1, 2, 3, 4, 5], 95)  # 4.8
```

**mode(lista)** — El valor que más veces aparece. Acepta cualquier tipo hasheable, no solo números. En caso de empate, devuelve el primero que alcanzó la frecuencia máxima.
```python
fx.mode([1, 2, 2, 3, 3, 3, 4])  # 3
```

## FastList: encadenamiento de métodos

`unique_by()`, `partition()`, `partition_gt()` y algunas otras funciones devuelven un `FastList`: un subtipo de `list` que se comporta como una lista normal (indexable, iterable, con `len()`, slicing, etc.) pero además expone métodos propios para seguir encadenando sin volver a pasar por funciones sueltas del módulo.

Métodos disponibles en `FastList`:

- **`.groupby(clave)`** — igual que `fx.groupby()`, pero devuelve otro `FastList` (de pares `[clave, sublista]`) en vez de un dict, para poder seguir encadenando.
- **`.sum(campo=None)`** — sin argumento, suma los elementos como números. Con argumento, asume que el `FastList` viene de un `.groupby()` y devuelve un dict `{clave: suma_del_campo}`.
- **`.count()`** — asume que el `FastList` viene de un `.groupby()` y devuelve un dict `{clave: cantidad}`.
- **`.filter(predicado)`** — devuelve un `FastList` con los elementos donde `predicado(item)` es verdadero.
- **`.map(función)`** — devuelve un `FastList` con `función(item)` aplicada a cada elemento.
- **`.unique_by(clave)`** — igual que `fx.unique_by()`, encadenable.
- **`.flatten()`** — igual que `fx.flatten()`, encadenable.
- **`.chunk(tamaño)`** — igual que `fx.chunk()`, encadenable. Nota: el `FastList` contenedor es encadenable, pero cada sublista interna es una lista normal, no un `FastList`.
- **`.partition(predicado)`** — igual que `fx.partition()`, devuelve una tupla de dos `FastList`.
- **`.dedupe_consecutive()`** — igual que `fx.dedupe_consecutive()`, encadenable.
- **`.clip(minimo, maximo)`** *(0.3.0)* — igual que `fx.clip_outliers()`, encadenable.
- **`.rolling(tamaño)`** *(0.3.0)* — igual que `fx.rolling_window()`, encadenable.
- **`.filter_range(minimo, maximo, inclusive=True)`** *(0.3.0)* — igual que `fx.filter_range()`, sin callback, encadenable.
- **`.partition_gt(umbral)`** *(0.3.0)* — igual que `fx.partition_gt()`, sin callback, devuelve una tupla de dos `FastList`.

```python
ventas_unicas = fx.unique_by(ventas, "id")
totales = ventas_unicas.groupby("categoria").sum("monto")
# {'ropa': 240.0, 'comida': 30.0, 'tech': 500.0}
```

Encadenamientos que combinan métodos de distintas versiones en una sola expresión:

```python
# Promedio móvil real: ventana deslizante + promedio de cada ventana
precios = fx.FastList([10.0, 12.0, 11.0, 15.0, 14.0, 20.0])
promedios_moviles = precios.rolling(3).map(lambda ventana: sum(ventana) / len(ventana))
# [11.0, 12.67, 13.33, 16.33]

# Filtrar por rango sin callback, acotar outliers, y transformar — sin
# invocar Python en los dos primeros pasos
resultado = (
    fx.FastList(mediciones)
    .filter_range(5, 25)
    .clip(10, 20)
    .map(lambda n: n * 2)
)

# Separar por umbral sin callback, y seguir operando sobre cada mitad
mayores, resto = fx.FastList(valores).partition_gt(100)
top = mayores.map(lambda n: n - 100)
ventanas_del_resto = resto.rolling(5)

# Limpieza de datos: colapsar repetidos, acotar rango, filtrar
resultado = (
    fx.FastList(lecturas_sensor)
    .dedupe_consecutive()
    .clip(0, 100)
    .filter_range(0, 50)
)
```

Nota: `.sum(campo)` y `.count()` esperan específicamente el formato que produce `.groupby()` (una lista de pares `[clave, sublista]`); si se les pasa otra cosa, lanzan `TypeError` con un mensaje explicando qué esperaban.

## Ejemplo real combinando varias funciones

Sin fastcorex (19 líneas): un loop para deduplicar por id, otro para agrupar por categoría, y un tercer loop anidado para sumar montos por grupo.

Con fastcorex, dos formas equivalentes:

```python
# Con funciones sueltas (4 líneas)
ventas_unicas = fx.unique_by(ventas, "id")
grupos = fx.groupby(ventas_unicas, "categoria")
totales = {cat: fx.fast_sum([i["monto"] for i in items]) for cat, items in grupos.items()}

# Con encadenamiento de FastList (2 líneas)
ventas_unicas = fx.unique_by(ventas, "id")
totales = ventas_unicas.groupby("categoria").sum("monto")
```

Ambas versiones dan el mismo resultado: `{'ropa': 240.0, 'comida': 30.0, 'tech': 500.0}`

## Resultados de benchmark

Medido con listas de 200,000 y 2,000,000 de elementos, mejor tiempo de 5 corridas (ver `benchmark.py` en el repositorio para reproducirlo).

**Con 200,000 elementos:** fast_sum 2.5x, count_freq 1.6x, unique 2.0x, filter_gt 2.0x, groupby 1.9x, flatten 11.7x, unique_by 1.2x, chunk 1.4x, safe_get 1.1x, clamp 2.0x, pick 1.8x, omit 5.6x, deep_merge 14.0x, slugify 31.6x, dedupe_consecutive 2.2x, flatten_dict 1.3x, invert_dict 1.4x, clip_outliers 14.2x, rolling_window 2.4x, filter_range 2.5x (vs Python puro) / 7.1x (vs `.filter(lambda)`), partition_gt 3.1x (vs Python puro) / 6.2x (vs `partition(lambda)`), pipeline `unique_by→groupby→sum` 1.7x, pipeline nuevo `filter_range→clip→rolling` 3.4x. **partition: 0.51x** (más lento que Python puro; con Vectorcall desde 0.3.2, antes 0.60x — ver la explicación abajo). **pad: 0.64x** contra una función Python equivalente con la misma validación, **0.26x** contra `str.center()` desnudo sin validación — ver la explicación abajo.

**Con 2,000,000 elementos:** fast_sum 1.9x, count_freq 1.5x, unique 1.6x, filter_gt 1.6x, groupby 1.8x, flatten 8.8x, unique_by 1.2x, chunk 1.1x, safe_get 1.1x, clamp 2.0x, pick 1.7x, omit 5.9x, deep_merge 13.8x, slugify 31.5x, dedupe_consecutive 1.9x, flatten_dict 1.3x, invert_dict 1.4x, clip_outliers 13.2x, rolling_window 2.0x, filter_range 2.2x / 5.7x, partition_gt 2.3x / 4.1x, pipeline `unique_by→groupby→sum` 1.5x, pipeline nuevo 3.1x. **partition: 0.51x** (antes 0.59x en 0.3.1). **pad: 0.63x / 0.24x** (sin cambios; ver la sección 0.3.2 sobre el experimento revertido).

### Las dos especializadas de 0.3.0: filter_range y partition_gt

Estas dos son la respuesta directa a la limitación de `partition`/`.filter()` documentada abajo: cuando el "predicado" es en realidad una comparación numérica simple (un rango o un umbral), evitar el callback de Python cambia por completo el resultado. `partition_gt` pasa de la zona de "más lento que Python puro" (0.53x-0.60x, el número de `partition` con lambda) a **6.2x-4.1x más rápido que ese mismo `partition(lambda)`**, y **3.1x-2.3x más rápido que Python puro**. `filter_range` muestra el mismo patrón: **7.1x-5.7x más rápido que `.filter(lambda)`**, y **2.5x-2.2x más rápido que Python puro**. La causa es exactamente la que se sospechaba: sin el cruce Python→C→Python por elemento, el loop en C vuelve a tener la ventaja que se esperaría de una extensión nativa. El pipeline `filter_range→clip→rolling` (tres pasos, ninguno con callback) rinde 3.4x-3.1x frente a su equivalente en Python puro, frente al 0.64x-0.57x que daba el pipeline `filter→map→dedupe_consecutive` de la versión anterior (que sí tiene dos pasos con callback).

### clip_outliers y flatten_dict/invert_dict

`clip_outliers` tiene una de las mejores ganancias (14.2x-13.2x) por la misma razón que `deep_merge`: hace trabajo genuinamente pesado en C sin ningún callback, aplicando la comparación y el `PyFloat_AsDouble`/`PyFloat_FromDouble` directamente en el loop, evitando el overhead de bytecode de una list comprehension con `max(min(...))` por elemento. `flatten_dict` e `invert_dict` tienen ganancias más modestas (1.3x-1.4x) porque su costo ya está dominado por operaciones de dict que CPython ya implementa eficientemente en C por debajo (`PyDict_Next`, `PyDict_SetItem`); el beneficio ahí es sobre todo evitar escribir y mantener la recursión o el comprehension a mano, no la velocidad bruta.

### partition sigue siendo la excepción negativa real (sin cambios respecto a 0.2.0)

**partition es consistentemente ~40-41% más lento que el equivalente en Python** (0.59x-0.60x), sin cambios respecto a la versión anterior — no se tocó su implementación en esta ronda porque el problema nunca fue la implementación en sí. La razón es estructural: `partition` invoca el predicado de Python una vez por elemento vía `PyObject_CallFunctionObjArgs`, y ese cruce Python→C→Python en cada iteración cuesta más de lo que se ahorra teniendo el loop externo en C. Lo mismo aplica a `.filter()` y `.map()` de `FastList`. **La solución que aporta esta versión no es optimizar `partition` en sí (no se puede, sin cambiar qué acepta como argumento) sino ofrecer `partition_gt`/`filter_range` como alternativas sin callback para el caso — muy común en la práctica — donde el predicado es una comparación numérica simple.** Si el caso de uso necesita un predicado arbitrario de Python, `partition`/`.filter()`/`.map()` siguen siendo la única opción, y su ganancia real ahí es de expresividad y de mantener todo en una cadena legible, no de velocidad.

### pad: dos comparaciones honestas, no una

`pad` es un caso nuevo con una particularidad: el número "correcto" depende de contra qué se lo compare, y por eso el README reporta dos.

Contra `str.center()`/`str.ljust()` **desnudos**, sin ninguna validación ni soporte de `mode` unificado, `pad` es 0.24x-0.26x — notablemente más lento. Esto se investigó a fondo, no es un descuido: se probaron tres optimizaciones sucesivas (un único buffer de salida en vez de tres objetos intermedios, `PyUnicode_Fill` — la misma rutina que usa CPython internamente para estos métodos — en vez de escribir carácter por carácter, y una ruta de parseo de argumentos sin el mecanismo de keywords cuando no se pasa ninguno) y cada una redujo algo el costo pero ninguna cerró la brecha por completo. Lo que queda es el costo fijo de cruzar a través de la C API de extensión en cada llamada (empaquetar `args`, resolver el método, incrementar/decrementar referencias en el camino de llamada), que los métodos nativos de `str` evitan por estar integrados directamente en el tipo built-in con un camino de despacho más corto. Esto no tiene solución sin cambiar qué es `pad` — convertirlo en un método de `str` no es algo que una extensión externa pueda hacer.

Contra una función de Python que replique la **misma funcionalidad** de `pad` (validar que `fill` sea un solo carácter, unificar `ljust`/`rjust`/`center` bajo un parámetro `mode`, dar mensajes de error explícitos), `pad` rinde prácticamente igual: 0.63x-0.64x. Ese es el punto de comparación honesto, porque nadie que necesite esa validación va a usar `str.center()` a secas — va a escribir (o ya tiene escrita) una función wrapper con ese mismo costo de dispatch. La razón real de ser de `pad` nunca fue ganar velocidad sobre el built-in desnudo: es no tener que escribir y mantener esa función de validación uno mismo.

### Funciones sin cambios de 0.1.1/0.2.0

flatten sigue teniendo una de las mayores ganancias (11.7x-8.8x) porque en Python puro depende de recursión con overhead de llamadas a función, que en C es casi gratis. deep_merge (14.0x-13.8x) y slugify (31.6x-31.5x) mantienen sus ganancias porque hacen trabajo pesado en C sin callbacks. safe_get se mantiene en ~1.1x más rápido que Python puro (mejora que ya se documentó en la versión 0.2.0, sin cambios en esta ronda). Las funciones que ya dependen de dict/set de Python (count_freq, unique, unique_by, fast_sum) ganan menos, porque esas estructuras ya están optimizadas en C por debajo del intérprete.

## Cambios de la versión 0.3.1: auditoría de velocidad y simplificación de API

Esta versión no agrega funciones nuevas. Es una auditoría completa de las 23 funciones del módulo y los 14 métodos de `FastList`, revisando cada una en busca de (a) búsquedas o allocaciones redundantes que pudieran eliminarse sin cambiar el comportamiento observable, y (b) inconsistencias en nombres de parámetros o soporte de keywords entre funciones que resuelven problemas similares. El resultado se documenta con la misma honestidad que el resto del README: algunas mejoras son reales y medibles, otras son cambios estructuralmente correctos que no se traducen en una diferencia perceptible, y se reportan como tales en vez de inflar el número.

### Optimizaciones de velocidad

**groupby (función suelta) y FastList.groupby** son las dos mejoras con impacto medible de esta ronda. La versión anterior de `groupby` hacía `PyDict_GetItemWithError` para comprobar si el grupo ya existía, y solo en el caso de grupo nuevo agregaba un `PyDict_SetItem` adicional — es decir, para cada elemento de un grupo que ya existe, había una búsqueda "de más" en el sentido de que `PyDict_SetDefault` puede resolver "buscar o crear con un valor por defecto" en una sola operación. `FastList.groupby()` tenía una capa adicional de indirección: mantenía un `index_dict` separado (clave → índice numérico en la lista de resultado) solo para poder envolver la salida en pares `[clave, sublista]` en vez de un dict plano. Como los dicts de Python (3.7+) ya garantizan orden de inserción, esa capa de índices era innecesaria: la nueva versión usa directamente un dict `clave → [clave, sublista]` y extrae `dict.values()` al final, preservando exactamente el mismo orden de aparición que antes (verificado con un caso de categorías intercaladas de forma no trivial). Medido sobre 200,000 elementos con 5 categorías: `groupby` mejora 1.02x-1.10x, `FastList.groupby()` mejora 1.01x-1.04x, y el pipeline completo `groupby().sum()` apenas 1.01x (la mejora se diluye porque `.sum()` vuelve a recorrer todos los grupos con su propio costo, que domina el tiempo total).

**count_freq** recibió el mismo tratamiento con `PyDict_SetDefault` (antes: `PyDict_GetItemWithError` + posible `PyDict_SetItem`, dos búsquedas para una clave repetida). **flatten_dict** se ajustó para evitar la llamada a `PyObject_Str()` cuando la clave de un dict ya es un `str` (el caso inmensamente más común), comprobándolo primero con `PyUnicode_Check`. Ambos cambios son correctos y más explícitos sobre la intención del código, pero medidos repetidamente no mostraron una mejora consistente por encima del ruido de medición (~0.97x-1.03x, oscilando de corrida a corrida) — el costo real en ambos casos está dominado por el hashing de los propios objetos, no por el número de operaciones de búsqueda en el dict. Se documentan aquí en vez de callarlos porque siguen siendo una limpieza estructural válida (menos trabajo redundante en el peor caso), solo que no es una ganancia que se pueda anunciar con un número honesto.

**pad** recibió tres intentos de optimización adicionales en esta ronda, más allá de los ya aplicados en 0.3.0: un único buffer de salida (ya estaba), `PyUnicode_Fill` en vez de escribir carácter por carácter (la misma rutina que usa CPython internamente para `ljust`/`rjust`/`center`), y una ruta de parseo de argumentos que evita `PyArg_ParseTupleAndKeywords` cuando no se pasa ningún keyword. Cada uno redujo algo el costo medido de forma aislada, pero el número final frente a `str.center()` desnudo no cambió de forma significativa (sigue en la misma zona ya documentada en 0.3.0). Se investigó a fondo — incluyendo aislar cuánto cuesta específicamente pasar un argumento como keyword (hasta ~1.4x más lento que la misma llamada sin keywords, medido de forma aislada) — y la conclusión sigue siendo la misma que en 0.3.0: el costo restante es el overhead fijo de cruzar la C API de extensión en cada llamada, que no tiene solución sin convertir `pad` en un método nativo de `str`, algo que una extensión externa no puede hacer. No se revirtió ningún cambio de `pad` porque, aunque no mejoraron el número frente al built-in desnudo, tampoco lo empeoraron, y el código quedó más claro sobre qué hace cada paso.

**Se auditaron sin cambios** (ya estaban en su forma óptima, o el margen de mejora no justificaba el riesgo): `fast_sum`, `unique`/`unique_by` (el patrón `PySet_Contains` + `PySet_Add` es el estándar de C para esto; no existe una alternativa pública más barata sin acceder a símbolos internos no garantizados entre versiones de Python), `filter_gt`, `chunk`, `safe_get` (ya optimizada en 0.2.0), `clamp`, `pick`, `omit`, `deep_merge` (se evaluó evitar la copia completa de sub-dicts en cada nivel de fusión, pero el caso de uso real —configs de tamaño moderado— no muestra un costo perceptible, y la complejidad de un "copy-on-write" parcial no se justificaba), `slugify`, `partition`/`.filter()`/`.map()` (se midió que el callback de Python representa ~72% del tiempo total; el 28% restante en overhead de crecimiento de listas no compensa el riesgo de una estrategia de pre-alocación que además desperdiciaría memoria en el caso típico), `dedupe_consecutive`, `invert_dict` (no existe una función pública de la C API para pre-dimensionar un dict antes de llenarlo, a diferencia de las listas), `rolling_window`, `filter_range` y `partition_gt` (ya en su forma óptima desde 0.3.0).

### Simplificación de API

**safe_get** y **flatten_dict** renombraron su primer parámetro de `dict_obj` (un nombre idiomático de la C API interna, no de Python) a `d`, más corto y consistente con la convención usada en el resto de la documentación y los ejemplos del README. **filter_range** renombró `input_list` a `lst` por la misma razón. Estos cambios solo afectan a quien llamaba estas funciones con el nombre de keyword explícito (algo no documentado ni usado en ningún ejemplo previo del README); la forma posicional, que es la única documentada, sigue funcionando exactamente igual.

**clip_outliers** ganó soporte de keywords (`lst`, `min_val`, `max_val`), que antes no tenía pese a ser conceptualmente la función hermana más cercana de `filter_range` (ambas trabajan sobre un rango `[min, max]` de una lista de números) — antes solo se le podía pasar posicional, mientras que `filter_range` sí aceptaba nombres. Ahora `fx.clip_outliers(lista, min_val=0, max_val=10)` funciona igual que `fx.filter_range(lista, min_val=0, max_val=10)`. Se verificó que agregar el mecanismo de keywords no penalizó el caso de llamada posicional (que sigue siendo la forma más común): medido repetidamente, el ratio quedó en la misma zona de ruido que antes del cambio, sin regresión.

**Se consideró y se descartó** agregar keywords a `rolling_window` y `partition_gt`: ambas reciben solo dos argumentos (lista + un número), donde el orden es obvio y el valor de nombrarlos es mínimo — agregar esa superficie de API habría sido complejidad sin beneficio real, lo opuesto al objetivo de esta ronda. También se descartó fusionar `isinstance(value, list)` + `isinstance(value, tuple)` en `ensure_list` en una sola comprobación `isinstance(value, (list, tuple))`: parecía una simplificación razonable, pero medido directamente resultó ser ~10-20% más lento que las dos comprobaciones separadas (CPython tiene un atajo más corto para el chequeo de un único tipo que para una tupla de tipos candidatos), así que se revirtió — se prefirió el código "menos elegante" porque es el que de verdad es más rápido.

## Cambios de la versión 0.3.2: intento serio de cerrar la brecha en partition/.filter()/.map() y pad

Esta versión ataca directamente las dos únicas zonas documentadas como "igual o más lentas que Python": `partition`/`.filter()`/`.map()` (0.53x-0.60x) y `pad` (0.24x-0.64x según la comparación). El resultado es parcial y se documenta con la misma honestidad de siempre: una mejora real mantenida, un experimento que se probó y se revirtió, y un límite que sigue sin solución posible sin cambiar la API pública.

### partition, .filter() y .map(): de CallFunctionObjArgs a Vectorcall

Se reemplazó `PyObject_CallFunctionObjArgs` por `PyObject_Vectorcall` (API pública y estable desde Python 3.9, el mínimo de este proyecto) en las tres funciones que invocan un callable de Python por elemento. `Vectorcall` pasa los argumentos como un array de punteros C en vez de empaquetarlos en una tupla de Python, evitando esa construcción intermedia en cada llamada. Medido de forma aislada con una función mínima en C, esto da ~5% de mejora consistente. Medido en el contexto real de `partition()` con una lambda como predicado, sobre listas de 200,000 y 2,000,000 elementos: el ratio pasó de 0.53x-0.60x a **0.51x-0.52x** — es decir, dentro del margen de ruido, sin cambiar la conclusión de fondo. `.filter()` y `.map()` de `FastList` no mostraron ninguna mejora medible en absoluto (~0.99x-1.00x comparado contra la versión anterior). El cambio se mantiene de todas formas porque es correcto, no tiene riesgo, y no empeora nada — pero no se anuncia como la solución al problema, porque no lo es.

**El techo real, confirmado con un experimento dirigido:** se aisló específicamente cuánto cuesta invocar una lambda de Python (que crea un frame de ejecución interpretado) frente a invocar un builtin de C puro con el mismo trabajo — la lambda resultó ~39% más lenta *solo por ese motivo*, sin que ninguna API de invocación del lado de C pueda evitarlo. Este es el verdadero cuello de botella: no es cómo se invoca el callable desde la extensión, es que el callable en sí mismo es código Python interpretado. Ninguna optimización posible desde `_fastcorex.c` puede acelerar la ejecución del código Python que el usuario proporciona.

**Se descartó explícitamente** una vía más agresiva que sí se evaluó en profundidad: inspeccionar el bytecode de la lambda del usuario para detectar patrones de comparación simple (`x > N`) y resolverlos directamente en C sin invocar el protocolo de llamada. Es técnicamente posible — el bytecode de una lambda como `lambda x: x > 5` es corto y reconocible — pero se rechazó por tres razones: (1) el bytecode exacto de CPython cambia entre versiones menores de Python, lo que obligaría a mantener una tabla de compatibilidad por versión; (2) la cobertura sería limitada (no cubre closures, funciones `def`, ni `operator.gt`); y (3) el riesgo más serio: un bug sutil en la detección de patrones podría producir resultados **silenciosamente incorrectos** sin ningún error visible, un tipo de fallo mucho peor que "sigue siendo lento". El costo de mantenimiento y el riesgo de correctitud superan la ganancia, así que no se implementó.

**Hallazgo útil que sí se documenta como consejo práctico:** cuando el predicado o la función ya es un builtin de C (`bool`, `int`, `str.lower`, un método de una clase de C, etc.) en vez de una lambda de Python, ese callable no necesita crear un frame de ejecución, y el costo cae dramáticamente sin ningún cambio de código de por medio — medido: `partition(lista, bool)` es **3.56x más rápido** que `partition(lista, lambda n: bool(n))` para el mismo resultado. Esto no resuelve el caso general (una lambda con lógica arbitraria sigue pagando su propio costo), pero es una alternativa real para quien pueda expresar su condición con un builtin.

### pad: se probó METH_FASTCALL, se midió, y se revirtió

Se reescribió `pad` por completo usando `METH_FASTCALL | METH_KEYWORDS` en vez de `METH_VARARGS | METH_KEYWORDS`, extrayendo los argumentos manualmente desde un array de punteros C en vez de dejar que `PyArg_ParseTupleAndKeywords` construya y parsee una tupla. Un micro-benchmark aislado (una función mínima que solo recibe y devuelve dos argumentos) mostró **2.18x más rápido** con este mecanismo, una promesa considerable. Se implementó la reescritura completa: parseo manual de hasta 4 argumentos posicionales o con nombre en cualquier orden, detección de argumentos duplicados (posicional + keyword para el mismo parámetro), keywords desconocidos, y todos los mensajes de error que ya tenía la versión anterior — validado con una batería de 12 casos de error distintos, todos correctos, y sin leaks de memoria en ningún camino (incluido el de retorno temprano cuando el texto ya mide más que el ancho pedido).

Medido en el contexto real de `pad()` completa (no la función mínima aislada) contra la versión anterior de 0.3.1: el resultado fue **1.01x en el caso posicional y 0.97x-1.01x con el keyword `mode=`** — es decir, ninguna mejora neta perceptible. La razón: el ahorro de no construir la tupla de argumentos es una fracción minúscula del tiempo total de `pad()`, que está dominado por el resto del trabajo de la función (validar `fill`, resolver `mode`, calcular el relleno, escribir el buffer de salida) — el mismo patrón, en sentido inverso, que ya se había confirmado con `count_freq`/`flatten_dict` en la ronda 0.3.1 (una optimización teóricamente sólida que no se traduce en una ganancia medible porque el costo real está en otro lado).

Dado que la reescritura no aportó ninguna mejora real y sí agregó considerablemente más código (parseo manual de argumentos en vez de una lista `kwlist` declarativa, más superficie para bugs de mantenimiento futuro), **se revirtió por completo**: `pad` en 0.3.2 tiene exactamente la misma implementación que en 0.3.1 (`PyArg_ParseTupleAndKeywords` con la ruta rápida sin keywords ya aplicada en la ronda anterior). Los números de benchmark de `pad` en la sección anterior siguen siendo los vigentes; no cambiaron en esta ronda.

**Conclusión honesta de esta ronda:** se intentó en serio cerrar la brecha en ambos casos, con dos técnicas de la C API que en teoría debían dar mejoras sustanciales (`Vectorcall`, `METH_FASTCALL`), midiendo cada paso en vez de asumir que la teoría se traduciría en práctica. En `partition`/`.filter()`/`.map()` la mejora real quedó dentro del margen de ruido. En `pad` la mejora resultó ser cero, y el cambio se revirtió correctamente en vez de mantenerlo por inercia. Ambas funciones siguen exactamente en la misma categoría que antes de esta ronda: `partition`/`.filter()`/`.map()` con una lambda arbitraria seguirán siendo más lentas que Python puro porque el costo real es el propio código Python del usuario, no la extensión; y `pad` seguirá rindiendo por debajo de `str.center()`/`ljust()` desnudos por el costo fijo, inevitable desde una extensión externa, de cruzar la C API en cada llamada.

## Cambios de la versión 0.3.3: robustez ante casos borde

Esta versión no toca velocidad ni API pública; se centró en encontrar y corregir comportamiento incorrecto o inseguro ante entradas extremas: recursión profunda, overflow de enteros, valores especiales de punto flotante, y Unicode fuera de lo común. El hallazgo principal es serio y se corrigió: tres funciones podían crashear el proceso Python completo.

### El hallazgo principal: tres funciones podían producir un segmentation fault

`flatten()`, `deep_merge()` y `flatten_dict()` recursan una vez por cada nivel de anidamiento de su entrada, sin ningún límite. Se verificó empíricamente que una lista o dict con suficiente anidamiento —entre 150,000 y 200,000 niveles, dependiendo de la función— desborda la pila de llamadas de C y produce un **segmentation fault que mata el proceso Python entero**, con código de salida 139 (SIGSEGV) en dos casos y un "Killed" por out-of-memory (código 137) en el tercero (`flatten_dict`, porque cada nivel construye un prefijo cada vez más largo con `PyUnicode_FromFormat` antes de llegar al límite de pila). En ningún caso hay una excepción capturable: el proceso simplemente termina, sin que ningún `try/except` del código que llama pueda hacer nada al respecto.

Esto es explotable con datos no confiables: cualquier programa que llame estas funciones sobre JSON deserializado de una fuente externa, o sobre estructuras construidas a partir de entrada de usuario, puede ver su proceso completo morir si esa entrada tiene anidamiento adversarial — sin que el error se registre como una excepción, sin traceback, y potencialmente sin ningún log más allá de lo que el sistema operativo reporte sobre la señal recibida.

**La corrección:** se envolvió el cuerpo de las tres funciones recursivas (`flatten_into`, `deep_merge_into`, `flatten_dict_into`) con `Py_EnterRecursiveCall()`/`Py_LeaveRecursiveCall()`, el mecanismo oficial y público de la C API de CPython para esto exacto. `Py_EnterRecursiveCall` lleva la cuenta de la profundidad de recursión de C y, al acercarse al límite (el mismo que usa `sys.setrecursionlimit()` para la propia pila de Python, aunque con un margen distinto para C — medido empíricamente en ~9,970 niveles para estas funciones, sensiblemente antes del punto real de segfault), deja pendiente un `RecursionError` normal de Python en vez de seguir recursando hasta reventar la pila real. Verificado explícitamente: tras capturar el `RecursionError`, el intérprete queda en un estado completamente normal y se puede seguir llamando a cualquier función de `fastcorex` sin ningún problema — no es un estado de "corrupción parcial" del que haya que recuperarse de alguna forma especial.

El costo de esta protección es, en la práctica, cero: medido sobre un caso de anidamiento normal (unos pocos niveles, como cualquier estructura de datos real), el ratio entre la versión con protección y sin ella fue de 0.979x — dentro del ruido de medición, sin ninguna penalización perceptible. `Py_EnterRecursiveCall` es, en esencia, un incremento y una comparación de un contador entero; el costo real de la recursión (crear el frame de C, las variables locales, etc.) es el mismo con o sin la protección.

### Overflow de enteros

Los parámetros numéricos que el usuario controla directamente (`width` en `pad`, el tamaño en `chunk`/`rolling_window`) ya estaban protegidos sin necesidad de ningún cambio: usan el formato `"n"` de `PyArg_ParseTuple`, que internamente mapea a `Py_ssize_t` y **ya lanza `OverflowError`** de forma nativa si el entero de Python pasado no cabe en ese tipo — verificado explícitamente con enteros de 100 dígitos (`10**100`), muy por encima de cualquier entero de 64 bits. `count_freq` sí se ajustó: usaba `PyLong_AsLong`/`PyLong_FromLong` (basados en `long`, que en Windows de 64 bits es de solo 32 bits) para el conteo de repeticiones, cuando el conteo real nunca puede superar el tamaño de la lista de entrada (ya acotado por `Py_ssize_t`, consistentemente de 64 bits en cualquier plataforma moderna). Se cambió a `PyLong_AsSsize_t`/`PyLong_FromSsize_t`, evitando un límite artificialmente más bajo en esa plataforma específica sin ningún costo adicional.

### Valores especiales de punto flotante (NaN, infinito)

Se auditaron las siete funciones que convierten a `double` (`fast_sum`, `filter_gt`, `clip_outliers`, `filter_range`, `partition_gt`, y las dos variantes de `FastList.sum()`) con NaN e infinito en distintas combinaciones. En todos los casos el comportamiento coincide exactamente con el equivalente en Python puro, porque las comparaciones (`>`, `<`, `==`) sobre `double` en C siguen el mismo estándar IEEE 754 que usa CPython internamente para `float` — no hay ninguna divergencia que corregir. Este resultado, aunque no requirió ningún cambio de código, se documenta con pruebas explícitas para que quede registrado y protegido contra una futura regresión accidental (por ejemplo, alguien podría "optimizar" una comparación de forma que accidentalmente trate NaN de forma distinta).

### Unicode extremo

Se probó `slugify` y `pad` con emojis del plano astral, caracteres combinantes, múltiples alfabetos (cirílico, chino, árabe), caracteres de control, y emojis compuestos de varios code points (como los emojis de familia, que en realidad son 7 code points de Python unidos con zero-width joiners). Todos los casos se comportan de forma seria y predecible, con un único hallazgo real que se documenta explícitamente en el README y en el docstring del módulo: `slugify()` sobre texto completamente en un alfabeto no cubierto por su tabla de transliteración (que solo cubre acentos latinos comunes) da como resultado una **cadena vacía**, no un error. Esto no es un bug — es consistente con la propia regla de la función (cualquier carácter no reconocido colapsa a separador, y los separadores se colapsan y recortan al final) — pero antes no estaba documentado con la claridad suficiente, y una cadena vacía silenciosa puede ser peligrosa si se usa como identificador único (por ejemplo, un nombre de archivo o slug de URL). `pad` correctamente rechaza con `ValueError` cualquier `fill` que mida más de un code point de Python, incluidos los emojis compuestos, que es el comportamiento correcto aunque visualmente parezcan "un solo carácter".

## Cambios de la versión 0.4.0: cinco áreas nuevas fuera de listas y diccionarios

Hasta la versión 0.3.3, `fastcorex` solo cubría patrones de listas y diccionarios. Esta versión agrega 18 funciones nuevas en 5 áreas que no tocan listas ni dicts: texto, números, fechas, geometría 2D, y estadística básica — pensadas para que la librería sirva en contextos donde antes no aportaba nada (juegos, reportes, validación de formularios, cualquier script que no procese datos tabulares).

No todas las 18 funciones nuevas son más rápidas que su equivalente en Python puro, y este README lo documenta con la misma honestidad que el resto del proyecto en vez de promediar el resultado o mostrar solo los casos favorables.

### Lo que gana de forma clara y consistente

`word_count` (1.50x-1.73x), `find_all` (3.05x-3.20x), `truncate` (1.79x-1.85x), `round_to` (1.90x-2.00x), `random_int_fast` (13.55x-14.17x) y `business_days_between` (127.96x-164.46x) ganan de forma consistente en ambos tamaños medidos (200,000 y 2,000,000 elementos donde aplica). `business_days_between` tiene la ganancia más grande de esta ronda porque la comparación en Python puro más directa (iterar día por día construyendo un objeto `date` nuevo en cada paso) es genuinamente costosa; la versión en C solo hace aritmética modular sobre el offset de día de la semana, sin construir ningún objeto de fecha intermedio.

### Lo que pierde contra Python puro, y por qué (el mismo patrón ya visto con pad/partition)

**lerp** (0.23x-0.25x), **is_weekend** (0.42x-0.44x), **distance** (0.46x) y **point_in_rect** (0.64x-0.65x) son más lentas que Python puro. La razón es la misma que ya se documentó extensamente para `pad` y `partition` en rondas anteriores: son operaciones tan triviales del lado de Python (una expresión aritmética directa, o un desempaquetado de tupla seguido de una comparación) que el costo fijo de cruzar la C API — parsear los argumentos, empaquetar el resultado como objeto Python — pesa más que el cómputo en sí.

`is_weekend` en particular se investigó y se corrigió parcialmente durante esta misma ronda: la primera implementación invocaba `.weekday()` de Python desde dentro de C (vía `PyObject_CallMethod`), pagando el cruce a la C API **dos veces** — una para invocar el método, otra para el resultado — cuando el equivalente en Python puro paga ese costo una sola vez. Se reemplazó por un cálculo directo del día de la semana en C (algoritmo de Sakamoto, verificado exhaustivamente contra `date.weekday()` real en 7,200 fechas distintas a lo largo de 200 años, incluyendo años bisiestos y el cambio de siglo, con 0 discrepancias), lo que mejoró el ratio de 0.20x a 0.42x-0.44x — casi el triple de rápido, pero sin cruzar 1x, porque sigue quedando un cruce a la C API para una comparación que en Python ya es prácticamente gratis.

Para `distance`/`point_in_rect`/`midpoint`/`point_in_circle` se investigó una vía adicional: reemplazar la función auxiliar genérica de parseo de puntos (`parse_point`, que usa `PySequence_Fast`) por el formato anidado `"(dd)(dd)"` de `PyArg_ParseTuple`, que en teoría delega el desempaquetado directamente a la maquinaria ya optimizada de la C API. Medido de forma aislada, esta alternativa resultó **más lenta** que la implementación genérica actual (0.70x), no más rápida — se descartó por no aportar ninguna mejora real. La conclusión, consistente con `lerp`/`is_weekend`/`pad`/`partition`: cuando la operación equivalente en Python es una expresión directa sin llamadas a función, ninguna vía de la C API le gana, porque el costo fijo de cruzar esa API es mayor que el cómputo que se está acelerando.

### Estadística: el resultado depende de la distribución de los datos

`mean` y `stdev` ganan de forma aplastante contra `statistics.mean`/`statistics.pstdev` de la librería estándar (75x-96x en las mediciones iniciales) porque ese módulo prioriza precisión exacta con aritmética de fracciones sobre velocidad. Pero contra la forma más directa posible en Python (`sum(lista)/len(lista)`), `mean` rinde prácticamente igual (1.01x-1.02x) — la ganancia real frente a `statistics` no es "C es más rápido que cualquier código Python", es "C es más rápido que priorizar precisión exacta sobre velocidad".

`median` y `percentile` (que ambas ordenan los datos con `qsort` de C antes de calcular el resultado) mostraron algo más sutil durante la medición: el resultado depende genuinamente de la **distribución** de los datos, no solo de su tamaño. Con datos aleatorios de tamaño moderado o grande (a partir de aproximadamente 5,000 elementos, verificado con una búsqueda por tamaños), ganan de forma consistente (1.13x-1.39x con datos aleatorios de 20,000 a 2,000,000 de elementos) porque ordenar un array de `double` crudos en C evita el overhead de comparar objetos `PyObject*` que paga `sorted()` de Python. Pero con datos **muy repetitivos** (pocos valores únicos repetidos muchas veces — el caso típico de puntajes discretos, categorías codificadas como número, o mediciones redondeadas), el resultado se invierte: Timsort de Python detecta y aprovecha las corridas repetidas de forma mucho más eficiente que un `qsort` genérico, y en ese escenario Python puro gana (0.74x-0.76x, medido explícitamente con los mismos 10 valores repetidos 2000 veces). `mode()` tiene una particularidad adicional: su caso de uso típico es precisamente sobre datos con muchas repeticiones (si no hubiera repeticiones, pedir "el valor más frecuente" no tendría sentido), así que el escenario donde `mode()` pierde contra `statistics.mode()` (0.74x-0.80x) es, irónicamente, el escenario más realista para esa función específica.

**Recomendación práctica:** si los datos que se van a medir son mediciones continuas y variadas (tiempos de respuesta, precios, coordenadas), `median`/`percentile` de fastcorex son la opción más rápida a partir de unos pocos miles de elementos. Si los datos tienen pocos valores únicos muy repetidos (categorías, puntajes discretos, calificaciones), la versión de Python puro (`sorted()` + indexado, o `statistics.mode()`) puede ser más rápida — vale la pena medir el caso propio antes de asumir que fastcorex siempre gana.

## Licencia

MIT License. Ver el archivo LICENSE para el texto completo.

## Estado del proyecto

Versión 0.4.0. Cubre patrones de listas, diccionarios y strings (de las versiones 0.1.x-0.3.x) más cinco áreas nuevas que amplían el alcance más allá de datos tabulares: texto (`word_count`, `find_all`, `truncate`), números de uso general (`round_to`, `random_int_fast`, `lerp`), fechas (`is_weekend`, `business_days_between`, `format_relative`), geometría 2D (`distance`, `midpoint`, `point_in_rect`, `point_in_circle`), y estadística básica (`mean`, `median`, `stdev`, `percentile`, `mode`). El tipo `FastList` sigue permitiendo encadenar catorce operaciones sobre listas y dicts sin pasar por estructuras intermedias. No pretende reemplazar NumPy para cómputo numérico ni Pandas para análisis de datos tabulares, y las 18 funciones nuevas de esta versión documentan honestamente cuáles ganan y cuáles pierden contra Python puro — ver la sección de benchmarks para el detalle completo antes de asumir que toda función nueva acelera el caso propio.

Cubierto por pruebas automatizadas (`tests/`, ejecutables con `python -m unittest discover -s tests` o con `pytest tests/`): 404 pruebas en total — funciones originales de 0.1.1 a 0.3.3 (regresión), y una suite dedicada a la ronda 0.4.0 con 96 pruebas cubriendo las 18 funciones nuevas, incluyendo una verificación exhaustiva de `is_weekend` contra `date.weekday()` real en un rango amplio de fechas (40 años, múltiples meses y días por año) para confirmar que el cálculo de día de la semana implementado directamente en C es exacto.



