06
Programación en C

Ciclos anidados y números aleatorios

Programación en C

Semana 6 — Ciclos anidados y números aleatorios

Objetivos

  • Escribir ciclos anidados y contar cuántas veces corre cada línea.
  • Generar números aleatorios en un rango y reproducir un resultado con una semilla.
  • Estimar una probabilidad mediante una simulación.

Teoría — Ciclos anidados · Números aleatorios

Lectura: Apuntes, semana 6.

Temas:

  • Ciclos anidados: tablas de multiplicar y patrones. Cuántas veces corre cada línea.
  • rand y srand: los números son pseudoaleatorios. Para qué sirve fijar la semilla.
  • Obtener un número en un rango.
  • Simulación: estimar la probabilidad de sumar 7 con dos dados.
  • El error de la estimación baja con más repeticiones.

Laboratorio — Ciclos anidados y una simulación

  1. Dibujar un triángulo de asteriscos de altura N.
  2. Escribir los primos hasta N con ciclos anidados.
  3. Simular lanzamientos de dos dados y estimar la probabilidad de sumar 7, con 100, 10 000 y 1 000 000 de intentos.
  4. Repetir con la misma semilla y con otra. ¿Qué cambia?

Preguntas de repaso

  1. Dos ciclos anidados de N vueltas: ¿cuántas veces corre la línea interior? ¿Y si N se duplica?
  2. ¿Por qué dos corridas con la misma semilla dan los mismos resultados? ¿Cuándo conviene y cuándo no?
  3. ¿Cómo obtendrían un número al azar entre 1 y 6 con rand?
  4. Estiman una probabilidad con 100 intentos y con 10 000. ¿En cuál confiarían más y por qué?
  5. Sigan a mano un ciclo que imprima una tabla de multiplicar de 3 × 3.

Código de la semana

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

Ejemplos (14)Errores (6)Pruebas (3)Laboratorio (3)Compilación (1)

Ejemplos

Los programas que usan los apuntes y la clase.

count_runs.cDescargar count_runs.c
#include <stdio.h>

// Lee n y cuenta cuántas veces corre cada línea en dos formas de anidar ciclos.
int main(void) {
    int n;
    int outer_runs = 0;
    int square_runs = 0;
    int triangle_runs = 0;

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

    // Con 5000 como límite, los contadores caben de sobra en un int.
    if (n < 1 || n > 5000) {
        printf("n debe estar entre 1 y 5000\n");
        return 1;
    }

    // El ciclo interior siempre da n vueltas.
    for (int row = 1; row <= n; row++) {
        outer_runs++;
        for (int col = 1; col <= n; col++) {
            square_runs++;
        }
    }

    // El ciclo interior da row vueltas: depende del exterior.
    for (int row = 1; row <= n; row++) {
        for (int col = 1; col <= row; col++) {
            triangle_runs++;
        }
    }

    printf("n = %d\n", n);
    printf("Línea del ciclo exterior: %d\n", outer_runs);
    printf("Línea interior, cuadrado: %d\n", square_runs);
    printf("Línea interior, triángulo: %d\n", triangle_runs);
    return 0;
}
dice_exact.cDescargar dice_exact.c
#include <stdio.h>

// Recorre los 36 resultados de dos dados, una vez por cada suma de 2 a 12.
int main(void) {
    for (int target = 2; target <= 12; target++) {
        int ways = 0;

        for (int d1 = 1; d1 <= 6; d1++) {
            for (int d2 = 1; d2 <= 6; d2++) {
                if (d1 + d2 == target) {
                    ways++;
                }
            }
        }
        printf("Suma %2d: %d de 36, %.4f\n", target, ways, (double) ways / 36);
    }
    return 0;
}
dice_seven.cDescargar dice_seven.c
#include <stdio.h>
#include <stdlib.h>

// Lee una semilla y una cantidad de intentos.
// Simula dos dados y estima la probabilidad de que sumen 7.
int main(void) {
    int seed, trials;
    int hits = 0;

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

    if (seed < 0) {
        printf("La semilla no puede ser negativa\n");
        return 1;
    }

    if (trials < 1 || trials > 10000000) {
        printf("Los intentos deben estar entre 1 y 10000000\n");
        return 1;
    }

    srand((unsigned) seed);

    for (int i = 1; i <= trials; i++) {
        int d1 = 1 + rand() % 6;
        int d2 = 1 + rand() % 6;

        if (d1 + d2 == 7) {
            hits++;
        }
    }

    printf("%d intentos, %d sumaron 7, estimación %.4f\n", trials, hits, (double) hits / trials);
    return 0;
}
die_counts.cDescargar die_counts.c
#include <stdio.h>
#include <stdlib.h>

