Semana 2 — Variables y programas secuenciales

Objetivos

  • Declarar variables con el tipo adecuado y evaluar expresiones.
  • Explicar la división entera y la conversión entre tipos.
  • Leer datos con scanf y escribir resultados con printf.
  • Pasar de una fórmula a un programa, primero en pseudocódigo y después en C.

Teoría — Variables, tipos, operadores, entrada y salida

Lectura: Apuntes, semana 2.

Temas:

  • Variable: un nombre para un lugar en memoria. Asignar no es igualar: x = x + 1.
  • Tipos int, double y char. Cuántos bytes ocupan con sizeof.
  • Operadores y precedencia. El resto con %. División entera: 7 / 2 vale 3 y 7.0 / 2 vale 3.5.
  • Error común: usar una variable sin darle valor.
  • printf y sus formatos: %d, %f, %.2f, %c.
  • scanf, el & y revisar lo que devuelve.
  • Funciones de math.h y la opción -lm.

Laboratorio — Fórmulas en C

  1. Escribir un programa que lea una temperatura en Celsius y la escriba en Fahrenheit con dos decimales.
  2. Predecir y comprobar qué escriben 7 / 2, 7 % 2, 7.0 / 2 y (double) 7 / 2.
  3. Escribir un programa que lea un número de segundos y lo escriba como horas, minutos y segundos.
  4. Guardar datos en entrada.txt y correr ./programa < entrada.txt > salida.txt.
  5. Mostrar cuántos bytes ocupa cada tipo con sizeof.

Preguntas de repaso

  1. Sigan a mano: int a = 5; int b = a; a = a + 2; b = b * a;. ¿Cuánto valen a y b?
  2. ¿Por qué double p = 1 / 3; guarda 0? ¿Cómo se corrige?
  3. ¿Cuánto vale 2 + 3 * 4 % 5? Muestren el orden de las operaciones.
  4. ¿Cómo detecta un programa que el usuario escribió hola cuando scanf("%d", &n) espera un número?
  5. Escriban en pseudocódigo y en C un programa que calcule el promedio de tres notas.

Código de la semana

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

Ejemplos (30)Errores (1)Pruebas (2)Laboratorio (5)Compilación (1)

Ejemplos

Los programas que usan los apuntes y la clase.

arithmetic.cDescargar arithmetic.c
// Precedencia: primero * / %, después + -. A igual nivel, de izquierda a derecha.
#include <stdio.h>

int main(void) {
    printf("10 - 2 * 3 = %d\n", 10 - 2 * 3);
    printf("(10 - 2) * 3 = %d\n", (10 - 2) * 3);
    printf("20 / 2 * 5 = %d\n", 20 / 2 * 5);
    printf("10 - 4 - 3 = %d\n", 10 - 4 - 3);
    printf("7 + 8 %% 3 * 2 = %d\n", 7 + 8 % 3 * 2);
    return 0;
}
balance.cDescargar balance.c
// Una variable cambia de valor: la asignación no es una ecuación.
#include <stdio.h>

int main(void) {
    int balance = 10;           // saldo de la tarjeta, en soles
    printf("Saldo inicial: %d\n", balance);

    balance = balance - 3;      // un pasaje
    printf("Después del pasaje: %d\n", balance);

    balance = balance + 5;      // una recarga
    printf("Después de la recarga: %d\n", balance);

    return 0;
}
change.cDescargar change.c
// Vuelto con la menor cantidad de monedas, usando / y % en lugar de un ciclo.
// Aún no revisa lo que devuelve scanf: eso llega con el if, en la semana 4.
#include <stdio.h>

int main(void) {
    int remaining = 0;

    printf("Vuelto en céntimos: ");
    scanf("%d", &remaining);

    int coins_500 = remaining / 500;
    remaining = remaining % 500;
    int coins_200 = remaining / 200;
    remaining = remaining % 200;
    int coins_100 = remaining / 100;
    remaining = remaining % 100;
    int coins_50 = remaining / 50;
    remaining = remaining % 50;
    int coins_20 = remaining / 20;
    remaining = remaining % 20;
    int coins_10 = remaining / 10;
    remaining = remaining % 10;

    printf("5 soles: %d\n", coins_500);
    printf("2 soles: %d\n", coins_200);
    printf("1 sol: %d\n", coins_100);
    printf("50 céntimos: %d\n", coins_50);
    printf("20 céntimos: %d\n", coins_20);
    printf("10 céntimos: %d\n", coins_10);
    printf("Sin cubrir: %d céntimos\n", remaining);
    return 0;
}
char_codes.cDescargar char_codes.c
// Un char guarda un número pequeño, el código de un carácter.
#include <stdio.h>

