Semana 11 — Biblioteca de cadenas y matrices

Objetivos

  • Usar las funciones principales de string.h.
  • Declarar matrices y recorrerlas por filas y por columnas.
  • Explicar cómo se guarda una matriz en memoria.

Teoría — Biblioteca de cadenas · Matrices

Lectura: Apuntes, semana 11.

Temas:

  • strlen, strcmp, strncpy, strchr y strstr. Por qué == no compara cadenas.
  • Matrices: arreglos bidimensionales en memoria, fila por fila.
  • Recorrer por filas y por columnas. Ejemplo: notas por estudiante y por curso.
  • Pasar una matriz a una función.
  • Sumar y transponer matrices.

Laboratorio — Cadenas y matrices

Esta semana la sesión de laboratorio es para la Práctica 3. Las actividades de abajo son trabajo autónomo y no se entregan. Conviene hacerlas antes de la práctica.

  1. Contar cuántas veces aparece una palabra en un texto con strstr.
  2. Guardar las notas de varios estudiantes en una matriz. Calcular el promedio por estudiante y por curso.
  3. Hallar al mejor estudiante de cada curso.
  4. Transponer una matriz y comprobarlo con assert.

Entregas

Preguntas de repaso

  1. ¿Por qué if (a == b) no compara dos cadenas? ¿Qué compara?
  2. ¿Qué devuelve strcmp y cómo se usa para ordenar?
  3. ¿Cuántos bytes ocupa int m[3][4] si cada int ocupa 4? ¿Dónde está m[1][2] si empieza en 1000?
  4. ¿Cómo sumarían cada columna de una matriz? Escríbanlo en pseudocódigo.
  5. ¿Por qué al pasar una matriz a una función hay que indicar el número de columnas?

Código de la semana

Compilar y probar dentro del contenedor del curso con make y make test.

Ejemplos (12)Errores (18)Pruebas (3)Laboratorio (2)Compilación (1)

Ejemplos

Los programas que usan los apuntes y la clase.

append.cDescargar append.c
// Agregar al final de una cadena sin pasarse del tamaño del arreglo.
#include <stdio.h>
#include <string.h>

#define BIG 20
#define SMALL 8

// strncat agrega como máximo el número que recibe, y después siempre pone '\0'.
// Por eso se le da lo que queda libre, sin contar el byte del '\0'.
void safe_append(char dest[], const char src[], int size) {
    int used = (int) strlen(dest);
    strncat(dest, src, size - used - 1);
}

int main(void) {
    char big[BIG] = "Ana";
    char small[SMALL] = "Ana";

    safe_append(big, " Quispe", BIG);
    printf("%s\n", big);
    safe_append(small, " Quispe", SMALL);
    printf("%s\n", small);
    return 0;
}
check_lab.cDescargar check_lab.c
// Parte de las pruebas del laboratorio: test_lab.py llama con esto a sus funciones.
// No hace falta leerlo. Usa argumentos de la línea de comandos, que no vemos en el curso.
#include <stdio.h>
#include <stdlib.h>

#define STUDENTS 4
#define COURSES 3

int count_word(const char text[], const char word[]);
void student_averages(int grades[][COURSES], int students, double averages[]);
void course_averages(int grades[][COURSES], int students, double averages[]);
void best_students(int grades[][COURSES], int students, int best[]);
void transpose(int grades[][COURSES], int result[][STUDENTS]);
int is_transpose(int grades[][COURSES], int result[][STUDENTS]);

// Copia el texto a un arreglo propio, para que la función reciba memoria de la pila.
static void copy(char dest[], const char src[], int size) {
    int i = 0;
    while (src[i] != '\0' && i < size - 1) {
        dest[i] = src[i];
        i++;
    }
    dest[i] = '\0';
}

// Lee la matriz de los argumentos, fila por fila, y guarda una copia.
static void read_matrix(char *argv[], int first, int grades[][COURSES], int saved[][COURSES]) {
    for (int i = 0; i < STUDENTS; i++) {
        for (int j = 0; j < COURSES; j++) {
            grades[i][j] = atoi(argv[first + i * COURSES + j]);
            saved[i][j] = grades[i][j];
        }
    }
}

// Avisa si la función cambió la matriz que debía solo leer.
static void check_intact(int grades[][COURSES], int saved[][COURSES]) {
    for (int i = 0; i < STUDENTS; i++) {
        for (int j = 0; j < COURSES; j++) {
            if (grades[i][j] != saved[i][j]) {
                printf("la función cambió grades\n");
                return;
            }
        }
    }
}

static void print_doubles(const double a[], int n) {
    for (int i = 0; i < n; i++) {
        printf("%.2f ", a[i]);
    }
    printf("\n");
}

static int run_words(int argc, char *argv[]) {
    char text[128];
    char word[64];
    if (argc != 4) {
        return 2;
    }
    copy(text, argv[2], 128);
    copy(word, argv[3], 64);
    printf("%d\n", count_word(text, word));
    return 0;
}

static int run_matrix(char op, int argc, char *argv[]) {
    int grades[STUDENTS][COURSES];
    int saved[STUDENTS][COURSES];
    int total_args = 3 + STUDENTS * COURSES;

    if (argc < total_args) {
        return 2;
    }
    int students = atoi(argv[2]);
    read_matrix(argv, 3, grades, saved);

    if (op == 's') {
        double averages[STUDENTS] = {-1, -1, -1, -1};
        student_averages(grades, students, averages);
        print_doubles(averages, STUDENTS);
    } else if (op == 'c') {
        double averages[COURSES] = {-1, -1, -1};
        course_averages(grades, students, averages);
        print_doubles(averages, COURSES);
    } else if (op == 'b') {
        int best[COURSES] = {-1, -1, -1};
        best_students(grades, students, best);
        printf("%d %d %d\n", best[0], best[1], best[2]);
    } else if (op == 't') {
        int result[COURSES][STUDENTS];
        transpose(grades, result);
        for (int j = 0; j < COURSES; j++) {
            for (int i = 0; i < STUDENTS; i++) {
                printf("%d ", result[j][i]);
            }
            printf("\n");
        }
    } else if (op == 'i') {
        int other[COURSES][STUDENTS];
        if (argc != total_args + STUDENTS * COURSES) {
            return 2;
        }
        for (int j = 0; j < COURSES; j++) {
            for (int i = 0; i < STUDENTS; i++) {
                other[j][i] = atoi(argv[total_args + j * STUDENTS + i]);
            }
        }
        printf("%d\n", is_transpose(grades, other));
    } else {
        return 2;
    }
    check_intact(grades, saved);
    return 0;
}

int main(int argc, char *argv[]) {
    if (argc < 2) {
        return 2;
    }
    if (argv[1][0] == 'w') {
        return run_words(argc, argv);
    }
    return run_matrix(argv[1][0], argc, argv);
}
compare.cDescargar compare.c
// strcmp: solo importa el signo de lo que devuelve.
#include <stdio.h>
#include <string.h>

