added support for multi language column selection in configuration files

This commit is contained in:
john30
2017-04-17 09:56:46 +02:00
parent ac90586b15
commit 50d1aee7d3
12 changed files with 227 additions and 99 deletions
+7 -1
View File
@@ -87,6 +87,7 @@ static struct options opt = {
CONFIG_PATH, // configPath
false, // scanConfig
BROADCAST, // initialScan
getenv("LANG"), // preferLanguage
false, // checkConfig
false, // dumpConfig
5, // pollInterval
@@ -136,7 +137,8 @@ static const char argpdoc[] =
#define O_INISND 1
#define O_DEVLAT (O_INISND+1)
#define O_CHKCFG (O_DEVLAT+1)
#define O_CFGLNG (O_DEVLAT+1)
#define O_CHKCFG (O_CFGLNG+1)
#define O_DMPCFG (O_CHKCFG+1)
#define O_POLINT (O_DMPCFG+1)
#define O_ANSWER (O_POLINT+1)
@@ -177,6 +179,7 @@ static const struct argp_option argpoptions[] = {
"\"none\" or empty for no initial scan message, \"full\" for full scan, or a single hex address to scan, "
"default is broadcast ident message). If combined with --checkconfig, you can add scan message data as "
"arguments for checking a particular scan configuration, e.g. \"FF08070400/0AB5454850303003277201\".", 0 },
{"configlang", O_CFGLNG, "LANG", 0, "Prefer LANG in multilingual configuration files [system default language]", 0 },
{"checkconfig", O_CHKCFG, NULL, 0, "Check CSV config files, then stop", 0 },
{"dumpconfig", O_DMPCFG, NULL, 0, "Check and dump CSV config files, then stop", 0 },
{"pollinterval", O_POLINT, "SEC", 0, "Poll for data every SEC seconds (0=disable) [5]", 0 },
@@ -308,6 +311,9 @@ error_t parse_opt(int key, char *arg, struct argp_state *state) {
}
}
break;
case O_CFGLNG: // --configlang=LANG
opt->preferLanguage = arg;
break;
case O_CHKCFG: // --checkconfig
opt->checkConfig = true;
break;
+1
View File
@@ -45,6 +45,7 @@ struct options {
/** the initial address to scan for scanconfig
* (@a ESC=none, 0xfe=broadcast ident, @a SYN=full scan, else: single slave address). */
symbol_t initialScan;
string preferLanguage; //!< preferred language in configuration files
bool checkConfig; //!< check CSV config files, then stop
bool dumpConfig; //!< dump CSV config files, then stop
unsigned int pollInterval; //!< poll interval in seconds, 0 to disable [5]
+1 -1
View File
@@ -41,7 +41,7 @@ using std::ifstream;
#define RECONNECT_MISSING_SIGNAL 60
result_t UserList::getFieldMap(vector<string>& row, string& errorDescription) const {
result_t UserList::getFieldMap(vector<string>& row, string& errorDescription, const string preferLanguage) const {
// name,secret,level[,level]*
if (row.empty()) {
row.push_back("name");
+1 -1
View File
@@ -62,7 +62,7 @@ class UserList : public UserInfo, public MappedFileReader {
virtual ~UserList() {}
// @copydoc
result_t getFieldMap(vector<string>& row, string& errorDescription) const override;
result_t getFieldMap(vector<string>& row, string& errorDescription, const string preferLanguage) const override;
// @copydoc
result_t addFromFile(map<string, string>& row, vector< map<string, string> >& subRows,
+67 -38
View File
@@ -46,7 +46,7 @@ static const char* defaultTemplateFieldMap[] = {
string getDataFieldName(const string name) {
string getDataFieldName(const string name, bool& supportsLanguage) {
if (name.find("name") != string::npos || name.find("field") != string::npos) {
return "name";
}
@@ -58,10 +58,12 @@ string getDataFieldName(const string name) {
}
if (name.find("divisor") != string::npos) {
if (name.find("values") != string::npos) {
supportsLanguage = true;
return "divisor/values";
}
return "divisor";
}
supportsLanguage = true;
if (name == "values" || name == "unit") {
return name;
}
@@ -72,6 +74,12 @@ string getDataFieldName(const string name) {
}
const string AttributedItem::formatInt(size_t value) {
ostringstream stream;
stream << dec << static_cast<unsigned>(value);
return stream.str();
}
const string AttributedItem::pluck(map<string, string>& row, string key) {
map<string, string>::iterator it = row.find(key);
if (it == row.end()) {
@@ -174,12 +182,6 @@ bool AttributedItem::appendAttributes(ostringstream& output, OutputFormat output
}
string formatInt(size_t value) {
ostringstream stream;
stream << dec << static_cast<unsigned>(value);
return stream.str();
}
result_t DataField::create(vector< map<string, string> >& rows, string& errorDescription,
DataFieldTemplates* templates, const DataField*& returnField,
const bool isWriteMessage,
@@ -1120,7 +1122,8 @@ result_t DataFieldTemplates::add(const DataField* field, string name, bool repla
return RESULT_OK;
}
result_t DataFieldTemplates::getFieldMap(vector<string>& row, string& errorDescription) const {
result_t DataFieldTemplates::getFieldMap(vector<string>& row, string& errorDescription, const string preferLanguage)
const {
// name[:usename],basetype[:len]|template[:usename][,[divisor|values][,[unit][,[comment]]]]
if (row.empty()) {
// default map does not include separate field name
@@ -1130,39 +1133,65 @@ result_t DataFieldTemplates::getFieldMap(vector<string>& row, string& errorDescr
return RESULT_OK;
}
bool inDataFields = false;
map<string, string> seen;
for (auto &name : row) {
string useName = name;
tolower(useName);
if (inDataFields) {
string chkName = getDataFieldName(useName);
if (!chkName.empty()) {
useName = chkName;
if (seen.find(useName) != seen.end()) {
if (seen.find("type") == seen.end()) {
errorDescription = "missing type";
return RESULT_ERR_EOF; // require at least type
}
seen.clear();
map<string, size_t> seen;
for (size_t col = 0; col < row.size(); col++) {
string &name = row[col];
string lowerName = name;
tolower(lowerName);
trim(lowerName);
if (lowerName.empty()) {
errorDescription = "missing name in column " + AttributedItem::formatInt(col);
return RESULT_ERR_INVALID_ARG;
}
bool supportsLang = false;
string useName = getDataFieldName(lowerName, supportsLang);
bool unknown = useName.empty();
size_t langPos = supportsLang ? lowerName.find_last_of('.') : string::npos;
map<string, size_t>::iterator previous;
if (langPos == lowerName.length()-3) {
string lang = lowerName.substr(langPos+1);
if (unknown) {
useName = lowerName.substr(0, langPos);
}
previous = seen.find(useName);
if (previous != seen.end()) {
if (lang != preferLanguage) {
// skip this column
name = SKIP_COLUMN;
continue;
}
// replace previous
row[previous->second] = SKIP_COLUMN;
seen.erase(useName);
previous = seen.end();
}
} else {
if (useName == "name" && seen.find("name") == seen.end()) {
// keep first name for template
} else {
string chkName = getDataFieldName(useName);
if (!chkName.empty()) {
useName = chkName;
if (seen.find("name") == seen.end()) {
errorDescription = "missing name";
return RESULT_ERR_EOF; // require at least name
}
inDataFields = true;
seen.clear();
if (unknown) {
useName = lowerName;
}
previous = seen.find(useName);
}
if (inDataFields) {
if (!unknown && previous != seen.end()) {
if (seen.find("type") == seen.end()) {
errorDescription = "missing field type";
return RESULT_ERR_EOF; // require at least type
}
seen.clear();
}
} else {
if (!unknown && useName == "name" && seen.find("name") == seen.end()) {
// keep first name for template
} else if (!unknown) {
if (seen.find("name") == seen.end()) {
errorDescription = "missing template name";
return RESULT_ERR_EOF; // require at least name
}
inDataFields = true;
seen.clear();
}
if (!inDataFields && seen.find(useName) != seen.end()) {
errorDescription = "duplicate " + useName;
errorDescription = "duplicate template " + useName;
return RESULT_ERR_INVALID_ARG;
}
}
@@ -1171,14 +1200,14 @@ result_t DataFieldTemplates::getFieldMap(vector<string>& row, string& errorDescr
} else {
name = useName;
}
seen[useName] = useName;
seen[useName] = col;
}
if (!inDataFields) {
errorDescription = "missing fields";
errorDescription = "missing template fields";
return RESULT_ERR_EOF; // require at least one field
}
if (seen.find("type") == seen.end()) {
errorDescription = "missing type";
errorDescription = "missing field type";
return RESULT_ERR_EOF; // require at least type
}
return RESULT_OK;
+10 -2
View File
@@ -53,9 +53,10 @@ namespace ebusd {
/**
* Get the normalized data field name for the given name.
* @param name the input field name.
* @param supportsLanguage set to true when the field supports multiple language.
* @return the normalized data field name, or empty if unknown.
*/
string getDataFieldName(const string name);
string getDataFieldName(const string name, bool& supportsLanguage);
class DataFieldTemplates;
class SingleDataField;
@@ -86,6 +87,13 @@ class AttributedItem {
virtual ~AttributedItem() {}
/**
* Format an int value to a string.
* @param value the int value.
* @return the formatted string.
*/
static const string formatInt(size_t value);
/**
* Remove and return a certain value from a map.
* @param row the map to remove the value from.
@@ -709,7 +717,7 @@ class DataFieldTemplates : public MappedFileReader {
result_t add(const DataField* field, string name = "", bool replace = false);
// @copydoc
result_t getFieldMap(vector<string>& row, string& errorDescription) const override;
result_t getFieldMap(vector<string>& row, string& errorDescription, const string preferLanguage) const override;
// @copydoc
result_t addFromFile(map<string, string>& row, vector< map<string, string> >& subRows,
+22 -1
View File
@@ -202,6 +202,25 @@ bool FileReader::splitFields(istream& ifs, vector<string>& row, unsigned int& li
}
string MappedFileReader::normalizeLanguage(string lang) {
tolower(lang);
if (lang.size() > 2) {
size_t pos = lang.find('.');
if (pos == string::npos) {
pos = lang.size();
}
size_t strip = lang.find('_');
if (strip == string::npos || strip > pos) {
strip = pos;
}
if (strip > 2) {
strip = 2;
}
lang = lang.substr(0, strip);
}
return lang;
}
result_t MappedFileReader::readFromFile(const string filename, string& errorDescription, bool verbose,
map<string, string>* defaults, size_t* hash, size_t* size, time_t* time) {
m_mutex.lock();
@@ -223,7 +242,7 @@ result_t MappedFileReader::addFromFile(vector<string>& row, string& errorDescrip
const string filename, unsigned int lineNo) {
result_t result;
if (lineNo == 1) { // first line defines column names
result = getFieldMap(row, errorDescription);
result = getFieldMap(row, errorDescription, m_preferLanguage);
if (result != RESULT_OK) {
return result;
}
@@ -271,6 +290,8 @@ result_t MappedFileReader::addFromFile(vector<string>& row, string& errorDescrip
columnName = columnName.substr(1);
lastRepeatStart = colNameIdx;
empty = true;
} else if (columnName == SKIP_COLUMN) {
continue;
}
string value = row[colIdx];
empty &= value.empty();
+20 -2
View File
@@ -58,6 +58,8 @@ using std::mutex;
/** the separator character used between multiple values (in CSV only). */
#define VALUE_SEPARATOR ';'
/** special marker string for skipping columns in @a MappedFileReader. */
static const string SKIP_COLUMN = "\b";
/**
* An abstract class that support reading definitions from a file.
@@ -159,8 +161,11 @@ class MappedFileReader : public FileReader {
/**
* Constructor.
* @param supportsDefaults whether this instance supports rows with defaults (starting with a star).
* @param preferLanguage the preferred language code, or empty.
*/
explicit MappedFileReader(bool supportsDefaults) : FileReader(), m_supportsDefaults(supportsDefaults) {}
explicit MappedFileReader(bool supportsDefaults, const string preferLanguage = "")
: FileReader(), m_supportsDefaults(supportsDefaults), m_preferLanguage(normalizeLanguage(preferLanguage)) {
}
/**
* Destructor.
@@ -171,6 +176,13 @@ class MappedFileReader : public FileReader {
m_lastSubDefaults.clear();
}
/**
* Normalize the language string to a lower case, max. 2 characters long language code.
* @param lang the language string to normalize.
* @return the normalized language code.
*/
static string normalizeLanguage(string lang);
// @copydoc
result_t readFromFile(const string filename, string& errorDescription, bool verbose = false,
map<string, string>* defaults = NULL, size_t* hash = NULL, size_t* size = NULL, time_t* time = NULL) override;
@@ -196,10 +208,13 @@ class MappedFileReader : public FileReader {
/**
* Get the field mapping from the given first line.
* @param row the first line from which to extract the field mapping, or empty to use the default mapping.
* Columns set to @a SKIP_COLUMN are skipped in @a addFromFile(). Columns starting with a "*" mark the beginning of
* a repeated sub row.
* @param errorDescription a string in which to store the error description in case of error.
* @param preferLanguage the preferred language code (up to 2 characters), or empty.
* @return @a RESULT_OK on success, or an error code.
*/
virtual result_t getFieldMap(vector<string>& row, string& errorDescription) const = 0;
virtual result_t getFieldMap(vector<string>& row, string& errorDescription, const string preferLanguage) const = 0;
/**
* Add a default row that was read from a file.
@@ -253,6 +268,9 @@ class MappedFileReader : public FileReader {
/** whether this instance supports rows with defaults (starting with a star). */
const bool m_supportsDefaults;
/** the preferred language code (up to 2 characters), or empty. */
const string m_preferLanguage;
/** a @a mutex for access to defaults. */
mutex m_mutex;
+66 -29
View File
@@ -84,9 +84,10 @@ extern DataFieldTemplates* getTemplates(const string filename);
/**
* Get the normalized message field name for the given name.
* @param name the input field name.
* @param supportsLanguage set to true when the field supports multiple language.
* @return the normalized message field name, or empty if unknown.
*/
string getMessageFieldName(const string name) {
string getMessageFieldName(const string name, bool& supportsLanguage) {
if (name.find("type") != string::npos) {
return "type";
}
@@ -96,6 +97,7 @@ string getMessageFieldName(const string name) {
if (name.find("name") != string::npos) {
return "name";
}
supportsLanguage = true;
if (name.find("comment") == 0) {
return "comment";
}
@@ -1775,7 +1777,7 @@ result_t MessageMap::add(Message* message, bool storeByName) {
return RESULT_OK;
}
result_t MessageMap::getFieldMap(vector<string>& row, string& errorDescription) const {
result_t MessageMap::getFieldMap(vector<string>& row, string& errorDescription, const string preferLanguage) const {
// type (r[1-9];w;u),circuit,name,[comment],[QQ],ZZ,PBSB,[ID],field1,part (m/s),datatypes/templates,divider/values,
// unit,comment
// minimum: type,name,PBSB,field,datatype
@@ -1786,37 +1788,72 @@ result_t MessageMap::getFieldMap(vector<string>& row, string& errorDescription)
return RESULT_OK;
}
bool inDataFields = false;
map<string, string> seen;
for (auto &name : row) {
string useName = name;
tolower(useName);
map<string, size_t> seen;
for (size_t col = 0; col < row.size(); col++) {
string &name = row[col];
string lowerName = name;
tolower(lowerName);
trim(lowerName);
if (lowerName.empty()) {
errorDescription = "missing name in column " + AttributedItem::formatInt(col);
return RESULT_ERR_INVALID_ARG;
}
bool supportsLang = false, toDataFields = false;
string useName;
if (inDataFields) {
string chkName = getDataFieldName(useName);
if (!chkName.empty()) {
useName = chkName;
if (seen.find(useName) != seen.end()) {
if (seen.find("name") == seen.end() || seen.find("type") == seen.end()) {
errorDescription = "missing field name/type as of already seen "+useName;
return RESULT_ERR_EOF; // require at least name and type
}
seen.clear();
useName = getDataFieldName(lowerName, supportsLang);
} else {
useName = getMessageFieldName(lowerName, supportsLang);
if (useName.empty()) {
useName = getDataFieldName(lowerName, supportsLang);
toDataFields = !useName.empty();
}
}
bool unknown = useName.empty();
size_t langPos = supportsLang ? lowerName.find_last_of('.') : string::npos;
map<string, size_t>::iterator previous;
if (langPos == lowerName.length()-3) {
string lang = lowerName.substr(langPos+1);
if (unknown) {
useName = lowerName.substr(0, langPos);
}
previous = seen.find(useName);
if (previous != seen.end()) {
if (lang != preferLanguage) {
// skip this column
name = SKIP_COLUMN;
continue;
}
// replace previous
row[previous->second] = SKIP_COLUMN;
seen.erase(useName);
previous = seen.end();
}
} else {
string chkName = getMessageFieldName(useName);
if (!chkName.empty() && (chkName != "name" || seen.find("name") == seen.end())) {
useName = chkName;
} else {
chkName = getDataFieldName(useName);
if (!chkName.empty()) {
useName = chkName;
if (seen.find("type") == seen.end() || seen.find("name") == seen.end() || seen.find("pbsb") == seen.end()) {
errorDescription = "missing message name/type/pbsb";
return RESULT_ERR_EOF; // require at least type, name, and pbsb
}
inDataFields = true;
seen.clear();
if (unknown) {
useName = lowerName;
}
previous = seen.find(useName);
}
if (inDataFields) {
if (!unknown && previous != seen.end()) {
if (seen.find("name") == seen.end() || seen.find("type") == seen.end()) {
errorDescription = "missing field name/type as of already seen "+useName;
return RESULT_ERR_EOF; // require at least name and type
}
seen.clear();
}
} else {
/*if (!unknown && (useName != "name" || seen.find("name") == seen.end())) {
// keep first name for message
} else {*/
if (toDataFields) {
if (seen.find("type") == seen.end() || seen.find("name") == seen.end() || seen.find("pbsb") == seen.end()) {
errorDescription = "missing message name/type/pbsb";
return RESULT_ERR_EOF; // require at least type, name, and pbsb
}
inDataFields = true;
seen.clear();
}
if (!inDataFields && seen.find(useName) != seen.end()) {
errorDescription = "duplicate message " + useName;
@@ -1828,7 +1865,7 @@ result_t MessageMap::getFieldMap(vector<string>& row, string& errorDescription)
} else {
name = useName;
}
seen[useName] = useName;
seen[useName] = col;
}
if (inDataFields) {
if (seen.find("name") == seen.end() || seen.find("type") == seen.end()) {
+3 -2
View File
@@ -1213,8 +1213,9 @@ class MessageMap : public MappedFileReader {
/**
* Construct a new instance.
* @param addAll whether to add all messages, even if duplicate.
* @param preferLanguage the preferred language to use, or empty.
*/
explicit MessageMap(const bool addAll = false) : MappedFileReader::MappedFileReader(true),
explicit MessageMap(const bool addAll = false, const string preferLanguage = "") : MappedFileReader::MappedFileReader(true),
m_addAll(addAll), m_additionalScanMessages(false), m_maxIdLength(0), m_maxBroadcastIdLength(0),
m_messageCount(0), m_conditionalMessageCount(0), m_passiveMessageCount(0) {
m_scanMessage = Message::createScanMessage();
@@ -1246,7 +1247,7 @@ class MessageMap : public MappedFileReader {
result_t add(Message* message, bool storeByName = true);
// @copydoc
result_t getFieldMap(vector<string>& row, string& errorDescription) const override;
result_t getFieldMap(vector<string>& row, string& errorDescription, const string preferLanguage) const override;
// @copydoc
result_t addDefaultFromFile(map<string, string>& row, vector< map<string, string> >& subRows,
+1 -1
View File
@@ -53,7 +53,7 @@ class TestReader : public MappedFileReader {
TestReader(DataFieldTemplates* templates, bool isSet, bool isMasterDest)
: MappedFileReader::MappedFileReader(true), m_templates(templates), m_isSet(isSet), m_isMasterDest(isMasterDest),
m_fields(NULL) {}
result_t getFieldMap(vector<string>& row, string& errorDescription) const override {
result_t getFieldMap(vector<string>& row, string& errorDescription, const string preferLanguage) const override {
if (row.empty()) {
row.push_back("*name");
row.push_back("part");
+28 -21
View File
@@ -49,13 +49,13 @@ void verify(bool expectFailMatch, string type, string input,
string resultlines[][3] = {
{"col 1", "col 2", "col 3"},
{"line 2 col 1", "line 2 col 2", "line 2 \"col 3\";default of col 3"},
{"line 2 col 1 de", "line 2 col 2", "line 2 \"col 3\";default of col 3"},
{"", "", ""},
{"line 4 col 1", "line 4 col 2 part 1;line 4 col 2 part 2", "line 4 col 3;default of col 3"},
{"line 4 col 1 de", "line 4 col 2 part 1;line 4 col 2 part 2", "line 4 col 3;default of col 3"},
{"", "", ""},
{"line 6 col 1", "", "line 6 col 3;default of col 3"},
{"line 6 col 1 de", "", "line 6 col 3;default of col 3"},
{"", "", ""},
{"line 8 col 1", "line 8 col 2 part 1;line 8 col 2 part 2", "line 8 col 3;default of col 3"},
{"line 8 col 1 de", "line 8 col 2 part 1;line 8 col 2 part 2", "line 8 col 3;default of col 3"},
};
string resultsublines[][2][4] = {
@@ -81,14 +81,20 @@ class NoopReader : public FileReader {
class TestReader : public MappedFileReader {
public:
TestReader(size_t expectedCols) : MappedFileReader::MappedFileReader(false), m_expectedCols(expectedCols) {}
result_t getFieldMap(vector<string>& row, string& errorDescription) const override {
if (row.size() == m_expectedCols) {
TestReader(size_t expectedCols, size_t langCols)
: MappedFileReader::MappedFileReader(false, ""), m_expectedCols(expectedCols), m_langCols(langCols) {}
result_t getFieldMap(vector<string>& row, string& errorDescription, const string preferLanguage) const override {
if (row.size() == m_expectedCols+m_langCols) {
cout << "get field map: split OK" << endl;
if (m_langCols == 1) {
row[0] = SKIP_COLUMN;
size_t pos = row[1].find_last_of('.');
row[1] = row[1].substr(0, pos);
}
return RESULT_OK;
}
cout << "get field map: error got " << static_cast<unsigned>(row.size()) << " columns, expected " <<
static_cast<unsigned>(m_expectedCols) << endl;
static_cast<unsigned>(m_expectedCols+m_langCols) << endl;
return RESULT_ERR_EOF;
}
result_t addFromFile(map<string, string>& row, vector< map<string, string> >& subRows,
@@ -179,6 +185,7 @@ class TestReader : public MappedFileReader {
}
private:
size_t m_expectedCols;
size_t m_langCols;
};
@@ -203,17 +210,17 @@ int main(int argc, char** argv) {
}
baseLine = __LINE__+1;
istringstream ifs(
"col 1,col 2,col 3\n"
"line 2 col 1,\"line 2 col 2\",\"line 2 \"\"col 3\"\";default of col 3\"\n"
"line 4 col 1,\"line 4 col 2 part 1\n"
"col 1.en,col 1.de,col 2,col 3\n"
"line 2 col 1 en,line 2 col 1 de,\"line 2 col 2\",\"line 2 \"\"col 3\"\";default of col 3\"\n"
"line 4 col 1 en,line 4 col 1 de,\"line 4 col 2 part 1\n"
"line 4 col 2 part 2\",line 4 col 3;default of col 3\n"
",,,\n"
"line 6 col 1,,line 6 col 3;default of col 3\n"
"line 8 col 1,\"line 8 col 2 part 1;\n"
"line 6 col 1 en,line 6 col 1 de,,line 6 col 3;default of col 3\n"
"line 8 col 1 en,line 8 col 1 de,\"line 8 col 2 part 1;\n"
"line 8 col 2 part 2\",line 8 col 3;default of col 3\n"
);
size_t hash = 0, size = 0, expectHash = 0xfd58724c2984595d, expectSize = 301;
TestReader reader{3};
size_t hash = 0, size = 0, expectHash = 0x5e5e086475ab3bd9, expectSize = 389;
TestReader reader{3, 1};
unsigned int lineNo = 0;
vector<string> row;
string errorDescription;
@@ -242,16 +249,16 @@ int main(int argc, char** argv) {
baseLine = __LINE__+1;
ifs.str(
"col 1,col 2,col 3,*subcol 1,subcol 2,*subcol 2,subcol 3\n"
"line 2 col 1,\"line 2 col 2\",\"line 2 \"\"col 3\"\"\",line 2 subcol 1,line 2 subcol 2,line 2 subcol 2,line 2 subcol 3\n"
"line 4 col 1,\"line 4 col 2 part 1\n"
"line 2 col 1 de,\"line 2 col 2\",\"line 2 \"\"col 3\"\"\",line 2 subcol 1,line 2 subcol 2,line 2 subcol 2,line 2 subcol 3\n"
"line 4 col 1 de,\"line 4 col 2 part 1\n"
"line 4 col 2 part 2\",line 4 col 3,line 4 subcol 1,line 4 subcol 2,line 4 subcol 2,line 4 subcol 3\n"
",,,\n"
"line 6 col 1,,line 6 col 3,line 6 subcol 1,line 6 subcol 2,line 6 subcol 2,line 6 subcol 3\n"
"line 8 col 1,\"line 8 col 2 part 1;\n"
"line 6 col 1 de,,line 6 col 3,line 6 subcol 1,line 6 subcol 2,line 6 subcol 2,line 6 subcol 3\n"
"line 8 col 1 de,\"line 8 col 2 part 1;\n"
"line 8 col 2 part 2\",line 8 col 3,line 8 subcol 1,line 8 subcol 2,line 8 subcol 2,line 8 subcol 3\n"
);
hash = 0, size = 0, expectHash = 0xec31914e32c6fb6f, expectSize = 527;
TestReader reader2{7};
hash = 0, size = 0, expectHash = 0x9a675169d5837bb5, expectSize = 539;
TestReader reader2{7, 0};
lineNo = 0;
map<string, string> defaults;
reader2.getDefaults()[""]["col 3"] = ";default of col 3";