Semana 4 — Decisiones y lógica

Objetivos

  • Escribir programas que toman decisiones con if, else y switch.
  • Combinar condiciones con operadores lógicos y evaluarlas con tablas de verdad.
  • Explicar con un ejemplo cómo las compuertas implementan la lógica en hardware.

Teoría — Decisiones y lógica booleana · ✂️ Compuertas

Lectura: Apuntes, semana 4.

Temas:

  • Decisiones en pseudocódigo y en diagramas de flujo. Operadores relacionales.
  • if, else, else if y decisiones anidadas. Error común: = en lugar de ==.
  • switch, case, break y default.
  • &&, || y !. Tablas de verdad, cortocircuito y leyes de De Morgan.
  • ✂️ Compuertas AND, OR, NOT y XOR. Un sumador de un bit como ejemplo.

Laboratorio — Decisiones

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

  1. Escribir un programa que lea una nota de 0 a 20 y diga si aprobó, rechazando las notas fuera de rango.
  2. Escribir un programa que diga si un año es bisiesto. Probarlo con 1900, 2000, 2024 y 2026.
  3. Escribir una calculadora con switch, que avise si se divide entre 0.

Entregas

  • Práctica 1: Algoritmos, variables, representación de datos y decisiones. Ver practicas/README.md.

Preguntas de repaso

  1. ¿Qué escribe if (x = 0) printf("cero"); else printf("otro");? ¿Por qué?
  2. Simplifiquen !(a > 3 && b != 2).
  3. En n != 0 && 10 / n > 1, ¿qué evita el orden de las condiciones?
  4. ¿Qué pasa en un switch si se olvida el break?
  5. Sumen en binario 0110 + 0111 indicando el acarreo de cada columna.

Código de la semana

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

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

Ejemplos

Los programas que usan los apuntes y la clase.

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

// Suma dos números de 4 bits con cuatro sumadores de un bit en cadena.
// Sin ciclos, la misma cuenta se repite para cada columna.
int main(void) {
    int a3, a2, a1, a0;
    int b3, b2, b1, b0;

    // %1d lee un solo dígito, así que 0101 se lee como cuatro números.
    if (scanf("%1d%1d%1d%1d %1d%1d%1d%1d",
              &a3, &a2, &a1, &a0, &b3, &b2, &b1, &b0) != 8) {
        printf("Entrada inválida\n");
        return 1;
    }

    if (a3 > 1 || a2 > 1 || a1 > 1 || a0 > 1 ||
        b3 > 1 || b2 > 1 || b1 > 1 || b0 > 1) {
        printf("Los bits deben ser 0 o 1\n");
        return 1;
    }

    int carry0 = 0;

    int partial0 = a0 != b0;
    int sum0 = partial0 != carry0;
    int carry1 = (a0 && b0) || (partial0 && carry0);

    int partial1 = a1 != b1;
    int sum1 = partial1 != carry1;
    int carry2 = (a1 && b1) || (partial1 && carry1);

    int partial2 = a2 != b2;
    int sum2 = partial2 != carry2;
    int carry3 = (a2 && b2) || (partial2 && carry2);

    int partial3 = a3 != b3;
    int sum3 = partial3 != carry3;
    int carry4 = (a3 && b3) || (partial3 && carry3);

    printf("acarreos: %d %d %d %d\n", carry4, carry3, carry2, carry1);
    printf("suma:     %d%d%d%d\n", sum3, sum2, sum1, sum0);
    return 0;
}
combo.cDescargar combo.c
#include <stdio.h>

// Precio en céntimos de un menú según la opción elegida.
int main(void) {
    int option;
    int price;

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

    switch (option) {
        case 1:
            price = 1200;
            break;
        case 2:
            price = 1500;
            break;
        case 3:
            price = 1800;
            break;
        default:
            printf("Opción inválida\n");
            return 1;
    }

    printf("Precio: %d céntimos\n", price);
    return 0;
}
compare_double.cDescargar compare_double.c
#include <math.h>
#include <stdio.h>