int main(void) {
    char letter = 'A';
    char digit = '7';

    printf("%c tiene el código %d\n", letter, letter);
    printf("La letra siguiente es %c\n", letter + 1);
    printf("%c tiene el código %d\n", digit, digit);
    printf("El valor numérico de %c es %d\n", digit, digit - '0');
    return 0;
}
char_fixed.cDescargar char_fixed.c
// El espacio antes de %c salta los espacios y saltos de línea pendientes.
// Aún no revisa lo que devuelve scanf: eso llega con el if, en la semana 4.
#include <stdio.h>

int main(void) {
    int quantity = 0;
    char size = '?';

    printf("Cantidad: ");
    scanf("%d", &quantity);
    printf("Talla: ");
    scanf(" %c", &size);

    printf("Leí %d y la talla %c\n", quantity, size);
    return 0;
}
char_trap.cDescargar char_trap.c
// Tiene un error a propósito: %c también lee el salto de línea.
// Aún no revisa lo que devuelve scanf: eso llega con el if, en la semana 4.
#include <stdio.h>

int main(void) {
    int quantity = 0;
    char size = '?';

    printf("Cantidad: ");
    scanf("%d", &quantity);
    printf("Talla: ");
    scanf("%c", &size);

    printf("Leí %d y un carácter de código %d\n", quantity, size);
    return 0;
}
compound_interest.cDescargar compound_interest.c
// Interés compuesto: final = capital * (1 + tasa / 100) elevado a años.
// Aún no revisa lo que devuelve scanf: eso llega con el if, en la semana 4.
#include <math.h>
#include <stdio.h>

int main(void) {
    double capital = 0.0;
    double rate = 0.0;
    int years = 0;

    printf("Capital en soles: ");
    scanf("%lf", &capital);
    printf("Tasa anual en %%: ");
    scanf("%lf", &rate);
    printf("Años: ");
    scanf("%d", &years);

    double final_amount = capital * pow(1 + rate / 100, years);

    printf("Monto final: %.2f soles\n", final_amount);
    return 0;
}
conversions.cDescargar conversions.c
// Conversiones entre int y double, automáticas y pedidas con un cast.
#include <stdio.h>

int main(void) {
    int sum = 7;
    int count = 2;

    double late = sum / count;              // divide enteros y luego convierte
    double cast_first = (double) sum / count;
    double cast_late = (double) (sum / count);
    double from_int = count;                // de int a double: 2.0

    int truncated = 3.99;                   // de double a int: se pierden los decimales
    int cast_positive = (int) 3.99;
    int cast_negative = (int) -3.99;

    printf("sum / count = %f\n", late);
    printf("(double) sum / count = %f\n", cast_first);
    printf("(double) (sum / count) = %f\n", cast_late);
    printf("from_int = %f\n", from_int);
    printf("truncated = %d\n", truncated);
    printf("(int) 3.99 = %d\n", cast_positive);
    printf("(int) -3.99 = %d\n", cast_negative);
    return 0;
}
diagonal.cDescargar diagonal.c
// Diagonal de un rectángulo, con el teorema de Pitágoras.
// Aún no revisa lo que devuelve scanf: eso llega con el if, en la semana 4.
#include <math.h>
#include <stdio.h>

int main(void) {
    double width = 0.0;
    double height = 0.0;

    printf("Ancho y alto en metros: ");
    scanf("%lf %lf", &width, &height);

    double diagonal = sqrt(width * width + height * height);

    printf("Diagonal: %.2f metros\n", diagonal);
    return 0;
}
division_by_zero.cDescargar division_by_zero.c
// Dividir enteros entre cero es un error de ejecución.
// Aún no revisa lo que devuelve scanf: eso llega con el if, en la semana 4.
#include <stdio.h>

int main(void) {
    int a = 0;
    int b = 0;

    printf("Dos enteros: ");
    scanf("%d %d", &a, &b);

    int quotient = a / b;

    printf("Cociente: %d\n", quotient);
    return 0;
}
division.cDescargar division.c
// Entre dos enteros, / da un entero. Si uno es double, da un double.
#include <stdio.h>

int main(void) {
    printf("17 / 5 = %d\n", 17 / 5);
    printf("17 %% 5 = %d\n", 17 % 5);
    printf("17.0 / 5 = %f\n", 17.0 / 5);
    printf("17 / 5.0 = %f\n", 17 / 5.0);
    printf("-17 / 5 = %d\n", -17 / 5);
    printf("-17 %% 5 = %d\n", -17 % 5);
    printf("17 %% -5 = %d\n", 17 % -5);
    return 0;
}
double_by_zero.cDescargar double_by_zero.c
// Con double, dividir entre cero no detiene el programa: da inf.
// Aún no revisa lo que devuelve scanf: eso llega con el if, en la semana 4.
#include <stdio.h>

