Semana 5 — Ciclos

Objetivos

  • Elegir entre while, do while y for según el problema.
  • Razonar sobre un ciclo: inicio, condición de parada y lo que avanza en cada vuelta.
  • Aplicar los patrones de contador, acumulador y centinela.

Teoría — Ciclos: while, do while y for

Lectura: Apuntes, semana 5.

Temas:

  • Repetir en pseudocódigo y en diagramas de flujo.
  • while, do while y for. Validar una entrada con do while.
  • Patrones: contador, acumulador, centinela y buscar el mayor.
  • Leer hasta el fin de la entrada con getchar y EOF.
  • Error común: ciclo infinito, y salirse por uno (off-by-one).

Laboratorio — Ciclos

  1. Escribir un programa que lea números hasta un 0 y escriba la suma, el promedio y el mayor.
  2. Escribir un programa que diga si un número es primo.
  3. Contar con getchar las líneas de un archivo redirigido con <.
  4. Validar con do while que una nota esté entre 0 y 20.

Preguntas de repaso

  1. ¿Cuántas veces corre el cuerpo de for (i = 1; i <= 10; i += 3)? Sigan los valores de i.
  2. Reescriban ese for con while.
  3. ¿Cuándo es más claro do while que while? ¿Hay algo que solo se pueda hacer con do while?
  4. ¿Qué hace while (n > 0) { printf("%d", n); }? ¿Cómo se corrige?
  5. Escriban en pseudocódigo un algoritmo que cuente los dígitos de un número.

Código de la semana

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

Ejemplos (17)Errores (11)Pruebas (3)Laboratorio (7)Compilación (1)

Ejemplos

Los programas que usan los apuntes y la clase.

cities.txtDescargar cities.txt
Puno
Cusco
Piura
coins.cDescargar coins.c
#include <stdio.h>

// Cuenta cuántas monedas de S/ 5 caben en un vuelto dado en céntimos.
int main(void) {
    int change;
    int coins = 0;

    if (scanf("%d", &change) != 1) {
        printf("Entrada inválida\n");
        return 1;
    }

    while (change >= 500) {
        change = change - 500;
        coins = coins + 1;
    }

    printf("%d monedas de S/ 5; quedan %d céntimos\n", coins, change);
    return 0;
}
copy.cDescargar copy.c
#include <stdio.h>

// Copia la entrada en la salida, byte por byte, con la forma corta del ciclo.
int main(void) {
    int c;

    while ((c = getchar()) != EOF) {
        putchar(c);
    }

    return 0;
}
count_bytes.cDescargar count_bytes.c
#include <stdio.h>

// Cuenta los bytes de la entrada, hasta EOF.
int main(void) {
    int count = 0;

    int c = getchar();
    while (c != EOF) {
        count++;
        c = getchar();
    }

    printf("%d bytes\n", count);
    return 0;
}
count_passed.cDescargar count_passed.c
#include <stdio.h>

// Lee notas hasta el fin de la entrada y cuenta cuántas aprueban (11 o más).
int main(void) {
    int grade;
    int total = 0;
    int passed = 0;

    int status = scanf("%d", &grade);
    while (status == 1) {
        total++;
        if (grade >= 11) {
            passed++;
        }
        status = scanf("%d", &grade);
    }

    if (status != EOF) {
        printf("Entrada inválida\n");
        return 1;
    }

    printf("Aprobaron %d de %d\n", passed, total);
    return 0;
}
double_steps.cDescargar double_steps.c
#include <stdio.h>

// Cuenta con un entero y calcula el double en cada vuelta: siempre son 11 valores.
int main(void) {
    for (int i = 0; i <= 10; i++) {
        double x = i / 10.0;
        printf("%.1f\n", x);
    }

    return 0;
}
for_as_while.cDescargar for_as_while.c
#include <stdio.h>

// El mismo ciclo de for_trace.c, escrito con while.
int main(void) {
    int i;

    i = 0;
    while (i < 10) {
        printf("i vale %d\n", i);
        i += 3;
    }

    printf("Al salir, i vale %d\n", i);
    return 0;
}
for_trace.cDescargar for_trace.c
#include <stdio.h>

// Muestra los valores de i en un for con paso 3.
int main(void) {
    int i;

    for (i = 0; i < 10; i += 3) {
        printf("i vale %d\n", i);
    }

    printf("Al salir, i vale %d\n", i);
    return 0;
}
grades.txtDescargar grades.txt
14
9
11
20
10
max_of_n.cDescargar max_of_n.c
#include <stdio.h>