// Lee una semilla y una cantidad de lanzamientos. Cuenta cuánto salió cada cara.
int main(void) {
    int seed, rolls;
    int one = 0, two = 0, three = 0, four = 0, five = 0, six = 0;

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

    if (seed < 0) {
        printf("La semilla no puede ser negativa\n");
        return 1;
    }

    if (rolls < 1 || rolls > 10000000) {
        printf("Los lanzamientos deben estar entre 1 y 10000000\n");
        return 1;
    }

    srand((unsigned) seed);

    for (int i = 1; i <= rolls; i++) {
        int die = 1 + rand() % 6;

        switch (die) {
            case 1: one++; break;
            case 2: two++; break;
            case 3: three++; break;
            case 4: four++; break;
            case 5: five++; break;
            default: six++; break;
        }
    }

    printf("Cara 1: %d\n", one);
    printf("Cara 2: %d\n", two);
    printf("Cara 3: %d\n", three);
    printf("Cara 4: %d\n", four);
    printf("Cara 5: %d\n", five);
    printf("Cara 6: %d\n", six);
    return 0;
}
divisors.cDescargar divisors.c
#include <stdio.h>

// Lee limit. Para cada número de 1 a limit escribe sus divisores y cuántos son.
int main(void) {
    int limit;

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

    if (limit < 1 || limit > 1000) {
        printf("El límite debe estar entre 1 y 1000\n");
        return 1;
    }

    for (int n = 1; n <= limit; n++) {
        int count = 0;

        printf("%d:", n);
        for (int d = 1; d <= n; d++) {
            if (n % d == 0) {
                printf(" %d", d);
                count++;
            }
        }
        printf(" (%d)\n", count);
    }
    return 0;
}
fake_rand.cDescargar fake_rand.c
// Reemplaza rand y srand para las pruebas del laboratorio. No hace falta leerlo.
// rand da 2, 3, 2, 3...: con rand() % 6 + 1, dos dados siempre suman 7.
// srand termina el programa si se llama dos veces.
#include <stdio.h>
#include <stdlib.h>

static int next_value = 2;
static int srand_calls = 0;

int rand(void) {
    int value = next_value;
    next_value = 5 - next_value;
    return value;
}

void srand(unsigned seed) {
    (void) seed;
    srand_calls++;
    if (srand_calls > 1) {
        printf("srand se llamó más de una vez\n");
        exit(3);
    }
}
rand_max.cDescargar rand_max.c
#include <stdio.h>
#include <stdlib.h>

// Escribe RAND_MAX y los primeros números de rand, sin llamar a srand.
int main(void) {
    printf("RAND_MAX = %d\n", RAND_MAX);

    for (int i = 1; i <= 5; i++) {
        printf("%d\n", rand());
    }
    return 0;
}
range.cDescargar range.c
#include <stdio.h>
#include <stdlib.h>

// Lee un mínimo, un máximo y una semilla. Escribe diez números en ese rango.
int main(void) {
    int min, max, seed;

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

    // Con estos límites, max - min + 1 no pasa de 32767, el menor RAND_MAX permitido.
    if (min > max || min < -10000 || max > 10000) {
        printf("Rango inválido\n");
        return 1;
    }

    if (seed < 0) {
        printf("La semilla no puede ser negativa\n");
        return 1;
    }

    srand((unsigned) seed);

    for (int i = 1; i <= 10; i++) {
        printf("%d ", min + rand() % (max - min + 1));
    }
    printf("\n");
    return 0;
}
rectangle.cDescargar rectangle.c
#include <stdio.h>

// Lee filas y columnas, y dibuja un rectángulo de asteriscos.
int main(void) {
    int rows, cols;

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

    if (rows < 1 || cols < 1) {
        printf("Filas y columnas deben ser positivas\n");
        return 1;
    }

    for (int row = 1; row <= rows; row++) {
        for (int col = 1; col <= cols; col++) {
            printf("*");
        }
        printf("\n");
    }
    return 0;
}
seed.cDescargar seed.c
#include <stdio.h>
#include <stdlib.h>

// Lee una semilla y escribe los primeros cinco números de rand con esa semilla.
int main(void) {
    int seed;

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

    if (seed < 0) {
        printf("La semilla no puede ser negativa\n");
        return 1;
    }

    srand((unsigned) seed);

    for (int i = 1; i <= 5; i++) {
        printf("%d\n", rand());
    }
    return 0;
}
staircase.cDescargar staircase.c
#include <stdio.h>

