// Step 1 -- the parser everyone writes first: getline + string copies.
//
// It is CORRECT (quoted commas, doubled quotes, even newlines inside quoted
// fields -- the case that silently breaks most homemade CSV readers), and it
// is the baseline every later step is measured against. The costs to notice:
//
// * istream::getline hands us lines, but a CSV record is NOT a line -- a
// quoted field may contain '\n', so we re-join lines while a quote is
// open. Record assembly alone forces one std::string append per line.
// * Every field is copied out of the record into its own std::string:
// one heap allocation per field, six per row, ~7 million for bench.csv.
// The allocation counter at the end makes that visible.
// * std::stod is used to read the value column, the way scripts do.
//
// Build: c++ -std=c++20 -O2 step1_naive.cpp -o step1
// Run: ./step1 (expects edge.csv + bench.csv in this directory)
#include "csv_test_common.hpp"
#include <fstream>
#include <string>
#include <vector>
// Split one fully assembled record into unescaped field copies.
static std::vector<std::string> split_record(const std::string& rec) {
std::vector<std::string> out;
std::string cur;
bool in_quotes = false;
for (size_t i = 0; i < rec.size(); ++i) {
char c = rec[i];
if (in_quotes) {
if (c == '"') {
if (i + 1 < rec.size() && rec[i + 1] == '"') { cur += '"'; ++i; }
else in_quotes = false;
} else cur += c;
} else {
if (c == '"') in_quotes = true;
else if (c == ',') { out.push_back(std::move(cur)); cur.clear(); }
else cur += c;
}
}
out.push_back(std::move(cur));
return out;
}
// A record continues onto the next line while it has an odd number of quotes.
static bool quotes_balanced(const std::string& s) {
size_t n = 0;
for (char c : s) n += (c == '"');
return n % 2 == 0;
}
template <typename OnRecord>
static void parse_naive(const char* path, OnRecord on_record) {
std::ifstream f(path, std::ios::binary);
std::string line, record;
while (std::getline(f, line)) {
record = line;
while (!quotes_balanced(record) && std::getline(f, line)) {
record += '\n'; // getline ate the '\n'; the '\r' before
record += line; // it is still there -- CRLF survives.
}
if (!record.empty() && record.back() == '\r') record.pop_back();
on_record(split_record(record));
}
}
static std::vector<std::vector<std::string>> parse_all_materialized(const char* path) {
std::vector<std::vector<std::string>> rows;
parse_naive(path, [&](std::vector<std::string> r) { rows.push_back(std::move(r)); });
return rows;
}
int main() {
if (!run_edge_checks("edge.csv", parse_all_materialized)) return 1;
std::ifstream sz("bench.csv", std::ios::binary | std::ios::ate);
const double mb = sz.tellg() / 1e6;
BenchResult res;
for (int rep = 0; rep < 3; ++rep) {
unsigned long long a0 = allocs_now();
double t0 = now_secs();
unsigned long long rows = 0, fields = 0, byte_sum = 0;
double value_sum = 0.0;
parse_naive("bench.csv", [&](std::vector<std::string> r) {
++rows;
fields += r.size();
for (auto& f : r) byte_sum += f.size();
if (rows > 1 && r.size() > 3) value_sum += std::stod(r[3]);
});
double dt = now_secs() - t0;
if (dt < res.best_secs) res.best_secs = dt;
res.rows = rows; res.fields = fields; res.byte_sum = byte_sum;
res.last_pass_allocs = allocs_now() - a0;
if (rep == 0) std::printf("sum(value) = %.6f\n", value_sum);
}
print_bench("step1_naive", res, mb);
return 0;
}