// Lee cuántas temperaturas vienen y luego cada una. Escribe la mayor.
int main(void) {
    int n;

    if (scanf("%d", &n) != 1 || n < 1) {
        printf("Entrada inválida\n");
        return 1;
    }

    int max = 0;  // Este 0 nunca se usa: la primera vuelta lo reemplaza.
    for (int i = 0; i < n; i++) {
        int temperature;
        if (scanf("%d", &temperature) != 1) {
            printf("Entrada inválida\n");
            return 1;
        }
        if (i == 0 || temperature > max) {
            max = temperature;
        }
    }

    printf("La mayor es %d\n", max);
    return 0;
}
mult_table.cDescargar mult_table.c
#include <stdio.h>

// Escribe la tabla de multiplicar de un número, del 1 al 12.
int main(void) {
    int n;

    if (scanf("%d", &n) != 1) {
        printf("Entrada inválida\n");
        return 1;
    }

    if (n < -1000 || n > 1000) {
        printf("Número fuera de rango\n");
        return 1;
    }

    for (int i = 1; i <= 12; i++) {
        printf("%d x %2d = %d\n", n, i, n * i);
    }

    return 0;
}
read_month.cDescargar read_month.c
#include <stdio.h>

// Pide un mes hasta que esté entre 1 y 12. Si no se puede leer un número, termina.
int main(void) {
    int month;

    do {
        if (scanf("%d", &month) != 1) {
            printf("Entrada inválida\n");
            return 1;
        }
        if (month < 1 || month > 12) {
            printf("Mes fuera de rango, escriba otro\n");
        }
    } while (month < 1 || month > 12);

    printf("Mes elegido: %d\n", month);
    return 0;
}
sentinel_sum.cDescargar sentinel_sum.c
#include <stdio.h>

// Suma precios en céntimos hasta que llegue un 0, que marca el final.
int main(void) {
    int price;
    int total = 0;
    int items = 0;

    int status = scanf("%d", &price);
    while (status == 1 && price != 0) {
        total += price;
        items++;
        status = scanf("%d", &price);
    }

    if (status != 1) {
        printf("Entrada inválida\n");
        return 1;
    }

    printf("%d productos: %d.%02d soles\n", items, total / 100, total % 100);
    return 0;
}
sum_to_n.cDescargar sum_to_n.c
#include <stdio.h>

// Suma los enteros de 1 a n.
int main(void) {
    int n;

    if (scanf("%d", &n) != 1) {
        printf("Entrada inválida\n");
        return 1;
    }

    int sum = 0;
    int i = 1;
    while (i <= n) {
        sum = sum + i;
        i = i + 1;
    }

    printf("La suma de 1 a %d es %d\n", n, sum);
    return 0;
}
to_binary.cDescargar to_binary.c
#include <stdio.h>

// Escribe en binario un número de 0 a 255, del bit de mayor peso al de menor peso.
int main(void) {
    int n;

    if (scanf("%d", &n) != 1) {
        printf("Entrada inválida\n");
        return 1;
    }

    if (n < 0 || n > 255) {
        printf("Número fuera de rango\n");
        return 1;
    }

    for (int weight = 128; weight >= 1; weight /= 2) {
        if (n >= weight) {
            printf("1");
            n -= weight;
        } else {
            printf("0");
        }
    }

    printf("\n");
    return 0;
}
to_upper.cDescargar to_upper.c
#include <stdio.h>

// Copia la entrada y cambia las minúsculas ASCII por mayúsculas.
int main(void) {
    int c = getchar();
    while (c != EOF) {
        if (c >= 'a' && c <= 'z') {
            c = c - 'a' + 'A';
        }
        putchar(c);
        c = getchar();
    }
    return 0;
}
while_vs_do.cDescargar while_vs_do.c
#include <stdio.h>

// Con una condición falsa desde el inicio, while no entra y do while entra una vez.
int main(void) {
    int n = 5;

    while (n < 5) {
        printf("while: entré\n");
        n++;
    }

    do {
        printf("do while: entré\n");
        n++;
    } while (n < 5);

    printf("n vale %d\n", n);
    return 0;
}

Errores

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

errors/char_eof.cDescargar errors/char_eof.c
#include <stdio.h>