// Dos double casi nunca se comparan con ==. Se pregunta si están muy cerca.
int main(void) {
    double sum = 0.1 + 0.2;

    if (sum == 0.3) {
        printf("Con ==: iguales\n");
    } else {
        printf("Con ==: distintos\n");
    }

    if (fabs(sum - 0.3) < 0.000001) {
        printf("Con tolerancia: iguales\n");
    } else {
        printf("Con tolerancia: distintos\n");
    }

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

// Evalúa las dos formas de cada ley de De Morgan con los valores leídos.
int main(void) {
    int x, y;

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

    printf("!(x >= 0 && y < 10) vale %d\n", !(x >= 0 && y < 10));
    printf("x < 0 || y >= 10    vale %d\n", x < 0 || y >= 10);
    printf("!(x == 0 || y == 0) vale %d\n", !(x == 0 || y == 0));
    printf("x != 0 && y != 0    vale %d\n", x != 0 && y != 0);

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

// Lee el total de una compra en céntimos. Desde 100 soles se descuenta el 10 %.
int main(void) {
    int total;

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

    if (total < 0) {
        printf("El total no puede ser negativo\n");
        return 1;
    }

    if (total >= 10000) {
        total = total - total / 10;
    }

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

// Sumador de un bit: suma a, b y el acarreo que llega.
int main(void) {
    int a, b, carry_in;

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

    if (a < 0 || a > 1 || b < 0 || b > 1 || carry_in < 0 || carry_in > 1) {
        printf("Los bits deben ser 0 o 1\n");
        return 1;
    }

    int partial = a != b;
    int sum = partial != carry_in;
    int carry_out = (a && b) || (partial && carry_in);

    printf("suma %d, acarreo %d\n", sum, carry_out);
    return 0;
}
gates.cDescargar gates.c
#include <stdio.h>

// Las cuatro compuertas, para dos bits a y b (cada uno 0 o 1).
int main(void) {
    int a, b;

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

    if (a < 0 || a > 1 || b < 0 || b > 1) {
        printf("Los bits deben ser 0 o 1\n");
        return 1;
    }

    printf("AND %d\n", a && b);
    printf("OR  %d\n", a || b);
    printf("NOT %d\n", !a);
    printf("XOR %d\n", a != b);

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

// Escribe el mayor de tres enteros, con decisiones anidadas.
int main(void) {
    int a, b, c;

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

    if (a > b) {
        if (a > c) {
            printf("%d\n", a);
        } else {
            printf("%d\n", c);
        }
    } else {
        if (b > c) {
            printf("%d\n", b);
        } else {
            printf("%d\n", c);
        }
    }

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

// Convierte un promedio entero de 0 a 20 en una mención.
// La escala es un ejemplo de la clase, no un reglamento.
int main(void) {
    int average;

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

    if (average < 0 || average > 20) {
        printf("Promedio fuera de rango\n");
    } else if (average >= 17) {
        printf("Excelente\n");
    } else if (average >= 14) {
        printf("Bueno\n");
    } else if (average >= 11) {
        printf("Regular\n");
    } else {
        printf("Insuficiente\n");
    }

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

// Escribe los días de un mes de un año no bisiesto.
int main(void) {
    int month;
    int days;

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

    switch (month) {
        case 4:
        case 6:
        case 9:
        case 11:
            days = 30;
            break;
        case 2:
            days = 28;
            break;
        case 1:
        case 3:
        case 5:
        case 7:
        case 8:
        case 10:
        case 12:
            days = 31;
            break;
        default:
            printf("Mes inválido\n");
            return 1;
    }

    printf("%d días\n", days);
    return 0;
}
parity.cDescargar parity.c
#include <stdio.h>

// Dice si un entero es par o impar.
int main(void) {
    int n;

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

    if (n % 2 == 0) {
        printf("%d es par\n", n);
    } else {
        printf("%d es impar\n", n);
    }

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

// La aritmética va antes que las comparaciones, y estas antes que && y ||.
int main(void) {
    int x = 5;
    int y = 2;

    printf("%d\n", x + 1 > y * 3);
    printf("%d\n", x > 3 && x < 10);
    printf("%d\n", (x > 3 && y > 3) || y == 2);
    printf("%d\n", x > 3 && (y > 3 || y == 2));
    printf("%d\n", !(x > 3));

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

// Lee y valida en una sola condición: si scanf falla, n no se llega a mirar.
int main(void) {
    int n;

    if (scanf("%d", &n) == 1 && n >= 1 && n <= 12) {
        printf("Mes válido: %d\n", n);
    } else {
        printf("Mes inválido\n");
        return 1;
    }

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

// Una comparación es una expresión: vale 1 si es cierta y 0 si es falsa.
int main(void) {
    int a = 5;
    int b = 3;

    printf("a > b  vale %d\n", a > b);
    printf("a < b  vale %d\n", a < b);
    printf("a == b vale %d\n", a == b);
    printf("a != b vale %d\n", a != b);
    printf("a >= 5 vale %d\n", a >= 5);
    printf("a <= 4 vale %d\n", a <= 4);

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

// Si n es 0, && no evalúa su lado derecho y la división nunca ocurre.
int main(void) {
    int n;

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

    if (n != 0 && 10 / n > 1) {
        printf("10 / %d es mayor que 1\n", n);
    } else {
        printf("No se cumple\n");
    }

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

// Las tablas de verdad de &&, || y !, calculadas por el propio C.
int main(void) {
    printf("a b | a&&b a||b !a\n");
    printf("0 0 | %4d %4d %2d\n", 0 && 0, 0 || 0, !0);
    printf("0 1 | %4d %4d %2d\n", 0 && 1, 0 || 1, !0);
    printf("1 0 | %4d %4d %2d\n", 1 && 0, 1 || 0, !1);
    printf("1 1 | %4d %4d %2d\n", 1 && 1, 1 || 1, !1);

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

// En C, 0 es falso y cualquier otro número es verdadero.
int main(void) {
    int n;

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

    if (n) {
        printf("%d cuenta como verdadero\n", n);
    } else {
        printf("%d cuenta como falso\n", n);
    }
    printf("!n vale %d\n", !n);

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

// Dice si un carácter es una vocal sin tilde.
int main(void) {
    char letter;

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

    switch (letter) {
        case 'a':
        case 'e':
        case 'i':
        case 'o':
        case 'u':
        case 'A':
        case 'E':
        case 'I':
        case 'O':
        case 'U':
            printf("Vocal\n");
            break;
        default:
            printf("No es vocal\n");
    }

    return 0;
}

Errores

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

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

// Funciona, pero se lee mal: && se agrupa antes que ||.
int main(void) {
    int a = 1;
    int b = 0;
    int c = 1;

    printf("%d\n", a && b || c);

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

// Error a propósito: = asigna, == compara.
int main(void) {
    int x = 5;

    if (x = 0) {
        printf("cero\n");
    } else {
        printf("otro\n");
    }
    printf("x vale %d\n", x);

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

// Error a propósito: una variable declarada entre llaves solo existe ahí dentro.
int main(void) {
    int n = 4;

    if (n > 0) {
        int twice = n * 2;
        printf("%d\n", twice);
    }
    printf("%d\n", twice);

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

// Error a propósito: en C, 0 <= grade <= 20 no significa lo que parece.
int main(void) {
    int grade;

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

    if (0 <= grade <= 20) {
        printf("En rango\n");
    } else {
        printf("Fuera de rango\n");
    }

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

// Error a propósito: 'a' es un carácter, "a" es otra cosa, una cadena.
int main(void) {
    char letter = 'a';

    if (letter == "a") {
        printf("Es la letra a\n");
    }

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

// Error a propósito: sin break, la ejecución sigue en el case de abajo.
int main(void) {
    int option;
    int price = 0;

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

    switch (option) {
        case 1:
            price = 1200;
        case 2:
            price = 1500;
        case 3:
            price = 1800;
            break;
        default:
            printf("Opción inválida\n");
            return 1;
    }

    printf("Precio: %d céntimos\n", price);
    return 0;
}
errors/dangling_else.cDescargar errors/dangling_else.c
#include <stdio.h>

// Error a propósito: el else va con el if más cercano, no con el de su sangría.
int main(void) {
    int a, b;

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

    if (a > 0)
        if (b > 0)
            printf("Ambos positivos\n");
    else
        printf("a no es positivo\n");

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

// Error a propósito: la primera rama cierta gana, y aquí va primero la más amplia.
int main(void) {
    int average;

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

    if (average >= 11) {
        printf("Regular\n");
    } else if (average >= 14) {
        printf("Bueno\n");
    } else if (average >= 17) {
        printf("Excelente\n");
    } else {
        printf("Insuficiente\n");
    }

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

// Error a propósito: sin llaves, el if solo gobierna una instrucción.
int main(void) {
    int grade;

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

    if (grade < 0)
        printf("Nota negativa\n");
        printf("Rechazada\n");

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

// Error a propósito: ! se aplica antes que ==, solo a la variable a.
int main(void) {
    int a = 5;
    int b = 3;

    if (!a == b) {
        printf("a es distinto de b\n");
    } else {
        printf("a es igual a b\n");
    }

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

// Error a propósito: || necesita una condición completa a cada lado.
int main(void) {
    int option;

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

    if (option == 1 || 2) {
        printf("Opción 1 o 2\n");
    } else {
        printf("Otra opción\n");
    }

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

// Error a propósito: con negativos, el resto puede ser -1.
int main(void) {
    int n;

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

    if (n % 2 == 1) {
        printf("%d es impar\n", n);
    } else {
        printf("%d es par\n", n);
    }

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

// Error a propósito: el ; después del if termina la decisión.
int main(void) {
    int n;

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

    if (n > 0);
    printf("%d es positivo\n", n);

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

// Error a propósito: switch solo trabaja con enteros, y char es un entero.
int main(void) {
    double grade = 10.5;

    switch (grade) {
        case 10:
            printf("Diez\n");
            break;
        default:
            printf("Otra\n");
    }

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

// Error a propósito: primero divide y recién después pregunta si n es 0.
int main(void) {
    int n;

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

    if (10 / n > 1 && n != 0) {
        printf("10 / %d es mayor que 1\n", n);
    } else {
        printf("No se cumple\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 4."""
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 run_demo(binary, stdin=""):
    done = subprocess.run(
        [str(binary)], input=stdin, text=True,
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=10,
    )
    return done.stdout, done.returncode


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 Decisions(unittest.TestCase):
    def test_discount_boundary(self):
        self.assertEqual(run("discount", "9999")[0], "A pagar: 99.99 soles\n")
        self.assertEqual(run("discount", "10000")[0], "A pagar: 90.00 soles\n")
        self.assertEqual(run("discount", "12050")[0], "A pagar: 108.45 soles\n")

    def test_discount_rejects(self):
        self.assertEqual(run("discount", "abc"), ("Entrada inválida\n", 1))
        self.assertEqual(run("discount", ""), ("Entrada inválida\n", 1))
        self.assertEqual(run("discount", "-5")[1], 1)

    def test_parity_with_negatives(self):
        self.assertEqual(run("parity", "7")[0], "7 es impar\n")
        self.assertEqual(run("parity", "-3")[0], "-3 es impar\n")
        self.assertEqual(run("parity", "-4")[0], "-4 es par\n")
        self.assertEqual(run("parity", "0")[0], "0 es par\n")

    def test_parity_wrong_fails_on_negatives(self):
        _, _, binary = compile_error_demo("parity_wrong")
        self.assertEqual(run_demo(binary, "-3")[0], "-3 es par\n")

    def test_max_of_three_all_orders_and_ties(self):
        for a, b, c in [(4, 9, 2), (9, 4, 2), (2, 4, 9), (5, 5, 1), (1, 5, 5), (5, 1, 5), (3, 3, 3), (-1, -2, -3)]:
            with self.subTest(values=(a, b, c)):
                self.assertEqual(run("max_of_three", f"{a} {b} {c}")[0], f"{max(a, b, c)}\n")

    def test_mention_boundaries(self):
        expected = {-1: "Promedio fuera de rango", 0: "Insuficiente", 10: "Insuficiente",
                    11: "Regular", 13: "Regular", 14: "Bueno", 16: "Bueno",
                    17: "Excelente", 20: "Excelente", 21: "Promedio fuera de rango"}
        for grade, text in expected.items():
            with self.subTest(grade=grade):
                self.assertEqual(run("mention", str(grade))[0], text + "\n")

    def test_mention_wrong_order_says_regular(self):
        _, _, binary = compile_error_demo("mention_wrong_order")
        self.assertEqual(run_demo(binary, "18")[0], "Regular\n")


class Logic(unittest.TestCase):
    def test_relational_are_one_or_zero(self):
        lines = run("relational")[0].splitlines()
        self.assertEqual([line.split()[-1] for line in lines], ["1", "0", "0", "1", "1", "0"])

    def test_nonzero_is_true(self):
        self.assertEqual(run("truthy", "-7")[0], "-7 cuenta como verdadero\n!n vale 0\n")
        self.assertEqual(run("truthy", "0")[0], "0 cuenta como falso\n!n vale 1\n")

    def test_precedence_values(self):
        self.assertEqual(run("precedence")[0].split(), ["0", "1", "1", "1", "0"])

    def test_truth_table(self):
        rows = run("truth_table")[0].splitlines()[1:]
        cells = [row.replace("|", " ").split() for row in rows]
        for a, b, conj, disj, neg in (map(int, cell) for cell in cells):
            self.assertEqual(conj, a and b)
            self.assertEqual(disj, a or b)
            self.assertEqual(neg, 1 - a)

    def test_de_morgan_on_a_grid(self):
        for x in range(-2, 13):
            for y in [-1, 0, 1, 9, 10, 11]:
                values = [line.split()[-1] for line in run("de_morgan", f"{x} {y}")[0].splitlines()]
                with self.subTest(x=x, y=y):
                    self.assertEqual(values[0], values[1])
                    self.assertEqual(values[2], values[3])

    def test_short_circuit_avoids_division(self):
        output, status = run("short_circuit", "0")
        self.assertEqual((output, status), ("No se cumple\n", 0))
        self.assertEqual(run("short_circuit", "5")[0], "10 / 5 es mayor que 1\n")
        self.assertEqual(run("short_circuit", "20")[0], "No se cumple\n")

    def test_swapped_operands_divide_by_zero(self):
        _, _, binary = compile_error_demo("unsafe_division", "-g", "-fsanitize=undefined")
        self.assertIn("runtime error: division by zero", run_demo(binary, "0")[0])

    def test_read_in_range(self):
        self.assertEqual(run("read_in_range", "12"), ("Mes válido: 12\n", 0))
        for text in ["0", "13", "abc", ""]:
            with self.subTest(text=text):
                self.assertEqual(run("read_in_range", text), ("Mes inválido\n", 1))

    def test_double_comparison(self):
        self.assertEqual(run("compare_double")[0], "Con ==: distintos\nCon tolerancia: iguales\n")


class Switch(unittest.TestCase):
    def test_month_days(self):
        days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
        for month, count in enumerate(days, start=1):
            with self.subTest(month=month):
                self.assertEqual(run("month_days", str(month)), (f"{count} días\n", 0))
        self.assertEqual(run("month_days", "13"), ("Mes inválido\n", 1))
        self.assertEqual(run("month_days", "0"), ("Mes inválido\n", 1))

    def test_combo(self):
        self.assertEqual(run("combo", "1")[0], "Precio: 1200 céntimos\n")
        self.assertEqual(run("combo", "2")[0], "Precio: 1500 céntimos\n")
        self.assertEqual(run("combo", "3")[0], "Precio: 1800 céntimos\n")
        self.assertEqual(run("combo", "4"), ("Opción inválida\n", 1))

    def test_missing_break_falls_through(self):
        messages, status, binary = compile_error_demo("combo_no_break")
        self.assertEqual(status, 0)
        self.assertIn("-Wimplicit-fallthrough", messages)
        self.assertEqual(run_demo(binary, "1")[0], "Precio: 1800 céntimos\n")

    def test_vowel(self):
        for text in ["a", "E", "  u"]:
            with self.subTest(text=text):
                self.assertEqual(run("vowel", text)[0], "Vocal\n")
        for text in ["b", "7", "ñ", "á"]:
            with self.subTest(text=text):
                self.assertEqual(run("vowel", text)[0], "No es vocal\n")


class Gates(unittest.TestCase):
    def test_gates(self):
        for a in (0, 1):
            for b in (0, 1):
                with self.subTest(a=a, b=b):
                    self.assertEqual(
                        run("gates", f"{a} {b}")[0],
                        f"AND {a & b}\nOR  {a | b}\nNOT {1 - a}\nXOR {a ^ b}\n",
                    )
        self.assertEqual(run("gates", "2 0"), ("Los bits deben ser 0 o 1\n", 1))

    def test_full_adder_all_inputs(self):
        for a in (0, 1):
            for b in (0, 1):
                for c in (0, 1):
                    total = a + b + c
                    with self.subTest(a=a, b=b, c=c):
                        self.assertEqual(run("full_adder", f"{a} {b} {c}")[0],
                                         f"suma {total % 2}, acarreo {total // 2}\n")

    def test_adder4_all_inputs(self):
        for a in range(16):
            for b in range(16):
                text = f"{a:04b} {b:04b}"
                lines = run("adder4", text)[0].splitlines()
                with self.subTest(text=text):
                    self.assertEqual(lines[1], f"suma:     {(a + b) % 16:04b}")
                    self.assertEqual(lines[0].split()[1], str((a + b) // 16))

    def test_adder4_rejects(self):
        self.assertEqual(run("adder4", "0102 0011"), ("Los bits deben ser 0 o 1\n", 1))
        self.assertEqual(run("adder4", "01 01"), ("Entrada inválida\n", 1))


class CompilerMessages(unittest.TestCase):
    """Cada error a propósito da el aviso que dicen los apuntes y se comporta como dicen."""

    def check(self, name, message, stdin, expected):
        messages, status, binary = compile_error_demo(name)
        self.assertEqual(status, 0, messages)
        self.assertIn(message, messages)
        self.assertEqual(run_demo(binary, stdin)[0], expected)

    def test_assignment(self):
        self.check("assign_in_if", "-Wparentheses", "", "otro\nx vale 0\n")

    def test_chained(self):
        self.check("chained_compare", "do not have their mathematical meaning", "25", "En rango\n")

    def test_semicolon(self):
        self.check("semicolon_if", "-Wempty-body", "-3", "-3 es positivo\n")

    def test_indentation(self):
        self.check("misleading_indent", "-Wmisleading-indentation", "15", "Rechazada\n")

    def test_dangling(self):
        self.check("dangling_else", "-Wdangling-else", "5 -1", "a no es positivo\n")
        self.check("dangling_else", "-Wdangling-else", "-5 1", "")

    def test_not(self):
        self.check("not_precedence", "-Wlogical-not-parentheses", "", "a es igual a b\n")

    def test_and_or(self):
        self.check("and_or_no_parens", "within", "", "1\n")

    def test_or_constant_is_silent(self):
        messages, status, binary = compile_error_demo("or_constant")
        self.assertEqual((messages, status), ("", 0))
        self.assertEqual(run_demo(binary, "7")[0], "Opción 1 o 2\n")

    def test_char_vs_string(self):
        messages, _, _ = compile_error_demo("char_vs_string")
        self.assertIn("comparison between pointer and integer", messages)

    def test_errors_that_do_not_compile(self):
        messages, status, _ = compile_error_demo("block_scope")
        self.assertNotEqual(status, 0)
        self.assertIn("undeclared", messages)
        messages, status, _ = compile_error_demo("switch_double")
        self.assertNotEqual(status, 0)
        self.assertIn("switch quantity not an integer", messages)


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")
PASSING_GRADE = 11


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.strip(), 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 ["grade", "leap_year", "calculator"]:
            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_grade_passes_and_fails(self):
        cases = {0: "Desaprobado", PASSING_GRADE - 1: "Desaprobado",
                 PASSING_GRADE: "Aprobado", 20: "Aprobado"}
        for grade, text in cases.items():
            with self.subTest(grade=grade):
                self.assertEqual(run("grade", str(grade)), (text, 0))

    def test_grade_out_of_range(self):
        for grade in [-1, 21, 100]:
            with self.subTest(grade=grade):
                self.assertEqual(run("grade", str(grade)), ("Nota fuera de rango", 1))

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

    def test_leap_years(self):
        cases = {1900: "No bisiesto", 2000: "Bisiesto", 2024: "Bisiesto", 2026: "No bisiesto",
                 2100: "No bisiesto", 2400: "Bisiesto", 2023: "No bisiesto"}
        for year, text in cases.items():
            with self.subTest(year=year):
                self.assertEqual(run("leap_year", str(year)), (text, 0))

    def test_leap_year_not_a_number(self):
        self.assertEqual(run("leap_year", "abc"), ("Entrada inválida", 1))

    def test_calculator_operations(self):
        cases = {"8 + 2": "10.00", "8 - 2": "6.00", "8 * 2": "16.00", "8 / 2": "4.00",
                 "7 / 2": "3.50", "0.1 + 0.2": "0.30", "8+2": "10.00", "0 / 5": "0.00"}
        for text, expected in cases.items():
            with self.subTest(text=text):
                self.assertEqual(run("calculator", text), (expected, 0))

    def test_calculator_division_by_zero(self):
        for text in ["5 / 0", "5 / 0.0", "0 / 0"]:
            with self.subTest(text=text):
                self.assertEqual(run("calculator", text), ("Error: división entre 0", 1))

    def test_calculator_unknown_operator(self):
        self.assertEqual(run("calculator", "8 x 2"), ("Operador desconocido", 1))

    def test_calculator_bad_input(self):
        for text in ["abc", "8 /", "8 / abc", ""]:
            with self.subTest(text=text):
                self.assertEqual(run("calculator", text), ("Entrada inválida", 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-04.md"
SLIDES = ROOT / "slides" / "week-04.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/calculator.cDescargar lab/calculator.c
// Laboratorio, parte 3: calculadora con switch.
// Lee una operación como 8 / 2: un número, un operador (+, -, * o /) y otro número.
//
// El programa escribe exactamente una de estas líneas:
//   el resultado con dos decimales, como 4.00
//   Error: división entre 0    termina con return 1
//   Operador desconocido       termina con return 1
//   Entrada inválida           no se pudieron leer los tres datos; termina con return 1
//
// Pruébenlo con make test-lab.
#include <stdio.h>

int main(void) {
    // Escriban aquí su programa.
    return 0;
}
lab/grade.cDescargar lab/grade.c
// Laboratorio, parte 1: lee una nota entera y dice si aprobó.
// En este laboratorio se aprueba con 11 o más.
//
// El programa escribe exactamente una de estas líneas:
//   Aprobado               nota de 11 a 20; termina con return 0
//   Desaprobado            nota de 0 a 10; termina con return 0
//   Nota fuera de rango    nota menor que 0 o mayor que 20; 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/leap_year.cDescargar lab/leap_year.c
// Laboratorio, parte 2: lee un año y dice si es bisiesto.
// Pruébenlo con 1900, 2000, 2024 y 2026.
//
// El programa escribe exactamente una de estas líneas:
//   Bisiesto
//   No bisiesto
//   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 4: decisiones y lógica. 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 = discount parity max_of_three mention relational truthy precedence \
           truth_table de_morgan short_circuit read_in_range compare_double \
           month_days combo vowel gates full_adder adder4
LAB_PROGRAMS = grade leap_year calculator

.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__