void show_order(const char a[], const char b[]) {
    int result = strcmp(a, b);
    if (result < 0) {
        printf("\"%s\" va antes que \"%s\"\n", a, b);
    } else if (result > 0) {
        printf("\"%s\" va después de \"%s\"\n", a, b);
    } else {
        printf("\"%s\" y \"%s\" son iguales\n", a, b);
    }
}

int main(void) {
    show_order("ana", "ana");
    show_order("ana", "ano");
    show_order("an", "ana");
    show_order("sol", "luna");
    show_order("Z", "a");
    show_order("z", "ñ");
    return 0;
}
find_text.cDescargar find_text.c
// strchr y strstr devuelven una dirección, o NULL si no encuentran nada.
#include <stdio.h>
#include <string.h>

int main(void) {
    char email[] = "ana@example.com";
    char text[] = "el año pasado llovió en Lima";

    if (strchr(email, '@') != NULL) {
        printf("Tiene @: %s\n", strchr(email, '@'));
    }
    if (strchr(email, '#') == NULL) {
        printf("No tiene #\n");
    }
    if (strstr(text, "Lima") != NULL) {
        printf("Desde Lima: %s\n", strstr(text, "Lima"));
    }
    if (strstr(text, "lima") == NULL) {
        printf("lima, en minúsculas, no aparece\n");
    }
    if (strstr(text, "ñ") != NULL) {
        printf("Desde la ñ: %s\n", strstr(text, "ñ"));
    }
    // &text[3] es la dirección del byte 3: lo que recibe la función empieza ahí.
    printf("Desde el byte 3: %s\n", &text[3]);
    if (strstr(text, "el") != NULL) {
        printf("Desde el byte 0 hay un el\n");
    }
    if (strstr(&text[1], "el") == NULL) {
        printf("Desde el byte 1 no hay ninguno\n");
    }
    return 0;
}
grades_table.cDescargar grades_table.c
// Notas de 3 estudiantes en 4 cursos: promedios por fila y por columna.
#include <stdio.h>

#define STUDENTS 3
#define COURSES 4

int main(void) {
    int grades[STUDENTS][COURSES] = {
        {14, 9, 17, 12},
        {11, 16, 8, 13},
        {18, 10, 15, 20},
    };

    // Por filas: cada estudiante, y dentro de él, cada curso.
    for (int i = 0; i < STUDENTS; i++) {
        int total = 0;
        for (int j = 0; j < COURSES; j++) {
            total = total + grades[i][j];
        }
        printf("Estudiante %d: suma %d, promedio %.2f\n", i, total, (double) total / COURSES);
    }

    // Por columnas: cada curso, y dentro de él, cada estudiante.
    for (int j = 0; j < COURSES; j++) {
        int total = 0;
        for (int i = 0; i < STUDENTS; i++) {
            total = total + grades[i][j];
        }
        printf("Curso %d: suma %d, promedio %.2f\n", j, total, (double) total / STUDENTS);
    }
    return 0;
}
lengths.cDescargar lengths.c
// strlen cuenta bytes hasta el '\0'. Devuelve size_t, un tipo sin signo.
#include <stdio.h>
#include <string.h>

int main(void) {
    char t[8] = "hola";

    printf("strlen(t) = %zu\n", strlen(t));
    printf("sizeof(t) = %zu\n", sizeof(t));
    printf("strlen(\"\") = %zu\n", strlen(""));
    printf("strlen(\"año\") = %zu\n", strlen("año"));

    // Para recorrer con un int, se guarda la longitud una vez, con un cast.
    int n = (int) strlen(t);
    for (int i = n - 1; i >= 0; i--) {
        printf("%c", t[i]);
    }
    printf("\n");
    return 0;
}
matrix_add.cDescargar matrix_add.c
// Sumar dos matrices del mismo tamaño, casilla por casilla.
#include <stdio.h>

#define ROWS 2
#define COLS 3

void add_matrices(int a[][COLS], int b[][COLS], int result[][COLS], int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < COLS; j++) {
            result[i][j] = a[i][j] + b[i][j];
        }
    }
}

int main(void) {
    int a[ROWS][COLS] = {{1, 2, 3}, {4, 5, 6}};
    int b[ROWS][COLS] = {{10, 20, 30}, {40, 50, 60}};
    int result[ROWS][COLS];

    add_matrices(a, b, result, ROWS);
    for (int i = 0; i < ROWS; i++) {
        for (int j = 0; j < COLS; j++) {
            printf("%3d", result[i][j]);
        }
        printf("\n");
    }
    return 0;
}
matrix_layout.cDescargar matrix_layout.c
// Una matriz se guarda fila por fila, con los elementos seguidos.
#include <stdio.h>

#define ROWS 3
#define COLS 4

int main(void) {
    int m[ROWS][COLS] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12},
    };

    printf("sizeof(m) = %zu\n", sizeof(m));
    printf("sizeof(m[0]) = %zu\n", sizeof(m[0]));
    printf("sizeof(m[0][0]) = %zu\n", sizeof(m[0][0]));
    printf("m[1][2] = %d\n", m[1][2]);

    for (int i = 0; i < ROWS; i++) {
        for (int j = 0; j < COLS; j++) {
            printf("m[%d][%d] = %2d está en %p\n", i, j, m[i][j], (void *) &m[i][j]);
        }
    }
    return 0;
}
matrix_param.cDescargar matrix_param.c
// Pasar una matriz a una función: las columnas van en el tipo, las filas en un parámetro.
#include <stdio.h>

#define COLS 4

void print_matrix(int m[][COLS], int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < COLS; j++) {
            printf("%3d", m[i][j]);
        }
        printf("\n");
    }
}

int column_sum(int m[][COLS], int rows, int column) {
    int total = 0;
    for (int i = 0; i < rows; i++) {
        total = total + m[i][column];
    }
    return total;
}

// Como con un arreglo, la función trabaja sobre la matriz original.
void add_bonus(int m[][COLS], int rows, int bonus) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < COLS; j++) {
            m[i][j] = m[i][j] + bonus;
        }
    }
}

int main(void) {
    int grades[3][COLS] = {
        {14, 9, 17, 12},
        {11, 16, 8, 13},
        {18, 10, 15, 20},
    };

    print_matrix(grades, 3);
    printf("Suma del curso 0: %d\n", column_sum(grades, 3, 0));
    printf("Suma del curso 0, solo 2 filas: %d\n", column_sum(grades, 2, 0));
    add_bonus(grades, 2, 1);
    print_matrix(grades, 3);
    return 0;
}
names.cDescargar names.c
// Un arreglo de cadenas es una matriz de char: cada fila guarda una cadena.
#include <stdio.h>
#include <string.h>

#define COUNT 4
#define NAME_SIZE 12

// Requiere size >= 1. Es la copia segura de safe_copy.c.
void safe_copy(char dest[], const char src[], int size) {
    strncpy(dest, src, size - 1);
    dest[size - 1] = '\0';
}

// Devuelve el índice de la cadena que va primero según strcmp.
int first_name(char names[][NAME_SIZE], int n) {
    int first = 0;
    for (int i = 1; i < n; i++) {
        if (strcmp(names[i], names[first]) < 0) {
            first = i;
        }
    }
    return first;
}

