145 / Entrada, salida y datos

Un CSV grande sin split() por fila

fila.split(",") es claro, pero crea un Array y un String por campo. En un CSV de millones de filas esas asignaciones son cerca de la mitad del tiempo. string_to_int_range(s, ini, fin, def) y string_to_float_range(s, ini, fin, def) parsean el número de los bytes [ini, fin) del texto sin crear el substring, y dan siempre lo mismo que string_to_X_or(s.substring(ini, fin), def).

145-csv-sin-split.nxFuente →
// 145: CSV grande sin split() por fila — indexOf + string_to_X_range
//
// `fila.split(",")` crea un Array y un String por campo: en un CSV de millones
// de filas esas asignaciones son la mitad del tiempo (medido 2026-10-25, 5M
// filas). string_to_int_range / string_to_float_range parsean el número
// directo de los bytes [ini, fin) del texto, sin crear el substring. Dan
// siempre lo mismo que string_to_X_or(s.substring(ini, fin), def).

fn main() -> int {
    // Un CSV chico armado en memoria (en la vida real: read_file(ruta)).
    let texto: String = "id,ciudad,valor,cantidad\n1,lima,10.5,3\n2,quito,99.25,1\n3,lima,0.75,10\n"

    // Estilo split: claro, pero aloca un Array + un String por campo y por fila.
    var total_split: float = 0.0
    let lineas: Array = texto.split("\n")
    var i: int = 1
    while i < lineas.length() {
        let linea: String = lineas[i]
        i = i + 1
        if linea.length() == 0 { continue }
        let campos: Array = linea.split(",")
        total_split = total_split + string_to_float(campos[2])
    }

    // Estilo rango: las comas se ubican con indexOf y los números se parsean
    // en su lugar. Solo la ciudad, que sí se guarda, se copia con substring.
    var total: float = 0.0
    var unidades: int = 0
    var por_ciudad: Map<String, float> = Map.new()
    let n: int = texto.length()
    var p: int = texto.indexOf("\n") + 1          // saltar la cabecera
    while p < n {
        var fin: int = texto.indexOf("\n", p)
        if fin < 0 { fin = n }
        let c1: int = texto.indexOf(",", p)
        let c2: int = texto.indexOf(",", c1 + 1)
        let c3: int = texto.indexOf(",", c2 + 1)
        let ciudad: String = texto.substring(c1 + 1, c2)
        // El default (0.0 / 0) entra si el campo viene vacío o no es número.
        let valor: float = string_to_float_range(texto, c2 + 1, c3, 0.0)
        unidades = unidades + string_to_int_range(texto, c3 + 1, fin, 0)
        total = total + valor
        por_ciudad[ciudad] = por_ciudad.get_or(ciudad, 0.0) + valor
        p = fin + 1
    }

    print("total con split: " + float_to_string(total_split))
    print("total con rango: " + float_to_string(total))
    print("unidades: " + int_to_string(unidades))
    print("lima: " + float_to_string(por_ciudad["lima"]))
    return 0
}
Salidastdout
total con split: 110.5
total con rango: 110.5
unidades: 14
lima: 11.25

Cómo funciona

La receta suma la misma columna dos veces. El estilo split parte el texto en líneas y cada línea en campos. El estilo rango recorre el texto una sola vez: texto.indexOf("\n", p) ubica el fin de la fila y tres llamadas a indexOf(",", …) ubican las comas, todas con la posición absoluta que devuelve indexOf(s, desde). Los números se parsean en su lugar; solo la ciudad, que se guarda como clave del mapa, se copia con substring.

El default (0.0 o 0) entra cuando el campo viene vacío o no es un número, y un entero que no entra en 64 bits también cuenta como inválido en vez de saturarse. Cuando las columnas y sus tipos se conocen de antemano, el lector columnar de la receta 146 hace la pasada entera en C y es el paso siguiente.