LUMIERA.clone/src/lib/stat/csv.hpp

234 lines
6.7 KiB
C++

/*
CSV.hpp - Parser and Encoder for CSV data
Copyright (C) Lumiera.org
2022, Hermann Vosseler <Ichthyostega@web.de>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
/** @file csv.hpp
** Encoding and decoding of data into CSV format.
** The sequence of values transformed here is part of a data table, with columns
** holding data of various primitive value types; persisted CSV data is human readable,
** can be checked into Git and loaded into various spreadsheet and statistics applications.
**
** # CSV Format
** Even while there is a standard defined in [RFC 4180], a plethora of format variations
** can be found _in the wild._ Since the primary purpose of this implementation is _to read
** back our own data,_ by deliberate choice only one single form of CSV is accepted.
** - first line is a header line and used to verify the number of columns
** - one record per line, embedded line breaks prohibited
** - fields separated by comma, semicolon tolerated
** - fields are trimmed and may be empty
** - a field may be double quoted
** - only quoted fields may contain whitespace or comma
** - no escaping of quotes, i.e. no quotes within quotes
** [RFC 4180]: https://datatracker.ietf.org/doc/html/rfc4180
** @see lib::stat::DataFile
**
*/
#ifndef LIB_STAT_CSV_H
#define LIB_STAT_CSV_H
#include "lib/error.hpp"
#include "lib/null-value.hpp"
#include "lib/format-string.hpp"
#include "lib/format-obj.hpp"
#include "lib/regex.hpp"
#include <limits>
#include <string>
namespace lib {
namespace stat {
namespace error = lumiera::error;
using util::_Fmt;
using util::toString;
using std::string;
using std::regex;
namespace { // Implementation details...
const string MATCH_SINGLE_TOKEN { R"~(([^,;"\s]*)\s*)~"};
const string MATCH_QUOTED_TOKEN { R"~("([^"]*)"\s*)~" };
const string MATCH_DELIMITER { R"~((?:^|,|;)\s*)~" };
const regex ACCEPT_FIELD{ MATCH_DELIMITER + "(?:"+ MATCH_QUOTED_TOKEN +"|"+ MATCH_SINGLE_TOKEN +")"
, regex::optimize};
template<typename VAL>
inline string
format4Csv (VAL const& val)
{
if constexpr (std::is_floating_point_v<VAL>)
return util::showDecimal (val);
// standard textual rendering
auto res = util::toString (val);
if constexpr (std::is_arithmetic_v<VAL>)
return res; // includes bool
else
return '"'+res+'"';
}
}//(End)Implementation
/**
* Format and append a data value to a CSV string representation
*/
template<typename VAL>
inline void
appendCsvField (string& csv, VAL const& val)
{
csv += (0 == csv.length()? "":",")
+ format4Csv(val);
}
/** parse string representation into typed value */
template<typename TAR>
inline TAR
parseAs (string const& encodedVal)
{
std::istringstream converter{encodedVal};
TAR value;
converter >> value;
if (converter.fail())
throw error::Invalid{_Fmt{"unable to parse \"%s\""} % encodedVal};
return value;
}
template<>
inline bool
parseAs(string const& encodedBool)
{
return util::boolVal(encodedBool);
}
template<>
inline string
parseAs(string const& string)
{
return string; // pass-through (even if empty)
}
/**
* Parser to split one line of CSV data into fields.
* @remarks iterator-like throw-away object
* - the `bool` evaluation indicates more fields to extract
* - dereference to get the field as string
* - increment to move to the next field
* @throws error::Invalid on CSV format violation
*/
class CsvLine
: public util::RegexSearchIter
{
string const& line_{};
size_t field_{0};
size_t pos_{0};
util::RegexSearchIter const& curr() const { return *this; }
util::RegexSearchIter end() const { return util::RegexSearchIter{}; }
public:
CsvLine()
: line_{lib::NullValue<string>::get()}
{ }
CsvLine (string& line) // NOTE: string and reg-exp must exist elsewhere
: RegexSearchIter(line, ACCEPT_FIELD)
, line_{line}
{ }
explicit operator bool() const
{
return isValid();
}
ENABLE_USE_IN_STD_RANGE_FOR_LOOPS (CsvLine);
string operator*() const
{
if (not isValid()) fail();
auto& mat = *curr();
return mat[2].matched? mat[2]
: mat[1];
}
void
operator++()
{
if (not isValid())
fail();
pos_ = curr()->position() + curr()->length();
util::RegexSearchIter::operator ++();
if (pos_ < line_.length() and not isValid())
fail ();
++field_;
}
size_t
getParsedFieldCnt()
{
return field_;
}
bool
isValid() const
{
return curr() != end()
and pos_ == size_t(curr()->position())
and not curr()->empty();
}
bool
isParseFail() const
{
return curr() != end()
and not isValid();
}
void
fail() const
{
if (curr() == end())
if (pos_ >= line_.length())
throw error::Invalid{_Fmt{"Only %d data fields. Line:%s"}
% field_ % line_};
else
throw error::Invalid{_Fmt{"Garbage after last field. Line:%s|↯|%s"}
% line_.substr(0,pos_) % line_.substr(pos_)};
else
if (pos_ != size_t(curr()->position()))
throw error::Invalid{_Fmt{"Garbage before field(%d):%s|↯|%s"}
% (field_+1)
% line_.substr(0,pos_) % line_.substr(pos_)};
throw error::Invalid{_Fmt{"CSV parse floundered. Line:%s"} % line_};
}
};
}} // namespace lib::stat
#endif /*LIB_STAT_CSV_H*/