int main(void) {
    char names[COUNT][NAME_SIZE] = {"Rosa", "Ana", "Luis", "Zoila"};

    printf("sizeof(names) = %zu\n", sizeof(names));
    for (int i = 0; i < COUNT; i++) {
        printf("names[%d] = %s, %zu bytes de texto\n", i, names[i], strlen(names[i]));
    }
    printf("Primero: %s\n", names[first_name(names, COUNT)]);

    safe_copy(names[1], "Maria de los Angeles", NAME_SIZE);
    printf("names[1] = %s\n", names[1]);
    printf("Primero: %s\n", names[first_name(names, COUNT)]);
    return 0;
}
safe_copy.cDescargar safe_copy.c
// Copiar con strncpy sin perder el '\0': se copia size - 1 y se pone a mano.
#include <stdio.h>
#include <string.h>

#define SIZE 5

// Requiere size >= 1. Da el mismo texto que copy_text; además, strncpy rellena lo que sobra.
void safe_copy(char dest[], const char src[], int size) {
    strncpy(dest, src, size - 1);
    dest[size - 1] = '\0';
}

int main(void) {
    char word[SIZE];

    safe_copy(word, "sol", SIZE);
    printf("%s\n", word);
    safe_copy(word, "luna", SIZE);
    printf("%s\n", word);
    safe_copy(word, "estrella", SIZE);
    printf("%s\n", word);
    return 0;
}
strncpy_bytes.cDescargar strncpy_bytes.c
// Lo que strncpy deja en el destino: a veces sin '\0', a veces con muchos.
#include <stdio.h>
#include <string.h>

#define SIZE 5

// Escribe los bytes como números. No usa %s: el arreglo puede no ser una cadena.
void print_bytes(const char a[], int size) {
    for (int i = 0; i < size; i++) {
        printf(" %d", a[i]);
    }
    printf("\n");
}

void fill(char dest[], const char src[]) {
    strncpy(dest, src, SIZE);
}

int main(void) {
    char word[SIZE];

    fill(word, "sol");
    print_bytes(word, SIZE);
    fill(word, "luna");
    print_bytes(word, SIZE);
    fill(word, "estrella");
    print_bytes(word, SIZE);
    return 0;
}

Errores

Ejemplos con errores a propósito: no compilan, fallan o dan un resultado incorrecto. Los apuntes explican cada uno.

errors/const_matrix.cDescargar errors/const_matrix.c
// const en un parámetro matriz: gcc lo acepta, pero C17 no lo permite.
#include <stdio.h>

#define COLS 3

int first(const int m[][COLS]) {
    return m[0][0];
}

int main(void) {
    int m[2][COLS] = {{1, 2, 3}, {4, 5, 6}};

    printf("%d\n", first(m));
    return 0;
}
errors/equal_arrays.cDescargar errors/equal_arrays.c
// Error: == con dos arreglos compara sus direcciones, no su contenido.
#include <stdio.h>

int main(void) {
    char a[] = "hola";
    char b[] = "hola";

    if (a == b) {
        printf("iguales\n");
    } else {
        printf("distintas\n");
    }
    return 0;
}
errors/equal_literals.cDescargar errors/equal_literals.c
// Error: C no dice si dos literales iguales están en el mismo lugar.
#include <stdio.h>

int main(void) {
    printf("%d\n", "hola" == "hola");
    return 0;
}
errors/equal_params.cDescargar errors/equal_params.c
// Error: dentro de una función, == no da ninguna advertencia. Sigue comparando direcciones.
#include <stdio.h>

int same_text(const char a[], const char b[]) {
    return a == b;
}

int main(void) {
    char a[] = "hola";
    char b[] = "hola";

    printf("a con b: %d\n", same_text(a, b));
    printf("a con a: %d\n", same_text(a, a));
    return 0;
}
errors/last_char.cDescargar errors/last_char.c
// Error: con la cadena vacía, s[strlen(s) - 1] está fuera del arreglo.
#include <stdio.h>
#include <string.h>

char last_char(const char s[]) {
    return s[strlen(s) - 1];
}

int main(void) {
    char word[] = "sol";
    char empty[] = "";

    printf("%c\n", last_char(word));
    printf("%c\n", last_char(empty));
    return 0;
}
errors/matrix_bounds.cDescargar errors/matrix_bounds.c
// Error: una columna que no existe. Los índices se leen de la entrada.
#include <stdio.h>

int main(void) {
    int m[2][3] = {{1, 2, 3}, {4, 5, 6}};
    int j;

    if (scanf("%d", &j) != 1) {
        printf("Entrada inválida\n");
        return 1;
    }
    printf("m[0][%d] = %d\n", j, m[0][j]);
    return 0;
}
errors/matrix_comma.cDescargar errors/matrix_comma.c
// Error: m[1, 2] no es la casilla de la fila 1 y la columna 2.
#include <stdio.h>

int main(void) {
    int m[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};

    printf("%d\n", m[1, 2]);
    return 0;
}
errors/matrix_no_columns.cDescargar errors/matrix_no_columns.c
// Error: el parámetro no dice cuántas columnas tiene la matriz.
#include <stdio.h>

int first(int m[][]) {
    return m[0][0];
}

int main(void) {
    int m[2][3] = {{1, 2, 3}, {4, 5, 6}};

    printf("%d\n", first(m));
    return 0;
}
errors/matrix_wrong_columns.cDescargar errors/matrix_wrong_columns.c
// Error: la función espera 3 columnas y la matriz tiene 4.
#include <stdio.h>

int first(int m[][3]) {
    return m[0][0];
}

int main(void) {
    int m[2][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}};

    printf("%d\n", first(m));
    return 0;
}
errors/sign_compare.cDescargar errors/sign_compare.c
// Advertencia: comparar un int con el resultado de strlen mezcla signo y sin signo.
#include <stdio.h>
#include <string.h>

int main(void) {
    char s[] = "sol";

    for (int i = 0; i < strlen(s); i++) {
        printf("%c\n", s[i]);
    }
    return 0;
}
errors/strcat_overflow.cDescargar errors/strcat_overflow.c
// Error: strcat no recibe el tamaño del destino. Compila sin advertencias.
#include <stdio.h>
#include <string.h>

int main(void) {
    char word[20];
    char full[8] = "Ana ";

    if (scanf("%19s", word) != 1) {
        printf("Entrada inválida\n");
        return 1;
    }
    strcat(full, word);
    printf("%s\n", full);
    return 0;
}
errors/strcat_overlap.cDescargar errors/strcat_overlap.c
// Error: el origen y el destino de strcat no pueden pisarse.
#include <stdio.h>
#include <string.h>

int main(void) {
    char s[20] = "hola";

    strcat(s, s);
    printf("%s\n", s);
    return 0;
}
errors/strcmp_wrong.cDescargar errors/strcmp_wrong.c
// Error: strcmp devuelve 0 si son iguales, y en un if el 0 es falso.
#include <stdio.h>
#include <string.h>