// Error a propósito: getchar devuelve int, y aquí se guarda en un char.
int main(void) {
    char c;
    int count = 0;

    while ((c = getchar()) != EOF) {
        count++;
    }

    printf("%d bytes\n", count);
    return 0;
}
errors/do_scope.cDescargar errors/do_scope.c
#include <stdio.h>

// Error a propósito: month nace dentro del bloque y la condición está afuera.
int main(void) {
    do {
        int month;
        if (scanf("%d", &month) != 1) {
            return 1;
        }
    } while (month < 1 || month > 12);

    return 0;
}
errors/double_extra_turn.cDescargar errors/double_extra_turn.c
#include <stdio.h>

// Error a propósito: se esperaban 10 vueltas, pero con x < 1.0 hay una más.
int main(void) {
    int turns = 0;

    for (double x = 0.0; x < 1.0; x += 0.1) {
        turns++;
    }

    printf("Vueltas: %d\n", turns);
    return 0;
}
errors/double_never_equal.cDescargar errors/double_never_equal.c
#include <stdio.h>

// Error a propósito: x nunca vale exactamente 1.0, así que el ciclo no termina.
int main(void) {
    for (double x = 0.0; x != 1.0; x += 0.1) {
        printf("%.17f\n", x);
    }

    return 0;
}
errors/getchar_no_parens.cDescargar errors/getchar_no_parens.c
#include <stdio.h>

// Error a propósito: sin paréntesis, c recibe el resultado de la comparación.
int main(void) {
    int c;
    int newlines = 0;

    while (c = getchar() != EOF) {
        if (c == '\n') {
            newlines++;
        }
    }

    printf("%d saltos de línea\n", newlines);
    return 0;
}
errors/max_from_zero.cDescargar errors/max_from_zero.c
#include <stdio.h>

// Error a propósito: el mayor empieza en 0, que no es un dato de la entrada.
int main(void) {
    int n;

    if (scanf("%d", &n) != 1 || n < 1) {
        printf("Entrada inválida\n");
        return 1;
    }

    int max = 0;
    for (int i = 0; i < n; i++) {
        int temperature;
        if (scanf("%d", &temperature) != 1) {
            printf("Entrada inválida\n");
            return 1;
        }
        if (temperature > max) {
            max = temperature;
        }
    }

    printf("La mayor es %d\n", max);
    return 0;
}
errors/no_update.cDescargar errors/no_update.c
#include <stdio.h>

// Error a propósito: nada cambia n dentro del ciclo.
int main(void) {
    int n = 3;

    while (n > 0) {
        printf("%d\n", n);
    }

    return 0;
}
errors/not_eof.cDescargar errors/not_eof.c
#include <stdio.h>

// Error a propósito: != EOF deja pasar el 0 que devuelve scanf ante un dato inválido.
int main(void) {
    int n = 0;
    int count = 0;

    while (scanf("%d", &n) != EOF) {
        count++;
        printf("Llevo %d datos; el último es %d\n", count, n);
    }

    return 0;
}
errors/off_by_one.cDescargar errors/off_by_one.c
#include <stdio.h>

// Error a propósito: quería escribir del 1 al 5, pero la condición corta antes.
int main(void) {
    for (int i = 1; i < 5; i++) {
        printf("%d\n", i);
    }

    return 0;
}
errors/read_forever.cDescargar errors/read_forever.c
#include <stdio.h>

// Error a propósito: no revisa scanf. Con abc, el dato nunca se consume.
int main(void) {
    int month = 0;

    do {
        scanf("%d", &month);
        printf("Mes fuera de rango\n");
    } while (month < 1 || month > 12);

    printf("Mes elegido: %d\n", month);
    return 0;
}
errors/semicolon_for.cDescargar errors/semicolon_for.c
#include <stdio.h>