int main(void) {
    double a = 0.0;
    double b = 0.0;

    printf("Dos números: ");
    scanf("%lf %lf", &a, &b);

    printf("Cociente: %f\n", a / b);
    return 0;
}
final_grade.cDescargar final_grade.c
// Promedio final ponderado: (EP + 2 EF + PP) / 4.
// Aún no revisa lo que devuelve scanf: eso llega con el if, en la semana 4.
#include <stdio.h>

int main(void) {
    int ep = 0;
    int ef = 0;
    int pp = 0;

    printf("Examen parcial: ");
    scanf("%d", &ep);
    printf("Examen final: ");
    scanf("%d", &ef);
    printf("Promedio de prácticas: ");
    scanf("%d", &pp);

    double final_grade = (ep + 2 * ef + pp) / 4.0;

    printf("Promedio final: %.2f\n", final_grade);
    return 0;
}
grades.txtDescargar grades.txt
13 15 14
igv_price.cDescargar igv_price.c
// Precio con IGV. La tasa es una constante: el programa no puede cambiarla.
// Aún no revisa lo que devuelve scanf: eso llega con el if, en la semana 4.
#include <stdio.h>

int main(void) {
    const double IGV_RATE = 0.18;
    double price = 0.0;

    printf("Precio sin IGV: ");
    scanf("%lf", &price);

    double igv = price * IGV_RATE;
    double total = price + igv;

    printf("IGV: %.2f\n", igv);
    printf("Total: %.2f\n", total);
    return 0;
}
leftover.cDescargar leftover.c
// Lo que scanf no pudo leer se queda en la entrada, esperando a la siguiente lectura.
#include <stdio.h>

int main(void) {
    // Revisar si la lectura funcionó requiere if: llega en la semana 4.
    int number = 0;
    char next = '?';

    printf("Un entero: ");
    int count = scanf("%d", &number);
    scanf(" %c", &next);

    printf("scanf devolvió %d\n", count);
    printf("number = %d\n", number);
    printf("Lo siguiente en la entrada es %c\n", next);
    return 0;
}
math_demo.cDescargar math_demo.c
// Funciones de math.h. Reciben y devuelven double. Se compila con -lm.
// Aún no revisa lo que devuelve scanf: eso llega con el if, en la semana 4.
#include <math.h>
#include <stdio.h>

int main(void) {
    double x = 0.0;

    printf("x: ");
    scanf("%lf", &x);

    printf("sqrt(x)   = %f\n", sqrt(x));
    printf("pow(x, 3) = %f\n", pow(x, 3));
    printf("fabs(x)   = %f\n", fabs(x));
    printf("floor(x)  = %f\n", floor(x));
    printf("ceil(x)   = %f\n", ceil(x));
    printf("round(x)  = %f\n", round(x));
    return 0;
}
not_a_formula.cDescargar not_a_formula.c
// Una asignación guarda un valor una vez: no deja una fórmula viva.
#include <stdio.h>

int main(void) {
    int price = 2;
    int total = price * 3;      // total recibe 6, no la fórmula

    price = 10;                 // total no se entera de este cambio

    printf("price = %d\n", price);
    printf("total = %d\n", total);
    return 0;
}
printf_formats.cDescargar printf_formats.c
// Los formatos de printf. Cada % se reemplaza por un valor, en orden.
#include <stdio.h>

int main(void) {
    int grade = 15;
    double height = 1.685;
    char section = 'B';

    printf("Nota: %d\n", grade);
    printf("Estatura: %f\n", height);
    printf("Estatura: %.2f\n", height);
    printf("Sección: %c\n", section);
    printf("Nota %d, sección %c\n", grade, section);
    printf("Avance: %d%%\n", 80);
    printf("Columna 1\tColumna 2\n");
    printf("2.675 con dos decimales: %.2f\n", 2.675);
    return 0;
}
read_check.cDescargar read_check.c
// scanf devuelve cuántos datos pudo guardar. Aquí solo lo mostramos:
// actuar según ese número necesita el if de la semana 4.
#include <stdio.h>

int main(void) {
    int first = 0;
    int second = 0;

    printf("Dos enteros: ");
    int count = scanf("%d %d", &first, &second);

    printf("scanf devolvió %d\n", count);
    printf("first = %d, second = %d\n", first, second);
    return 0;
}
read_grade.cDescargar read_grade.c
// Primer programa que lee un dato del teclado.
#include <stdio.h>

int main(void) {
    int grade = 0;

    printf("Nota: ");
    scanf("%d", &grade);    // aún no revisamos lo que devuelve: ver read_check.c
    printf("Leí la nota %d\n", grade);
    printf("Con dos puntos más sería %d\n", grade + 2);
    return 0;
}
sizes.cDescargar sizes.c
// Cuántos bytes ocupa cada tipo. sizeof da un resultado que se escribe con %zu.
#include <stdio.h>