int main(void) {
    char a[] = "hola";
    char b[] = "hola";

    if (strcmp(a, b)) {
        printf("iguales\n");
    } else {
        printf("distintas\n");
    }
    return 0;
}
errors/strlen_wrap.cDescargar errors/strlen_wrap.c
// Error: strlen da un valor sin signo. Restar 1 a un 0 da la vuelta.
#include <stdio.h>
#include <string.h>

int main(void) {
    char empty[] = "";

    printf("strlen(empty) - 1 = %zu\n", strlen(empty) - 1);
    if (strlen(empty) - 1 >= 0) {
        printf("Esta condición siempre se cumple\n");
    }
    return 0;
}
errors/strncpy_no_null.cDescargar errors/strncpy_no_null.c
// Error: si el texto no cabe, strncpy no pone '\0', y %s sigue de largo.
#include <stdio.h>
#include <string.h>

#define SIZE 5

void fill(char dest[], const char src[]) {
    strncpy(dest, src, SIZE);
}

int main(void) {
    char word[SIZE];

    fill(word, "estrella");
    printf("%s\n", word);
    return 0;
}
errors/strncpy_warn.cDescargar errors/strncpy_warn.c
// Si el compilador ve el texto, avisa que strncpy lo va a cortar.
#include <stdio.h>
#include <string.h>

int main(void) {
    char word[5];

    strncpy(word, "estrella", 5);
    printf("%c\n", word[0]);
    return 0;
}
errors/strstr_null.cDescargar errors/strstr_null.c
// Error: usar lo que devuelve strstr sin revisar si es NULL.
#include <stdio.h>
#include <string.h>

int main(void) {
    char text[] = "sol y luna";

    printf("%s\n", strstr(text, "luna"));
    printf("%s\n", strstr(text, "mar"));
    return 0;
}
errors/transpose_in_place.cDescargar errors/transpose_in_place.c
// Error: al transponer en el mismo arreglo, cada par se cambia dos veces.
#include <stdio.h>

#define N 3

void transpose_wrong(int m[][N]) {
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            int saved = m[i][j];
            m[i][j] = m[j][i];
            m[j][i] = saved;
        }
    }
}

int main(void) {
    int m[N][N] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};

    transpose_wrong(m);
    for (int i = 0; i < N; i++) {
        printf("%d %d %d\n", m[i][0], m[i][1], m[i][2]);
    }
    return 0;
}

Pruebas

Comprueban lo que afirman los apuntes; se ejecutan con make test.

test_examples.pyDescargar test_examples.py
"""Comprueba lo que los apuntes afirman sobre los programas de la semana 11."""
import pathlib
import re
import subprocess
import tempfile
import unittest

HERE = pathlib.Path(__file__).parent
BUILD = HERE / "build"
FLAGS = ["-Wall", "-Wextra", "-std=c17"]
SANITIZE = ["-g", "-fsanitize=address,undefined"]


def execute(command, stdin=""):
    done = subprocess.run(
        command, input=stdin, encoding="utf-8", errors="replace",
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=10,
    )
    return done.stdout, done.returncode


def run(program, stdin=""):
    """Ejecuta un programa de build/; devuelve su salida (con errores) y su estado."""
    return execute([str(BUILD / program)], stdin)


def build_source(source, *extra):
    """Compila un archivo; devuelve los mensajes, el estado y el ejecutable."""
    out = pathlib.Path(tempfile.mkdtemp()) / pathlib.Path(source).stem
    done = subprocess.run(
        ["gcc", *FLAGS, *extra, source, "-o", str(out)], cwd=HERE, encoding="utf-8",
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
    )
    return done.stdout, done.returncode, out


class CleanCompile(unittest.TestCase):
    def test_examples_have_no_warnings(self):
        # Se compila de verdad: -Wstringop-truncation no sale solo revisando la sintaxis.
        for source in sorted(HERE.glob("*.c")):
            with self.subTest(source=source.name):
                messages, status, _ = build_source(
                    source.name, *SANITIZE, "-Werror", *(["-c"] if source.name == "check_lab.c" else []))
                self.assertEqual(status, 0, messages)

    def test_no_pointer_variables_or_pointer_arithmetic_in_examples(self):
        # La GUIA deja los punteros para cursos posteriores.
        for source in sorted(HERE.glob("*.c")) + sorted((HERE / "solution").glob("*.c")):
            if source.name == "check_lab.c":
                continue
            code = re.sub(r"//[^\n]*", "", source.read_text(encoding="utf-8"))
            with self.subTest(source=source.name):
                self.assertNotRegex(code, r"\b(?:char|int|double)\s*\*\s*\w")
                self.assertNotRegex(code, r"str(?:str|chr)\([^;]*\)\s*[-+]")


class Lengths(unittest.TestCase):
    def test_lengths(self):
        self.assertEqual(run("lengths"), (
            'strlen(t) = 4\nsizeof(t) = 8\nstrlen("") = 0\nstrlen("año") = 4\naloh\n', 0))

    def test_subtracting_from_zero_wraps(self):
        messages, status, binary = build_source("errors/strlen_wrap.c")
        self.assertEqual(status, 0)
        self.assertIn("-Wtype-limits", messages)
        output, _ = execute([str(binary)])
        value = int(re.search(r"= (\d+)", output).group(1))
        self.assertGreater(value, 2 ** 31)      # el valor exacto depende del tamaño de size_t
        self.assertEqual((value + 1) & value, 0)  # es 2^n - 1
        self.assertIn("siempre se cumple", output)

    def test_last_char_of_empty_string_is_reported(self):
        messages, status, binary = build_source("errors/last_char.c", *SANITIZE)
        self.assertEqual(status, 0)
        self.assertEqual(messages, "")          # gcc no avisa
        output, code = execute([str(binary)])
        self.assertIn("AddressSanitizer", output)
        self.assertNotEqual(code, 0)

    def test_sign_compare_warning(self):
        messages, status, _ = build_source("errors/sign_compare.c")
        self.assertEqual(status, 0)
        self.assertIn("-Wsign-compare", messages)


class Comparing(unittest.TestCase):
    def test_equal_arrays(self):
        messages, status, binary = build_source("errors/equal_arrays.c")
        self.assertEqual(status, 0)
        self.assertIn("-Warray-compare", messages)
        self.assertEqual(execute([str(binary)]), ("distintas\n", 0))

    def test_equal_params_is_silent(self):
        messages, status, binary = build_source("errors/equal_params.c")
        self.assertEqual((messages, status), ("", 0))
        self.assertEqual(execute([str(binary)]), ("a con b: 0\na con a: 1\n", 0))

    def test_equal_literals_warns_and_its_result_is_not_tested(self):
        messages, status, binary = build_source("errors/equal_literals.c")
        self.assertEqual(status, 0)
        self.assertIn("unspecified behavior", messages)
        output, _ = execute([str(binary)])
        self.assertIn(output, ("0\n", "1\n"))

    def test_strcmp_order(self):
        output, code = run("compare")
        self.assertEqual(code, 0)
        self.assertEqual(output.splitlines(), [
            '"ana" y "ana" son iguales',
            '"ana" va antes que "ano"',
            '"an" va antes que "ana"',
            '"sol" va después de "luna"',
            '"Z" va antes que "a"',
            '"z" va antes que "ñ"',
        ])

    def test_strcmp_order_does_not_depend_on_char_signedness(self):
        expected, _ = run("compare")
        for option in ["-fsigned-char", "-funsigned-char"]:
            with self.subTest(option=option):
                _, status, binary = build_source("compare.c", option)
                self.assertEqual(status, 0)
                self.assertEqual(execute([str(binary)]), (expected, 0))

    def test_strcmp_in_an_if_reads_backwards(self):
        _, status, binary = build_source("errors/strcmp_wrong.c")
        self.assertEqual(status, 0)
        self.assertEqual(execute([str(binary)]), ("distintas\n", 0))


