# 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 /* 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 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 #include /* 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 ``. 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 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 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 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 /* 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. `` 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.