// Error a propósito: el ; termina el for, que queda sin cuerpo.
int main(void) {
    int i;

    for (i = 1; i <= 3; i++);
    {
        printf("%d\n", i);
    }

    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 5."""
import pathlib
import subprocess
import tempfile
import unittest

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


def run(program, stdin=""):
    """Ejecuta un programa de build/; devuelve su salida (con errores) y su estado."""
    done = subprocess.run(
        [str(BUILD / program)], input=stdin, text=True,
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=10,
    )
    return done.stdout, done.returncode


def compile_error_demo(name, *extra):
    """Compila un programa de errors/; devuelve los mensajes, el estado y el ejecutable."""
    out = pathlib.Path(tempfile.mkdtemp()) / name
    done = subprocess.run(
        ["gcc", *FLAGS, *extra, f"errors/{name}.c", "-o", str(out)], cwd=HERE,
        text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
    )
    return done.stdout, done.returncode, out


def first_lines(binary, stdin, count):
    """Lee las primeras líneas de un programa que no termina solo, y lo detiene."""
    process = subprocess.Popen(
        [str(binary)], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True,
    )
    process.stdin.write(stdin)
    process.stdin.close()
    lines = [process.stdout.readline() for _ in range(count)]
    still_running = process.poll() is None
    process.kill()
    process.stdout.close()
    process.wait()
    return lines, still_running


class CleanCompile(unittest.TestCase):
    def test_examples_have_no_warnings(self):
        for source in sorted(HERE.glob("*.c")):
            with self.subTest(source=source.name):
                done = subprocess.run(
                    ["gcc", *FLAGS, "-Werror", "-fsyntax-only", str(source)],
                    text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                )
                self.assertEqual(done.returncode, 0, done.stdout)


class WhileLoops(unittest.TestCase):
    def test_coins(self):
        self.assertEqual(run("coins", "1780")[0], "3 monedas de S/ 5; quedan 280 céntimos\n")
        self.assertEqual(run("coins", "500")[0], "1 monedas de S/ 5; quedan 0 céntimos\n")
        self.assertEqual(run("coins", "499")[0], "0 monedas de S/ 5; quedan 499 céntimos\n")
        self.assertEqual(run("coins", "abc"), ("Entrada inválida\n", 1))

    def test_sum_matches_the_formula(self):
        for n in [0, 1, 2, 4, 10, 100, 65535]:
            with self.subTest(n=n):
                self.assertEqual(run("sum_to_n", str(n))[0],
                                 f"La suma de 1 a {n} es {n * (n + 1) // 2}\n")

    def test_sum_with_no_turns(self):
        self.assertEqual(run("sum_to_n", "-3")[0], "La suma de 1 a -3 es 0\n")

    def test_sum_overflow_is_reported(self):
        # Solo el aviso: lo que escribe después es comportamiento indefinido.
        self.assertIn("signed integer overflow", run("sum_to_n", "70000")[0])
        self.assertNotIn("runtime error", run("sum_to_n", "65535")[0])

    def test_no_update_never_ends(self):
        messages, status, binary = compile_error_demo("no_update")
        self.assertEqual((messages, status), ("", 0))
        lines, still_running = first_lines(binary, "", 50)
        self.assertEqual(lines, ["3\n"] * 50)
        self.assertTrue(still_running)


class ForLoops(unittest.TestCase):
    def test_for_equals_its_while(self):
        expected = "i vale 0\ni vale 3\ni vale 6\ni vale 9\nAl salir, i vale 12\n"
        self.assertEqual(run("for_trace")[0], expected)
        self.assertEqual(run("for_as_while")[0], expected)

    def test_mult_table(self):
        for n in [7, -3, 0, 1000]:
            lines = run("mult_table", str(n))[0].splitlines()
            self.assertEqual(len(lines), 12)
            self.assertEqual(lines[0], f"{n} x  1 = {n}")
            self.assertEqual(lines[11], f"{n} x 12 = {n * 12}")

    def test_mult_table_rejects(self):
        self.assertEqual(run("mult_table", "1001"), ("Número fuera de rango\n", 1))
        self.assertEqual(run("mult_table", "abc"), ("Entrada inválida\n", 1))

    def test_to_binary_all_values(self):
        for n in range(256):
            self.assertEqual(run("to_binary", str(n))[0], f"{n:08b}\n")

    def test_to_binary_rejects(self):
        for text in ["-1", "256", "300"]:
            self.assertEqual(run("to_binary", text), ("Número fuera de rango\n", 1))

    def test_semicolon_for(self):
        messages, status, binary = compile_error_demo("semicolon_for")
        self.assertEqual(status, 0)
        self.assertIn("-Wmisleading-indentation", messages)
        self.assertEqual(subprocess.run([str(binary)], text=True, stdout=subprocess.PIPE).stdout, "4\n")

    def test_off_by_one(self):
        messages, status, binary = compile_error_demo("off_by_one")
        self.assertEqual((messages, status), ("", 0))
        self.assertEqual(subprocess.run([str(binary)], text=True, stdout=subprocess.PIPE).stdout,
                         "1\n2\n3\n4\n")

    def test_turn_counts_of_the_table(self):
        # Las cuatro filas de la tabla de "Salirse por uno", calculadas con C.
        for bounds, turns in [("int i = 0; i < n; i++", "n"), ("int i = 1; i <= n; i++", "n"),
                              ("int i = 0; i <= n; i++", "n + 1"), ("int i = 1; i < n; i++", "n - 1")]:
            source = ('#include <stdio.h>\nint main(void) { int n = 7; int t = 0;\n'
                      f'for ({bounds}) {{ t++; }}\nprintf("%d %d\\n", t, {turns}); return 0; }}\n')
            folder = pathlib.Path(tempfile.mkdtemp())
            (folder / "t.c").write_text(source)
            subprocess.run(["gcc", *FLAGS, str(folder / "t.c"), "-o", str(folder / "t")], check=True)
            first, second = subprocess.run([str(folder / "t")], text=True,
                                           stdout=subprocess.PIPE).stdout.split()
            with self.subTest(bounds=bounds):
                self.assertEqual(first, second)


class DoubleLoops(unittest.TestCase):
    def test_never_equal(self):
        _, status, binary = compile_error_demo("double_never_equal")
        self.assertEqual(status, 0)
        lines, _ = first_lines(binary, "", 12)
        self.assertEqual(lines[10], "0.99999999999999989\n")
        self.assertEqual(lines[11], "1.09999999999999987\n")

    def test_extra_turn(self):
        _, _, binary = compile_error_demo("double_extra_turn")
        self.assertEqual(subprocess.run([str(binary)], text=True, stdout=subprocess.PIPE).stdout,
                         "Vueltas: 11\n")

    def test_integer_counter(self):
        lines = run("double_steps")[0].splitlines()
        self.assertEqual(lines, [f"{i / 10:.1f}" for i in range(11)])


class DoWhile(unittest.TestCase):
    def test_runs_once(self):
        self.assertEqual(run("while_vs_do")[0], "do while: entré\nn vale 6\n")

    def test_read_month_retries(self):
        self.assertEqual(run("read_month", "13 0 7"),
                         ("Mes fuera de rango, escriba otro\n" * 2 + "Mes elegido: 7\n", 0))
        for month in [1, 12]:
            self.assertEqual(run("read_month", str(month)), (f"Mes elegido: {month}\n", 0))

    def test_read_month_ends_on_bad_input(self):
        self.assertEqual(run("read_month", "abc"), ("Entrada inválida\n", 1))
        self.assertEqual(run("read_month", ""), ("Entrada inválida\n", 1))
        self.assertEqual(run("read_month", "13"),
                         ("Mes fuera de rango, escriba otro\nEntrada inválida\n", 1))

    def test_read_forever_does_not_end(self):
        for text in ["abc", ""]:
            _, status, binary = compile_error_demo("read_forever")
            self.assertEqual(status, 0)
            lines, still_running = first_lines(binary, text, 500)
            self.assertEqual(lines, ["Mes fuera de rango\n"] * 500)
            self.assertTrue(still_running)

    def test_do_scope_does_not_compile(self):
        messages, status, _ = compile_error_demo("do_scope")
        self.assertNotEqual(status, 0)
        self.assertIn("undeclared", messages)


class Patterns(unittest.TestCase):
    def test_count_passed(self):
        grades = (HERE / "grades.txt").read_text()
        self.assertEqual(grades.split(), ["14", "9", "11", "20", "10"])
        self.assertEqual(run("count_passed", grades), ("Aprobaron 3 de 5\n", 0))
        self.assertEqual(run("count_passed", ""), ("Aprobaron 0 de 0\n", 0))
        self.assertEqual(run("count_passed", "14 abc"), ("Entrada inválida\n", 1))

    def test_not_eof_spins_only_on_bad_data(self):
        _, status, binary = compile_error_demo("not_eof")
        self.assertEqual(status, 0)
        lines, still_running = first_lines(binary, "abc", 500)
        self.assertEqual(lines[2], "Llevo 3 datos; el último es 0\n")
        self.assertTrue(still_running)
        done = subprocess.run([str(binary)], input="4\n", text=True, stdout=subprocess.PIPE, timeout=10)
        self.assertEqual(done.stdout, "Llevo 1 datos; el último es 4\n")

    def test_sentinel(self):
        self.assertEqual(run("sentinel_sum", "1250 300 4050 0"), ("3 productos: 56.00 soles\n", 0))
        self.assertEqual(run("sentinel_sum", "0"), ("0 productos: 0.00 soles\n", 0))
        self.assertEqual(run("sentinel_sum", "5 0 7 0"), ("1 productos: 0.05 soles\n", 0))

    def test_sentinel_ends_on_bad_input(self):
        for text in ["", "1250 300", "1250 abc", "abc"]:
            with self.subTest(text=text):
                self.assertEqual(run("sentinel_sum", text), ("Entrada inválida\n", 1))

    def test_max_of_n(self):
        cases = {"3 -3 -7 -1": -1, "3 8 -7 12": 12, "1 -5": -5, "3 5 5 5": 5, "3 9 1 2": 9, "3 1 2 9": 9}
        for text, expected in cases.items():
            with self.subTest(text=text):
                self.assertEqual(run("max_of_n", text), (f"La mayor es {expected}\n", 0))

    def test_max_of_n_rejects(self):
        for text in ["", "abc", "0", "-1", "3 1 2", "2 1 abc"]:
            with self.subTest(text=text):
                self.assertEqual(run("max_of_n", text), ("Entrada inválida\n", 1))

    def test_max_from_zero_is_wrong(self):
        _, status, binary = compile_error_demo("max_from_zero")
        self.assertEqual(status, 0)
        done = subprocess.run([str(binary)], input="3 -3 -7 -1", text=True, stdout=subprocess.PIPE)
        self.assertEqual(done.stdout, "La mayor es 0\n")


class Getchar(unittest.TestCase):
    def test_count_bytes(self):
        self.assertEqual(run("count_bytes", "")[0], "0 bytes\n")
        self.assertEqual((HERE / "cities.txt").read_bytes(), b"Puno\nCusco\nPiura\n")
        self.assertEqual(run("count_bytes", "Puno\nCusco\nPiura\n")[0], "17 bytes\n")
        self.assertEqual(run("count_bytes", "ñ\n")[0], "3 bytes\n")

    def test_to_upper_only_ascii(self):
        self.assertEqual(run("to_upper", "hola mundo\n")[0], "HOLA MUNDO\n")
        self.assertEqual(run("to_upper", "ñandú\n")[0], "ñANDú\n")
        self.assertEqual(run("to_upper", "az AZ 09 `{\n")[0], "AZ AZ 09 `{\n")

    def test_copy_keeps_every_byte(self):
        data = bytes(range(256)) * 3
        done = subprocess.run([str(BUILD / "copy")], input=data, stdout=subprocess.PIPE, timeout=10)
        self.assertEqual(done.stdout, data)

    def test_missing_parentheses(self):
        messages, status, binary = compile_error_demo("getchar_no_parens")
        self.assertEqual(status, 0)
        self.assertIn("-Wparentheses", messages)
        done = subprocess.run([str(binary)], input="a\nb\n", text=True, stdout=subprocess.PIPE, timeout=10)
        self.assertEqual(done.stdout, "0 saltos de línea\n")

    def test_char_without_sign_never_ends(self):
        # -funsigned-char da el mismo comportamiento en ARM y en x86-64.
        messages, status, binary = compile_error_demo("char_eof", "-funsigned-char")
        self.assertEqual(status, 0)
        self.assertIn("comparison is always true due to limited range of data type", messages)
        with self.assertRaises(subprocess.TimeoutExpired):
            subprocess.run([str(binary)], input="a\n", text=True, stdout=subprocess.PIPE, timeout=1)

    def test_char_with_sign_stops_at_0xff(self):
        messages, status, binary = compile_error_demo("char_eof", "-fsigned-char")
        self.assertEqual((messages, status), ("", 0))
        done = subprocess.run([str(binary)], input=b"a\xffb\n", stdout=subprocess.PIPE, timeout=10)
        self.assertEqual(done.stdout, b"1 bytes\n")


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 subprocess
import unittest

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


def run(program, stdin):
    done = subprocess.run(
        [str(HERE / "build" / LAB_DIR / program)], input=stdin, text=True,
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=10,
    )
    return done.stdout, done.returncode


def run_with_file(program, name):
    with open(HERE / "lab" / name, encoding="utf-8") as data:
        done = subprocess.run(
            [str(HERE / "build" / LAB_DIR / program)], stdin=data, text=True,
            stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=10,
        )
    return done.stdout, done.returncode


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe {LAB_DIR}/")
class LabTests(unittest.TestCase):
    def test_no_warnings(self):
        for name in ["sum_until_zero", "is_prime", "count_lines", "validate_grade"]:
            with self.subTest(name=name):
                done = subprocess.run(
                    ["gcc", "-Wall", "-Wextra", "-std=c17", "-Werror", "-fsyntax-only",
                     str(HERE / LAB_DIR / f"{name}.c")],
                    text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                )
                self.assertEqual(done.returncode, 0, done.stdout)

    def test_sum_average_max(self):
        cases = {
            "10 15 5 0": ("Suma: 30\nPromedio: 10.00\nMayor: 15\n"),
            "7 0": ("Suma: 7\nPromedio: 7.00\nMayor: 7\n"),
            "-5 -2 -9 0": ("Suma: -16\nPromedio: -5.33\nMayor: -2\n"),
            "1 2 0": ("Suma: 3\nPromedio: 1.50\nMayor: 2\n"),
            "3\n4\n0\n99\n": ("Suma: 7\nPromedio: 3.50\nMayor: 4\n"),
        }
        for text, expected in cases.items():
            with self.subTest(text=text):
                self.assertEqual(run("sum_until_zero", text), (expected, 0))

    def test_sum_no_data(self):
        self.assertEqual(run("sum_until_zero", "0"), ("Sin datos\n", 0))

    def test_sum_bad_input(self):
        for text in ["", "abc", "4 abc", "4 5", "4 5 x 0"]:
            with self.subTest(text=text):
                self.assertEqual(run("sum_until_zero", text), ("Entrada inválida\n", 1))

    def test_primes(self):
        for n in [2, 3, 5, 7, 11, 13, 97, 7919, 2147483647]:
            with self.subTest(n=n):
                self.assertEqual(run("is_prime", str(n)), ("Primo\n", 0))

    def test_not_primes(self):
        for n in [-7, -1, 0, 1, 4, 6, 9, 15, 25, 49, 121, 2147483646, 2147395600]:
            with self.subTest(n=n):
                self.assertEqual(run("is_prime", str(n)), ("No primo\n", 0))

    def test_prime_bad_input(self):
        for text in ["abc", ""]:
            with self.subTest(text=text):
                self.assertEqual(run("is_prime", text), ("Entrada inválida\n", 1))

    def test_count_lines_from_files(self):
        self.assertEqual(run_with_file("count_lines", "three_lines.txt"), ("3\n", 0))
        self.assertEqual(run_with_file("count_lines", "no_final_newline.txt"), ("2\n", 0))
        self.assertEqual(run_with_file("count_lines", "empty.txt"), ("0\n", 0))

    def test_count_lines_other_inputs(self):
        self.assertEqual(run("count_lines", "\n\n\n\n"), ("4\n", 0))
        self.assertEqual(run("count_lines", "ñandú\n"), ("1\n", 0))

    def test_count_lines_with_byte_0xff(self):
        # Un byte 0xFF no es EOF. Con c de tipo char, esta prueba falla.
        done = subprocess.run(
            [str(HERE / "build" / LAB_DIR / "count_lines")], input=b"a\xffb\nc\n",
            stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=10,
        )
        self.assertEqual(done.stdout, b"2\n")

    def test_validate_grade(self):
        cases = {
            "15": "Nota válida: 15\n",
            "0": "Nota válida: 0\n",
            "20": "Nota válida: 20\n",
            "25 -1 20": "Nota fuera de rango\nNota fuera de rango\nNota válida: 20\n",
            "21 7 30": "Nota fuera de rango\nNota válida: 7\n",
        }
        for text, expected in cases.items():
            with self.subTest(text=text):
                self.assertEqual(run("validate_grade", text), (expected, 0))

    def test_validate_grade_bad_input(self):
        self.assertEqual(run("validate_grade", "abc"), ("Entrada inválida\n", 1))
        self.assertEqual(run("validate_grade", ""), ("Entrada inválida\n", 1))
        self.assertEqual(run("validate_grade", "25"), ("Nota fuera de rango\nEntrada inválida\n", 1))
        self.assertEqual(run("validate_grade", "25 abc"), ("Nota fuera de rango\nEntrada inválida\n", 1))


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-05.md"
SLIDES = ROOT / "slides" / "week-05.tex"
# Antes de un bloque cuya salida cambia con el procesador. Solo se exigen sus
# líneas con "runtime error".
MACHINE_DEPENDENT = "<!-- depende del procesador -->"


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


def run_shell(command):
    done = subprocess.run(
        ["bash", "-c", command], cwd=HERE, text=True,
        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, depende_del_procesador) de cada bloque de los apuntes."""
    blocks, current, flagged, previous = [], None, False, ""
    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() == MACHINE_DEPENDENT
            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:
                        wanted = [line for line in expected if "runtime error" in line]
                        for line in wanted:
                            self.assertIn(line, actual)
                    else:
                        self.assertEqual(actual, "\n".join(expected))
        self.assertGreater(checked, 10)

    @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)


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/count_lines.cDescargar lab/count_lines.c
// Laboratorio, parte 3: cuenta las líneas de la entrada con getchar, hasta EOF.
// Una línea termina con un salto de línea, así que se cuentan los '\n', igual
// que hace wc -l. Un último renglón sin '\n' no se cuenta.
//
// El programa escribe solo el número, por ejemplo:
//   3
// y termina con return 0. Un archivo vacío tiene 0 líneas.
//
// Pruébenlo con los archivos de esta carpeta:
//   build/lab/count_lines < lab/three_lines.txt
//   build/lab/count_lines < lab/no_final_newline.txt
//   build/lab/count_lines < lab/empty.txt
// y con make test-lab.
#include <stdio.h>