class Copying(unittest.TestCase):
    def test_strncpy_bytes(self):
        output, code = run("strncpy_bytes")
        self.assertEqual(code, 0)
        rows = [line.split() for line in output.splitlines()]
        self.assertEqual(rows[0], ["115", "111", "108", "0", "0"])       # sol, con relleno
        self.assertEqual(rows[1], ["108", "117", "110", "97", "0"])      # luna, justo
        self.assertEqual(rows[2], ["101", "115", "116", "114", "101"])   # sin '\0'
        self.assertNotIn("0", rows[2])

    def test_gcc_warns_only_when_it_sees_the_text(self):
        messages, status, _ = build_source("errors/strncpy_warn.c")
        self.assertEqual(status, 0)
        self.assertIn("-Wstringop-truncation", messages)
        messages, status, _ = build_source("errors/strncpy_no_null.c")
        self.assertEqual((messages, status), ("", 0))

    def test_strncpy_without_terminator_is_reported(self):
        _, status, binary = build_source("errors/strncpy_no_null.c", *SANITIZE)
        self.assertEqual(status, 0)
        output, code = execute([str(binary)])
        self.assertIn("AddressSanitizer: stack-buffer-overflow", output)
        self.assertIn("READ of size", output)
        self.assertNotEqual(code, 0)

    def test_safe_copy(self):
        self.assertEqual(run("safe_copy"), ("sol\nluna\nestr\n", 0))

    def test_append(self):
        self.assertEqual(run("append"), ("Ana Quispe\nAna Qui\n", 0))

    def test_strcat_overflows_only_with_long_input(self):
        messages, status, binary = build_source("errors/strcat_overflow.c", *SANITIZE)
        self.assertEqual((messages, status), ("", 0))
        self.assertEqual(execute([str(binary)], "Li\n"), ("Ana Li\n", 0))
        output, code = execute([str(binary)], "Quispe_Huaman\n")
        self.assertIn("AddressSanitizer: stack-buffer-overflow", output)
        self.assertIn("WRITE of size 14", output)
        self.assertNotEqual(code, 0)
        self.assertEqual(execute([str(binary)], ""), ("Entrada inválida\n", 1))

    def test_strcat_overlap(self):
        messages, status, binary = build_source("errors/strcat_overlap.c", *SANITIZE)
        self.assertEqual(status, 0)
        self.assertIn("-Wrestrict", messages)
        output, code = execute([str(binary)])
        self.assertIn("strcat-param-overlap", output)
        self.assertNotEqual(code, 0)


class Searching(unittest.TestCase):
    def test_find_text(self):
        self.assertEqual(run("find_text"), (
            "Tiene @: @example.com\nNo tiene #\nDesde Lima: Lima\n"
            "lima, en minúsculas, no aparece\nDesde la ñ: ño pasado llovió en Lima\n"
            "Desde el byte 3: año pasado llovió en Lima\n"
            "Desde el byte 0 hay un el\nDesde el byte 1 no hay ninguno\n", 0))

    def test_null_result_used_without_checking_does_not_exit_cleanly(self):
        # Es comportamiento indefinido: no se prueba cómo muere, solo con el sanitizer.
        _, status, binary = build_source("errors/strstr_null.c", *SANITIZE)
        self.assertEqual(status, 0)
        output, code = execute([str(binary)])
        self.assertNotEqual(code, 0)
        self.assertIn("null", output)


class Matrices(unittest.TestCase):
    def test_layout_sizes(self):
        output, code = run("matrix_layout")
        self.assertEqual(code, 0)
        self.assertIn("sizeof(m) = 48\nsizeof(m[0]) = 16\nsizeof(m[0][0]) = 4\nm[1][2] = 7\n", output)

    def test_layout_is_row_by_row(self):
        output, _ = run("matrix_layout")
        cells = re.findall(r"m\[(\d)\]\[(\d)\] = +(\d+) está en (0x[0-9a-f]+)", output)
        self.assertEqual(len(cells), 12)
        self.assertEqual([int(c[2]) for c in cells], list(range(1, 13)))
        addresses = [int(c[3], 16) for c in cells]
        for row, column, _, _ in cells:
            position = int(row) * 4 + int(column)   # la fórmula de los apuntes
            self.assertEqual(addresses[position] - addresses[0], position * 4)

    def test_grades_table(self):
        self.assertEqual(run("grades_table")[0].splitlines(), [
            "Estudiante 0: suma 52, promedio 13.00",
            "Estudiante 1: suma 48, promedio 12.00",
            "Estudiante 2: suma 63, promedio 15.75",
            "Curso 0: suma 43, promedio 14.33",
            "Curso 1: suma 35, promedio 11.67",
            "Curso 2: suma 40, promedio 13.33",
            "Curso 3: suma 45, promedio 15.00",
        ])

    def test_matrix_param_changes_only_the_rows_it_receives(self):
        lines = run("matrix_param")[0].splitlines()
        self.assertEqual(lines[3:5], ["Suma del curso 0: 43", "Suma del curso 0, solo 2 filas: 25"])
        self.assertEqual(lines[5:], [" 15 10 18 13", " 12 17  9 14", " 18 10 15 20"])

    def test_missing_columns_do_not_compile(self):
        messages, status, _ = build_source("errors/matrix_no_columns.c")
        self.assertNotEqual(status, 0)
        self.assertIn("must have bounds for all dimensions except the first", messages)

    def test_wrong_columns_are_noticed(self):
        messages, _, _ = build_source("errors/matrix_wrong_columns.c")
        self.assertIn("incompatible pointer type", messages)

    def test_const_matrix_is_only_seen_with_pedantic(self):
        messages, status, _ = build_source("errors/const_matrix.c")
        self.assertEqual((messages, status), ("", 0))
        messages, status, _ = build_source("errors/const_matrix.c", "-Wpedantic")
        self.assertEqual(status, 0)
        self.assertIn("before C23", messages)

    def test_no_example_uses_const_matrix_parameters(self):
        for source in sorted(HERE.glob("*.c")) + sorted((HERE / "solution").glob("*.c")):
            with self.subTest(source=source.name):
                self.assertNotRegex(source.read_text(encoding="utf-8"), r"const\s+\w+\s+\w+\[\]\[")

    def test_comma_warns(self):
        messages, status, _ = build_source("errors/matrix_comma.c")
        self.assertEqual(status, 0)
        self.assertIn("left-hand operand of comma", messages)

    def test_column_out_of_bounds_seen_by_undefined_not_by_address(self):
        _, status, binary = build_source("errors/matrix_bounds.c", *SANITIZE)
        self.assertEqual(status, 0)
        self.assertEqual(execute([str(binary)], "2\n"), ("m[0][2] = 3\n", 0))
        output, _ = execute([str(binary)], "3\n")
        self.assertIn("runtime error: index 3 out of bounds for type 'int [3]'", output)
        self.assertNotIn("AddressSanitizer", output)
        # Solo con address, nada avisa en este acceso.
        _, status, binary = build_source("errors/matrix_bounds.c", "-g", "-fsanitize=address")
        self.assertEqual(status, 0)
        output, _ = execute([str(binary)], "3\n")
        self.assertNotIn("AddressSanitizer", output)
        self.assertEqual(execute([str(binary)], "x\n"), ("Entrada inválida\n", 1))

    def test_names(self):
        self.assertEqual(run("names"), (
            "sizeof(names) = 48\n"
            "names[0] = Rosa, 4 bytes de texto\nnames[1] = Ana, 3 bytes de texto\n"
            "names[2] = Luis, 4 bytes de texto\nnames[3] = Zoila, 5 bytes de texto\n"
            "Primero: Ana\nnames[1] = Maria de lo\nPrimero: Luis\n", 0))

    def test_matrix_add(self):
        self.assertEqual(run("matrix_add"), (" 11 22 33\n 44 55 66\n", 0))

    def test_transposing_in_place_over_every_cell_changes_nothing(self):
        messages, status, binary = build_source("errors/transpose_in_place.c")
        self.assertEqual((messages, status), ("", 0))
        self.assertEqual(execute([str(binary)]), ("1 2 3\n4 5 6\n7 8 9\n", 0))