int main(void) {
    int age = 19;

    printf("int: %zu bytes\n", sizeof(int));
    printf("double: %zu bytes\n", sizeof(double));
    printf("char: %zu bytes\n", sizeof(char));
    printf("la variable age: %zu bytes\n", sizeof(age));
    printf("age vale %d\n", age);
    return 0;
}
swap_values.cDescargar swap_values.c
// Intercambiar dos variables necesita una tercera.
#include <stdio.h>

int main(void) {
    int left = 4;
    int right = 9;

    int saved = left;           // guardamos left antes de perderlo
    left = right;
    right = saved;

    printf("left = %d, right = %d\n", left, right);
    return 0;
}
broken/accent_char.cDescargar broken/accent_char.c
// Error a propósito: la ñ no cabe en un char.
#include <stdio.h>

int main(void) {
    char letter = 'ñ';

    printf("%c\n", letter);
    return 0;
}
broken/missing_ampersand.cDescargar broken/missing_ampersand.c
// Error a propósito: falta el & en scanf.
#include <stdio.h>

int main(void) {
    int grade = 0;

    scanf("%d", grade);
    printf("%d\n", grade);
    return 0;
}
broken/mod_double.cDescargar broken/mod_double.c
// Error a propósito: % solo funciona con enteros.
#include <stdio.h>

int main(void) {
    double x = 7.5;

    printf("%f\n", x % 2);
    return 0;
}
broken/printf_wrong_format.cDescargar broken/printf_wrong_format.c
// Error a propósito: %d con un valor double.
#include <stdio.h>

int main(void) {
    double average = 14.25;

    printf("%d\n", average);
    return 0;
}
broken/scanf_float_format.cDescargar broken/scanf_float_format.c
// Error a propósito: para leer un double, scanf necesita %lf.
#include <stdio.h>

int main(void) {
    double price = 0.0;

    scanf("%f", &price);
    printf("%.2f\n", price);
    return 0;
}
broken/uninitialized.cDescargar broken/uninitialized.c
// Error a propósito: usa una variable sin darle valor.
#include <stdio.h>

int main(void) {
    int total;

    printf("total = %d\n", total);
    return 0;
}
broken/uses_m_pi.cDescargar broken/uses_m_pi.c
// Error a propósito: con -std=c17, math.h no define M_PI.
#include <math.h>
#include <stdio.h>

int main(void) {
    printf("%f\n", M_PI);
    return 0;
}

Errores

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

final_grade_bug.cDescargar final_grade_bug.c
// Tiene un error a propósito: compila sin avisos, pero divide entre enteros.
// Aún no revisa lo que devuelve scanf: eso llega con el if, en la semana 4.
#include <stdio.h>

int main(void) {
    int ep = 0;
    int ef = 0;
    int pp = 0;

    printf("Examen parcial: ");
    scanf("%d", &ep);
    printf("Examen final: ");
    scanf("%d", &ef);
    printf("Promedio de prácticas: ");
    scanf("%d", &pp);

    double final_grade = (ep + 2 * ef + pp) / 4;

    printf("Promedio final: %.2f\n", final_grade);
    return 0;
}

Pruebas

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

test_examples.pyDescargar test_examples.py
"""Pruebas de los ejemplos de los apuntes. Corren cada programa en una terminal de
verdad, comparan su salida y comprueban que los apuntes y las diapositivas muestran
exactamente lo que salió. Si cambia el código o la versión de gcc, estas pruebas lo
avisan."""
import os
import pty
import re
import select
import subprocess
import time
import unittest
from pathlib import Path

HERE = Path(__file__).resolve().parent
COURSE = HERE.parent.parent
NOTES = COURSE / "notes" / "week-02.md"
SLIDES = COURSE / "slides" / "week-02.tex"

GCC = "gcc -Wall -Wextra -std=c17"


def clean(text):
    """Quita colores y lo que cambia entre máquinas: nombres temporales y desplazamientos."""
    text = re.sub(r"\x1b\[[0-9;]*m", "", text)      # colores de la terminal
    text = re.sub(r"/tmp/cc\w+\.o", "/tmp/cc.o", text)
    return re.sub(r"\.text\+0x[0-9a-f]+", ".text", text)


