Back to Documentations

Signature Description Public Member Functions
template<typename S, typename DF>
class   ChunkedReader;
This is not a DataFrame memeber function, but ChunkedReader is a resumable cursor over an io_format::csv2 or io_format::binary source (an open std::istream, std::FILE *, or similar). Repeated calls to next_chunk() each pick up exactly where the previous call left off: the header/column-spec is parsed once, and there is no re-scanning of previously consumed rows.

This is different from calling DataFrame::read() several times with increasing ReadParams::starting_row values. That approach re-opens -- and re-reads from the beginning of -- the source on every single call. For io_format::csv2 that also means re-scanning and discarding every row before starting_row each time -- reading a window at offset 900 costs 900 rows of re-scanning, every time.
ChunkedReader Header File

S: A stream type
DF: A DataFrame type
static void test_read_chunked_data()  {

    std::cout << "\nTesting read_chunked_data( ) ..." << std::endl;

    using MyDataFrame = StdDataFrame<unsigned long>;

    // Build a reasonably sized DataFrame and write it out as csv2 ----
    //
    // deliberately not a multiple of any "nice" chunk size
    //
    const std::size_t           n { 10037 };
    std::vector<unsigned long>  idx(n);
    std::vector<double>         col_a(n);
    std::vector<int>            col_b(n);
    std::vector<std::string>    col_c(n);

    for (std::size_t i = 0; i < n; ++i)  {
        idx[i] = i;
        col_a[i] = double(i) * 1.5;
        col_b[i] = int(i) * 2;
        col_c[i] = "row_" + std::to_string(i);
    }

    MyDataFrame df;

    df.load_data(std::move(idx),
                 std::make_pair("col_a", col_a),
                 std::make_pair("col_b", col_b),
                 std::make_pair("col_c", col_c));

    std::filesystem::path   file_name { "./chunked_reader_test.csv2" };

    df.write<double, int, std::string>(file_name.string().c_str(), io_format::csv2);

    // Baseline: read the whole thing back in one shot
    //
    MyDataFrame whole;

    whole.read(file_name.string().c_str(), io_format::csv2);
    assert(whole.get_index().size() == n);

    //  Now read it back via ChunkedReader, in chunks
    //
    std::ifstream   stream(file_name.string().c_str());

    assert(stream.is_open());

    ChunkedReader<std::ifstream, MyDataFrame>   reader(stream, io_format::csv2);
    MyDataFrame                                 chunk;
    std::size_t                                 total_rows_seen { 0 };
    std::size_t                                 chunk_count { 0 };
    const std::size_t                           chunk_size { 777 };
    std::vector<unsigned long>                  reassembled_idx;
    std::vector<double>                         reassembled_a;
    std::vector<int>                            reassembled_b;
    std::vector<std::string>                    reassembled_c;

    while (reader.next_chunk(chunk, chunk_size))  {
        chunk_count += 1;

        const auto  &chunk_idx { chunk.get_index() };
        const auto  &chunk_a { chunk.get_column<double>("col_a") };
        const auto  &chunk_b { chunk.get_column<int>("col_b") };
        const auto  &chunk_c { chunk.get_column<std::string>("col_c") };


        // Every chunk except possibly the last should be exactly
        // chunk_size rows.
        //
        assert(! ((total_rows_seen + chunk_size) <= n && chunk_idx.size() != chunk_size));

        assert(chunk_idx.size() <= 777);
        assert(chunk_idx.size() == chunk_a.size());
        assert(chunk_idx.size() == chunk_b.size());
        assert(chunk_idx.size() == chunk_c.size());
        for (std::size_t i = 0; i < chunk_idx.size(); ++i)  {
            reassembled_idx.push_back(chunk_idx[i]);
            reassembled_a.push_back(chunk_a[i]);
            reassembled_b.push_back(chunk_b[i]);
            reassembled_c.push_back(chunk_c[i]);
        }

        total_rows_seen += chunk_idx.size();
    }

    assert(reader.is_eof());
    assert(total_rows_seen == n);
    assert(reassembled_idx.size() == n);

    // Verify the reassembled data matches the one-shot read exactly
    //
    const auto  &whole_idx { whole.get_index() };
    const auto  &whole_a { whole.get_column<double>("col_a") };
    const auto  &whole_b { whole.get_column<int>("col_b") };
    const auto  &whole_c { whole.get_column<std::string>("col_c") };

    for (std::size_t i = 0; i < n; ++i)  {
        assert(reassembled_idx[i] == whole_idx[i]);
        assert(reassembled_a[i] == whole_a[i]);
        assert(reassembled_b[i] == whole_b[i]);
        assert(reassembled_c[i] == whole_c[i]);
    }

    // Sanity check: a stream truly positioned mid-way through (no
    // rewinding) still works -- prove there's no "rescan from
    // beginning" happening internally by reading a small file
    //  entirely via tiny chunks and checking chunk_count > 1
    //
    {
        std::ifstream                               stream2 { file_name.string().c_str() };
        ChunkedReader<std::ifstream, MyDataFrame>   reader2 { stream2, io_format::csv2 };
        MyDataFrame                                 small_chunk;
        std::size_t                                 small_chunk_count { 0 };
        std::size_t                                 small_total { 0 };

        while (reader2.next_chunk(small_chunk, 10))  {
            small_chunk_count += 1;
            small_total += small_chunk.get_index().size();
        }
        assert(small_total == n);
        assert(small_chunk_count == (n + 9) / 10);
    }

    // Sanity check: unsupported format throws
    //
    {
        bool    threw { false };

        try  {
            std::ifstream                               stream3 { file_name.string().c_str() };
            ChunkedReader<std::ifstream, MyDataFrame>   reader3 { stream3, io_format::csv };
        }
        catch (const NotImplemented &)  { threw = true; }
        assert(threw);
    }
    std::filesystem::remove(file_name);

    // Now the same battery of checks, but for io_format::binary
    //
    std::filesystem::path   bin_file_name { "./chunked_reader_test.hmdf" };

    df.write<double, int, std::string>(bin_file_name.string().c_str(), io_format::binary);

    MyDataFrame whole_bin;

    whole_bin.read(bin_file_name.string().c_str(), io_format::binary);
    assert(whole_bin.get_index().size() == n);

    std::ifstream   bin_stream { bin_file_name.string().c_str(), std::ios::binary };

    assert(bin_stream.is_open());

    ChunkedReader<std::ifstream, MyDataFrame>   bin_reader { bin_stream, io_format::binary };
    MyDataFrame                                 bin_chunk;
    std::size_t                                 bin_total_rows_seen { 0 };
    std::size_t                                 bin_chunk_count { 0 };
    std::vector<unsigned long>                  bin_reassembled_idx;
    std::vector<double>                         bin_reassembled_a;
    std::vector<int>                            bin_reassembled_b;
    std::vector<std::string>                    bin_reassembled_c;

    while (bin_reader.next_chunk(bin_chunk, chunk_size))  {
        bin_chunk_count += 1;

        const auto  &chunk_idx { bin_chunk.get_index() };
        const auto  &chunk_a { bin_chunk.get_column<double>("col_a") };
        const auto  &chunk_b { bin_chunk.get_column<int>("col_b") };
        const auto  &chunk_c { bin_chunk.get_column<std::string>("col_c") };

        if (bin_total_rows_seen + chunk_size <= n)
            assert(chunk_idx.size() == chunk_size);

        for (std::size_t i = 0; i < chunk_idx.size(); ++i)  {
            bin_reassembled_idx.push_back(chunk_idx[i]);
            bin_reassembled_a.push_back(chunk_a[i]);
            bin_reassembled_b.push_back(chunk_b[i]);
            bin_reassembled_c.push_back(chunk_c[i]);
        }

        bin_total_rows_seen += chunk_idx.size();
    }

    assert(bin_reader.is_eof());
    assert(bin_total_rows_seen == n);
    assert(bin_reassembled_idx.size() == n);

    for (std::size_t i = 0; i < n; ++i)  {
        assert(bin_reassembled_idx[i] == whole_idx[i]);
        assert(bin_reassembled_a[i] == whole_a[i]);
        assert(bin_reassembled_b[i] == whole_b[i]);
        assert(bin_reassembled_c[i] == whole_c[i]);
    }

    // Tiny-chunk-size sanity check for binary too.
    //
    {
        std::ifstream                               bin_stream2 { bin_file_name.string().c_str(), std::ios::binary };
        ChunkedReader<std::ifstream, MyDataFrame>   bin_reader2 { bin_stream2, io_format::binary };
        MyDataFrame                                 small_bin_chunk;
        std::size_t                                 small_bin_chunk_count { 0 };
        std::size_t                                 small_bin_total { 0 };

        while (bin_reader2.next_chunk(small_bin_chunk, 10))  {
            small_bin_chunk_count += 1;
            small_bin_total += small_bin_chunk.get_index().size();
        }
        assert(small_bin_total == n);
        assert(small_bin_chunk_count == (n + 9) / 10);
    }

    // A starting_row offset applied on the very first binary chunk too.
    //
    {
        std::ifstream  bin_stream3 { bin_file_name.string().c_str(), std::ios::binary };
        ReadParams     p;

        p.starting_row = 100;

        ChunkedReader<std::ifstream, MyDataFrame>   bin_reader3 { bin_stream3, io_format::binary, p };
        MyDataFrame                                 offset_chunk;

        assert(bin_reader3.next_chunk(offset_chunk, 50));
        assert(offset_chunk.get_index().size() == 50);
        assert(offset_chunk.get_index()[0] == whole_idx[100]);
        assert(offset_chunk.get_column<double>("col_a")[0] == whole_a[100]);
    }
    std::filesystem::remove(bin_file_name);
}

C++ DataFrame