if __name__ == "__main__":
    unittest.main()
test_lab.pyDescargar test_lab.py
"""Pruebas del laboratorio. LAB_DIR dice dónde está el código: lab o solution."""
import os
import pathlib
import re
import subprocess
import tempfile
import unittest

HERE = pathlib.Path(__file__).parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")

# Cuatro filas de tres cursos. Las notas 99 están en una fila que no se usa: solo importan
# si la función lee más filas de las que recibió.
GRADES = [14, 9, 17,
          11, 16, 8,
          18, 10, 15,
          12, 20, 7]
THREE_ROWS = [14, 9, 17,
              11, 16, 8,
              18, 10, 15,
              99, 99, 99]
TIED = [14, 0, 20,
        14, 0, 20,
        10, 0, 5,
        13, 0, 20]
TRANSPOSED = [14, 11, 18, 12,
              9, 16, 10, 20,
              17, 8, 15, 7]


def run(program):
    done = subprocess.run(
        [str(HERE / "build" / LAB_DIR / program)], encoding="utf-8", errors="replace",
        stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=10,
    )
    return done.stdout, done.returncode


def call(*args):
    """Llama a una función con check_lab y devuelve lo que imprime, sin los bordes."""
    done = subprocess.run(
        [str(HERE / "build" / LAB_DIR / "check_lab")] + [str(a).encode("utf-8") for a in args],
        stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=10,
    )
    return done.stdout.decode("utf-8", "replace").strip()


def code_without_comments(name):
    text = (HERE / LAB_DIR / name).read_text(encoding="utf-8")
    return re.sub(r"//[^\n]*", "", text)


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe {LAB_DIR}/")
class LabTests(unittest.TestCase):
    def test_no_warnings(self):
        # Se compila de verdad: algunas advertencias no salen solo revisando la sintaxis.
        with tempfile.TemporaryDirectory() as folder:
            for name in ["words", "grades"]:
                with self.subTest(name=name):
                    done = subprocess.run(
                        ["gcc", "-Wall", "-Wextra", "-std=c17", "-Werror", "-c",
                         str(HERE / LAB_DIR / f"{name}.c"), "-o", f"{folder}/{name}.o"],
                        text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                    )
                    self.assertEqual(done.returncode, 0, done.stdout)

    def test_your_own_asserts_pass(self):
        for name in ["words", "grades"]:
            with self.subTest(name=name):
                self.assertEqual(run(name), ("Todas las pruebas pasaron\n", 0))

    def test_count_word_uses_strstr(self):
        self.assertRegex(code_without_comments("words.c"), r"\bstrstr\s*\(")

    def test_grades_main_checks_with_assert(self):
        self.assertRegex(code_without_comments("grades.c"), r"assert\s*\(\s*is_transpose\s*\(")

    def test_constants_were_not_changed(self):
        code = code_without_comments("grades.c")
        self.assertRegex(code, r"#define\s+STUDENTS\s+4\b")
        self.assertRegex(code, r"#define\s+COURSES\s+3\b")

    def test_count_word(self):
        cases = [
            ("sol y luna", "sol", "1"),
            ("sol y sol", "sol", "2"),
            ("luna y sol", "sol", "1"),      # al final
            ("sol", "sol", "1"),             # el texto completo
            ("solsolsol", "sol", "3"),       # seguidas
            ("sol y luna", "mar", "0"),
            ("so", "sol", "0"),              # la palabra es más larga que el texto
            ("", "sol", "0"),
            ("Sol sol SOL", "sol", "1"),     # byte por byte
            ("girasol y sol", "sol", "2"),   # no hace falta una palabra completa
            ("banana", "ana", "1"),          # no se pisan
            ("aaaaa", "aa", "2"),
            ("año y ñandú", "ñ", "2"),       # bytes de UTF-8
        ]
        for text, word, expected in cases:
            with self.subTest(text=text, word=word):
                self.assertEqual(call("w", text, word), expected)

    def test_count_word_with_empty_word(self):
        for text in ["", "sol"]:
            with self.subTest(text=text):
                self.assertEqual(call("w", text, ""), "0")

    def test_student_averages(self):
        self.assertEqual(call("s", 4, *GRADES), "13.33 11.67 14.33 13.00")

    def test_student_averages_uses_the_rows_it_receives(self):
        # La cuarta casilla no se debe tocar: check_lab la deja en -1.
        self.assertEqual(call("s", 3, *THREE_ROWS), "13.33 11.67 14.33 -1.00")

    def test_course_averages(self):
        self.assertEqual(call("c", 4, *GRADES), "13.75 13.75 11.75")

    def test_course_averages_uses_the_rows_it_receives(self):
        self.assertEqual(call("c", 3, *THREE_ROWS), "14.33 11.67 13.33")
        self.assertEqual(call("c", 1, *THREE_ROWS), "14.00 9.00 17.00")

    def test_best_students(self):
        self.assertEqual(call("b", 4, *GRADES), "2 3 0")

    def test_best_students_uses_the_rows_it_receives(self):
        self.assertEqual(call("b", 3, *THREE_ROWS), "2 1 0")
        self.assertEqual(call("b", 1, *THREE_ROWS), "0 0 0")

    def test_best_students_ties_and_zeros(self):
        # Empates: gana el de índice menor. El curso 1 tiene solo ceros, y aun así hay mejor.
        self.assertEqual(call("b", 4, *TIED), "0 0 0")

    def test_best_students_restarts_in_each_course(self):
        # Si el máximo no se reinicia, el 20 del curso 0 tapa a los demás cursos.
        self.assertEqual(call("b", 4, 20, 1, 2, 3, 5, 6, 4, 9, 8, 7, 8, 9), "0 2 3")

    def test_transpose(self):
        expected = "14 11 18 12 \n9 16 10 20 \n17 8 15 7"
        self.assertEqual(call("t", 4, *GRADES), expected)

    def test_is_transpose_accepts_the_right_one(self):
        self.assertEqual(call("i", 4, *GRADES, *TRANSPOSED), "1")

    def test_is_transpose_finds_any_wrong_cell(self):
        for position in [0, 5, 11]:
            wrong = list(TRANSPOSED)
            wrong[position] = 0
            with self.subTest(position=position):
                self.assertEqual(call("i", 4, *GRADES, *wrong), "0")

    def test_is_transpose_rejects_the_same_values_in_rows(self):
        # Las mismas notas sin transponer: fila por fila, igual que grades.
        self.assertEqual(call("i", 4, *GRADES, *GRADES), "0")