def run_in_terminal(command, dialog=(), eof=False):
    """Ejecuta un comando en una terminal: `dialog` son pares (mensaje, respuesta),
    y lo escrito aparece en la salida, igual que cuando lo escriben ustedes."""
    master, slave = pty.openpty()
    env = dict(os.environ, TERM="dumb", LANG="C.UTF-8")
    process = subprocess.Popen(["sh", "-c", command], stdin=slave, stdout=slave,
                               stderr=slave, cwd=HERE, env=env)
    os.close(slave)
    output = b""

    def read_until(done):
        nonlocal output
        limit = time.time() + 30
        while not done():
            if time.time() > limit:
                raise TimeoutError(output.decode())
            ready, _, _ = select.select([master], [], [], 0.05)
            if ready:
                try:
                    data = os.read(master, 4096)
                except OSError:     # el programa ya terminó
                    return
                if not data:
                    return
                output += data

    for prompt, answer in dialog:
        read_until(lambda: output.endswith(prompt.encode()))
        os.write(master, answer.encode() + b"\n")
    if eof:
        read_until(lambda: output != b"")
        os.write(master, b"\x04")
    read_until(lambda: False)
    process.wait()
    os.close(master)
    text = output.decode().replace("\r\n", "\n")
    return clean(f"$ {command}\n{text}")


# nombre: (comando, diálogo, fin de entrada, lo que debe salir)
GRADES = [("Examen parcial: ", "13"), ("Examen final: ", "15"),
          ("Promedio de prácticas: ", "14")]
TWO = "Dos enteros: "
SESSIONS = {
    "balance": ("build/balance", (), False, "Después de la recarga: 12\n"),
    "not_a_formula": ("build/not_a_formula", (), False, "total = 6\n"),
    "swap_values": ("build/swap_values", (), False, "left = 9, right = 4\n"),
    "sizes": ("build/sizes", (), False,
              "int: 4 bytes\ndouble: 8 bytes\nchar: 1 bytes\n"),
    "arithmetic": ("build/arithmetic", (), False, "7 + 8 % 3 * 2 = 11\n"),
    "division": ("build/division", (), False, "-17 % 5 = -2\n17 % -5 = 2\n"),
    "conversions": ("build/conversions", (), False, "(int) -3.99 = -3\n"),
    "char_codes": ("build/char_codes", (), False, "A tiene el código 65\n"),
    "printf_formats": ("build/printf_formats", (), False,
                       "2.675 con dos decimales: 2.67\n"),
    "read_grade": ("build/read_grade", [("Nota: ", "15")], False, "sería 17\n"),
    "check_ok": ("build/read_check", [(TWO, "4 9")],
                 False, "scanf devolvió 2\n"),
    "check_second": ("build/read_check", [(TWO, "4 hola")], False,
                     "scanf devolvió 1\nfirst = 4, second = 0\n"),
    "check_first": ("build/read_check", [(TWO, "hola")], False,
                    "scanf devolvió 0\nfirst = 0, second = 0\n"),
    "check_decimal": ("build/read_check", [(TWO, "3.7 5")], False,
                      "scanf devolvió 1\nfirst = 3, second = 0\n"),
    "check_eof": ("build/read_check", [], True, "scanf devolvió -1\n"),
    "leftover": ("build/leftover", [("Un entero: ", "hola")], False,
                 "Lo siguiente en la entrada es h\n"),
    "char_trap": ("build/char_trap", [("Cantidad: ", "3")], False,
                  "Leí 3 y un carácter de código 10\n"),
    "char_fixed": ("build/char_fixed", [("Cantidad: ", "3"), ("Talla: ", "M")], False,
                   "Leí 3 y la talla M\n"),
    "final_grade": ("build/final_grade", GRADES, False, "Promedio final: 14.25\n"),
    "final_grade_bug": ("build/final_grade_bug", GRADES, False,
                        "Promedio final: 14.00\n"),
    "igv_hola": ("build/igv_price", [("Precio sin IGV: ", "hola")], False,
                 "IGV: 0.00\nTotal: 0.00\n"),
    "igv_price": ("build/igv_price", [("Precio sin IGV: ", "50")], False,
                  "IGV: 9.00\nTotal: 59.00\n"),
    "change_780": ("build/change", [("Vuelto en céntimos: ", "780")], False,
                   "Sin cubrir: 0 céntimos\n"),
    "change_785": ("build/change", [("Vuelto en céntimos: ", "785")], False,
                   "Sin cubrir: 5 céntimos\n"),
    "math_demo": ("build/math_demo", [("x: ", "2.5")], False,
                  "sqrt(x)   = 1.581139\npow(x, 3) = 15.625000\nfabs(x)   = 2.500000\n"
                  "floor(x)  = 2.000000\nceil(x)   = 3.000000\nround(x)  = 3.000000\n"),
    "diagonal": ("build/diagonal", [("Ancho y alto en metros: ", "3 4")], False,
                 "Diagonal: 5.00 metros\n"),
    "compound_interest": ("build/compound_interest",
                          [("Capital en soles: ", "1000"), ("Tasa anual en %: ", "5"),
                           ("Años: ", "3")], False, "Monto final: 1157.63 soles\n"),
    "double_by_zero": ("build/double_by_zero", [("Dos números: ", "5 0")], False,
                       "Cociente: inf\n"),
    "redirect_in": ("build/final_grade < grades.txt", (), False,
                    "Promedio final: 14.25\n"),
    "redirect_out": ("build/final_grade < grades.txt > build/result.txt", (), False, ""),
    "redirect_cat": ("cat build/result.txt", (), False, "Promedio final: 14.25\n"),
    "uninitialized": (f"{GCC} broken/uninitialized.c -o build/uninitialized", (), False,
                      "is used uninitialized"),
    "missing_ampersand": (f"{GCC} broken/missing_ampersand.c -o build/missing_ampersand",
                          (), False, "expects argument of type ‘int *’"),
    "scanf_float": (f"{GCC} broken/scanf_float_format.c -o build/scanf_float_format",
                    (), False, "%lf\n"),
    "printf_wrong": (f"{GCC} broken/printf_wrong_format.c -o build/printf_wrong_format",
                     (), False, "%f\n"),
    "mod_double": (f"{GCC} broken/mod_double.c -o build/mod_double", (), False,
                   "invalid operands to binary %"),
    "missing_lm": (f"{GCC} diagonal.c -o build/diagonal_no_lm", (), False,
                   "undefined reference to `sqrt'"),
}