int main(void) {
    // Escriban aquí su programa.
    return 0;
}
lab/empty.txtDescargar lab/empty.txt
lab/is_prime.cDescargar lab/is_prime.c
// Laboratorio, parte 2: lee un entero y dice si es primo. Un primo es un entero
// mayor que 1 que solo se divide exactamente entre 1 y entre sí mismo.
//
// El programa escribe exactamente una de estas líneas:
//   Primo              termina con return 0
//   No primo           también para 0, 1 y los negativos; termina con return 0
//   Entrada inválida   lo escrito no es un número; termina con return 1
//
// Tiene que funcionar también con el mayor int, 2147483647, que es primo.
//
// Pruébenlo con make test-lab.
#include <stdio.h>

int main(void) {
    // Escriban aquí su programa.
    return 0;
}
lab/no_final_newline.txtDescargar lab/no_final_newline.txt
Lima
Arequipa
Trujillo
lab/sum_until_zero.cDescargar lab/sum_until_zero.c
// Laboratorio, parte 1: lee enteros hasta un 0, que no forma parte de los datos.
// Pueden ser negativos. Se supone que la suma cabe en un int.
//
// El programa escribe exactamente esto y termina con return 0:
//   Suma: 30
//   Promedio: 10.00
//   Mayor: 15
//
// Si el primer número es 0, escribe solo:
//   Sin datos                  termina con return 0
//
// Si un dato no es un número, o la entrada se acaba antes del 0, escribe solo:
//   Entrada inválida           termina con return 1
//
// Pruébenlo con make test-lab.
#include <stdio.h>