if __name__ == "__main__":
    unittest.main()
test_notes.pyDescargar test_notes.py
"""Vuelve a correr los comandos que aparecen en los apuntes y en las diapositivas
y comprueba que el código citado existe de verdad en esta carpeta."""
import pathlib
import re
import subprocess
import unittest

HERE = pathlib.Path(__file__).parent
ROOT = HERE.parent.parent
NOTES = ROOT / "notes" / "week-11.md"
SLIDES = ROOT / "slides" / "week-11.tex"
# Antes de un bloque con direcciones o con comportamiento indefinido. De él solo se
# exigen estas cadenas estables, si el bloque las muestra.
MACHINE_DEPENDENT = "<!-- depende de la corrida -->"
STABLE = [
    "AddressSanitizer: stack-buffer-overflow",
    "READ of size 1",
    "AddressSanitizer: strcat-param-overlap",
    "runtime error: index 3 out of bounds for type 'int [3]'",
    "warning: comparison with string literal results in unspecified behavior",
    "-Wrestrict",
    "sizeof(m) = 48",
    "m[1][2] = 7",
]
# Antes de una salida recortada. Sus líneas deben aparecer en la real, y en el mismo orden.
TRIMMED = "<!-- recortado -->"
# En esta salida, solo las direcciones cambian.
ADDRESSES = re.compile(r"0x[0-9a-f]+")


def normalize(text):
    # Según el idioma del entorno, gcc usa comillas tipográficas o rectas.
    return ADDRESSES.sub("0x", text.replace("‘", "'").replace("’", "'"))


def run_shell(command):
    done = subprocess.run(
        ["bash", "-c", command], cwd=HERE, encoding="utf-8", errors="replace",
        stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
    )
    return normalize(done.stdout)


def split_commands(lines):
    """Parte un bloque de terminal en pares (comando, líneas de salida)."""
    pairs = []
    for line in lines:
        if line.startswith("$ "):
            pairs.append((line[2:], []))
        elif pairs:
            pairs[-1][1].append(normalize(line.rstrip()))
    return pairs


def note_blocks(language):
    """Devuelve (líneas, marca) de cada bloque de los apuntes. La marca es el comentario
    que lo precede, o un texto cualquiera si no tiene."""
    blocks, current, flagged, previous = [], None, "", ""
    for raw in NOTES.read_text(encoding="utf-8").splitlines():
        line = re.sub(r"^> ?", "", raw)
        if current is None:
            if line.strip() == "```" + language:
                current, flagged = [], previous.strip()
            elif line.strip():
                previous = line
        elif line.strip() == "```":
            blocks.append((current, flagged))
            current = None
        else:
            current.append(line)
    return blocks


def slide_blocks(environments):
    text = SLIDES.read_text(encoding="utf-8")
    pattern = r"\\begin\{(?:%s)\}[^\n]*\n(.*?)\\end\{(?:%s)\}" % (environments, environments)
    return [match.group(1).splitlines() for match in re.finditer(pattern, text, re.S)]


def known_code_lines():
    lines = set()
    for source in list(HERE.glob("*.c")) + list((HERE / "errors").glob("*.c")):
        lines.update(line.strip() for line in source.read_text(encoding="utf-8").splitlines())
    return lines


class NotesAreTrue(unittest.TestCase):
    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    def test_terminal_blocks_in_notes(self):
        checked = 0
        for lines, flagged in note_blocks("text"):
            if not lines or not lines[0].startswith("$ "):
                continue
            for command, expected in split_commands(lines):
                actual = run_shell(command).rstrip("\n")
                checked += 1
                with self.subTest(command=command):
                    if flagged == MACHINE_DEPENDENT:
                        for token in STABLE:
                            if any(token in line for line in expected):
                                self.assertIn(token, actual)
                    elif flagged == TRIMMED:
                        real = [line.rstrip() for line in actual.split("\n")]
                        position = 0
                        for line in expected:
                            found = [i for i in range(position, len(real)) if real[i] == line]
                            self.assertTrue(found, f"no aparece en la salida real: {line}")
                            position = found[0] + 1
                    else:
                        lines = [line.rstrip() for line in actual.split("\n")]
                        self.assertEqual("\n".join(lines), "\n".join(expected))
        self.assertGreater(checked, 20)

    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    def test_code_blocks_in_notes_exist(self):
        known = known_code_lines()
        for lines, _ in note_blocks("c"):
            for line in lines:
                if line.strip() and line.strip() != "...":
                    with self.subTest(line=line):
                        self.assertIn(line.strip(), known)

    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    def test_notes_show_the_report_and_flag_undefined_behavior(self):
        text = NOTES.read_text(encoding="utf-8")
        self.assertIn("WRITE of size 14", text)
        self.assertIn("en esta corrida", " ".join(text.split()))


class SlidesAreTrue(unittest.TestCase):
    @unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
    def test_terminal_blocks_in_slides(self):
        for block in slide_blocks("terminal|salidalarga"):
            for command, expected in split_commands(block):
                actual = run_shell(command).splitlines()
                position = 0
                # Una diapositiva puede omitir líneas, pero no cambiarlas ni reordenarlas.
                for line in expected:
                    line = line.strip()
                    if not line:
                        continue
                    with self.subTest(command=command, line=line):
                        found = [i for i in range(position, len(actual)) if actual[i].strip() == line]
                        self.assertTrue(found, f"no aparece en la salida real: {line}")
                        if found:
                            position = found[0] + 1

    @unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
    def test_code_blocks_in_slides_exist(self):
        known = known_code_lines()
        for block in slide_blocks("codigo|codigochico|codigomini"):
            for line in block:
                if line.strip() and line.strip() != "...":
                    with self.subTest(line=line):
                        self.assertIn(line.strip(), known)


if __name__ == "__main__":
    unittest.main()

Laboratorio

Archivos de partida del laboratorio de la semana.