# Los apuntes dan estos resultados en una frase o una tabla, sin la sesión completa.
ONLY_IN_PROSE = {"check_ok", "check_first", "check_decimal", "change_785", "char_fixed",
                 "mod_double", "double_by_zero", "printf_wrong", "math_demo",
                 "compound_interest"}


def first_part(transcript, marker):
    """Lo que viene después de `marker` depende del procesador: no se compara."""
    end = transcript.index(marker)
    return transcript[:transcript.index("\n", end) + 1]


def session_transcripts():
    """Corre todas las sesiones una vez, y las reutilizan las demás pruebas."""
    subprocess.run(["mkdir", "-p", "build"], cwd=HERE, check=True)
    result = {}
    for name in ("redirect_in", "redirect_out", "redirect_cat"):
        command, dialog, eof, _ = SESSIONS[name]
        result[name] = run_in_terminal(command, dialog, eof)
    for name, (command, dialog, eof, _) in SESSIONS.items():
        if name not in result:
            result[name] = run_in_terminal(command, dialog, eof)
    # Por procesador cambian el resto de la salida y el segundo aviso de gcc.
    result["division_by_zero"] = first_part(
        run_in_terminal("build/division_by_zero", [(TWO, "5 0")]), "runtime error")
    accent = run_in_terminal(f"{GCC} broken/accent_char.c -o build/accent_char")
    result["accent_char"] = accent[:accent.index("broken/accent_char.c:5:19: warning: ",
                                                 accent.index("^~~"))]
    return result


TRANSCRIPTS = session_transcripts()


class Sessions(unittest.TestCase):
    def test_each_program_prints_what_the_notes_say(self):
        for name, (_, _, _, expected) in SESSIONS.items():
            with self.subTest(name):
                self.assertIn(expected, TRANSCRIPTS[name])

    def test_integer_division_by_zero_is_reported(self):
        self.assertIn("division_by_zero.c:12:22: runtime error: division by zero\n",
                      TRANSCRIPTS["division_by_zero"])

    def test_accent_char_warns_about_multichar(self):
        self.assertIn("multi-character character constant", TRANSCRIPTS["accent_char"])


class Compilation(unittest.TestCase):
    def compile(self, source, flags=("-Wall", "-Wextra")):
        return subprocess.run(["gcc", *flags, "-std=c17", "-c", str(source),
                               "-o", "/dev/null"], capture_output=True, text=True, cwd=HERE)

    def test_programs_have_no_warnings(self):
        sources = [*HERE.glob("*.c"), *HERE.glob("lab/*.c"), *HERE.glob("solution/*.c")]
        self.assertTrue(sources)
        for source in sources:
            with self.subTest(source.name):
                self.assertEqual(self.compile(source).stderr, "")

    def test_broken_programs_do_warn(self):
        for source in HERE.glob("broken/*.c"):
            with self.subTest(source.name):
                self.assertRegex(self.compile(source).stderr, "warning:|error:")

    def test_without_wall_gcc_says_nothing_about_uninitialized(self):
        self.assertEqual(self.compile("broken/uninitialized.c", ()).stderr, "")

    def test_sanitizers_do_not_detect_uninitialized(self):
        subprocess.run(f"{GCC} -w -fsanitize=address,undefined broken/uninitialized.c "
                       "-o build/uninitialized_san", shell=True, cwd=HERE, check=True)
        result = subprocess.run(["build/uninitialized_san"], cwd=HERE, capture_output=True)
        self.assertEqual(result.stderr, b"")

    def test_m_pi_is_not_available_with_c17(self):
        self.assertIn("M_PI", self.compile("broken/uses_m_pi.c").stderr)

    def test_assignment_to_const_and_expression_do_not_compile(self):
        source = HERE / "build" / "const_check.c"
        for body in ("const double RATE = 0.18; RATE = 0.2; return (int) RATE;",
                     "int b = 1; b - 3 = b; return b;",
                     "int b = 1; int b = 7; return b;"):
            with self.subTest(body):
                source.write_text(f"int main(void) {{ {body} }}\n", encoding="utf-8")
                self.assertIn("error:", self.compile(source).stderr)