int main(void) {
    // Escriban aquí su programa.
    return 0;
}
lab/three_lines.txtDescargar lab/three_lines.txt
Lima
Arequipa
Trujillo
lab/validate_grade.cDescargar lab/validate_grade.c
// Laboratorio, parte 4: lee una nota entera con do while hasta que esté entre
// 0 y 20. No escribe ningún mensaje antes de leer.
//
// Por cada nota fuera de rango, escribe esta línea y vuelve a leer:
//   Nota fuera de rango
//
// Cuando la nota es válida, por ejemplo 15, escribe esta línea y termina con return 0:
//   Nota válida: 15
//
// Si lo escrito no es un número, o la entrada se acaba, escribe esta línea y
// termina con return 1:
//   Entrada inválida
//
// Pruébenlo con make test-lab.
#include <stdio.h>

int main(void) {
    // Escriban aquí su programa.
    return 0;
}

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# Semana 5: ciclos. 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.

CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g -fsanitize=address,undefined
BUILD = build
PROGRAMS = coins sum_to_n for_trace for_as_while mult_table to_binary while_vs_do \
           read_month count_passed sentinel_sum max_of_n double_steps count_bytes \
           to_upper copy
LAB_PROGRAMS = sum_until_zero is_prime count_lines validate_grade

.PHONY: all lab test test-lab clean

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

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

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

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

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

lab: $(LAB_PROGRAMS:%=$(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_PROGRAMS:%=$(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__