lab/grades.cDescargar lab/grades.c
// Laboratorio, partes 2, 3 y 4: notas en una matriz.
//
// Cada fila es un estudiante y cada columna un curso. Las funciones reciben students, las
// filas que están en uso, que van de 1 a STUDENTS. No cambien STUDENTS ni COURSES.
// Borren las líneas (void): solo están para que el archivo compile sin advertencias.
//
// En main hay algunas pruebas. Agreguen más con assert: casos normales y bordes.
//
// Pruébenlo con make test-lab.
#include <assert.h>
#include <stdio.h>

#define STUDENTS 4
#define COURSES 3

// Guarda en averages[i] el promedio de las notas del estudiante i.
void student_averages(int grades[][COURSES], int students, double averages[]) {
    (void) grades;
    (void) students;
    (void) averages;
}

// Guarda en averages[j] el promedio de las notas del curso j.
void course_averages(int grades[][COURSES], int students, double averages[]) {
    (void) grades;
    (void) students;
    (void) averages;
}

// Guarda en best[j] el índice del estudiante con la nota más alta del curso j.
// Si hay un empate, gana el de índice menor.
void best_students(int grades[][COURSES], int students, int best[]) {
    (void) grades;
    (void) students;
    (void) best;
}

// Guarda en result la transpuesta de grades, que tiene STUDENTS filas: la fila i de
// grades pasa a ser la columna i de result.
void transpose(int grades[][COURSES], int result[][STUDENTS]) {
    (void) grades;
    (void) result;
}

// Devuelve 1 si result es la transpuesta de grades, y 0 si no.
// Revisa todas las casillas.
int is_transpose(int grades[][COURSES], int result[][STUDENTS]) {
    (void) grades;
    (void) result;
    return 0;
}

// Devuelve 1 si a y b se diferencian en menos de 0.001. Dos double no se comparan con ==.
int close_to(double a, double b) {
    double difference = a - b;
    return difference < 0.001 && difference > -0.001;
}

int main(void) {
    int grades[STUDENTS][COURSES] = {
        {14, 9, 17},
        {11, 16, 8},
        {18, 10, 15},
        {12, 20, 7},
    };
    double by_student[STUDENTS] = {0};
    double by_course[COURSES] = {0};
    int best[COURSES] = {0};
    int result[COURSES][STUDENTS] = {{0}};

    student_averages(grades, STUDENTS, by_student);
    assert(close_to(by_student[1], 35.0 / 3));

    course_averages(grades, STUDENTS, by_course);
    assert(close_to(by_course[0], 13.75));

    best_students(grades, STUDENTS, best);
    assert(best[0] == 2);

    transpose(grades, result);
    assert(result[2][1] == 8);
    assert(is_transpose(grades, result));

    result[0][0] = 0;
    assert(!is_transpose(grades, result));

    printf("Todas las pruebas pasaron\n");
    return 0;
}
lab/words.cDescargar lab/words.c
// Laboratorio, parte 1: contar cuántas veces aparece una palabra en un texto.
//
// Escriban count_word usando strstr. Borren las líneas (void): solo están para que el
// archivo compile sin advertencias mientras tanto.
//
// En main hay algunas pruebas. Agreguen más con assert: casos normales y bordes.
//
// Pruébenlo con make test-lab.
#include <assert.h>
#include <stdio.h>
#include <string.h>

// Devuelve cuántas veces aparece word dentro de text, según estas reglas:
//   - Se compara byte por byte: 'S' y 's' son distintos.
//   - No hace falta que sea una palabra completa: "sol" aparece una vez en "girasol".
//   - Las apariciones no se pisan: al encontrar una, se sigue buscando después de ella.
//     "ana" aparece una vez en "banana", y "aa" aparece 2 veces en "aaaaa".
//   - Si word es la cadena vacía, devuelve 0.
// Basta buscar desde &text[i], como explican los apuntes: no hace falta declarar punteros.
int count_word(const char text[], const char word[]) {
    (void) text;
    (void) word;
    return 0;
}

int main(void) {
    assert(count_word("sol y luna", "sol") == 1);
    assert(count_word("sol y sol", "sol") == 2);
    assert(count_word("sol y luna", "mar") == 0);
    printf("Todas las pruebas pasaron\n");
    return 0;
}

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# Semana 11: biblioteca de cadenas y matrices. Se usa dentro del entorno del curso.
#
#   make           compila los programas de la clase en build/
#   make test      corre las pruebas de los ejemplos y de los apuntes
#   make test-lab  prueba su laboratorio (lab/*.c); falla hasta que lo terminen
#   make clean     borra lo generado
#
# Los programas de errors/ tienen errores a propósito. No se compilan con make:
# los compilan las pruebas, y ustedes, con los comandos de los apuntes.
#
# check_lab.c es parte de las pruebas: llama a las funciones de words.c y grades.c.
# Para enlazarlos, el main de cada archivo se compila con otro nombre.

CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g -fsanitize=address,undefined
BUILD = build
PROGRAMS = lengths compare strncpy_bytes safe_copy append find_text matrix_layout \
           grades_table matrix_param names matrix_add

.PHONY: all lab test test-lab clean

all: $(PROGRAMS:%=$(BUILD)/%)

$(BUILD)/%: %.c | $(BUILD)
	$(CC) $(CFLAGS) $< -o $@

$(BUILD) $(BUILD)/lab $(BUILD)/solution:
	mkdir -p $@

$(BUILD)/lab/% : lab/%.c | $(BUILD)/lab
	$(CC) $(CFLAGS) $< -o $@

$(BUILD)/solution/% : solution/%.c | $(BUILD)/solution
	$(CC) $(CFLAGS) $< -o $@

$(BUILD)/lab/check_lab: check_lab.c lab/words.c lab/grades.c | $(BUILD)/lab
	$(CC) $(CFLAGS) -Dmain=words_main -c lab/words.c -o $(BUILD)/lab/words.o
	$(CC) $(CFLAGS) -Dmain=grades_main -c lab/grades.c -o $(BUILD)/lab/grades.o
	$(CC) $(CFLAGS) check_lab.c $(BUILD)/lab/words.o $(BUILD)/lab/grades.o -o $@

$(BUILD)/solution/check_lab: check_lab.c solution/words.c solution/grades.c | $(BUILD)/solution
	$(CC) $(CFLAGS) -Dmain=words_main -c solution/words.c -o $(BUILD)/solution/words.o
	$(CC) $(CFLAGS) -Dmain=grades_main -c solution/grades.c -o $(BUILD)/solution/grades.o
	$(CC) $(CFLAGS) check_lab.c $(BUILD)/solution/words.o $(BUILD)/solution/grades.o -o $@

LAB_FILES = words grades check_lab

lab: $(LAB_FILES:%=$(BUILD)/lab/%)

# Si existe solution/, también se comprueba la solución del laboratorio.
test: all
	@if [ -d solution ]; then $(MAKE) --no-print-directory \
	  $(LAB_FILES:%=$(BUILD)/solution/%); fi
	python3 -m unittest discover -p 'test_*.py'

test-lab: all lab
	LAB_DIR=lab python3 -m unittest -v test_lab

clean:
	rm -rf $(BUILD) __pycache__