Skip to content

Commit

Permalink
Add tell/seek methods to CsvReader
Browse files Browse the repository at this point in the history
  • Loading branch information
mikee47 committed May 10, 2024
1 parent 2a03450 commit f593f9c
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 36 deletions.
35 changes: 28 additions & 7 deletions Sming/Core/Data/CsvReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,34 @@
#include "CsvReader.h"
#include <debug_progmem.h>

void CsvReader::reset()
CsvReader::CsvReader(IDataSourceStream* source, char fieldSeparator, const CStringArray& headings, size_t maxLineLength)
: source(source), maxLineLength(maxLineLength), headings(headings), fieldSeparator(fieldSeparator)
{
if(source) {
source->seekFrom(0, SeekOrigin::Start);
if(!userHeadingsProvided) {
readRow();
headings = row;
}
if(source && !headings) {
readRow();
this->headings = std::move(row);
start = cursor;
}
cursor = BOF;
}

bool CsvReader::seek(int cursor)
{
row = nullptr;
if(!source) {
return false;
}
auto newpos = std::max(cursor, int(start));
int pos = source->seekFrom(newpos, SeekOrigin::Start);
if(pos != newpos) {
return false;
}
this->cursor = cursor;
if(cursor < int(start)) {
// Before first record has been read
return true;
}
return readRow();
}

bool CsvReader::readRow()
Expand All @@ -46,6 +64,8 @@ bool CsvReader::readRow()
char lc{'\0'};
unsigned writepos{0};

cursor = source->seekFrom(0, SeekOrigin::Current);

while(true) {
if(buffer.length() == maxLineLength) {
debug_w("[CSV] Line buffer limit reached %u", maxLineLength);
Expand All @@ -58,6 +78,7 @@ bool CsvReader::readRow()
}
auto len = source->readBytes(buffer.begin() + writepos, buflen - writepos);
if(len == 0) {
// End of input
if(writepos == 0) {
return false;
}
Expand Down
45 changes: 35 additions & 10 deletions Sming/Core/Data/CsvReader.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,29 +40,28 @@ class CsvReader
public:
/**
* @brief Construct a CSV reader
* @param source Stream to read CSV text from
* @param source Stream to read CSV text from. Must support random seeking.
* @param fieldSeparator
* @param headings Required if source data does not contain field headings as first row
* @param maxLineLength Limit size of buffer to guard against malformed data
*/
CsvReader(IDataSourceStream* source, char fieldSeparator = ',', const CStringArray& headings = nullptr,
size_t maxLineLength = 2048)
: source(source), fieldSeparator(fieldSeparator), userHeadingsProvided(headings), maxLineLength(maxLineLength),
headings(headings)
{
reset();
}
size_t maxLineLength = 2048);

/**
* @brief Reset reader to start of CSV file
*
* Cursor is set to 'before start'.
* Call 'next()' to fetch first record.
*/
void reset();
void reset()
{
seek(start);
}

/**
* @brief Seek to next record
* @retval bool true on success, false if there are no more records
*/
bool next()
{
Expand Down Expand Up @@ -131,13 +130,39 @@ class CsvReader
return row;
}

/**
* @brief Get cursor position for current row
*
* The returned value indicates source stream offset for start of current row.
* After construction cursor is set to -1. This indicates 'Before first record' (BOF).
*/
int tell() const
{
return cursor;
}

/**
* @brief Set reader to previously noted position
* @param cursor Value obtained via `tell()`
* @retval bool true on success, false on failure or end of records
*
* If cursor is BOF then there will be no current record until `next()` is called.
* This is the same as if `next()` were called.
*
* Otherwise the corresponding row will be available via `getRow()`.
*/
bool seek(int cursor);

private:
bool readRow();

static constexpr int BOF{-1}; ///< Indicates 'Before First Record'

std::unique_ptr<IDataSourceStream> source;
char fieldSeparator;
bool userHeadingsProvided;
size_t maxLineLength;
size_t start{0}; ///< Stream position of first record
int cursor{BOF}; ///< Stream position for start of current row
CStringArray headings;
CStringArray row;
char fieldSeparator;
};
80 changes: 61 additions & 19 deletions tests/HostTests/modules/CsvReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@

DEFINE_FSTR_LOCAL(
test1_csv, "\"field1\",field2,field3,\"field four\"\n"
"Something \"awry\",\"datavalue 2\",\"where,are,\"\"the,bananas\",sausages abound,\"never surrender\"")
DEFINE_FSTR_LOCAL(csv_headings, "field1;field2;field3;field four;")
DEFINE_FSTR_LOCAL(csv_row1, "Something \"awry\";datavalue 2;where,are,\"the,bananas;sausages abound;never surrender;")
"Something \"awry\",\"datavalue 2\",\"where,are,\"\"the,bananas\",sausages abound,\"never surrender\"\n"
"one,two,three,four\n"
"a,b,c,d,e,f")

DEFINE_FSTR_LOCAL(csv_headings, "field1;field2;field3;field four")
DEFINE_FSTR_LOCAL(csv_row1, "Something \"awry\";datavalue 2;where,are,\"the,bananas;sausages abound;never surrender")
DEFINE_FSTR_LOCAL(csv_row2, "one;two;three;four")
DEFINE_FSTR_LOCAL(csv_row3, "a;b;c;d;e;f")

class CsvReaderTest : public TestGroup
{
Expand All @@ -18,22 +23,59 @@ class CsvReaderTest : public TestGroup
{
TEST_CASE("Basic")
{
auto str = [](const CStringArray& cs) {
String s = reinterpret_cast<const String&>(cs);
s.replace('\0', ';');
return s;
};

CsvReader reader(new FSTR::Stream(test1_csv));
String headings = str(reader.getHeadings());
Serial.println(headings);
CHECK(reader.next());
String row1 = str(reader.getRow());
Serial.println(row1);
CHECK(!reader.next());

CHECK(csv_headings == headings);
CHECK(csv_row1 == row1);
CsvReader reader(new FSTR::Stream(test1_csv, ','));

const char* sep = ";";

Serial << "Cursor " << reader.tell() << endl;
auto headings = reader.getHeadings();
Serial.println(headings.join(sep));
CHECK(csv_headings == headings.join(sep));

REQUIRE(reader.next());
auto cursor1 = reader.tell();
Serial << "Cursor " << cursor1 << endl;
auto row = reader.getRow();
Serial.println(row.join(sep));
CHECK(csv_row1 == row.join(sep));

REQUIRE(reader.next());
auto cursor2 = reader.tell();
Serial << "Cursor " << cursor2 << endl;
row = reader.getRow();
REQUIRE(csv_row2 == row.join(sep));

REQUIRE(reader.next());
auto cursor3 = reader.tell();
Serial << "Cursor " << cursor3 << endl;
row = reader.getRow();
REQUIRE(csv_row3 == row.join(sep));

REQUIRE(!reader.next());
Serial << "Cursor " << reader.tell() << endl;

TEST_CASE("reset")
{
reader.reset();
REQUIRE(reader.next());
row = reader.getRow();
REQUIRE(csv_row1 == row.join(sep));
}

TEST_CASE("seek")
{
reader.seek(cursor2);
row = reader.getRow();
REQUIRE(csv_row2 == row.join(sep));

reader.seek(cursor1);
row = reader.getRow();
REQUIRE(csv_row1 == row.join(sep));

reader.seek(cursor3);
row = reader.getRow();
REQUIRE(csv_row3 == row.join(sep));
}
}
}
};
Expand Down

0 comments on commit f593f9c

Please sign in to comment.