146 / Entrada, salida y datos

Un CSV grande por columnas

Para millones de filas, try_csv_read(texto, esquema, csv_opts()) de std/csv parsea el texto entero en una pasada a columnas tipadas — int, float, str y cat — con el núcleo escrito en C: sin split() y sin un String por campo. El esquema nombra solo las columnas que se usan; las demás se saltan sin parsearse.

146-csv-columnar.nxFuente →
// 146: CSV grande por columnas — try_csv_read, esquema y agrupación por códigos
//
// Para millones de filas, try_csv_read parsea en una pasada directo a columnas
// tipadas (el núcleo está en C): sin split() ni un String por campo. Una columna
// `cat` llega como códigos int + un diccionario, así que agrupar es indexar un
// Array, sin Map. En el bench de 5M filas (2026-10-25) este programa le gana a
// pandas; ver docs/design/specs/2026-10-25-csv-columnar-design.md.
import "std/csv"

fn main() -> int {
    // En la vida real: read_file("ventas.csv").
    let texto: String = "id,ciudad,monto,cantidad\n1,lima,10.5,3\n2,quito,99.25,1\n3,lima,0.75,10\n4,cusco,5.0,2\n"

    // ¿No conoces los tipos? try_csv_guess_schema propone un esquema para revisar.
    match try_csv_guess_schema(texto, csv_opts()) {
        Result.Ok(esq) => { print("esquema propuesto: " + esq) }
        Result.Err(e) => { print("no se pudo proponer: " + e.msg) }
    }

    // El esquema nombra solo lo que se usa: `id` se saltea sin parsear.
    match try_csv_read(texto, "ciudad:cat,monto:float,cantidad:int", csv_opts()) {
        Result.Ok(t) => {
            let montos: Array<float> = t.floats("monto")
            let cantidades: Array<int> = t.ints("cantidad")
            let codigos: Array<int> = t.codes("ciudad")
            let ciudades: Array<String> = t.dict("ciudad")

            // Un acumulador por valor distinto, indexado por el código.
            var totales: Array<float> = []
            var unidades: Array<int> = []
            for c in ciudades {
                totales.push(0.0)
                unidades.push(0)
            }
            var i: int = 0
            while i < t.rows() {
                let k: int = codigos[i]
                totales[k] = totales[k] + montos[i]
                unidades[k] = unidades[k] + cantidades[i]
                i = i + 1
            }
            var k: int = 0
            while k < ciudades.length() {
                let nombre: String = ciudades[k]
                print(nombre + ": " + float_to_string(totales[k]) + " (" + int_to_string(unidades[k]) + " u.)")
                k = k + 1
            }
        }
        Result.Err(e) => {
            print("error: " + e.msg)
            return 1
        }
    }

    // Un dato malo no se convierte en 0: es un Err con la fila y la columna.
    match try_csv_read("a,monto\n1,10\n2,diez\n", "monto:float", csv_opts()) {
        Result.Ok(t) => { print("no debería llegar acá") }
        Result.Err(e) => { print(e.msg) }
    }
    return 0
}
Salidastdout
esquema propuesto: id:int,ciudad:str,monto:float,cantidad:int
lima: 11.25 (13 u.)
quito: 99.25 (1 u.)
cusco: 5.0 (2 u.)
csv: fila 3, columna 'monto': "diez" no es float

Cómo funciona

Cuando no se conocen los tipos, try_csv_guess_schema propone un esquema para revisar. La tabla que vuelve da cada columna por nombre: t.floats("monto"), t.ints("cantidad"), y para la columna cat, t.codes("ciudad") (un entero por fila) y t.dict("ciudad") (los valores distintos). Agrupar es entonces un acumulador por valor distinto, indexado por el código — sin Map en el bucle.

El lector es estricto: un dato malo no se convierte en cero sino en un Err que nombra la fila y la columna. Las comillas, los saltos de línea entre comillas, CRLF y el BOM siguen la RFC 4180. En las notas de la versión 0.38.0, agrupar y filtrar un CSV de 5 millones de filas con este lector tardó 0.93 s, contra 1.21 s de pandas.