Semana 5 — Ciclos
Objetivos
- Elegir entre
while,do whileyforsegú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 whileyfor. Validar una entrada condo while.- Patrones: contador, acumulador, centinela y buscar el mayor.
- Leer hasta el fin de la entrada con
getcharyEOF. - Error común: ciclo infinito, y salirse por uno (off-by-one).
Laboratorio — Ciclos
- Escribir un programa que lea números hasta un 0 y escriba la suma, el promedio y el mayor.
- Escribir un programa que diga si un número es primo.
- Contar con
getcharlas líneas de un archivo redirigido con<. - Validar con
do whileque una nota esté entre 0 y 20.
Preguntas de repaso
- ¿Cuántas veces corre el cuerpo de
for (i = 1; i <= 10; i += 3)? Sigan los valores dei. - Reescriban ese
forconwhile. - ¿Cuándo es más claro
do whilequewhile? ¿Hay algo que solo se pueda hacer condo while? - ¿Qué hace
while (n > 0) { printf("%d", n); }? ¿Cómo se corrige? - 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.
Puno
Cusco
Piura#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;
}#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;
}#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;
}#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;
}#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;
}#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;
}#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;
}14
9
11
20
10#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;
}#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;
}#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;
}#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;
}#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;
}#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;
}#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;
}#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.
#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;
}#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;
}#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;
}#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;
}#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;
}#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;
}#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;
}#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;
}#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;
}#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;
}#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.
"""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()"""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()"""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.
// 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;
}// 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;
}Lima
Arequipa
Trujillo// 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;
}Lima
Arequipa
Trujillo// 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.
# 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__