// Lee n y escribe n filas: la fila 3 tiene 1 2 3, y así las demás.
int main(void) {
    int n;

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

    if (n < 1 || n > 30) {
        printf("n debe estar entre 1 y 30\n");
        return 1;
    }

    for (int row = 1; row <= n; row++) {
        for (int col = 1; col <= row; col++) {
            printf("%3d", col);
        }
        printf("\n");
    }
    return 0;
}
time_seed.cDescargar time_seed.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// Toma la semilla de la hora actual y lanza un dado diez veces.
int main(void) {
    srand((unsigned) time(NULL));

    for (int i = 1; i <= 10; i++) {
        printf("%d ", 1 + rand() % 6);
    }
    printf("\n");
    return 0;
}
times_table.cDescargar times_table.c
#include <stdio.h>

// Lee n y escribe la tabla de multiplicar de n por n, de 1 a 12.
int main(void) {
    int n;

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

    if (n < 1 || n > 12) {
        printf("n debe estar entre 1 y 12\n");
        return 1;
    }

    for (int row = 1; row <= n; row++) {
        for (int col = 1; col <= n; col++) {
            printf("%4d", row * col);
        }
        printf("\n");
    }
    return 0;
}
toy_generator.cDescargar toy_generator.c
#include <stdio.h>

// Un generador de juguete: cada número sale del anterior con una fórmula.
// Lee la semilla y escribe 18 números.
int main(void) {
    int x;

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

    if (x < 0 || x > 15) {
        printf("La semilla debe estar entre 0 y 15\n");
        return 1;
    }

    for (int i = 1; i <= 18; i++) {
        x = (5 * x + 3) % 16;
        printf("%d ", x);
    }
    printf("\n");
    return 0;
}

Errores

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

errors/die_off_by_one.cDescargar errors/die_off_by_one.c
#include <stdio.h>
#include <stdlib.h>

// ERROR: rand() % 6 da de 0 a 5, no de 1 a 6.
int main(void) {
    int smallest = 100;
    int largest = -100;

    srand(42);

    for (int i = 1; i <= 100000; i++) {
        int die = rand() % 6;

        if (die < smallest) {
            smallest = die;
        }
        if (die > largest) {
            largest = die;
        }
    }

    printf("Menor: %d\n", smallest);
    printf("Mayor: %d\n", largest);
    return 0;
}
errors/int_probability.cDescargar errors/int_probability.c
#include <stdio.h>
#include <stdlib.h>

// ERROR: hits / trials es una división entera.
int main(void) {
    int trials = 1000000;
    int hits = 0;

    srand(42);

    for (int i = 1; i <= trials; i++) {
        int d1 = 1 + rand() % 6;
        int d2 = 1 + rand() % 6;

        if (d1 + d2 == 7) {
            hits++;
        }
    }

    double estimate = hits / trials;
    printf("Estimación: %.4f\n", estimate);
    return 0;
}
errors/no_reset.cDescargar errors/no_reset.c
#include <stdio.h>

// ERROR: col no vuelve a 1 al empezar cada fila.
int main(void) {
    int row = 1;
    int col = 1;

    while (row <= 3) {
        while (col <= 4) {
            printf("*");
            col++;
        }
        printf("\n");
        row++;
    }
    return 0;
}
errors/no_stdlib.cDescargar errors/no_stdlib.c
#include <stdio.h>

// ERROR: rand se declara en stdlib.h, y falta incluirlo.
int main(void) {
    printf("%d\n", rand());
    return 0;
}
errors/same_counter.cDescargar errors/same_counter.c
#include <stdio.h>

