Files
c-programming/book/18-string-view.md
T

1450 lines
61 KiB
Markdown

# Capítulo 18 — Cadenas a la carta: construí tu propia librería `String_View`
> Capítulo avanzado, pero **escrito para que se entienda de verdad**. Se apoya en arreglos y cadenas
> (`06`), punteros (`07`), `struct` (`08`), tipos como `size_t` (`16`), memoria y alineación (`12`),
> archivos (`15`) y entrada/salida (`14`).
>
> Acá vas a dejar de pelear con el terminador `'\0'` y vas a **construir tu propia librería de
> cadenas**: un conjunto de funciones que miran, cortan, recortan y comparan texto **sin copiarlo** y
> **sin un solo `malloc`**. Al final vas a poder escribir un parser de archivos de configuración entero
> con esta caja de herramientas.
---
## Cómo leer este capítulo
Este es el capítulo donde "sé usar `strlen`" se convierte en "entiendo **cómo** se representa un texto
en memoria y puedo diseñar mi propia forma de tratarlo". No es teoría: cada idea viene con **código
para mirar**, y cada bloque cierra con **práctica** para que la ejercites con las manos.
Está dividido en **seis partes**. Después de cada concepto importante hay uno o varios ejercicios
cortos. **No los saltees**: son la mitad del capítulo.
| Parte | De qué habla | La idea en una frase |
|---|---|---|
| **1** | Los tres pilares y la estructura | Un texto es una calle de bytes; en vez de caminarla entera, guardás **dónde empieza** y **cuánto mide**. |
| **2** | Sub-vistas | Mirar un tramo del texto **sin copiarlo**: solo movés el inicio y acortás el largo. |
| **3** | Recorte (trim) | Sacar espacios de los bordes con solo ajustar el puntero y el contador. |
| **4** | Cortar por delimitador (chop / split) | Partir un texto en pedazos, uno por uno, sin reservar memoria. |
| **5** | Reemplazo en búfer | Como la vista no es dueña de la memoria, el reemplazo **escribe** en un búfer que vos manejás. |
| **6** | El catálogo completo + jefe | Comparar, buscar subcadenas, construir, cortar… y un parser de verdad para cerrar. |
Los **ejercicios** se numeran en orden: `18.1`, `18.2`, `18.3`, y así. Cada uno trae su **escenario de
prueba** (qué entra y qué tiene que salir, **nunca** el algoritmo) para que compiles y compruebes.
> **Regla de oro del libro:** el capítulo explica y muestra **cómo funciona** cada cosa (con código
> ilustrativo), pero **los ejercicios no traen la solución**. El enunciado te da el problema; la
> respuesta la escribís vos.
## Tu librería objetivo
Antes de arrancar, mirá el plano de la casa que vas a construir. Estas son las funciones que vas a
tener escritas **vos** al terminar el capítulo. Acá van **solo las declaraciones** (el *contrato*: qué
recibe y qué devuelve cada una), no el cuerpo. Es tu mapa de ruta.
```c
/* ==== sv.h — el contrato de tu librería (solo prototipos) ==== */
#include <stddef.h> /* size_t */
typedef struct {
const char *data; /* dónde empieza el texto */
size_t count; /* cuántos bytes tiene */
} String_View;
/* Impresión */
#define SV_Fmt "%.*s"
#define SV_Arg(sv) (int)(sv).count, (sv).data
/* Construcción */
String_View sv_from_cstr(const char *cstr);
String_View sv_from_parts(const char *data, size_t count);
/* Consultas */
int sv_index_of(String_View sv, char c); /* -1 si no está */
size_t sv_count_char(String_View sv, char c);
/* Sub-vistas (no copian: solo ajustan data y count) */
String_View sv_left(String_View sv, size_t n);
String_View sv_right(String_View sv, size_t n);
String_View sv_chop_left(String_View *sv, size_t n); /* "come" n del frente */
String_View sv_chop_right(String_View *sv, size_t n);
/* Recorte */
void sv_trim_left(String_View *sv);
void sv_trim_right(String_View *sv);
void sv_trim(String_View *sv);
/* Partición */
String_View sv_chop_by_delim(String_View *sv, char delim);
/* Comparación */
int sv_eq(String_View a, String_View b);
int sv_starts_with(String_View sv, String_View prefix);
int sv_ends_with(String_View sv, String_View suffix);
/* Búsqueda de subcadena */
int sv_find(String_View haystack, String_View needle); /* -1 si no está */
int sv_contains(String_View haystack, String_View needle);
/* Reemplazo (escribe en un búfer TUYO, nunca en la vista) */
size_t sv_replace_char(String_View sv, char from, char to,
char *out, size_t outCap);
/* Hacia una cadena C (necesita búfer, para poder agregar '\0') */
size_t sv_to_cstr(String_View sv, char *out, size_t outCap);
```
> **Fijate bien en esto.** La estructura `String_View` aparece **en todas** las firmas. Algunas
> funciones la reciben **por valor** (`String_View sv`) porque no la modifican; otras la reciben **por
> puntero** (`String_View *sv`) porque la **avanzan** (recortar y cortar mueven el inicio). Esa
> distinción es el 80% del diseño.
---
# PARTE 1 — Los tres pilares y la estructura `String_View`
Antes de escribir una línea de tu librería, hay que ver **tres cosas** sobre cómo C maneja el texto.
Si entendés estos tres pilares, todo lo demás cae solo.
## 1.1 Un texto es una calle, no un objeto
Imaginate una **calle con casas pegadas, una al lado de la otra**. Cada casa es un byte. Las casas no
tienen huecos entre sí: están **contiguas**. Un texto en C es exactamente eso: un **bloque de bytes en
memoria, uno pegado al otro**.
```text
calle (memoria) ─────────────────────────────►
┌───┬───┬───┬───┬───┐
│ h │ o │ l │ a │ │ ... la calle sigue
└───┴───┴───┴───┴───┘
1ª casa (cada celda = 1 byte)
```
La palabra clave es **contigua**: los bytes están uno detrás del otro. Eso es lo que hace que "moverse
por el texto" sea tan barato: avanzar una casa es **sumar 1** a la dirección.
## 1.2 El puntero es "la dirección de la primera casa"
Un **puntero** (`char *`) no guarda el texto: guarda **la dirección donde empieza** el texto, o sea
"el número de la primera casa". Cuando escribís `"hola"`, el compilador deja esas cuatro letras en
alguna parte de la memoria y te da la **dirección** de la `h`.
```c
#include <stdio.h>
int main(void) {
const char *s = "hola";
/* %p imprime la DIRECCIÓN (el número de la primera casa), no el texto.
s[0] es "la casa 0": la 'h'. s[1] es la siguiente: la 'o'. */
printf("dirección de inicio : %p\n", (const void *)s);
printf("la casa 0 : %c\n", s[0]); /* h */
printf("la casa 1 : %c\n", s[1]); /* o */
return 0;
}
```
> **Momento ajá.** `s + 1` **no** es "el texto más uno": es "la dirección de la casa siguiente", o sea
> apunta a la `o`. La **aritmética de punteros** (capítulo `07`) es, literalmente, **caminar por la
> calle**. Todo lo que vas a hacer con `String_View` es caminar por la calle sin salirse.
## 1.3 El cartel de FIN: el terminador `'\0'`
Acá está la primera gran decisión histórica de C. Como una calle no dice sola dónde termina, C puso
un **cartel de FIN** al final de cada texto: el byte `'\0'` (el carácter nulo, valor **0**).
```text
┌───┬───┬───┬───┬────┐
│ h │ o │ l │ a │ \0 │ ← el '\0' marca "fin del texto"
└───┴───┴───┴───┴────┘
las 4 letras reales ↑ no es una letra: es el cartel
```
Ese cartel es cómodo para algunas cosas y **un dolor de cabeza** para otras. Mirá las dos caras.
**Lo bueno:** no hace falta saber el largo de antemano. Funciones como `printf("%s", s)` caminan la
calle hasta encontrar el cartel, y listo. Puede pasar un texto de cualquier largo.
**Lo malo** (tres consecuencias enormes):
1. **Para saber el tamaño hay que caminar la calle entera.** `strlen` **no sabe** cuánto mide: recorre
byte por byte hasta topar el `'\0'`. Un texto de un millón de letras cuesta un millón de pasos.
2. **No podés mirar un tramo sin copiarlo.** Si querés "solo la palabra del medio", no hay forma de
hacerlo apuntando a la mitad: si cortás la calle, quedás sin cartel de FIN. O copiás a otra calle,
o **rompés el texto original** metiendo un `'\0'` en el medio.
3. **Un `\0` en el medio te corta el texto.** Si en algún lado hay un byte 0 (y en datos binarios
pasa), el texto "termina" ahí para `strlen`, aunque para vos siga.
```c
#include <stdio.h>
#include <string.h> /* strlen */
int main(void) {
const char *s = "hola mundo";
/* strlen cuenta PASOS hasta el '\0'. No hay un "largo" guardado en ningún lado:
lo descubre caminando. Por eso es O(n). */
printf("strlen = %zu\n", strlen(s)); /* 10 */
/* Y este ejemplo muestra la trampa del '\0' en el medio. */
const char bytes[5] = { 'h', 'o', '\0', 'l', 'a' };
printf("strlen del arreglo = %zu\n", strlen(bytes)); /* 2, ¡no 5! */
return 0;
}
```
> **El problema de fondo.** C guarda **solo la dirección**, nunca el largo. El largo hay que
> **descubrirlo** (caminando) o **adivinarlo**. Vos vas a hacer lo obvio: **guardar el largo**, una vez,
> y no volver a contarlo.
> **Nota fina.** En este capítulo `strlen` y compañía viven en `<string.h>`. Las vas a usar **para
> arrancar** (convertir lo viejo a lo nuevo), pero la gracia de tu librería es, justamente, **no
> depender** de tener que recorrer el texto de nuevo en cada operación.
## 1.4 Los literales viven en una vitrina blindada (`.rodata`)
Cuando escribís `"hola"` **directamente en el código**, esas letras no viven en cualquier lado: el
compilador las mete en una sección de **solo lectura** del binario, llamada **`.rodata`** (del inglés
*read-only data*). Es una **vitrina blindada**: podés **mirar** lo que hay adentro, pero si intentás
**tocarlo**, el sistema operativo te baja el telón con un **Segmentation Fault**.
```c
#include <stdio.h>
int main(void) {
/* El compilador ADVIERTE con esta línea (const char *). Prestá atención. */
const char *s = "hola";
/* ESTO ES UN ERROR EN TIEMPO DE EJECUCIÓN: s[0] es de solo lectura.
Descomentar la línea de abajo hace que el programa "crash" (Segmentation Fault).
La dejamos comentada a propósito para que puedas compilar y correr. */
/* s[0] = 'H'; */
printf("%s\n", s); /* imprimir (leer) sí se puede, siempre */
return 0;
}
```
Y si querés un texto **que sí podés modificar**, no lo definas con un literal: definilo como un
**arreglo** (`char buf[]`), que copia las letras a memoria escribible.
```c
#include <stdio.h>
int main(void) {
/* Con corchetes, el compilador crea un ARREGLO con su propia copia del texto:
esa copia SÍ es modificable. */
char buf[] = "hola";
buf[0] = 'H'; /* ahora sí se puede */
printf("%s\n", buf); /* Hola */
return 0;
}
```
> **La regla que te salva.** `const char *s = "literal";` → **solo leer**. Si necesitás escribir, usá
> `char buf[] = "literal";` (arreglo) o una `String_View`, que es **siempre de solo lectura** (mirá el
> `const` en `data`). Por eso tu librería **nunca** va a modificar la memoria original: solo la mira.
## 1.5 El nuevo paradigma: la ventana
Junta las tres ideas y tenés la solución. En vez de depender del cartel de FIN y de caminar la calle,
guardá **dos datos**:
- **`data`**: la dirección donde empieza lo que mirás.
- **`count`**: cuántos bytes querés mirar.
Eso es todo. Es como una **ventana** (o una foto recortada) apoyada sobre la calle: la calle sigue
estando completa; vos solo decidís **desde dónde** y **hasta dónde** mirás.
```c
typedef struct {
const char *data; /* Puntero al inicio del texto */
size_t count; /* Cantidad exacta de bytes */
} String_View;
```
Mirá la calle completa y una ventana sobre ella:
```text
calle: h o l a m u n d o \0
^
│
sv.data ─┘ sv.count = 10 → "hola mundo"
└────────── 10 bytes ──────────┘
la MISMA calle, otra ventana:
calle: h o l a m u n d o \0
^
│
sv.data ─┘ sv.count = 5 → "mundo"
```
Fijate algo hermoso: **ninguna de las dos ventanas cambió un byte**. Las dos apuntan a la misma calle.
"hola mundo" y "mundo" son **dos vistas** de la misma memoria. Eso es imposible de hacer gratis con
cadenas C terminadas en `'\0'`.
> **¿Y el `'\0'`?** Con `String_View` **no lo necesitás**: vos ya sabés el `count`. La vista puede
> apuntar al medio de un texto sin cartel de FIN alguno. El `\0` pasa de ser una **obligación** a ser
> un **detalle** que solo te importa cuando tenés que entregarle el texto a una función vieja que sí
> lo espera (como `printf`).
> **Cómo se lee `size_t`.** Es un entero **sin signo** pensado para tamaños (capítulo `16`). Es el tipo
> correcto para `count`: un largo **nunca** es negativo. Guardátelo para cuando quieras devolver un
> "no encontrado": ahí **no** vas a poder usar `size_t` (no tiene negativos) y vas a necesitar un
> centinela con signo. Lo ves en la Parte 2.
## 1.6 Imprimir sin `'\0'`: el truco del `%.*s`
Acá viene la primera piedra en el zapato. Si tenés una `String_View`, **no podés** hacer
`printf("%s", sv.data)`: `printf` va a caminar hasta encontrar un `\0`… que **quizás no exista** donde
vos querés (puede seguir el resto de la calle, o directamente no haber nunca). Necesitás decirle a
`printf`: **"mirá exactamente `count` bytes, ni uno más"**.
Para eso existe una combinación del formato de `printf`: **`%.*s`**. Se lee así:
- El **`.`** dice "precisión" (para `%s`, cuántos caracteres como máximo).
- El **`*`** dice "y ese máximo **no lo escribo acá**: te lo paso como argumento, justo antes del
puntero".
```c
#include <stdio.h>
int main(void) {
const char *data = "hola mundo entero";
size_t count = 10; /* queremos solo "hola mundo" */
/* %.*s = "imprimí una cadena, pero mirá a lo sumo N bytes".
El N llega como argumento (int) ANTES del puntero. Por eso van DOS argumentos. */
printf("[%.*s]\n", (int)count, data); /* [hola mundo] (sin el " entero") */
return 0;
}
```
¿Por qué `(int)count` y no `count` a secas? Porque el `*` de `printf` espera un **`int`**, y `count`
es `size_t` (que puede ser más ancho). El `cast` lo deja del tamaño exacto que `printf` va a leer. Si
te olvidás el cast, en muchas máquinas "funciona igual", pero es una bomba de tiempo: el tipo que
pasás tiene que coincidir con el que `printf` espera.
Como esto se usa **todo el tiempo**, lo natural es envolverlo en un **par de macros** que no te hagan
escribir el `.%*s` ni el `(int)` a mano en cada `printf`:
```c
#include <stdio.h>
/* SV_Fmt: el "molde" de formato, para pegar DENTRO de un printf junto al resto del texto. */
#define SV_Fmt "%.*s"
/* SV_Arg: expande a los DOS argumentos que %.*s necesita: el largo y el puntero. */
#define SV_Arg(sv) (int)(sv).count, (sv).data
```
Y se usan así, encajando en cualquier `printf`:
```c
printf("Nombre: [" SV_Fmt "]\n", SV_Arg(sv));
/* ↑ el molde ↑ los dos argumentos
equivale a:
printf("Nombre: [%.*s]\n", (int)sv.count, sv.data); */
```
> **Por qué las macros y no una función.** El `%.*s` tiene que quedar **pegado dentro** del `printf`
> (es parte del formato), y los dos argumentos tienen que ir **en el medio** de los demás. Una función
> no puede hacer eso: devuelve **un** valor, no "dos argumentos intercalados en un `printf`". Las
> macros sí, porque son **texto que se reemplaza**. Por eso esta pareja es el idioma estándar.
> **Pensalo así.** `%s` le dice a `printf` "caminá hasta el cartel de FIN". `%.*s` le dice "mirá
> exactamente tanto como yo te diga". Cambiaste "descubrir el largo" por "decir el largo". Es la misma
> idea de todo el capítulo, aplicada a la impresión.
## Práctica de la Parte 1
### Ejercicio 18.1 — El conversor (`sv_from_cstr`)
**Conceptos** · `struct`, `String_View`, `strlen`, `size_t`, punteros a `const`
**Contexto** · Todo tu código nuevo va a trabajar con `String_View`, pero el mundo entero está lleno de
cadenas C terminadas en `'\0'` (literales, `argv`, lo que devuelve `fgets`). Necesitás una **aduana**
que convierta de un mundo al otro. Simplificación: una función que mide una cadena C y la envuelve en
una vista.
**Consigna** · Implementá `sv_from_cstr`. Debe recibir un `const char *`, calcular su largo **una sola
vez** (con `strlen`) y devolver una `String_View` con ese `data` y ese `count`. Después, en el `main`,
creá una vista a partir de un literal y **imprimila** con `SV_Fmt`/`SV_Arg`, mostrando también el
`count`.
**Entrada** · Una cadena C (un literal o algo leído con `fgets`).
**Salida** · El texto de la vista y su largo en bytes.
**Escenario de prueba**
```text
entrada : "hola mundo"
salida : texto = [hola mundo]
count = 10
entrada : ""
salida : texto = []
count = 0
```
**Identificadores sugeridos** · `sv_from_cstr`, `sv`, `text`
**Criterios de aceptación**
- [ ] `strlen` se llama **una sola vez** (el largo no se recalcula después).
- [ ] `data` apunta al **mismo** texto original (no hay copia).
- [ ] `count` coincide con `strlen`.
- [ ] La cadena vacía da `count == 0` y no se rompe.
---
### Ejercicio 18.2 — La impresora de vistas (macro `SV_Fmt` + `sv_print`)
**Conceptos** · `%.*s`, `printf`, macros, `const char *`
**Contexto** · Cada vez que depures tu librería vas a querer ver una vista. Como `printf("%s", ...)` no
sirve (no hay `'\0'` garantizado), necesitás las macros de formato y una función cómoda. Simplificación:
imprimir una vista **sin** depender del terminador.
**Consigna** ·
1. Definí las macros `SV_Fmt` y `SV_Arg`.
2. Escribí `sv_print` que reciba una `String_View` y la imprima con `SV_Fmt`/`SV_Arg` (sin `\0`).
3. Comprobá que con una vista que **no** termina en `'\0'` (por ejemplo, un tramo en el medio de una
cadena C) la impresión **no** se pasa de `count`.
**Entrada** · Una o varias vistas (algunas a mitad de un texto).
**Salida** · El contenido exacto de cada vista, encerrado entre corchetes.
**Escenario de prueba**
```text
cadena completa : "hola mundo"
vista : data → "mundo", count = 5 (un tramo del medio, sin '\0' propio)
salida : [mundo] ← NO debe imprimir nada más allá del 'o'
```
```text
vista vacía (count = 0) → []
```
**Identificadores sugeridos** · `SV_Fmt`, `SV_Arg`, `sv_print`
**Criterios de aceptación**
- [ ] `sv_print` usa `%.*s` (no `%s`).
- [ ] Se imprime **exactamente** `count` bytes, sin mirar lo que sigue después.
- [ ] Una vista en el medio de un texto imprime **solo** su tramo.
- [ ] Una vista vacía imprime los corchetes vacíos, sin basura.
---
# PARTE 2 — Sub-vistas: mirar tramos sin copiar
Ya tenés la ventana. Ahora aprendé a **moverla**. Esta parte es corta y es el corazón del paradigma:
todo se reduce a **ajustar dos números**.
## 2.1 Recortar = mover el inicio, achicar el largo
Imaginate que querés la **primera palabra** de `"hola mundo"`. Con cadenas C: copiás a otro lado o
rompés el original. Con `String_View`: **no copiás nada**, solo hacés una **vista nueva** que empieza
donde mismo y mide menos.
```text
original: h o l a m u n d o \0
^
sv.data, sv.count = 10
sv_left(sv, 4): la MISMA dirección, pero count = 4
h o l a
^
misma data, count = 4 → "hola"
sv_right(sv, 5): esta vez SÍ movemos el inicio
m u n d o
^
data = data + 5, count = 5 → "mundo"
```
Fijate la diferencia entre las dos: `sv_left` mantiene el inicio y **acorta**; `sv_right` **mueve el
inicio** hacia adelante y ajusta el largo. Las dos devuelven una **vista nueva**, sin tocar la original
ni la memoria.
> **La cuenta que va a reaparecer siempre.** Para "los últimos `n`": el nuevo inicio es
> `data + (count - n)` y el nuevo largo es `n`. Para "los primeros `n`": el inicio no cambia, y el
> largo pasa a ser `min(n, count)`. Ese `min` es **la protección** contra pasarte: pedir 100 de un
> texto de 10 no es un error, es pedir "todo".
## 2.2 ¿Y si no encuentro lo que busco? El centinela
`sv_index_of` busca un carácter y devuelve **dónde** está. Pero, ¿qué devolvés si **no está**? Esta es
una decisión de diseño que se equivoca todo el mundo la primera vez.
- `size_t` **no sirve** para el "no está": es **sin signo**, no tiene `-1`. Si devolvieras el tamaño
del texto como "no encontrado", te confundirías con "está al final".
- Necesitás un valor que **nunca** sea un índice válido. El clásico es **`-1`**, y para eso necesitás un
tipo **con signo** (`int` o, más correcto para índices, `ptrdiff_t`, capítulo `16`).
```text
sv_index_of("banana", 'n') → 2 (índice de la primera 'n')
sv_index_of("banana", 'z') → -1 (centinela: "no está")
```
> **Regla general de diseño.** Cuando una función puede **fallar**, decidí **explícitamente** cómo
> avisa: o devuelve un **centinela** (como `-1`), o separa el "hubo suerte" en un `int` de retorno (como
> hace `sscanf`, capítulo `14`) y saca el valor por puntero. Nunca dejes el error "implícito": es el
> bug más caro de esta librería.
## Práctica de la Parte 2
### Ejercicio 18.3 — El buscador de un carácter (`sv_index_of`)
**Conceptos** · recorrido por índice, `const char *`, centinela `-1`, tipo con signo
**Contexto** · Antes de partir texto necesitás saber **dónde** están las marcas (la coma, el `=`, el
espacio). Simplificación: ubicar la primera aparición de un carácter.
**Consigna** · Implementá `sv_index_of`: recorre la vista y devuelve el **índice** (base 0) de la
primera aparición del carácter, o **`-1`** si no aparece. No uses `strchr` (esa necesita `'\0'`): el
recorrido es tuyo y está acotado por `count`. Comprobá con un carácter presente, uno ausente y el caso
de la vista vacía.
**Entrada** · Una `String_View` y un carácter.
**Salida** · El índice de la primera aparición, o `-1`.
**Escenario de prueba**
```text
sv = "banana"
sv_index_of(sv, 'b') → 0
sv_index_of(sv, 'n') → 2 (la PRIMERA 'n')
sv_index_of(sv, 'a') → 1
sv_index_of(sv, 'z') → -1 (centinela)
sv_index_of("", 'a') → -1
```
**Identificadores sugeridos** · `sv_index_of`, `i`, `found`
**Criterios de aceptación**
- [ ] El recorrido respeta `count` (no se pasa ni necesita `'\0'`).
- [ ] Se devuelve el índice **base 0** de la **primera** aparición.
- [ ] Ausente y vacío devuelven `-1`.
- [ ] El tipo de retorno es **con signo** (para poder representar `-1`).
---
### Ejercicio 18.4 — Los primeros n (`sv_left`)
**Conceptos** · sub-vista, `min`, protección de límites, `size_t`
**Contexto** · "Dame el prefijo de este texto" es lo que hacés para quedarte con una etiqueta, un código
de país, un encabezado. Simplificación: una vista con los primeros `n` bytes.
**Consigna** · Implementá `sv_left`: devuelve una **vista nueva** con los primeros `n` bytes. **No**
copies memoria: la `data` es la misma, solo cambia el `count`. Si `n` es **mayor** que el largo, devolvé
la vista completa (protección por `min`). `n = 0` da una vista vacía.
**Entrada** · Una `String_View` y un `n` (`size_t`).
**Salida** · Una nueva vista con la parte pedida.
**Escenario de prueba**
```text
sv = "hola mundo"
sv_left(sv, 4) → "hola"
sv_left(sv, 0) → "" (count = 0)
sv_left(sv, 100) → "hola mundo" (se recorta al total, no es error)
```
**Identificadores sugeridos** · `sv_left`, `n`, `result`
**Criterios de aceptación**
- [ ] No se copia ningún byte (misma `data`).
- [ ] `n` mayor que el largo devuelve la vista completa (usa `min`).
- [ ] `n = 0` da `count == 0`.
- [ ] La vista original **no** se modifica (se pasa por valor).
---
### Ejercicio 18.5 — Los últimos n (`sv_right`)
**Conceptos** · sub-vista, aritmética de punteros (`data + offset`), protección de límites
**Contexto** · "Dame la extensión de este archivo" (los últimos 4 caracteres de `.txt`) es un `sv_right`
de manual. Simplificación: una vista con los últimos `n` bytes.
**Consigna** · Implementá `sv_right`: devuelve una vista nueva con los **últimos** `n` bytes. Acá sí
hay que **mover el inicio**: `data + (count - n)`. Protegé el caso `n` mayor que el largo (devolvé la
vista completa) y `n = 0` (vista vacía), **sin** que la resta se dé vuelta (ojo con `size_t`, que no
tiene negativos).
**Entrada** · Una `String_View` y un `n` (`size_t`).
**Salida** · Una nueva vista con la parte pedida.
**Escenario de prueba**
```text
sv = "hola mundo"
sv_right(sv, 5) → "mundo"
sv_right(sv, 0) → ""
sv_right(sv, 100) → "hola mundo"
```
> **La trampa del `size_t`.** `count - n` con `size_t` y `n > count` **no** da un negativo: da un número
> gigante (le da la vuelta). Por eso **primero** hay que asegurar `n <= count`; recién después restar.
**Identificadores sugeridos** · `sv_right`, `n`, `start`, `result`
**Criterios de aceptación**
- [ ] El nuevo inicio es `data + (count - n)` cuando `n <= count`.
- [ ] `n` mayor que el largo devuelve la vista completa.
- [ ] `n = 0` da una vista vacía.
- [ ] No hay restas que "den la vuelta" por el `size_t`.
---
# PARTE 3 — Recorte (trim): sacar los bordes
Los archivos de configuración, las líneas de un CSV, lo que teclea un usuario… **todo** viene con
espacios de más. El **trim** los saca. Con `String_View` es casi gratis: recortar es **mover dos
números**.
## 3.1 `isspace`: qué cuenta como "espacio"
No es solo el espacio de la barra espaciadora. `<ctype.h>` te da `isspace`, que reconoce **seis**
caracteres en blanco:
| Carácter | Nombre | Cómo se escribe |
|---|---|---|
| espacio | *space* | `' '` |
| tabulación | *tab* | `'\t'` |
| salto de línea | *line feed* | `'\n'` |
| retorno de carro | *carriage return* | `'\r'` |
| tabulación vertical | *vertical tab* | `'\v'` |
| avance de formulario | *form feed* | `'\f'` |
`isspace(c)` devuelve **distinto de cero** ("verdadero") si `c` es uno de esos, y `0` ("falso") si no.
Es la forma **portable** de preguntar "¿es un blanco?", sin enumerar los seis a mano.
> **El detalle que casi nadie sabe.** A `isspace` hay que pasarle un valor que sea **representable como
> `unsigned char`** o `EOF`. Si le pasás un `char` con signo y el byte vale, por ejemplo, `0xC3` (el
> primer byte de una `á` en UTF-8), un `char` con signo lo ve como **negativo**, y eso es
> **comportamiento indefinido**. La forma segura es `isspace((unsigned char)c)`. Guardátelo.
## 3.2 Recortar a la izquierda: avanzar y achicar
`sv_trim_left` **mira desde el frente** y va descartando blancos. Cada blanco que saca **avanza `data`**
y **resta 1 a `count`**. Cuando topa con un byte que no es blanco, para.
```text
antes : " hola "
^ data, count = 9
mira el 1º: ' ' → blanco → data++, count--
mira el 1º (ahora): ' ' → blanco → data++, count--
...
después:"hola "
^ data ya movido hasta la 'h', count = 6
```
Fijate que **el texto original no cambia**: sigue teniendo los tres espacios al principio en memoria.
Lo que cambió es **qué tramo mira** la vista.
## 3.3 Recortar a la derecha: achicar el contador
`sv_trim_right` **mira desde el fondo** y da el mismo tratamiento, pero acortando `count` (el inicio no
se mueve).
```text
antes : " hola "
^ mirá los últimos
mira el último: ' ' → blanco → count--
...
después:" hola"
^ count ya achicado
```
> **El mismo dato, dos preguntas.** `trim_left` pregunta "¿el **primero** es blanco?"; `trim_right`,
> "¿el **último** es blanco?". El primero es `sv.data[0]`; el último es `sv.data[sv.count - 1]`.
> ¡Cuidado con `count == 0`! (una vista vacía no tiene "último": hay que chequear el largo **antes** de
> mirar el último byte).
## 3.4 `trim` = `trim_left` + `trim_right`
`sv_trim` es literalmente llamar a las dos, en cualquier orden (no se pisan: una toca el frente y la
otra el fondo). El resultado: el texto "pelado", sin blancos en ninguno de los dos extremos.
```text
" hola mundo " ──trim──► "hola mundo"
" " ──trim──► "" (una vista vacía: count = 0)
```
## 3.5 ¿Por qué estas reciben la vista por puntero?
Acá está la regla de oro del diseño de tu librería:
- Si una función **solo mira** y devuelve algo nuevo → recibe `String_View` **por valor** (una copia
del par `data`/`count`; cómodo y seguro).
- Si una función **modifica la vista misma** (la avanza) → recibe `String_View *` **por puntero**.
`trim_left` cambia la vista que le pasás ("consumila"), así que va **por puntero**: `sv_trim_left(&sv)`.
Así, quien llama ve el cambio. (Alternativa válida: devolver la vista nueva. Elegí una convención y
mantenela en **toda** la librería.)
## Práctica de la Parte 3
### Ejercicio 18.6 — Recorte a la izquierda (`sv_trim_left`)
**Conceptos** · `isspace`, `(unsigned char)`, punteros a `struct`, avance de inicio
**Contexto** · Al leer una línea de un archivo de configuración, el lado izquierdo casi siempre trae
indentación o tabs. Simplificación: descartar los blancos del frente, avanzando la vista.
**Consigna** · Implementá `sv_trim_left(String_View *sv)`: mientras el **primer** byte sea blanco
(`isspace`, con el cuidado del `unsigned char`), **avanzá** `data` y **restá** `count`. Detenete al
primer byte que no sea blanco o cuando la vista quede vacía. La función **no devuelve** nada: modifica
la vista recibida.
**Entrada** · Una vista, por puntero.
**Salida** · La misma vista, ya recortada por la izquierda (se ve en el `main`).
**Escenario de prueba**
```text
antes : " hola " (4 espacios al frente)
después: "hola "
antes : "hola" (sin blancos)
después: "hola" (no cambia)
antes : " " (solo blancos)
después: "" (count = 0)
```
**Identificadores sugeridos** · `sv_trim_left`, `sv`, `c`
**Criterios de aceptación**
- [ ] Usa `isspace` con `(unsigned char)` (no con un `char` con signo).
- [ ] Cada blanco del frente **avanza** `data` y **decrementa** `count`.
- [ ] Para en el primer byte no blanco (o al vaciarse).
- [ ] Modifica la vista **por puntero**; el texto original no se toca.
---
### Ejercicio 18.7 — Recorte a la derecha (`sv_trim_right`)
**Conceptos** · `isspace`, `count - 1`, chequear `count` antes de mirar, achicar el largo
**Contexto** · Las líneas de un archivo suelen venir con `'\r'` (retorno de carro) o espacios al final.
Simplificación: descartar los blancos del fondo, achicando el largo.
**Consigna** · Implementá `sv_trim_right(String_View *sv)`: mientras la vista **no esté vacía** y el
**último** byte (`data[count - 1]`) sea blanco, restá `1` a `count`. Chequeá **siempre** `count > 0`
antes de mirar el último byte (mirar `data[count - 1]` con `count == 0` es leer fuera de límites).
**Entrada** · Una vista, por puntero.
**Salida** · La misma vista, recortada por la derecha.
**Escenario de prueba**
```text
antes : " hola " (espacios al fondo)
después: " hola"
antes : "hola\r" (un retorno de carro al final)
después: "hola"
antes : "" (vista vacía)
después: "" (no se accede a data[-1])
```
**Identificadores sugeridos** · `sv_trim_right`, `sv`
**Criterios de aceptación**
- [ ] Chequea `count > 0` **antes** de leer `data[count - 1]`.
- [ ] Cada blanco del fondo **decrementa** `count` (no mueve `data`).
- [ ] Reconocen como blancos el `'\r'` y demás (vía `isspace`).
- [ ] Una vista vacía no produce accesos fuera de rango.
---
### Ejercicio 18.8 — Recorte total (`sv_trim`) y la trampa del doble recorte
**Conceptos** · composición de funciones, orden de operaciones, vistas que quedan vacías
**Contexto** · Un parser quiere el campo "pelado": sin blancos de ningún lado. Simplificación: recortar
frente y fondo de una vez.
**Consigna** · Implementá `sv_trim` llamando a `sv_trim_left` **y** a `sv_trim_right` (en el orden que
prefieras, y explicá en un comentario **por qué** el orden no importa). Comprobá el caso extremo: una
vista hecha **solo** de blancos tiene que terminar con `count == 0`, sin romperse.
**Entrada** · Varias vistas, con distinta cantidad de blancos.
**Salida** · Cada vista ya recortada, entre corchetes.
**Escenario de prueba**
```text
" hola mundo " → [hola mundo]
"hola" → [hola]
" " → [] (count = 0)
" " → [] (count = 0)
"a b" → [a b] (¡los espacios INTERNOS no se tocan!)
```
> **Ojo con esto.** `trim` saca los blancos de los **bordes**, no los de adentro. `"a b"` sigue siendo
> `"a b"`. Confundir "recortar los bordes" con "sacar todos los blancos" es un bug clásico de
> parseo.
**Identificadores sugeridos** · `sv_trim`
**Criterios de aceptación**
- [ ] `sv_trim` reusa `sv_trim_left` y `sv_trim_right` (no reimplementa).
- [ ] La vista "solo blancos" termina con `count == 0`.
- [ ] Los espacios **internos** se conservan.
- [ ] El comentario explica por qué el orden entre los dos recortes es indiferente.
---
# PARTE 4 — Cortar por delimitador (chop y split)
Esta es **la** parte donde el paradigma se saca la foto. Con `chop` vas a partir un texto en pedazos
**uno por uno**, avanzando la vista, **sin reservar un solo byte** de memoria nueva.
## 4.1 La tijera: `sv_chop_by_delim`
Imaginate una **tira larga de chicle** y una **tijera** en la coma. `chop` hace tres cosas, en orden:
1. **Busca** el delimitador (`sv_index_of`).
2. **Devuelve** el trozo de **antes** del delimitador.
3. **Avanza** la vista original para que arranque **justo después** del delimitador.
Pensalo como que **le cortás el primer pedazo a la tira**, y la tira que te queda (en `*sv`) empieza
después del corte.
```text
antes: sv ──► "ana,luis,pepe"
chop(sv, ',')
devuelve: "ana" (el trozo de antes)
y deja: sv ──► "luis,pepe" (arranca DESPUÉS de la coma)
```
Y el caso especial que **siempre** hay que resolver: **¿y si ya no queda delimitador?** (el último
pedazo). Entonces `chop` devuelve **todo** lo que queda y deja la vista **vacía**:
```text
antes: sv ──► "pepe"
devuelve: "pepe"
y deja: sv ──► "" (count = 0 → el bucle terminó)
```
Ese vaciado es lo que hace que el **bucle de split** corte solo, sin necesitar un contador aparte.
> **Fijate que `chop` combina todo lo anterior.** Usa `sv_index_of` (Parte 2) para ubicar la coma y
> **mueve el inicio** (como `sv_chop_left`, Parte 2) para consumirla. Lo que ya construiste se vuelve a
> usar. Eso es una librería: piezas pequeñas que se combinan.
## 4.2 El bucle de split: partir sin `malloc`
Con `chop`, partir una línea es un bucle de tres líneas. La condición de corte es elegante: **cuando la
vista queda vacía, se terminó**.
```text
línea: "ana,luis,pepe"
vuelta 1: chop → "ana" sv = "luis,pepe"
vuelta 2: chop → "luis" sv = "pepe"
vuelta 3: chop → "pepe" sv = "" ← sin coma, devuelve el resto
fin: sv vacía → el bucle corta
```
Cada pedazo es una **vista nueva** que apunta al **mismo texto original**. No copiaste nada, no pediste
memoria, no liberaste nada. Eso es un parser de velocidad absurda.
> **El detalle de la coma final.** Si el texto es `"a,b,"`, el último `chop` devuelve la vista vacía
> (entre la última coma y el final no hay nada). Un split correcto suele incluir ese último campo
> vacío. Decidí **explícitamente** qué querés y dejalo documentado: es una fuente inagotable de
> discusiones (mirá cómo `"a,b,".split(',')` se comporta distinto en cada lenguaje).
## Práctica de la Parte 4
### Ejercicio 18.9 — La tijera (`sv_chop_by_delim`)
**Conceptos** · `sv_index_of`, sub-vistas, pasar por puntero para consumir, avance de inicio
**Contexto** · Un tokenizador de CSV, de paths o de líneas `clave=valor` necesita **sacar el siguiente
pedazo** y dejar la vista lista para el siguiente. Simplificación: cortar por un delimitador.
**Consigna** · Implementá `sv_chop_by_delim(String_View *sv, char delim)`:
1. busca `delim` con `sv_index_of`;
2. si **lo encuentra** en el índice `i`, devolvé `sv_left(*sv, i)` y dejá `*sv` empezando **después** del
delimitador (`data + i + 1`, `count - i - 1`);
3. si **no** lo encuentra (`-1`), devolvé **todo** `*sv` y dejá `*sv` **vacía** (`count = 0`).
**Entrada** · Una vista por puntero y un delimitador.
**Salida** · El pedazo de antes del delimitador; la vista original queda avanzada.
**Escenario de prueba**
```text
sv = "ana,luis,pepe", delim = ','
chop → "ana" ; sv = "luis,pepe"
chop → "luis" ; sv = "pepe"
chop → "pepe" ; sv = "" (sin delimitador: devuelve el resto y vacía)
chop → "" ; sv = "" (ya estaba vacía)
sv = "sin,comas,aqui", delim = ';'
chop → "sin,comas,aqui" ; sv = "" (el delimitador no aparece)
```
**Identificadores sugeridos** · `sv_chop_by_delim`, `sv`, `delim`, `i`, `piece`
**Criterios de aceptación**
- [ ] Reusa `sv_index_of` (no reimplementa la búsqueda).
- [ ] Avanza la vista a `data + i + 1` con `count - i - 1` cuando encuentra el delimitador.
- [ ] Sin delimitador, devuelve el resto **y** deja la vista vacía.
- [ ] El delimitador **no** aparece en ningún pedazo devuelto.
---
### Ejercicio 18.10 — El split completo (bucle, sin `malloc`)
**Conceptos** · bucle `while` con condición sobre `count`, `chop`, impresión de tokens
**Contexto** · El `split` es el pan de cada día: partir una línea de campos. Simplificación: imprimir
cada campo de una línea separada por comas, **sin reservar memoria**.
**Consigna** · Escribí un programa que tome una línea separada por comas (fija en el `main`, o leída con
`fgets`) y, con un **bucle** que llame a `sv_chop_by_delim`, imprima **cada campo** entre corchetes, uno
por línea. Al terminar, mostrá **cuántos** campos salieron. Agregá al menos un campo con espacios
alrededor para forzar un `trim` por campo (reusá `sv_trim`).
**Entrada** · Una línea con campos separados por comas.
**Salida** · Un campo por línea y el total.
**Escenario de prueba**
```text
entrada : "ana, luis ,pepe"
salida :
[ana]
[luis]
[pepe]
campos: 3
```
```text
entrada : "solo"
salida :
[solo]
campos: 1
```
> **Regla de oro del bucle.** La condición es `sv.count > 0`. Cada `chop` **achica o vacía** la vista, así
> que el bucle **siempre** termina. Si te quedás en un bucle infinito, es porque olvidaste que el
> `chop` sin delimitador **vacía** la vista (revisá 18.9).
**Identificadores sugeridos** · `sv`, `field`, `count`, `line`
**Criterios de aceptación**
- [ ] El bucle usa `sv_chop_by_delim` y corta cuando la vista queda vacía.
- [ ] Cada campo se recorta con `sv_trim` (los espacios de alrededor se van).
- [ ] Se imprime un campo por línea.
- [ ] **No** hay ninguna llamada a `malloc` (ni a `strtok`, que además rompe el texto original).
---
# PARTE 5 — Reemplazar: mutación segura en un búfer
Llegó el momento incómodo: **reemplazar**. Acá la `String_View` muestra su límite, y entender **por qué**
es la clave de esta parte.
## 5.1 El problema: la vista no es dueña de la memoria
Una `String_View` es una **ventana**, no una casa: apunta a memoria que **no le pertenece** y que
normalmente es de **solo lectura** (recordá `.rodata`, Parte 1). Por eso no puede "crecer" el texto,
ni escribir en él.
¿Y cuando el reemplazo cambia el **tamaño**? Pasa siempre:
```text
reemplazar "o" por "oo":
"hola" → "hoola" el texto CRECIÓ: ¿dónde metés el byte de más?
reemplazar "aa" por "b":
"baaanana" → "banana" el texto ENCOGIÓ
```
Si el resultado ocupa distinto que la fuente, **no hay forma** de escribirlo "en el mismo lugar". La
vista no tiene lugar propio. La solución honesta es: **el que llama provee un búfer destino**, y la
función escribe ahí. La `String_View` sigue siendo **solo fuente de lectura**.
> **Nadie miente en esta librería.** Una operación que **no cambia el tamaño** (como reemplazar un
> carácter de 1 byte por otro de 1 byte) **sí podría** escribirse en el lugar… pero la vista es
> `const`: no se toca. La regla clara y segura es: **todas las transformaciones escriben en un búfer
> destino que vos manejás**. Menos sorpresas, menos *Segmentation Faults*.
## 5.2 El contrato: "escribí en MI búfer, sin desbordarlo"
Toda función de reemplazo de tu librería sigue **el mismo contrato**, y conviene que lo memorices:
```text
entradas:
sv → de dónde LEO (mi vista, const)
from → qué busco reemplazar
to → por qué lo cambio
out → dónde ESCRIBO (búfer que el que llama reservó)
outCap → cuántos bytes entran en out (SU tamaño)
salida:
cuántos bytes escribí realmente (para que sepas el largo del resultado)
```
Las **tres reglas de oro** de este contrato:
1. **Nunca escribas más de `outCap` bytes.** Si el resultado no entra, tenés que **avisar** (devolver
un valor especial) o **cortar**. Jamás desbordar.
2. **No confíes en que `out` es "suficientemente grande".** El que llama puede equivocarse; vos tenés
que ser el guardián.
3. **La vista de entrada es de solo lectura.** Escribís **solo** en `out`.
```text
sv: "banana" from='a' to='o'
│
▼ (leo, nunca escribo acá)
"banana"
│
▼ (escribo el resultado, con cuidado de no pasarme)
out:│ b | o | n | o | n | o | \0? |
└───────────────────────────┘
outCap = tamaño REAL de out
```
> **¿Y el `'\0'` al final del búfer?** Depende del contrato que elijas. Si querés que el resultado sea
> también una **cadena C** usable con `%s`, tenés que **reservar un byte** para el terminador y
> escribirlo. Por eso el contrato de `sv_to_cstr` pide "como máximo `outCap - 1` caracteres + el `\0`".
> Decidí y documentá: ¿el resultado lleva `'\0'` o no? (Con `String_View`, **no** lo necesita; para
> `printf("%s")`, **sí**.)
## 5.3 `sv_replace_char`: reemplazar un solo carácter
El caso más simple y el más común (cambiar comas por puntos, normalizar separadores). Recorrés la vista
**byte a byte** y, por cada uno, decidís qué escribís en `out`:
- si el byte es `from` → escribís `to`;
- si no → escribís el byte tal cual.
Contás cuántos reemplazaste (para informar) y **cuidás** no pasarte de `outCap`.
## Práctica de la Parte 5
### Ejercicio 18.11 — Reemplazo de un carácter (`sv_replace_char`)
**Conceptos** · búfer destino, contrato `out`/`outCap`, recorrido acotado, no desbordar
**Contexto** · Normalizar un CSV (cambiar `;` por `,`), tapar separadores, sanear texto: reemplazar un
carácter es una operación de todos los días. Simplificación: reemplazar **un** carácter por **otro**,
escribiendo en un búfer provisto por quien llama.
**Consigna** · Implementá `sv_replace_char(String_View sv, char from, char to, char *out, size_t outCap)`
que copie `sv` a `out`, cambiando cada `from` por `to`, y **devuelva cuántos bytes escribió**. Reglas:
nunca escribir más de `outCap` bytes (si el resultado no entra, avisá devolviendo un valor imposible,
por ejemplo `(size_t)-1`, o **cortá** y documentalo). La vista `sv` **no** se modifica. Comprobá con un
texto que entre justo, y con uno que **no** entre.
**Entrada** · Una vista, el carácter origen, el destino, y un búfer con su capacidad.
**Salida** · El texto reemplazado en el búfer y la cantidad de bytes escritos.
**Escenario de prueba**
```text
sv = "banana", from='a', to='o', outCap = 10 (búfer grande)
resultado → "bonono"
bytes escritos → 6
sv = "a;b;c", from=';', to=',', outCap = 10
resultado → "a,b,c"
bytes escritos → 5
sv = "aaaaaaaaaa" (10 'a'), from='a', to='o', outCap = 4 (¡no entra!)
resultado → (error/corte) : NO se desborda el búfer
```
**Identificadores sugeridos** · `sv_replace_char`, `from`, `to`, `out`, `outCap`, `written`
**Criterios de aceptación**
- [ ] Escribe **solo** en `out`; `sv` (o su memoria) no se toca.
- [ ] Nunca escribe más de `outCap` bytes (probado con el caso que no entra).
- [ ] Devuelve la cantidad de bytes realmente escritos.
- [ ] Si no entra, avisa de forma predecible (nunca desborda ni crashea).
---
### Ejercicio 18.12 — Reemplazo de una subcadena (`sv_replace_all`)
**Conceptos** · búsqueda de subcadena, búfer destino acotado, cambio de longitud, cálculo de tamaño
**Contexto** · Reemplazar **todas** las apariciones de una subcadena es el "buscar y reemplazar" de un
editor, o la limpieza de plantillas (`{{NAME}}`). Es más difícil porque **cambia el largo**.
Simplificación: reemplazar una subcadena `needle` por otra `with`, escribiendo en un búfer destino.
**Consigna** · Implementá `sv_replace_all(String_View sv, String_View needle, String_View with, char *out, size_t outCap)`
que recorra `sv` de izquierda a derecha; cada vez que encuentre `needle`, escriba `with` en `out`; si no,
copie el byte y avance uno. **Nunca** desbordes `outCap`. Devolvé cuántos bytes escribiste.
**Entrada** · La vista fuente, la subcadena a buscar, la subcadena de reemplazo, y el búfer destino con
su capacidad.
**Salida** · El texto con **todas** las apariciones reemplazadas y la cantidad de bytes escritos.
**Escenario de prueba**
```text
sv = "ababab", needle = "ab", with = "X", outCap = 16
resultado → "XXX"
bytes escritos → 3
sv = "aaabaaa", needle = "aaa", with = "Z", outCap = 16
resultado → "ZbZ"
sv = "hola", needle = "xy", with = "!!", outCap = 16
resultado → "hola" (no hay coincidencias: se copia tal cual)
```
> **El caso que te muerde: coincidencias solapadas.** En `"aaa"` buscando `"aa"`, ¿es una coincidencia
> o dos (con solape)? Decidilo y documentalo. La convención más común es **no solapar**: consumís la
> `needle` completa y seguís **después** de ella. Definí tu comportamiento y respetalo.
**Identificadores sugeridos** · `sv_replace_all`, `needle`, `with`, `out`, `outCap`, `written`, `i`
**Criterios de aceptación**
- [ ] Reemplaza **todas** las apariciones (no solo la primera).
- [ ] Nunca escribe más de `outCap` bytes.
- [ ] Reusa `sv_find` (Parte 6) o `sv_index_of` (Parte 2) para ubicar las coincidencias.
- [ ] Documenta qué hace con las coincidencias solapadas.
---
# PARTE 6 — El catálogo completo: comparar, buscar, construir
Ya tenés el esqueleto. Esta parte **redondea** la librería con las operaciones que faltan para que sea
de verdad usable. Ninguna es difícil, pero todas son **indispensables**.
## 6.1 Comparar sin `strcmp`: `sv_eq`
Comparar dos vistas es distinto que comparar dos cadenas C. Con cadenas C, `strcmp` camina hasta el
`'\0'` de cada una. Con vistas, vos **ya sabés** los largos: podés hacer dos chequeos rápidos.
**La idea del contrato de `sv_eq`:**
1. Si los `count` son **distintos** → ya son distintas (no compares nada más: un cortocircuito
gratis).
2. Si los `count` son **iguales** → recién ahí comparás byte a byte (con `memcmp` o con un bucle).
Esto es más rápido y más seguro que `strcmp`, y funciona con textos que no terminan en `'\0'`.
> **Ojo con el `'\0'` embebido.** `strcmp("a\0b", "a\0c")` diría "iguales" (para en el primer `\0`).
> `sv_eq` los ve **distintos**, porque compara los `count` (3 vs 3) y byte a byte (el `'\0'` del medio
> también cuenta). Ese es un beneficio: tu librería sirve para **datos**, no solo para texto.
**Y los "empieza con" / "termina con" (`starts_with` / `ends_with`)** son comparaciones **parciales**:
dicen si el **prefijo** o el **sufijo** de una vista coincide con otra. Se usan a montones: reconocer
una extensión (`.txt`), una etiqueta (`http://`), un comentario (`#`).
## 6.2 Buscar una subcadena: `sv_find` y `sv_contains`
`sv_index_of` busca **un** carácter. `sv_find` busca una **subcadena** completa: "¿en qué posición
aparece `needle` dentro de `haystack`?". Es el "buscar" de un editor.
La forma directa: en cada posición `i` desde donde todavía quepa `needle`, compará `needle` contra la
sub-vista que arranca en `i` (¡reusás `sv_left`/`sv_eq`!). Si coincide, devolvés `i`. Si no aparece en
ninguna, `-1`.
```text
haystack: b a n a n a
needle : a n a
posiciones a probar: i=0..3 (no más allá: no queda lugar)
i=0: "ban" vs "ana" → no
i=1: "ana" vs "ana" → ¡SÍ! → devuelve 1
```
`sv_contains` es solo `sv_find(...) != -1`: la pregunta "¿está?" como `int` (booleano).
## 6.3 Construir y cortar: `sv_from_parts`, `sv_take`/`sv_drop`
Para cerrar la librería, dos familias que vas a usar todo el tiempo:
- **`sv_from_parts(data, count)`**: envuelve un puntero y un largo **sin llamar a `strlen`**. Es el
equivalente "crudo" de `sv_from_cstr`, y la forma de crear una vista desde un **tramo** de memoria que
ya conocés (por ejemplo, de un `malloc`, o de un archivo leído con `fread`).
- **`sv_take` / `sv_drop`**: la pareja canónica de los *slice*. `sv_take(sv, n)` = "quedate con los
primeros `n`" (es `sv_left`). `sv_drop(sv, n)` = "avanzá `n`, tirando lo del frente" y devolvé el
resto. Con estas dos, "moverse por el texto" se vuelve una receta: **tomá lo que quieras, tirá el
resto**.
> **Por qué `from_parts` es clave.** Cuando leas un archivo entero a memoria (capítulo `15`) y quieras
> partirlo en líneas, tenés el puntero y el largo **en la mano**: no hay `'\0'` ni razón para llamar a
> `strlen`. `sv_from_parts(buf, n)` te deja **toda** la librería disponible sobre datos crudos, sin
> copiar y sin perder tiempo midiendo.
## Práctica de la Parte 6
### Ejercicio 18.13 — Comparaciones (`sv_eq`, `sv_starts_with`, `sv_ends_with`)
**Conceptos** · cortocircuito por `count`, comparación byte a byte, prefijo/sufijo
**Contexto** · Un clasificador de archivos decide por la extensión; un enrutador, por el prefijo de una
URL; un intérprete, por la primera palabra. Simplificación: tres comparaciones sobre vistas.
**Consigna** · Implementá:
1. `sv_eq(a, b)`: devuelve distinto de cero si son **iguales**. Cortocircuito: si los `count` difieren,
ya son distintas; si no, compará byte a byte.
2. `sv_starts_with(sv, prefix)`: ¿`sv` **empieza** con `prefix`?
3. `sv_ends_with(sv, suffix)`: ¿`sv` **termina** con `suffix`?
Probá casos verdaderos y falsos, incluido el `prefix`/`suffix` **más largo** que la vista (debe dar
falso, sin romperse) y el caso vacío.
**Entrada** · Pares de vistas.
**Salida** · El veredicto de cada comparación.
**Escenario de prueba**
```text
sv_eq("hola", "hola") → sí
sv_eq("hola", "hola ") → no (count distinto: 4 vs 5)
sv_eq("hola", "HOla") → no (distingue mayúsculas)
sv_starts_with("informe.txt", "inf") → sí
sv_starts_with("informe.txt", "txt") → no
sv_starts_with("txt", "informe") → no (prefijo más largo que la vista)
sv_ends_with("informe.txt", ".txt") → sí
sv_ends_with("informe.txt", ".doc") → no
```
**Identificadores sugeridos** · `sv_eq`, `sv_starts_with`, `sv_ends_with`
**Criterios de aceptación**
- [ ] `sv_eq` cortocircuita por `count` antes de comparar bytes.
- [ ] Las comparaciones son **sensibles a mayúsculas**.
- [ ] `prefix`/`suffix` más largos que la vista devuelven **falso** sin leer fuera de límites.
- [ ] El caso vacío (`count == 0`) se maneja bien.
---
### Ejercicio 18.14 — Búsqueda de subcadena (`sv_find`, `sv_contains`)
**Conceptos** · búsqueda subcadena, reusar `sv_eq`/sub-vistas, centinela, complejidad
**Contexto** · "¿Este log contiene `ERROR`?", "¿en qué columna queda la clave?" — buscar subcadenas está
por todos lados. Simplificación: ubicar la primera aparición de una aguja en un pajar.
**Consigna** · Implementá:
1. `sv_find(haystack, needle)`: devuelve el índice de la **primera** aparición de `needle` en
`haystack`, o **`-1`** si no aparece. Reusá `sv_eq` comparando cada sub-vista candidata (o `memcmp`).
2. `sv_contains(haystack, needle)`: devuelve "verdadero" si `needle` está.
Casos a cubrir: coincidencia al principio, en el medio, al final, ausente, `needle` vacío (¿da 0?),
`needle` más largo que `haystack` (debe dar `-1`).
**Entrada** · Pajar y aguja (vistas).
**Salida** · El índice encontrado, o `-1`; y el veredicto de `contains`.
**Escenario de prueba**
```text
sv_find("banana", "ana") → 1 (la primera)
sv_find("banana", "ban") → 0 (al principio)
sv_find("banana", "na") → 2
sv_find("banana", "xyz") → -1
sv_find("abc", "abcd") → -1 (aguja más larga que el pajar)
sv_find("abc", "") → 0 (aguja vacía: decidí y documentá)
sv_contains("hola mundo", "mundo") → sí
sv_contains("hola mundo", "Mundo") → no
```
**Identificadores sugeridos** · `sv_find`, `sv_contains`, `haystack`, `needle`, `i`
**Criterios de aceptación**
- [ ] El recorrido **no** prueba posiciones donde la aguja ya no entra.
- [ ] Reusa `sv_eq` (o `memcmp`) para comparar; no reimplementa la igualdad.
- [ ] Devuelve la **primera** aparición; `-1` si no está.
- [ ] El caso "aguja vacía" está decidido y documentado.
---
### Ejercicio 18.15 — Construir y cortar (`sv_from_parts`, `sv_take`, `sv_drop`)
**Conceptos** · construir sin `strlen`, *take*/*drop*, avanzar la vista
**Contexto** · Al partir un archivo entero en memoria tenés el puntero y el largo: no querés medir de
nuevo. Y para "caminar" por el texto, querés las dos operaciones de *slice*. Simplificación: construir
una vista "cruda" y moverse por ella.
**Consigna** · Implementá:
1. `sv_from_parts(const char *data, size_t count)`: envuelve puntero y largo **sin** `strlen`.
2. `sv_take(sv, n)`: devuelve los primeros `n` (podés reusar `sv_left`).
3. `sv_drop(sv, n)`: **avanza** la vista `n` bytes (tirando el frente) y devuelve el resto (protegé
`n > count`).
Demostralo con un texto: creá la vista con `from_parts`, hacé un `take` de la primera palabra y un
`drop` para saltarla, e imprimí los pedazos.
**Entrada** · Un puntero y un largo (o un literal), y un `n`.
**Salida** · Los pedazos pedidos.
**Escenario de prueba**
```text
sv = sv_from_parts("hola mundo", 10) (sin llamar a strlen)
sv_take(sv, 4) → "hola"
sv_drop(sv, 5) → "mundo" (saltea "hola " — 5 bytes)
sv_drop(sv, 100)→ "" (protegido: no da la vuelta)
además: sv_take(sv, 4) seguido de sv_left(sv,4) dan lo mismo (coherencia)
```
**Identificadores sugeridos** · `sv_from_parts`, `sv_take`, `sv_drop`, `n`
**Criterios de aceptación**
- [ ] `sv_from_parts` **no** llama a `strlen`.
- [ ] `sv_take` coincide con `sv_left`.
- [ ] `sv_drop` con `n > count` deja la vista vacía (sin resta que dé la vuelta).
- [ ] Los pedazos apuntan al texto original (no hay copias).
---
### Ejercicio 18.16 — El ejercicio jefe: un parser de configuración
**Conceptos** · todo: vistas, `trim`, `chop`, `index_of`, `eq`, `starts_with`, sin `malloc`
**Contexto** · Un archivo de configuración estilo `INI` tiene líneas `clave = valor`, comentarios que
empiezan con `#` o `;`, y líneas en blanco que se ignoran. Simplificación: un parser que lea el
contenido y muestre cada par `clave = valor` ya limpio, **sin reservar memoria**.
**Consigna** · Escribí un programa que:
1. Lea la entrada **completa** de `stdin` a un búfer acotado (por ejemplo 4 KB) leyendo con `fread` o
`fgets` en bucle (capítulos `14`/`15`).
2. Cree una vista del contenido con `sv_from_parts`.
3. Recorra la vista **línea por línea** (usá `\n` como delimitador de `chop`).
4. Por cada línea: `sv_trim`; si queda **vacía**, ignorala; si **empieza con** `#` o `;`, es un
comentario (ignorala o mostrala aparte).
5. Partila en `clave` y `valor` con `sv_chop_by_delim(line, '=')`; recortá **ambos** con `sv_trim`.
6. Imprimí cada par como `clave` → `valor`, separando la salida normal de los avisos (líneas raras) por
`stdout`/`stderr`.
7. Al final, mostrá **cuántas** claves válidas se leyeron.
**Entrada** · Un archivo INI por `stdin` (con comentarios, líneas en blanco y espacios de más).
**Salida** · Los pares `clave → valor`, más un resumen.
**Escenario de prueba**
```text
entrada (stdin):
# configuración del servidor
host = 127.0.0.1
; puerto por defecto
port= 8080
nombre = Servidor de Pruebas
salida (stdout):
host → 127.0.0.1
port → 8080
nombre → Servidor de Pruebas
claves válidas: 3
```
```text
entrada (stdin):
sin igual aca
salida:
(stderr) línea sin '=' (ignorada): "sin igual aca"
claves válidas: 0
```
**Identificadores sugeridos** · `sv`, `line`, `key`, `value`, `validCount`, `buffer`, `BUFFER_SIZE`
**Restricciones/pistas**
- **Prohibido `malloc`** y prohibido `strtok` (rompe el texto original metiendo `'\0'`). Todo el parseo
sale de tu librería.
- Reusá lo que ya escribiste: `sv_from_parts`, `sv_chop_by_delim`, `sv_trim`, `sv_starts_with`, `sv_eq`.
- Acotá **siempre** el búfer de lectura y detectá si el archivo no entra.
- La vista del contenido es de **solo lectura**: no modifiques el búfer mientras parseás.
- Ojo con una línea con **más de un** `=`: ¿te quedás con la primera parte como clave y el resto como
valor? Decidilo y documentalo.
- Avisos por `stderr`, resultado por `stdout`.
**Criterios de aceptación**
- [ ] **No** hay `malloc` ni `strtok` en ninguna parte.
- [ ] El contenido se envuelve con `sv_from_parts` (sin `strlen`).
- [ ] Se recorre línea por línea con `sv_chop_by_delim(sv, '\n')`.
- [ ] Cada línea se recorta con `sv_trim`; las vacías y los comentarios se ignoran.
- [ ] Clave y valor se recortan por separado.
- [ ] Una línea sin `=` se reporta por `stderr` y **no** rompe el parser.
- [ ] El conteo de claves válidas coincide con el escenario.
- [ ] El búfer de lectura está acotado y no hay desbordamientos.
---
## 18.17 Errores comunes
- **Recorrer sin acotar por `count`.** Tentador usar `while (*p) ...`, pero tu vista **no** promete un
`'\0'`. **Siempre** iterá de `0` a `count - 1`.
- **Devolver `-1` en un `size_t`.** `size_t` no tiene negativos: el "no encontrado" va en un tipo
**con signo** (`int`, `ptrdiff_t`).
- **Restar restar con `size_t` cuando `n > count`.** `count - n` da la vuelta y se vuelve gigante.
Chequeá `n <= count` **antes** de restar.
- **Usar `%s` con una vista.** `%s` camina hasta un `'\0'` que puede no existir. Usá **`%.*s`**
(`SV_Fmt`/`SV_Arg`).
- **Pasar `char` con signo a `isspace`.** Un byte `>= 0x80` se vuelve negativo → comportamiento
indefinido. Pasá `(unsigned char)c`.
- **Modificar memoria de solo lectura.** Un literal (`"..."`) vive en `.rodata`: escribir ahí es
*Segmentation Fault*. Tu vista es `const` por eso.
- **Escribir a `data[count - 1]` con `count == 0`.** Leer/escribir "un byte antes del inicio" es acceso
fuera de límites. Chequeá el largo **antes** de tocar el último byte.
- **Confundir trim con "sacar todos los blancos".** `trim` toca **solo los bordes**; los internos
quedan.
- **Reemplazar sin controlar `outCap`.** Un reemplazo que **crece** el texto desborda el búfer si no
chequeás el tamaño. Sé el guardián.
- **Usar `strtok`.** Rompe el texto original (mete `'\0'`) y no es *reentrante*. Con `chop` no lo
necesitás.
- **Casos solapados en `replace_all`** (`"aaa"` con `"aa"`): definí la convención y documentala.
- **`sv_find` con aguja vacía**: decidí (y documentá) si devuelve `0`, `count`, o es un error.
---
## 18.18 Cierre
Si tuvieras que quedarte con **una sola idea**, que sea esta: **un texto es una calle de bytes, y una
`String_View` es una ventana apoyada sobre esa calle**. Todo lo demás se deduce de ahí.
- Dejaste de depender del **`'\0'`**: en vez de caminar hasta el cartel, **guardás el largo** (`count`)
y listo.
- Aprendiste a **mirar y cortar sin copiar**: mover `data` y ajustar `count` es toda tu librería. Por
eso funciona **sin `malloc`** y a velocidad de vértigo.
- Diseñaste **contratos** claros: quién pasa por valor, quién por puntero, dónde va cada centinela,
dónde vive el resultado de un reemplazo (en **tu** búfer, nunca en la vista).
- Escribiste una **librería completa**: conversión, impresión, búsqueda, sub-vistas, recorte, corte por
delimitador, reemplazo, comparación y un parser de configuración **sin reservar un solo byte**.
**Resumen de la caja de herramientas.**
| Quiero… | Función | Idea en una frase |
|---|---|---|
| Convertir una cadena C | `sv_from_cstr` | Mide **una vez** con `strlen` y envuelve. |
| Construir sin medir | `sv_from_parts` | Envolvé puntero + largo que ya tenés. |
| Imprimir sin `'\0'` | `SV_Fmt` / `SV_Arg` | `%.*s`: decile a `printf` cuánto mirar. |
| Buscar un carácter | `sv_index_of` | Índice, o `-1`. |
| Buscar una subcadena | `sv_find` / `sv_contains` | Comparar la aguja en cada posición. |
| Quedarme con el principio | `sv_left` / `sv_take` | Misma `data`, `count` más chico. |
| Quedarme con el final | `sv_right` | Mover `data`, ajustar `count`. |
| Saltear el frente | `sv_drop` / `sv_chop_left` | Avanzar `data`. |
| Sacar blancos | `sv_trim_left` / `_right` / `sv_trim` | Ajustar inicio y/o largo. |
| Partir por delimitador | `sv_chop_by_delim` | Devolver el trozo y **avanzar** la vista. |
| Comparar | `sv_eq` | Si `count` difiere, ya son distintas. |
| Prefijo / sufijo | `sv_starts_with` / `sv_ends_with` | Comparación parcial. |
| Reemplazar | `sv_replace_char` / `sv_replace_all` | Escribir en **tu** búfer, sin desbordar. |
| Hacia cadena C | `sv_to_cstr` | Copiar y **agregar** el `'\0'`. |
> **El puente a lo que sigue.** Ahora que tenés vistas, el texto deja de ser un enemigo. Con la misma
> idea (puntero + largo) podés construir un **arena** de strings (capítulo `12`), un **tokenizador** de
> un lenguaje, o el parser de una línea de comandos sin reservar memoria. Y cuando una función vieja
> te pida una cadena C, sabés cerrar el círculo: `sv_to_cstr` y el `\0` vuelven a escena, pero **solo
> cuando vos querés**. La calle y la ventana ahora las manejás vos.