146 / Input, output and data

A large CSV by columns

For millions of rows, try_csv_read(text, schema, csv_opts()) from std/csv parses the whole text in one pass into typed columns — int, float, str and cat — with the core written in C: no split() and no String per field. The schema names only the columns that are used; the rest are skipped without being parsed.

146-csv-columnar.nxSource →
// 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
}
Outputstdout
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

How it works

When the types are not known, try_csv_guess_schema proposes a schema to review. The table that comes back gives each column by name: t.floats("monto"), t.ints("cantidad"), and for the cat column t.codes("ciudad") (one integer per row) and t.dict("ciudad") (the distinct values). Grouping is then one accumulator per distinct value, indexed by the code — no Map in the loop.

The reader is strict: a bad value is not turned into a zero but into an Err that names the row and the column. Quotes, line breaks inside quotes, CRLF and a BOM follow RFC 4180. In the release notes for 0.38.0, grouping and filtering a CSV of 5 million rows with this reader took 0.93 s, against 1.21 s for pandas.