// ERROR: los dos ciclos usan la misma variable i.
int main(void) {
    int i;

    for (i = 1; i <= 3; i++) {
        for (i = 1; i <= 3; i++) {
            printf("*");
        }
        printf("\n");
    }
    return 0;
}
errors/srand_in_loop.cDescargar errors/srand_in_loop.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// ERROR: siembra de nuevo en cada vuelta, con la misma hora.
int main(void) {
    for (int i = 1; i <= 10; i++) {
        srand((unsigned) time(NULL));
        printf("%d ", 1 + rand() % 6);
    }
    printf("\n");
    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 6.

Los números de rand dependen de la implementación de C. Por eso estas pruebas no
comparan valores aleatorios exactos: comprueban rangos y propiedades.
"""
import pathlib
import re
import subprocess
import tempfile
import time
import unittest

HERE = pathlib.Path(__file__).parent
BUILD = HERE / "build"
FLAGS = ["-Wall", "-Wextra", "-std=c17"]
EXACT = 1 / 6
# Con n intentos, la desviación típica de la estimación es la raíz de
# (1/6)(5/6)/n: unas 0.00037 con 1000000 y 0.0037 con 10000. Las tolerancias son
# 8 veces eso, así que cualquier generador razonable las cumple.
TOLERANCE_1M = 0.003
TOLERANCE_10K = 0.03


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=60,
    )
    return done.stdout, done.returncode


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


def run_demo(binary):
    done = subprocess.run(
        [str(binary)], text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
    )
    return done.stdout


def estimate(seed, trials):
    out, status = run("dice_seven", f"{seed} {trials}")
    assert status == 0, out
    match = re.fullmatch(r"(\d+) intentos, (\d+) sumaron 7, estimación (\d\.\d{4})\n", out)
    assert match, out
    return int(match[1]), int(match[2]), float(match[3])


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 NestedLoops(unittest.TestCase):
    def test_rectangle_shape(self):
        for rows, cols in [(1, 1), (2, 5), (4, 3)]:
            with self.subTest(rows=rows, cols=cols):
                self.assertEqual(run("rectangle", f"{rows} {cols}"), (("*" * cols + "\n") * rows, 0))

    def test_rectangle_rejects(self):
        self.assertEqual(run("rectangle", "abc"), ("Entrada inválida\n", 1))
        self.assertEqual(run("rectangle", "2"), ("Entrada inválida\n", 1))
        self.assertEqual(run("rectangle", "0 5"), ("Filas y columnas deben ser positivas\n", 1))

    def test_times_table(self):
        for n in [1, 3, 12]:
            expected = "".join(
                "".join(f"{r * c:4d}" for c in range(1, n + 1)) + "\n" for r in range(1, n + 1)
            )
            with self.subTest(n=n):
                self.assertEqual(run("times_table", str(n)), (expected, 0))

    def test_times_table_limits(self):
        for text in ["0", "13"]:
            self.assertEqual(run("times_table", text), ("n debe estar entre 1 y 12\n", 1))

    def test_count_formulas(self):
        for n in [1, 3, 10, 20, 100, 200, 5000]:
            out, status = run("count_runs", str(n))
            numbers = [int(x) for x in re.findall(r": (\d+)", out)]
            with self.subTest(n=n):
                self.assertEqual(status, 0)
                self.assertEqual(numbers, [n, n * n, n * (n + 1) // 2])

    def test_count_rejects_what_could_overflow(self):
        self.assertEqual(run("count_runs", "5001"), ("n debe estar entre 1 y 5000\n", 1))
        self.assertLess(5000 * 5000, 2 ** 31)

    def test_doubling_quadruples(self):
        for n in [10, 100]:
            small = [int(x) for x in re.findall(r": (\d+)", run("count_runs", str(n))[0])]
            big = [int(x) for x in re.findall(r": (\d+)", run("count_runs", str(2 * n))[0])]
            self.assertEqual(big[1], 4 * small[1])
            self.assertAlmostEqual(big[2] / small[2], 4, delta=0.2)

    def test_staircase(self):
        expected = "".join(
            "".join(f"{c:3d}" for c in range(1, r + 1)) + "\n" for r in range(1, 5)
        )
        self.assertEqual(run("staircase", "4"), (expected, 0))

    def test_divisors(self):
        expected = ""
        for n in range(1, 101):
            found = [d for d in range(1, n + 1) if n % d == 0]
            expected += f"{n}: {' '.join(map(str, found))} ({len(found)})\n"
        self.assertEqual(run("divisors", "100"), (expected, 0))

    def test_divisor_counts_show_the_primes(self):
        out, _ = run("divisors", "12")
        twos = [int(line.split(":")[0]) for line in out.splitlines() if line.endswith("(2)")]
        self.assertEqual(twos, [2, 3, 5, 7, 11])


class NestedErrors(unittest.TestCase):
    def test_same_counter_makes_one_row(self):
        messages, status, binary = compile_error_demo("same_counter")
        self.assertEqual((messages, status), ("", 0))
        self.assertEqual(run_demo(binary), "***\n")

    def test_no_reset_leaves_empty_rows(self):
        messages, status, binary = compile_error_demo("no_reset")
        self.assertEqual((messages, status), ("", 0))
        self.assertEqual(run_demo(binary), "****\n\n\n")

    def test_missing_stdlib_does_not_compile(self):
        messages, status, _ = compile_error_demo("no_stdlib")
        self.assertNotEqual(status, 0)
        self.assertIn("implicit declaration of function", messages)


class ToyGenerator(unittest.TestCase):
    def test_every_seed_has_period_16(self):
        for seed in range(16):
            out, status = run("toy_generator", str(seed))
            numbers = [int(x) for x in out.split()]
            with self.subTest(seed=seed):
                self.assertEqual(status, 0)
                self.assertEqual(sorted(numbers[:16]), list(range(16)))
                self.assertEqual(numbers[15], seed)
                self.assertEqual(numbers[16:], numbers[:2])

    def test_rejects_other_seeds(self):
        self.assertEqual(run("toy_generator", "16"), ("La semilla debe estar entre 0 y 15\n", 1))


class Rand(unittest.TestCase):
    def test_rand_max_is_allowed_and_values_fit(self):
        out, status = run("rand_max")
        lines = out.splitlines()
        rand_max = int(lines[0].split("= ")[1])
        self.assertEqual(status, 0)
        self.assertGreaterEqual(rand_max, 32767)
        for value in lines[1:]:
            self.assertTrue(0 <= int(value) <= rand_max)

    def test_no_srand_is_seed_1(self):
        without_srand = run("rand_max")[0].splitlines()[1:]
        self.assertEqual(without_srand, run("seed", "1")[0].splitlines())

    def test_same_seed_same_sequence(self):
        for seed in [0, 1, 42, 2000]:
            with self.subTest(seed=seed):
                self.assertEqual(run("seed", str(seed)), run("seed", str(seed)))

    def test_other_seed_other_sequence(self):
        self.assertNotEqual(run("seed", "1")[0], run("seed", "42")[0])

    def test_seed_rejects(self):
        self.assertEqual(run("seed", "-1"), ("La semilla no puede ser negativa\n", 1))
        self.assertEqual(run("seed", "abc"), ("Entrada inválida\n", 1))

    def test_time_seed_is_a_die(self):
        out, status = run("time_seed")
        self.assertEqual(status, 0)
        values = [int(x) for x in out.split()]
        self.assertEqual(len(values), 10)
        self.assertTrue(all(1 <= v <= 6 for v in values))

    def test_same_second_same_output(self):
        # Puede tocar justo el cambio de segundo: basta que coincidan en un intento.
        for _ in range(5):
            if run("time_seed") == run("time_seed"):
                return
        self.fail("dos corridas seguidas nunca coincidieron")

    def test_other_second_other_output(self):
        first = run("time_seed")[0]
        time.sleep(1.1)
        self.assertNotEqual(first, run("time_seed")[0])

    def test_srand_in_loop_repeats(self):
        _, status, binary = compile_error_demo("srand_in_loop")
        self.assertEqual(status, 0)
        values = run_demo(binary).split()
        self.assertEqual(len(values), 10)
        # Dos valores como máximo, por si el segundo cambia durante la corrida.
        self.assertLessEqual(len(set(values)), 2)


class Range(unittest.TestCase):
    def test_values_in_range_for_many_seeds(self):
        for low, high in [(1, 6), (0, 20), (-5, 5), (7, 7), (-10000, 10000)]:
            for seed in range(30):
                out, status = run("range", f"{low} {high} {seed}")
                with self.subTest(low=low, high=high, seed=seed):
                    self.assertEqual(status, 0)
                    values = [int(x) for x in out.split()]
                    self.assertEqual(len(values), 10)
                    self.assertTrue(all(low <= v <= high for v in values))

    def test_die_reaches_both_ends(self):
        seen = set()
        for seed in range(30):
            seen.update(int(x) for x in run("range", f"1 6 {seed}")[0].split())
        self.assertEqual(seen, {1, 2, 3, 4, 5, 6})

    def test_rejects(self):
        self.assertEqual(run("range", "6 1 42"), ("Rango inválido\n", 1))
        self.assertEqual(run("range", "-10001 0 42"), ("Rango inválido\n", 1))
        self.assertEqual(run("range", "0 10001 42"), ("Rango inválido\n", 1))
        self.assertEqual(run("range", "1 6 -1"), ("La semilla no puede ser negativa\n", 1))
        self.assertEqual(run("range", "1 6"), ("Entrada inválida\n", 1))

    def test_forgetting_the_one(self):
        _, status, binary = compile_error_demo("die_off_by_one")
        self.assertEqual(status, 0)
        self.assertEqual(run_demo(binary), "Menor: 0\nMayor: 5\n")

    def test_die_counts_add_up_and_stay_near(self):
        out, status = run("die_counts", "42 600000")
        counts = [int(x) for x in re.findall(r"Cara \d: (\d+)", out)]
        self.assertEqual(status, 0)
        self.assertEqual(sum(counts), 600000)
        # 100000 esperados, con desviación típica cerca de 289.
        for count in counts:
            self.assertLess(abs(count - 100000), 1000)

    def test_die_counts_rejects(self):
        self.assertEqual(run("die_counts", "42 0"),
                         ("Los lanzamientos deben estar entre 1 y 10000000\n", 1))

    def test_bias_arithmetic_in_the_notes(self):
        values = 2 ** 31
        self.assertEqual(values // 6, 357913941)
        self.assertEqual(values % 6, 2)
        self.assertEqual([sum(1 for v in range(8) if v % 6 == r) for r in range(6)],
                         [2, 2, 1, 1, 1, 1])


class Dice(unittest.TestCase):
    def test_exact_counts(self):
        out, status = run("dice_exact")
        self.assertEqual(status, 0)
        lines = out.splitlines()
        self.assertEqual(len(lines), 11)
        total = 0
        for target, line in zip(range(2, 13), lines):
            ways = sum(1 for a in range(1, 7) for b in range(1, 7) if a + b == target)
            total += ways
            self.assertEqual(line, f"Suma {target:2d}: {ways} de 36, {ways / 36:.4f}")
        self.assertEqual(total, 36)

    def test_estimate_is_consistent(self):
        for seed, trials in [(42, 100), (7, 100), (1, 10000)]:
            shown, hits, value = estimate(seed, trials)
            with self.subTest(seed=seed, trials=trials):
                self.assertEqual(shown, trials)
                self.assertTrue(0 <= hits <= trials)
                self.assertAlmostEqual(value, hits / trials, delta=0.00006)

    def test_same_seed_same_estimate(self):
        self.assertEqual(estimate(42, 10000), estimate(42, 10000))

    def test_tolerances(self):
        for seed in [1, 2, 3, 7, 42]:
            with self.subTest(seed=seed):
                self.assertLess(abs(estimate(seed, 10000)[2] - EXACT), TOLERANCE_10K)
                self.assertLess(abs(estimate(seed, 1000000)[2] - EXACT), TOLERANCE_1M)

    def test_error_falls_with_more_trials(self):
        worst = [max(abs(estimate(seed, trials)[2] - EXACT) for seed in [1, 2, 3, 7, 42])
                 for trials in [100, 10000, 1000000]]
        self.assertGreater(worst[0], worst[1])
        self.assertGreater(worst[1], worst[2])

    def test_rejects(self):
        message = "Los intentos deben estar entre 1 y 10000000\n"
        self.assertEqual(run("dice_seven", "42 0"), (message, 1))
        self.assertEqual(run("dice_seven", "42 10000001"), (message, 1))
        self.assertEqual(run("dice_seven", "-1 100"), ("La semilla no puede ser negativa\n", 1))
        self.assertEqual(run("dice_seven", "abc"), ("Entrada inválida\n", 1))

    def test_integer_division_gives_zero(self):
        messages, status, binary = compile_error_demo("int_probability")
        self.assertEqual((messages, status), ("", 0))
        self.assertEqual(run_demo(binary), "Estimación: 0.0000\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 re
import subprocess
import unittest

HERE = pathlib.Path(__file__).parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")
EXACT = 1 / 6
# Tolerancias: 8 desviaciones típicas de la estimación, un margen muy amplio.
TOLERANCES = {100: 0.3, 10000: 0.03, 1000000: 0.003}


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


def primes_up_to(limit):
    return [n for n in range(2, limit + 1)
            if all(n % d != 0 for d in range(2, int(n ** 0.5) + 1))]


def parse_estimates(out):
    """Parte la salida en (intentos, estimación, error). Falla si no tiene la forma pedida."""
    rows = []
    for line in out.splitlines():
        match = re.fullmatch(r"(\d+) (\d\.\d{4}) (\d\.\d{4})", line)
        if not match:
            raise AssertionError(f"línea con otra forma: {line!r}")
        rows.append((int(match[1]), float(match[2]), float(match[3])))
    return rows


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe {LAB_DIR}/")
class LabTests(unittest.TestCase):
    def test_no_warnings(self):
        for name in ["triangle", "primes", "dice_estimate"]:
            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_triangle(self):
        for height in [1, 3, 5, 50]:
            expected = "".join("*" * row + "\n" for row in range(1, height + 1))
            with self.subTest(height=height):
                self.assertEqual(run("triangle", str(height)), (expected, 0))

    def test_triangle_rejects(self):
        for text in ["0", "-3", "51"]:
            with self.subTest(text=text):
                self.assertEqual(run("triangle", text), ("Altura fuera de rango\n", 1))
        for text in ["abc", ""]:
            with self.subTest(text=text):
                self.assertEqual(run("triangle", text), ("Entrada inválida\n", 1))

    def test_primes(self):
        for limit in [1, 2, 3, 10, 49, 100, 1000, 10000]:
            expected = "".join(f"{p}\n" for p in primes_up_to(limit))
            with self.subTest(limit=limit):
                self.assertEqual(run("primes", str(limit)), (expected, 0))

    def test_primes_rejects(self):
        for text in ["0", "-7", "100001"]:
            with self.subTest(text=text):
                self.assertEqual(run("primes", text), ("N fuera de rango\n", 1))
        for text in ["abc", ""]:
            with self.subTest(text=text):
                self.assertEqual(run("primes", text), ("Entrada inválida\n", 1))

    def test_estimates_have_the_form_and_stay_near(self):
        for seed in [1, 7, 42]:
            out, status = run("dice_estimate", str(seed))
            with self.subTest(seed=seed):
                self.assertEqual(status, 0)
                rows = parse_estimates(out)
                self.assertEqual([row[0] for row in rows], [100, 10000, 1000000])
                for trials, value, error in rows:
                    self.assertLess(abs(value - EXACT), TOLERANCES[trials])
                    self.assertAlmostEqual(error, abs(value - EXACT), delta=0.00011)

    def test_each_row_starts_again_with_one_seed(self):
        # Con un rand falso, todos los intentos suman 7: cada fila debe dar 1.0000. Si hits
        # no vuelve a 0, una fila da más que 1, y una segunda llamada a srand se nota.
        binary = HERE / "build" / LAB_DIR / "dice_estimate_fake"
        subprocess.run(["gcc", "-std=c17", "-w", str(HERE / LAB_DIR / "dice_estimate.c"),
                        str(HERE / "fake_rand.c"), "-o", str(binary), "-lm"], check=True)
        done = subprocess.run([str(binary)], input="42", text=True, stdout=subprocess.PIPE,
                              stderr=subprocess.STDOUT, timeout=120)
        self.assertEqual(done.returncode, 0, done.stdout)
        self.assertEqual([row[1] for row in parse_estimates(done.stdout)], [1.0, 1.0, 1.0])

    def test_same_seed_same_output(self):
        self.assertEqual(run("dice_estimate", "42"), run("dice_estimate", "42"))

    def test_other_seed_other_output(self):
        self.assertNotEqual(run("dice_estimate", "1")[0], run("dice_estimate", "42")[0])

    def test_estimate_rejects(self):
        self.assertEqual(run("dice_estimate", "-1"), ("La semilla no puede ser negativa\n", 1))
        for text in ["abc", ""]:
            with self.subTest(text=text):
                self.assertEqual(run("dice_estimate", text), ("Entrada 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-06.md"
SLIDES = ROOT / "slides" / "week-06.tex"
# Antes de un bloque cuyos números cambian con la implementación de C o con la hora.
# De esos bloques solo se exige la forma: lo mismo, salvo los números.
VARIES = "<!-- depende de la implementación -->"


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


def shape(text):
    """Una salida con los números tapados y los espacios juntos."""
    return re.sub(r"\s+", " ", re.sub(r"\d+", "N", text)).strip()


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 strip_lines(text):
    return "\n".join(line.rstrip() for line in text.splitlines()).rstrip("\n")


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, varía) 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() == VARIES
            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 = strip_lines(run_shell(command))
                checked += 1
                with self.subTest(command=command):
                    if flagged:
                        self.assertEqual(shape(actual), shape("\n".join(expected)))
                    else:
                        self.assertEqual(actual, "\n".join(expected).rstrip("\n"))
        self.assertGreater(checked, 15)

    @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_estimate_table(self):
        """Los valores dependen de la implementación, pero la tabla debe ser coherente."""
        rows = re.findall(r"^\| (\d+|Peor error) \| (\d\.\d{4}) \| (\d\.\d{4}) \| (\d\.\d{4}) \|$",
                          NOTES.read_text(encoding="utf-8"), re.M)
        self.assertEqual(len(rows), 6)
        estimates = [[float(x) for x in row[1:]] for row in rows[:5]]
        worst = [float(x) for x in rows[5][1:]]
        for column in range(3):
            value = max(abs(row[column] - 1 / 6) for row in estimates)
            self.assertAlmostEqual(worst[column], value, delta=0.0001)
        self.assertGreater(worst[0], worst[1])
        self.assertGreater(worst[1], worst[2])

    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    def test_toy_generator_table(self):
        text = NOTES.read_text(encoding="utf-8")
        section = text.split("### Un generador de juguete")[1].split("### ")[0]
        rows = re.findall(r"^\| (\d) \| (\d+) \| (\d+) \| (\d+) \|$", section, re.M)
        self.assertEqual(len(rows), 5)
        for _, before, product, after in rows:
            self.assertEqual(5 * int(before) + 3, int(product))
            self.assertEqual(int(product) % 16, int(after))
        produced = run_shell("echo 7 | build/toy_generator").split()
        self.assertEqual(produced[:5], [row[3] for row in rows])

    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    def test_for_parts_table(self):
        """Cuenta de verdad cada parte del for de la tabla, con n = 3."""
        n = 3
        counts = dict.fromkeys(["row_init", "row_test", "row_inc", "col_init",
                                "col_test", "body", "col_inc"], 0)
        counts["row_init"] += 1
        row = 1
        while True:
            counts["row_test"] += 1
            if not row <= n:
                break
            counts["col_init"] += 1
            col = 1
            while True:
                counts["col_test"] += 1
                if not col <= n:
                    break
                counts["body"] += 1
                counts["col_inc"] += 1
                col += 1
            counts["row_inc"] += 1
            row += 1
        self.assertEqual(list(counts.values()), [1, 4, 3, 3, 12, 9, 9])
        text = NOTES.read_text(encoding="utf-8")
        for part, value in [("`row <= n`", 4), ("`col <= n`", 12), ("`int col = 1`", 3)]:
            self.assertRegex(text, re.escape(part) + r" \| [^|]+ \| " + str(value) + r" \|")


class SlidesAreTrue(unittest.TestCase):
    @unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
    def test_terminal_blocks_in_slides(self):
        for block in slide_blocks("terminal|terminalchica|salidalarga"):
            for command, expected in split_commands(block):
                actual = [line.rstrip() for line in 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_varying_blocks_in_slides(self):
        # corrida: salida cuyos números dependen de la implementación o de la hora.
        for block in slide_blocks("corrida"):
            for command, expected in split_commands(block):
                with self.subTest(command=command):
                    self.assertEqual(shape(run_shell(command)), shape("\n".join(expected)))

    @unittest.skipUnless(SLIDES.exists() and NOTES.exists(), "faltan archivos")
    def test_estimates_match_the_notes(self):
        rows = re.findall(r"^\| (?:\d+|Peor error) \| (\d\.\d{4}) \| (\d\.\d{4}) \| (\d\.\d{4}) \|$",
                          NOTES.read_text(encoding="utf-8"), re.M)
        slides = SLIDES.read_text(encoding="utf-8")
        for row in rows:
            for value in row:
                with self.subTest(value=value):
                    self.assertIn(value, slides)

    @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/dice_estimate.cDescargar lab/dice_estimate.c
// Laboratorio, parte 3: lee una semilla y estima la probabilidad de que dos dados
// sumen 7, con 100, con 10000 y con 1000000 de intentos.
//
// Escribe una línea por cada cantidad de intentos, con estos tres datos:
// los intentos, la estimación con %.4f y el error con %.4f.
// El error es la distancia entre la estimación y 1.0 / 6, sin signo. Usen fabs, de math.h.
//
// La forma de la salida es esta. Los números de aquí son inventados, solo muestran la forma:
//   100 0.1234 0.0433
//   10000 0.1234 0.0433
//   1000000 0.1234 0.0433
//
// Otras salidas:
//   La semilla no puede ser negativa    semilla menor que 0; termina con return 1
//   Entrada inválida                    lo escrito no es un número; termina con return 1
//
// Llamen a srand una sola vez, al principio.
//
// Parte 4: corran el programa dos veces con la misma semilla, y una vez con otra.
// Escriban al final del archivo, en un comentario, qué cambió, qué no cambió y por qué.
//
// Pruébenlo con make test-lab.
#include <stdio.h>

int main(void) {
    // Escriban aquí su programa.
    return 0;
}
lab/primes.cDescargar lab/primes.c
// Laboratorio, parte 2: lee N y escribe los primos desde 2 hasta N, uno por línea.
// Usen ciclos anidados: uno recorre los candidatos y otro busca sus divisores.
//
// Con N = 10 el programa escribe:
//   2
//   3
//   5
//   7
//
// Con N = 1 no hay primos: no escribe nada y termina con return 0.
//
// Otras salidas:
//   N fuera de rango     N menor que 1 o mayor que 100000; termina con return 1
//   Entrada inválida     lo escrito no es un número; termina con return 1
//
// Pruébenlo con make test-lab.
#include <stdio.h>

int main(void) {
    // Escriban aquí su programa.
    return 0;
}
lab/triangle.cDescargar lab/triangle.c
// Laboratorio, parte 1: lee una altura N y dibuja un triángulo de asteriscos.
// La fila 1 tiene un asterisco, la fila 2 tiene dos, y así hasta la fila N.
//
// Con N = 3 el programa escribe:
//   *
//   **
//   ***
//
// Otras salidas:
//   Altura fuera de rango    N menor que 1 o mayor que 50; termina con return 1
//   Entrada inválida         lo escrito no es un número; termina con return 1
//
// 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 6: ciclos anidados y números aleatorios. 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
LDLIBS = -lm
BUILD = build
PROGRAMS = rectangle times_table count_runs staircase divisors toy_generator \
           rand_max seed time_seed range die_counts dice_exact dice_seven
LAB_PROGRAMS = triangle primes dice_estimate

.PHONY: all lab test test-lab clean

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

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

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

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

$(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__