class Crash(unittest.TestCase):
    def test_missing_ampersand_crashes_at_run_time(self):
        subprocess.run(f"{GCC} -w broken/missing_ampersand.c -o build/missing_ampersand",
                       shell=True, cwd=HERE, check=True)
        result = subprocess.run(["build/missing_ampersand"], input=b"15\n", cwd=HERE,
                                capture_output=True)
        self.assertNotEqual(result.returncode, 0)


@unittest.skipUnless(NOTES.exists(), "no están los apuntes")
class Notes(unittest.TestCase):
    def setUp(self):
        self.notes = clean(NOTES.read_text(encoding="utf-8"))

    def test_notes_show_real_output(self):
        for name, transcript in TRANSCRIPTS.items():
            if name in ONLY_IN_PROSE:
                continue
            with self.subTest(name):
                self.assertIn(transcript, self.notes)

    def test_terminal_blocks_only_show_real_output(self):
        for block in re.findall(r"```console\n(.*?)```", self.notes, re.S):
            real = [t for t in TRANSCRIPTS.values() if t in block]
            rest = block
            for transcript in sorted(real, key=len, reverse=True):
                rest = rest.replace(transcript, "")
            with self.subTest(block[:50]):
                self.assertEqual(rest.strip(), "")

    def test_programs_in_notes_are_the_real_files(self):
        files = {p.read_text(encoding="utf-8") for p in
                 [*HERE.glob("*.c"), *HERE.glob("broken/*.c")]}
        blocks = re.findall(r"^```c\n(.*?)```", self.notes, re.S | re.M)
        complete = [b for b in blocks if "int main" in b]
        self.assertTrue(complete)
        for block in complete:
            with self.subTest(block[:50]):
                self.assertIn(block, files)


@unittest.skipUnless(SLIDES.exists(), "no están las diapositivas")
class Slides(unittest.TestCase):
    def test_terminal_lines_are_real(self):
        tex = SLIDES.read_text(encoding="utf-8")
        real = {line for t in TRANSCRIPTS.values() for line in t.split("\n")}
        for env in ("terminal", "terminalancha"):
            for block in re.findall(rf"\\begin\{{{env}\}}(?:\[.*?\])?\n(.*?)\\end\{{{env}\}}",
                                    tex, re.S):
                for line in clean(block).strip("\n").split("\n"):
                    with self.subTest(line):
                        self.assertIn(line, real)


if __name__ == "__main__":
    unittest.main()
test_lab.pyDescargar test_lab.py
"""Pruebas del laboratorio. Corren sobre lab/ con `make test-lab`, y sobre
solution/ dentro de `make test`, si esa carpeta existe."""
import os
import subprocess
import unittest
from pathlib import Path

HERE = Path(__file__).resolve().parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")
FOLDER = HERE / LAB_DIR
BIN = HERE / "build" / LAB_DIR


def run(name, stdin=""):
    result = subprocess.run([str(BIN / name)], input=stdin, capture_output=True,
                            text=True, timeout=20)
    return result.stdout


@unittest.skipUnless(FOLDER.is_dir(), f"no existe {LAB_DIR}/")
class Lab(unittest.TestCase):
    def test_1_celsius(self):
        # 37 descubre la división entera: 9 / 5 daría 1 y el resultado sería 69.00.
        cases = {"100": "212.00", "0": "32.00", "-40": "-40.00", "37": "98.60",
                 "36.6": "97.88"}
        for given, expected in cases.items():
            with self.subTest(celsius=given):
                self.assertEqual(run("celsius", given + "\n"), expected + "\n")

    def test_2_predict_program(self):
        self.assertEqual(run("predict"),
                         "7 / 2 = 3\n7 % 2 = 1\n7.0 / 2 = 3.5\n(double) 7 / 2 = 3.5\n")

    def test_2_predictions_match_the_program(self):
        predictions = (FOLDER / "predictions.txt").read_text(encoding="utf-8")
        self.assertNotIn("?", predictions, "falta completar predictions.txt")
        self.assertEqual(predictions, run("predict"))

    def test_3_time_split(self):
        cases = {"3725": "1 h 2 min 5 s", "0": "0 h 0 min 0 s", "59": "0 h 0 min 59 s",
                 "3600": "1 h 0 min 0 s", "86399": "23 h 59 min 59 s",
                 "90061": "25 h 1 min 1 s"}
        for given, expected in cases.items():
            with self.subTest(seconds=given):
                self.assertEqual(run("time_split", given + "\n"), expected + "\n")

    def test_4_redirect(self):
        entrada = FOLDER / "entrada.txt"
        salida = FOLDER / "salida.txt"
        self.assertTrue(entrada.exists(), f"falta {LAB_DIR}/entrada.txt")
        self.assertTrue(salida.exists(), f"falta {LAB_DIR}/salida.txt")
        self.assertEqual(salida.read_text(encoding="utf-8"),
                         run("time_split", entrada.read_text(encoding="utf-8")))

    def test_5_type_sizes(self):
        self.assertIn("sizeof", (FOLDER / "type_sizes.c").read_text(encoding="utf-8"))
        self.assertEqual(run("type_sizes"),
                         "int: 4 bytes\ndouble: 8 bytes\nchar: 1 bytes\n")


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

Laboratorio

Archivos de partida del laboratorio de la semana.

lab/celsius.cDescargar lab/celsius.c
// Laboratorio 2, actividad 1: de Celsius a Fahrenheit.
//
// Lee un número con decimales y escribe solo el resultado, con dos decimales y
// un salto de línea. No escriban un mensaje pidiendo el dato.
// Ejemplo: si la entrada es 100, la salida es 212.00
//
// Fórmula: F = C * 9 / 5 + 32. Antes de escribirla en C, escríbanla en
// pseudocódigo y síganla a mano con 37. Cuidado con la división entera.
#include <stdio.h>

int main(void) {
    // Completar aquí.

    return 0;
}
lab/predict.cDescargar lab/predict.c
// Laboratorio 2, actividad 2: predecir y comprobar.
//
// 1. Antes de ejecutar nada, completen predictions.txt con lo que creen que
//    vale cada expresión.
// 2. Agreguen aquí las tres líneas que faltan, con el mismo estilo.
//    Usen %d para un resultado int y %.1f para un resultado double.
// 3. Ejecuten el programa y comparen. Si fallaron alguna predicción, déjenla
//    corregida y anoten en un comentario qué pensaban y por qué no era así.
#include <stdio.h>

int main(void) {
    printf("7 / 2 = %d\n", 7 / 2);
    // Faltan 7 % 2, 7.0 / 2 y (double) 7 / 2, en ese orden.

    return 0;
}
lab/predictions.txtDescargar lab/predictions.txt
7 / 2 = ?
7 % 2 = ?
7.0 / 2 = ?
(double) 7 / 2 = ?
lab/time_split.cDescargar lab/time_split.c
// Laboratorio 2, actividad 3: de segundos a horas, minutos y segundos.
//
// Lee un número entero de segundos y escribe una línea con este formato exacto.
// No escriban un mensaje pidiendo el dato.
// Ejemplo: si la entrada es 3725, la salida es
// 1 h 2 min 5 s
//
// Actividad 4: creen entrada.txt en esta carpeta, con un número de segundos, y
// desde code/week-02 ejecuten:
// build/lab/time_split < lab/entrada.txt > lab/salida.txt
#include <stdio.h>

int main(void) {
    // Completar aquí.

    return 0;
}
lab/type_sizes.cDescargar lab/type_sizes.c
// Laboratorio 2, actividad 5: cuántos bytes ocupa cada tipo.
//
// Escriban una línea por tipo, usando sizeof, con este formato exacto:
// int: ... bytes
// double: ... bytes
// char: ... bytes
#include <stdio.h>

int main(void) {
    // Completar aquí.

    return 0;
}

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# Semana 2: variables y programas secuenciales. Se usa dentro del entorno del
# curso (Codespaces o env/run.sh).
#
#   make           compila los ejemplos de los apuntes en build/
#   make test      compila y prueba los ejemplos, y la solución si existe solution/
#   make test-lab  prueba su laboratorio (lab/); falla hasta que lo terminen
#   make clean     borra lo generado
#
# Los programas de broken/ tienen errores a propósito: make no los compila.
# Los apuntes muestran qué dice gcc sobre cada uno.

CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g -fsanitize=address,undefined
LDLIBS = -lm
BUILD = build
PROGRAMS = balance not_a_formula swap_values sizes arithmetic division \
           conversions char_codes printf_formats read_grade final_grade \
           final_grade_bug read_check leftover char_trap char_fixed \
           igv_price change math_demo diagonal compound_interest \
           division_by_zero double_by_zero
LAB_PROGRAMS = celsius predict time_split type_sizes

.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 -v test_examples test_lab

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

clean:
	rm -rf $(BUILD) __pycache__