diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 81a1e3ba34..ae263f9e07 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -244,6 +244,7 @@ target_link_libraries( kicommon fmt::fmt CURL::libcurl picosha2 + rapidcsv ${ZSTD_LIBRARY} ${wxWidgets_LIBRARIES} ${LIBGIT2_LIBRARIES} @@ -556,6 +557,8 @@ set( COMMON_IO_SRCS # EasyEDA pro io/easyedapro/easyedapro_parser.cpp io/easyedapro/easyedapro_import_utils.cpp + + io/csv.cpp ) set ( COMMON_TRANSLINE_CALCULATION_SRCS diff --git a/common/clipboard.cpp b/common/clipboard.cpp index 4463d157b5..fe891e51e6 100644 --- a/common/clipboard.cpp +++ b/common/clipboard.cpp @@ -26,6 +26,12 @@ #include #include #include +#include +#include + +#include + +#include bool SaveClipboard( const std::string& aTextUTF8 ) @@ -106,4 +112,66 @@ std::unique_ptr GetImageFromClipboard() } return image; -} \ No newline at end of file +} + + +bool SaveTabularDataToClipboard( const std::vector>& aData ) +{ + wxLogNull doNotLog; // disable logging of failed clipboard actions + + if( wxTheClipboard->Open() ) + { + wxDataObjectComposite* data = new wxDataObjectComposite(); + + // Set plain text CSV + { + wxStringOutputStream os; + CSV_WRITER writer( os ); + writer.WriteLines( aData ); + + data->Add( new wxTextDataObject( os.GetString() ), true ); + } + + // At this point, it would be great if we could add some format that spreadsheet + // programs can understand without asking the user for options: perhaps SYLK or DIF. + // But it doesn't seem like WX allows to put arbitrary MIME types on the clipboard, + // even with wxCustomDataObject( wxDataFormat( "mime/type" ) ), which just ends up as + // wxDF_PRIVATE, and wxDF_SYLK/DIF aren't mapped on GTK. + + wxTheClipboard->SetData( data ); + wxTheClipboard->Flush(); // Allow data to be available after closing KiCad + wxTheClipboard->Close(); + + return true; + } + + return false; +} + + +bool GetTabularDataFromClipboard( std::vector>& aData ) +{ + // Again, it would be ideal if we could detect a spreadsheet mimetype here, + // but WX doesn't seem to do that on Linux, at least. + + bool ok = false; + + // First try for text data + if( wxTheClipboard->Open() ) + { + if( wxTheClipboard->IsSupported( wxDF_TEXT ) ) + { + wxTextDataObject data; + if( wxTheClipboard->GetData( data ) ) + { + ok = AutoDecodeCSV( data.GetText(), aData ); + } + } + + // We could also handle .csv wxDF_FILENAMEs here + + wxTheClipboard->Close(); + } + + return ok; +} diff --git a/common/io/csv.cpp b/common/io/csv.cpp new file mode 100644 index 0000000000..f09973216d --- /dev/null +++ b/common/io/csv.cpp @@ -0,0 +1,98 @@ + + +#include "csv.h" + +#include + + +#include + + +CSV_WRITER::CSV_WRITER( wxOutputStream& aStream ) : + m_stream( aStream ), m_delimiter( wxT( "," ) ), m_quote( wxT( "\"" ) ), m_lineTerminator( wxT( "\n" ) ), + m_escape( wxEmptyString ) +{ +} + + +void CSV_WRITER::WriteLines( const std::vector>& aRows ) +{ + for( const auto& row : aRows ) + { + WriteLine( row ); + } +} + + +void CSV_WRITER::WriteLine( const std::vector& cols ) +{ + wxString line; + for( size_t i = 0; i < cols.size(); ++i ) + { + wxString colVal = cols[i]; + + if( i > 0 ) + line += m_delimiter; + + double test; + if( !colVal.ToDouble( &test ) ) + { + bool useEscape = m_escape.size(); + + if( useEscape ) + { + colVal.Replace( m_quote, m_escape + m_quote, true ); + } + else + { + colVal.Replace( m_quote, m_quote + m_quote, true ); + } + + colVal = m_quote + colVal + m_quote; + } + + line += colVal; + } + line += m_lineTerminator; + + m_stream.Write( line.c_str(), line.length() ); +} + + +bool AutoDecodeCSV( const wxString& aInput, std::vector>& aData ) +{ + // Read the first line to determine the delimiter + bool trimCells = true; + bool skipCommentLines = true; + bool skipEmptyLines = true; + char commentPrefix = '#'; + char delimiter = ','; + + // Assume if we find a tab, we are dealing with a TSV file + if( aInput.find( '\t' ) != std::string::npos ) + { + delimiter = '\t'; + } + + std::istringstream inputStream( aInput.ToStdString() ); + + rapidcsv::Document doc( inputStream, rapidcsv::LabelParams( -1, -1 ), + rapidcsv::SeparatorParams( delimiter, trimCells ), rapidcsv::ConverterParams(), + rapidcsv::LineReaderParams( skipCommentLines, commentPrefix, skipEmptyLines ) ); + + // Read the data into aData + aData.clear(); + + for( size_t i = 0; i < doc.GetRowCount(); ++i ) + { + std::vector& row = aData.emplace_back(); + for( size_t j = 0; j < doc.GetColumnCount(); ++j ) + { + std::string cell = doc.GetCell( j, i ); + row.emplace_back( cell ); + } + } + + // Anything in the first row? + return aData[0].size() > 0; +} diff --git a/common/io/csv.h b/common/io/csv.h new file mode 100644 index 0000000000..cdc98656aa --- /dev/null +++ b/common/io/csv.h @@ -0,0 +1,78 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright The KiCad Developers, see AUTHORS.txt for contributors. + * + * 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 3 + * 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, you may find one here: + * https://www.gnu.org/licenses/gpl-3.0.html + * or you may search the http://www.gnu.org website for the version 3 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#pragma once + +#include +#include + +#include + + +class CSV_WRITER +{ +public: + CSV_WRITER( wxOutputStream& aStream ); + + /** + * Write a single row to the stream. + */ + void WriteLine( const std::vector& aCols ); + /** + * Write a vector of rows to the stream. + * + * @param cols The rows to write. + */ + void WriteLines( const std::vector>& aRows ); + + void SetDelimiter( const wxString& aDelimiter ) + { + m_delimiter = aDelimiter; + } + + /** + * Set the delimiter escape char. If set to wxEmptyString (default), the + * delimiter is doubled for escaping. + */ + void SetEscape( const wxString& aEscape ) + { + m_escape = aEscape; + } + +private: + wxOutputStream& m_stream; + wxString m_delimiter; + wxString m_quote; + wxString m_lineTerminator; + wxString m_escape; +}; + + +/** + * Try to guess the format of a T/CSV file and decode it into aData. + * + * This can handle the most common cases of CSV files and TSV files + * (double/single quoted strings, commas or tabs as delimiters, but it's + * not completely foolproof). + */ +bool AutoDecodeCSV( const wxString& aInput, std::vector>& aData ); diff --git a/common/wildcards_and_files_ext.cpp b/common/wildcards_and_files_ext.cpp index a63853d4ca..ab0d45cc2c 100644 --- a/common/wildcards_and_files_ext.cpp +++ b/common/wildcards_and_files_ext.cpp @@ -198,6 +198,7 @@ const std::string FILEEXT::JpegFileExtension( "jpg" ); const std::string FILEEXT::TextFileExtension( "txt" ); const std::string FILEEXT::MarkdownFileExtension( "md" ); const std::string FILEEXT::CsvFileExtension( "csv" ); +const std::string FILEEXT::TsvFileExtension( "tsv" ); const std::string FILEEXT::XmlFileExtension( "xml" ); const std::string FILEEXT::JsonFileExtension( "json" ); const std::string FILEEXT::PythonFileExtension( "py" ); @@ -415,6 +416,12 @@ wxString FILEEXT::CsvFileWildcard() } +wxString FILEEXT::CsvTsvFileWildcard() +{ + return _( "CSV/TSV Files" ) + AddFileExtListToFilter( { CsvFileExtension, TsvFileExtension } ); +} + + wxString FILEEXT::PdfFileWildcard() { return _( "Portable document format files" ) + AddFileExtListToFilter( { "pdf" } ); diff --git a/eeschema/dialogs/dialog_lib_edit_pin_table.cpp b/eeschema/dialogs/dialog_lib_edit_pin_table.cpp index 5b60bbf644..3ad20d0d90 100644 --- a/eeschema/dialogs/dialog_lib_edit_pin_table.cpp +++ b/eeschema/dialogs/dialog_lib_edit_pin_table.cpp @@ -22,23 +22,33 @@ */ #include "dialog_lib_edit_pin_table.h" + +#include +#include +#include +#include + #include "grid_tricks.h" +#include #include #include #include "pgm_base.h" #include #include +#include #include #include #include +#include #include #include #include #include #include #include +#include #include -#include +#include #include #define UNITS_ALL _( "ALL" ) @@ -47,6 +57,266 @@ #define DEMORGAN_ALT _( "Alternate" ) +static wxString GetPinTableColLabel( int aCol ) +{ + switch( aCol ) + { + case COL_PIN_COUNT: return _( "Count" ); + case COL_NUMBER: return _( "Number" ); + case COL_NAME: return _( "Name" ); + case COL_TYPE: return _( "Electrical Type" ); + case COL_SHAPE: return _( "Graphic Style" ); + case COL_ORIENTATION: return _( "Orientation" ); + case COL_NUMBER_SIZE: return _( "Number Text Size" ); + case COL_NAME_SIZE: return _( "Name Text Size" ); + case COL_LENGTH: return _( "Length" ); + case COL_POSX: return _( "X Position" ); + case COL_POSY: return _( "Y Position" ); + case COL_VISIBLE: return _( "Visible" ); + case COL_UNIT: return _( "Unit" ); + case COL_DEMORGAN: return _( "De Morgan" ); + default: wxFAIL; return wxEmptyString; + } +} + + +static COL_ORDER GetColTypeForString( const wxString& aStr ) +{ + for( int i = 0; i < COL_COUNT; i++ ) + { + if( GetPinTableColLabel( i ).IsSameAs( aStr, false ) ) + return (COL_ORDER) i; + } + return COL_COUNT; +} + +/** + * Class that handles conversion of various pin data fields into strings for display in the + * UI or serialisation to formats like CSV. + */ +class PIN_INFO_FORMATTER +{ +public: + enum class BOOL_FORMAT + { + ZERO_ONE, + TRUE_FALSE, + }; + + PIN_INFO_FORMATTER( UNITS_PROVIDER& aUnitsProvider, bool aIncludeUnits, BOOL_FORMAT aBoolFormat ) : + m_unitsProvider( aUnitsProvider ), + m_includeUnits( aIncludeUnits ), + m_boolFormat( aBoolFormat ) + { + } + + wxString Format( const SCH_PIN& aPin, int aFieldId ) const + { + wxString val; + switch( aFieldId ) + { + case COL_NAME: + val << aPin.GetName(); + break; + case COL_NUMBER: + val << aPin.GetNumber(); + break; + case COL_TYPE: + val << PinTypeNames()[static_cast( aPin.GetType() )]; + break; + case COL_SHAPE: + val << PinShapeNames()[static_cast( aPin.GetShape() )]; + break; + case COL_ORIENTATION: + { + const int index = PinOrientationIndex( aPin.GetOrientation() ); + if( index >= 0) + val << PinOrientationNames()[ index ]; + break; + } + case COL_NUMBER_SIZE: + val << m_unitsProvider.StringFromValue( aPin.GetNumberTextSize(), m_includeUnits ); + break; + case COL_NAME_SIZE: + val << m_unitsProvider.StringFromValue( aPin.GetNameTextSize(), m_includeUnits ); + break; + case COL_LENGTH: + val << m_unitsProvider.StringFromValue( aPin.GetLength(), m_includeUnits ); + break; + case COL_POSX: + val << m_unitsProvider.StringFromValue( aPin.GetPosition().x, m_includeUnits ); + break; + case COL_POSY: + val << m_unitsProvider.StringFromValue( aPin.GetPosition().y, m_includeUnits ); + break; + case COL_VISIBLE: + val << stringFromBool( aPin.IsVisible() ); + break; + case COL_UNIT: + if( aPin.GetUnit() ) + val << LIB_SYMBOL::LetterSubReference( aPin.GetUnit(), 'A' ); + else + val << UNITS_ALL; + break; + case COL_DEMORGAN: + switch( aPin.GetBodyStyle() ) + { + case BODY_STYLE::BASE: + val << DEMORGAN_STD; + break; + case BODY_STYLE::DEMORGAN: + val << DEMORGAN_ALT; + break; + default: + val << DEMORGAN_ALL; + break; + } + break; + default: + wxFAIL_MSG( wxString::Format( "Invalid field id %d", aFieldId ) ); + break; + } + return val; + } + + /** + * Update the pin from the given col/string. + * + * How much this should follow the format is debatable, but for now it's fairly permissive + * (e.g. bools import as 0/1 and no/yes). + */ + void UpdatePin( SCH_PIN& aPin, const wxString& aValue, int aFieldId, const LIB_SYMBOL& aSymbol ) const + { + switch( aFieldId ) + { + case COL_NUMBER: + aPin.SetNumber( aValue ); + break; + + case COL_NAME: + aPin.SetName( aValue ); + break; + + case COL_TYPE: + if( PinTypeNames().Index( aValue, false ) != wxNOT_FOUND ) + aPin.SetType( (ELECTRICAL_PINTYPE) PinTypeNames().Index( aValue ) ); + break; + + case COL_SHAPE: + if( PinShapeNames().Index( aValue, false ) != wxNOT_FOUND ) + aPin.SetShape( (GRAPHIC_PINSHAPE) PinShapeNames().Index( aValue ) ); + break; + + case COL_ORIENTATION: + if( PinOrientationNames().Index( aValue, false ) != wxNOT_FOUND ) + aPin.SetOrientation( (PIN_ORIENTATION) PinOrientationNames().Index( aValue ) ); + break; + + case COL_NUMBER_SIZE: + aPin.SetNumberTextSize( m_unitsProvider.ValueFromString( aValue ) ); + break; + + case COL_NAME_SIZE: + aPin.SetNameTextSize( m_unitsProvider.ValueFromString( aValue ) ); + break; + + case COL_LENGTH: + aPin.ChangeLength( m_unitsProvider.ValueFromString( aValue ) ); + break; + + case COL_POSX: + aPin.SetPosition( VECTOR2I( m_unitsProvider.ValueFromString( aValue ), + aPin.GetPosition().y ) ); + break; + + case COL_POSY: + aPin.SetPosition( VECTOR2I( aPin.GetPosition().x, m_unitsProvider.ValueFromString( aValue ) ) ); + break; + + case COL_VISIBLE: + aPin.SetVisible(boolFromString( aValue )); + break; + + case COL_UNIT: + if( aValue == UNITS_ALL ) + { + aPin.SetUnit( 0 ); + } + else + { + for( int i = 1; i <= aSymbol.GetUnitCount(); i++ ) + { + if( aValue == LIB_SYMBOL::LetterSubReference( i, 'A' ) ) + { + aPin.SetUnit( i ); + break; + } + } + } + + break; + + case COL_DEMORGAN: + if( aValue == DEMORGAN_STD ) + aPin.SetBodyStyle( 1 ); + else if( aValue == DEMORGAN_ALT ) + aPin.SetBodyStyle( 2 ); + else + aPin.SetBodyStyle( 0 ); + break; + + default: + wxFAIL_MSG( wxString::Format( "Invalid field id %d", aFieldId ) ); + break; + } + } + +private: + wxString stringFromBool( bool aValue ) const + { + switch( m_boolFormat ) + { + case BOOL_FORMAT::ZERO_ONE: return aValue ? wxT( "1" ) : wxT( "0" ); + case BOOL_FORMAT::TRUE_FALSE: + return aValue ? _( "True" ) : _( "False" ); + // no default + } + wxCHECK_MSG( false, wxEmptyString, "Invalid BOOL_FORMAT" ); + } + + bool boolFromString( const wxString& aValue ) const + { + if( aValue == wxS( "1" ) ) + { + return true; + } + else if( aValue == wxS( "0" ) ) + { + return false; + } + else if( aValue.IsSameAs( _( "True" ), false ) ) + { + return true; + } + else if( aValue.IsSameAs( _( "False" ), false ) ) + { + return false; + } + else + { + wxFAIL_MSG( wxString::Format( "string '%s' can't be converted to boolean correctly, " + "it will have been perceived as FALSE", + aValue ) ); + return false; + } + } + + UNITS_PROVIDER& m_unitsProvider; + bool m_includeUnits; + BOOL_FORMAT m_boolFormat; +}; + + void getSelectedArea( WX_GRID* aGrid, int* aRowStart, int* aRowCount ) { wxGridCellCoordsArray topLeft = aGrid->GetSelectionBlockTopLeft(); @@ -114,24 +384,7 @@ public: wxString GetColLabelValue( int aCol ) override { - switch( aCol ) - { - case COL_PIN_COUNT: return _( "Count" ); - case COL_NUMBER: return _( "Number" ); - case COL_NAME: return _( "Name" ); - case COL_TYPE: return _( "Electrical Type" ); - case COL_SHAPE: return _( "Graphic Style" ); - case COL_ORIENTATION: return _( "Orientation" ); - case COL_NUMBER_SIZE: return _( "Number Text Size" ); - case COL_NAME_SIZE: return _( "Name Text Size" ); - case COL_LENGTH: return _( "Length" ); - case COL_POSX: return _( "X Position" ); - case COL_POSY: return _( "Y Position" ); - case COL_VISIBLE: return _( "Visible" ); - case COL_UNIT: return _( "Unit" ); - case COL_DEMORGAN: return _( "De Morgan" ); - default: wxFAIL; return wxEmptyString; - } + return GetPinTableColLabel( aCol ); } bool IsEmptyCell( int row, int col ) override @@ -163,87 +416,18 @@ public: if( pins.empty() ) return fieldValue; - for( SCH_PIN* pin : pins ) + PIN_INFO_FORMATTER formatter( *aParentFrame, true, PIN_INFO_FORMATTER::BOOL_FORMAT::ZERO_ONE ); + + for( const SCH_PIN* pin : pins ) { wxString val; - switch( aCol ) { case COL_PIN_COUNT: val << pins.size(); break; - - case COL_NUMBER: - val = pin->GetNumber(); - break; - - case COL_NAME: - val = pin->GetName(); - break; - - case COL_TYPE: - val = PinTypeNames()[static_cast( pin->GetType() )]; - break; - - case COL_SHAPE: - val = PinShapeNames()[static_cast( pin->GetShape() )]; - break; - - case COL_ORIENTATION: - if( PinOrientationIndex( pin->GetOrientation() ) >= 0 ) - val = PinOrientationNames()[ PinOrientationIndex( pin->GetOrientation() ) ]; - - break; - - case COL_NUMBER_SIZE: - val = aParentFrame->StringFromValue( pin->GetNumberTextSize(), true ); - break; - - case COL_NAME_SIZE: - val = aParentFrame->StringFromValue( pin->GetNameTextSize(), true ); - break; - - case COL_LENGTH: - val = aParentFrame->StringFromValue( pin->GetLength(), true ); - break; - - case COL_POSX: - val = aParentFrame->StringFromValue( pin->GetPosition().x, true ); - break; - - case COL_POSY: - val = aParentFrame->StringFromValue( pin->GetPosition().y, true ); - break; - - case COL_VISIBLE: - val = StringFromBool( pin->IsVisible() ); - break; - - case COL_UNIT: - if( pin->GetUnit() ) - val = LIB_SYMBOL::LetterSubReference( pin->GetUnit(), 'A' ); - else - val = UNITS_ALL; - - break; - - case COL_DEMORGAN: - switch( pin->GetBodyStyle() ) - { - case BODY_STYLE::BASE: - val = DEMORGAN_STD; - break; - case BODY_STYLE::DEMORGAN: - val = DEMORGAN_ALT; - break; - default: - val = DEMORGAN_ALL; - break; - } - break; - default: - wxFAIL; + val << formatter.Format( *pin, aCol ); break; } @@ -365,96 +549,11 @@ public: return; } + PIN_INFO_FORMATTER formatter( *m_frame, true, PIN_INFO_FORMATTER::BOOL_FORMAT::ZERO_ONE ); + for( SCH_PIN* pin : pins ) { - switch( aCol ) - { - case COL_NUMBER: - if( !m_pinTable->IsDisplayGrouped() ) - pin->SetNumber( value ); - - break; - - case COL_NAME: - pin->SetName( value ); - break; - - case COL_TYPE: - if( PinTypeNames().Index( value ) != wxNOT_FOUND ) - pin->SetType( (ELECTRICAL_PINTYPE) PinTypeNames().Index( value ) ); - - break; - - case COL_SHAPE: - if( PinShapeNames().Index( value ) != wxNOT_FOUND ) - pin->SetShape( (GRAPHIC_PINSHAPE) PinShapeNames().Index( value ) ); - - break; - - case COL_ORIENTATION: - if( PinOrientationNames().Index( value ) != wxNOT_FOUND ) - pin->SetOrientation( - PinOrientationCode( PinOrientationNames().Index( value ) ) ); - break; - - case COL_NUMBER_SIZE: - pin->SetNumberTextSize( m_frame->ValueFromString( value ) ); - break; - - case COL_NAME_SIZE: - pin->SetNameTextSize( m_frame->ValueFromString( value ) ); - break; - - case COL_LENGTH: - pin->ChangeLength( m_frame->ValueFromString( value ) ); - break; - - case COL_POSX: - pin->SetPosition( VECTOR2I( m_frame->ValueFromString( value ), - pin->GetPosition().y ) ); - break; - - case COL_POSY: - pin->SetPosition( VECTOR2I( pin->GetPosition().x, - m_frame->ValueFromString( value ) ) ); - break; - - case COL_VISIBLE: - pin->SetVisible(BoolFromString( value )); - break; - - case COL_UNIT: - if( value == UNITS_ALL ) - { - pin->SetUnit( 0 ); - } - else - { - for( int i = 1; i <= m_symbol->GetUnitCount(); i++ ) - { - if( value == LIB_SYMBOL::LetterSubReference( i, 'A' ) ) - { - pin->SetUnit( i ); - break; - } - } - } - - break; - - case COL_DEMORGAN: - if( value == DEMORGAN_STD ) - pin->SetBodyStyle( 1 ); - else if( value == DEMORGAN_ALT ) - pin->SetBodyStyle( 2 ); - else - pin->SetBodyStyle( 0 ); - break; - - default: - wxFAIL; - break; - } + formatter.UpdatePin( *pin, value, aCol, *m_symbol ); } m_edited = true; @@ -672,34 +771,6 @@ public: return m_edited; } -private: - static wxString StringFromBool( bool aValue ) - { - if( aValue ) - return wxT( "1" ); - else - return wxT( "0" ); - } - - static bool BoolFromString( wxString aValue ) - { - if( aValue == wxS( "1" ) ) - { - return true; - } - else if( aValue == wxS( "0" ) ) - { - return false; - } - else - { - wxFAIL_MSG( wxString::Format( "string '%s' can't be converted to boolean correctly, " - "it will have been perceived as FALSE", - aValue ) ); - return false; - } - } - private: SYMBOL_EDIT_FRAME* m_frame; @@ -719,6 +790,185 @@ private: }; +class PIN_TABLE_EXPORT +{ +public: + PIN_TABLE_EXPORT( UNITS_PROVIDER& aUnitsProvider ) : + m_unitsProvider( aUnitsProvider ) + { + } + + void ExportData( std::vector& aPins, const wxString& aToFile ) const + { + std::vector exportCols { + COL_NUMBER, + COL_NAME, + COL_TYPE, + COL_SHAPE, + COL_ORIENTATION, + COL_NUMBER_SIZE, + COL_NAME_SIZE, + COL_LENGTH, + COL_POSX, + COL_POSY, + COL_VISIBLE, + COL_UNIT, + COL_DEMORGAN, + }; + + std::vector> exportTable; + exportTable.reserve( aPins.size() + 1 ); + + std::vector headers; + for( int col : exportCols ) + { + headers.push_back( GetPinTableColLabel( col ) ); + } + exportTable.emplace_back( std::move( headers ) ); + + PIN_INFO_FORMATTER formatter( m_unitsProvider, false, PIN_INFO_FORMATTER::BOOL_FORMAT::TRUE_FALSE ); + + for( const SCH_PIN* pin : aPins ) + { + std::vector& cols = exportTable.emplace_back( 0 ); + cols.reserve( exportCols.size() ); + for( int col : exportCols ) + { + cols.emplace_back( formatter.Format( *pin, col ) ); + } + } + + if( !aToFile.IsEmpty() ) + { + wxFileOutputStream os( aToFile ); + CSV_WRITER writer( os ); + writer.WriteLines( exportTable ); + } + else + { + SaveTabularDataToClipboard( exportTable ); + } + } + +private: + UNITS_PROVIDER& m_unitsProvider; +}; + + +class PIN_TABLE_IMPORT +{ +public: + PIN_TABLE_IMPORT( EDA_BASE_FRAME& aFrame ) : + m_frame( aFrame ) + { + } + + std::vector> ImportData( bool aFromFile, LIB_SYMBOL& aSym ) const + { + wxString path; + + if( aFromFile ) + { + path = promptForFile(); + if( path.IsEmpty() ) + return {}; + } + + std::vector> csvData; + bool ok = false; + + if( !path.IsEmpty() ) + { + // Read file content + wxString csvFileContent = SafeReadFile( path, "r" ); + ok = AutoDecodeCSV( csvFileContent, csvData ); + } + else + { + ok = GetTabularDataFromClipboard( csvData ); + } + + std::vector> pins; + + PIN_INFO_FORMATTER formatter( m_frame, false, PIN_INFO_FORMATTER::BOOL_FORMAT::TRUE_FALSE ); + + if( ok ) + { + // The first thing we need to do is map the CSV columns to the pin table columns + // (in case the user reorders them) + std::vector headerCols = getColOrderFromCSV( csvData[0] ); + + for( size_t i = 1; i < csvData.size(); ++i ) + { + std::vector& cols = csvData[i]; + + auto pin = std::make_unique( &aSym ); + + // Ignore cells that stick out to the right of the headers + size_t maxCol = std::min( headerCols.size(), cols.size() ); + + for( size_t j = 0; j < maxCol; ++j ) + { + // Skip unrecognised columns + if( headerCols[j] == COL_COUNT ) + continue; + + formatter.UpdatePin( *pin, cols[j], headerCols[j], aSym ); + } + + pins.emplace_back( std::move( pin ) ); + } + } + return pins; + } + +private: + wxString promptForFile() const + { + wxFileDialog dlg( &m_frame, _( "Select pin data file" ), "", "", FILEEXT::CsvTsvFileWildcard(), + wxFD_OPEN | wxFD_FILE_MUST_EXIST ); + + if( dlg.ShowModal() == wxID_CANCEL ) + return wxEmptyString; + + return dlg.GetPath(); + } + + std::vector getColOrderFromCSV( const std::vector& aHeaderRow ) const + { + std::vector colOrder; + wxArrayString unknownHeaders; + + for( size_t i = 0; i < aHeaderRow.size(); ++i ) + { + COL_ORDER col = GetColTypeForString( aHeaderRow[i] ); + + if( col >= COL_COUNT ) + unknownHeaders.push_back( aHeaderRow[i] ); + + colOrder.push_back( col ); + } + + if( unknownHeaders.size() ) + { + wxString msg = wxString::Format( _( "Unknown columns in data: %s. These columns will be ignored." ), + AccumulateDescriptions( unknownHeaders ) ); + + showWarning( msg ); + } + + return colOrder; + } + + void showWarning( const wxString& aMsg ) const + { + wxMessageBox( aMsg, _( "CSV import warning" ), wxOK | wxICON_WARNING ); + } + + EDA_BASE_FRAME& m_frame; +}; + + DIALOG_LIB_EDIT_PIN_TABLE::DIALOG_LIB_EDIT_PIN_TABLE( SYMBOL_EDIT_FRAME* parent, LIB_SYMBOL* aSymbol ) : DIALOG_LIB_EDIT_PIN_TABLE_BASE( parent ), @@ -1134,6 +1384,82 @@ void DIALOG_LIB_EDIT_PIN_TABLE::OnFilterChoice( wxCommandEvent& event ) } +void DIALOG_LIB_EDIT_PIN_TABLE::OnImportButtonClick( wxCommandEvent& event ) +{ + bool fromFile = event.GetEventObject() == m_btnImportFromFile; + bool replaceAll = m_rbReplaceAll->GetValue(); + + PIN_TABLE_IMPORT importer( *m_editFrame ); + std::vector> newPins = importer.ImportData( fromFile, *m_symbol ); + + if( !newPins.size() ) + { + return; + } + + if( replaceAll ) + { + // This is quite a dance with a segfault without smart pointers + for( SCH_PIN* pin : m_pins ) + delete pin; + m_pins.clear(); + } + + for( auto& newPin : newPins ) + { + m_pins.push_back( newPin.release() ); + } + + m_cbGroup->SetValue( false ); + m_dataModel->RebuildRows( m_pins, false, false ); + + updateSummary(); +} + + +void DIALOG_LIB_EDIT_PIN_TABLE::OnExportButtonClick( wxCommandEvent& event ) +{ + bool toFile = event.GetEventObject() == m_btnExportToFile; + bool onlyShown = m_rbExportOnlyShownPins->GetValue(); + + wxString filePath = wxEmptyString; + + if( toFile ) + { + wxFileName fn( m_symbol->GetName() ); + fn.SetExt( FILEEXT::CsvFileExtension ); + wxFileDialog dlg( this, _( "Select pin data file" ), "", fn.GetFullName(), FILEEXT::CsvFileWildcard(), + wxFD_SAVE | wxFD_OVERWRITE_PROMPT ); + + if( dlg.ShowModal() == wxID_CANCEL ) + return; + + filePath = dlg.GetPath(); + } + + std::vector pinsToExport; + + if( onlyShown ) + { + for( int i = 0; i < m_dataModel->GetNumberRows(); ++i ) + { + for( SCH_PIN* pin : m_dataModel->GetRowPins( i ) ) + { + pinsToExport.push_back( pin ); + } + } + } + else + { + pinsToExport = m_pins; + } + + + PIN_TABLE_EXPORT exporter( *m_editFrame ); + exporter.ExportData( pinsToExport, filePath ); +} + + void DIALOG_LIB_EDIT_PIN_TABLE::adjustGridColumns() { // Account for scroll bars diff --git a/eeschema/dialogs/dialog_lib_edit_pin_table.h b/eeschema/dialogs/dialog_lib_edit_pin_table.h index 655a2cd944..9cad49334d 100644 --- a/eeschema/dialogs/dialog_lib_edit_pin_table.h +++ b/eeschema/dialogs/dialog_lib_edit_pin_table.h @@ -46,6 +46,7 @@ enum COL_ORDER }; +class ACTION_MENU; class PIN_TABLE_DATA_MODEL; class SYMBOL_EDIT_FRAME; @@ -69,6 +70,8 @@ public: void OnGroupSelected( wxCommandEvent& event ) override; void OnFilterCheckBox( wxCommandEvent& event ) override; void OnFilterChoice( wxCommandEvent& event ) override; + void OnImportButtonClick( wxCommandEvent& event ) override; + void OnExportButtonClick( wxCommandEvent& event ) override; void OnUpdateUI( wxUpdateUIEvent& event ) override; void OnCancel( wxCommandEvent& event ) override; void OnClose( wxCloseEvent& event ) override; diff --git a/eeschema/dialogs/dialog_lib_edit_pin_table_base.cpp b/eeschema/dialogs/dialog_lib_edit_pin_table_base.cpp index 84edb1bc00..0440799cc9 100644 --- a/eeschema/dialogs/dialog_lib_edit_pin_table_base.cpp +++ b/eeschema/dialogs/dialog_lib_edit_pin_table_base.cpp @@ -20,6 +20,12 @@ DIALOG_LIB_EDIT_PIN_TABLE_BASE::DIALOG_LIB_EDIT_PIN_TABLE_BASE( wxWindow* parent wxBoxSizer* top_sizer; top_sizer = new wxBoxSizer( wxVERTICAL ); + wxBoxSizer* bSizer6; + bSizer6 = new wxBoxSizer( wxHORIZONTAL ); + + wxBoxSizer* bLeftGridSizer; + bLeftGridSizer = new wxBoxSizer( wxVERTICAL ); + wxBoxSizer* bSummarySizer; bSummarySizer = new wxBoxSizer( wxHORIZONTAL ); @@ -54,7 +60,7 @@ DIALOG_LIB_EDIT_PIN_TABLE_BASE::DIALOG_LIB_EDIT_PIN_TABLE_BASE( wxWindow* parent bSummarySizer->Add( m_duplicate_pins, 0, wxRIGHT|wxLEFT|wxALIGN_CENTER_VERTICAL, 5 ); - top_sizer->Add( bSummarySizer, 0, wxEXPAND|wxTOP|wxRIGHT|wxLEFT, 10 ); + bLeftGridSizer->Add( bSummarySizer, 0, wxEXPAND|wxTOP|wxRIGHT|wxLEFT, 10 ); wxBoxSizer* bGridMarginsSizer; bGridMarginsSizer = new wxBoxSizer( wxVERTICAL ); @@ -115,7 +121,79 @@ DIALOG_LIB_EDIT_PIN_TABLE_BASE::DIALOG_LIB_EDIT_PIN_TABLE_BASE( wxWindow* parent bGridMarginsSizer->Add( m_grid, 1, wxEXPAND|wxLEFT|wxRIGHT|wxTOP, 10 ); - top_sizer->Add( bGridMarginsSizer, 1, wxEXPAND|wxRIGHT|wxLEFT, 5 ); + bLeftGridSizer->Add( bGridMarginsSizer, 1, wxEXPAND|wxRIGHT|wxLEFT, 5 ); + + + bSizer6->Add( bLeftGridSizer, 1, wxEXPAND, 5 ); + + wxBoxSizer* bRightPaneSizer; + bRightPaneSizer = new wxBoxSizer( wxVERTICAL ); + + m_exportPane = new wxCollapsiblePane( this, wxID_ANY, _("Export"), wxDefaultPosition, wxDefaultSize, wxCP_DEFAULT_STYLE ); + m_exportPane->Collapse( false ); + + wxBoxSizer* bExportSizer; + bExportSizer = new wxBoxSizer( wxVERTICAL ); + + m_rbExportAllPins = new wxRadioButton( m_exportPane->GetPane(), wxID_ANY, _("Export all pins"), wxDefaultPosition, wxDefaultSize, 0 ); + bExportSizer->Add( m_rbExportAllPins, 0, wxALL, 5 ); + + m_rbExportOnlyShownPins = new wxRadioButton( m_exportPane->GetPane(), wxID_ANY, _("Export only shown pins"), wxDefaultPosition, wxDefaultSize, 0 ); + bExportSizer->Add( m_rbExportOnlyShownPins, 0, wxALL, 5 ); + + wxBoxSizer* bExportBtnSizer; + bExportBtnSizer = new wxBoxSizer( wxHORIZONTAL ); + + m_btnExportToFile = new wxButton( m_exportPane->GetPane(), wxID_ANY, _("To File..."), wxDefaultPosition, wxDefaultSize, 0 ); + bExportBtnSizer->Add( m_btnExportToFile, 0, wxALL, 5 ); + + m_btnExportToClipboard = new wxButton( m_exportPane->GetPane(), wxID_ANY, _("To Clipboard"), wxDefaultPosition, wxDefaultSize, 0 ); + bExportBtnSizer->Add( m_btnExportToClipboard, 0, wxALL, 5 ); + + + bExportSizer->Add( bExportBtnSizer, 1, wxEXPAND, 5 ); + + + m_exportPane->GetPane()->SetSizer( bExportSizer ); + m_exportPane->GetPane()->Layout(); + bExportSizer->Fit( m_exportPane->GetPane() ); + bRightPaneSizer->Add( m_exportPane, 1, wxEXPAND | wxALL, 5 ); + + m_importPane = new wxCollapsiblePane( this, wxID_ANY, _("Import"), wxDefaultPosition, wxDefaultSize, wxCP_DEFAULT_STYLE ); + m_importPane->Collapse( false ); + + wxBoxSizer* bImportSizer; + bImportSizer = new wxBoxSizer( wxVERTICAL ); + + m_rbReplaceAll = new wxRadioButton( m_importPane->GetPane(), wxID_ANY, _("Replace all existing pins"), wxDefaultPosition, wxDefaultSize, 0 ); + bImportSizer->Add( m_rbReplaceAll, 0, wxALL, 5 ); + + m_radioBtn1 = new wxRadioButton( m_importPane->GetPane(), wxID_ANY, _("Append to existing pins"), wxDefaultPosition, wxDefaultSize, 0 ); + bImportSizer->Add( m_radioBtn1, 0, wxALL, 5 ); + + wxBoxSizer* bImportBtnSizer; + bImportBtnSizer = new wxBoxSizer( wxHORIZONTAL ); + + m_btnImportFromFile = new wxButton( m_importPane->GetPane(), wxID_ANY, _("From File..."), wxDefaultPosition, wxDefaultSize, 0 ); + bImportBtnSizer->Add( m_btnImportFromFile, 0, wxALL, 5 ); + + m_btnImportFromClipboard = new wxButton( m_importPane->GetPane(), wxID_ANY, _("From Clipboard"), wxDefaultPosition, wxDefaultSize, 0 ); + bImportBtnSizer->Add( m_btnImportFromClipboard, 0, wxALL, 5 ); + + + bImportSizer->Add( bImportBtnSizer, 1, wxEXPAND, 5 ); + + + m_importPane->GetPane()->SetSizer( bImportSizer ); + m_importPane->GetPane()->Layout(); + bImportSizer->Fit( m_importPane->GetPane() ); + bRightPaneSizer->Add( m_importPane, 1, wxEXPAND | wxALL, 5 ); + + + bSizer6->Add( bRightPaneSizer, 0, wxEXPAND, 5 ); + + + top_sizer->Add( bSizer6, 1, wxEXPAND, 5 ); wxBoxSizer* bBottomSizer; bBottomSizer = new wxBoxSizer( wxHORIZONTAL ); @@ -188,6 +266,10 @@ DIALOG_LIB_EDIT_PIN_TABLE_BASE::DIALOG_LIB_EDIT_PIN_TABLE_BASE( wxWindow* parent m_grid->Connect( wxEVT_GRID_CELL_LEFT_CLICK, wxGridEventHandler( DIALOG_LIB_EDIT_PIN_TABLE_BASE::OnCellSelected ), NULL, this ); m_grid->Connect( wxEVT_GRID_EDITOR_SHOWN, wxGridEventHandler( DIALOG_LIB_EDIT_PIN_TABLE_BASE::OnCellSelected ), NULL, this ); m_grid->Connect( wxEVT_SIZE, wxSizeEventHandler( DIALOG_LIB_EDIT_PIN_TABLE_BASE::OnSize ), NULL, this ); + m_btnExportToFile->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_LIB_EDIT_PIN_TABLE_BASE::OnExportButtonClick ), NULL, this ); + m_btnExportToClipboard->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_LIB_EDIT_PIN_TABLE_BASE::OnExportButtonClick ), NULL, this ); + m_btnImportFromFile->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_LIB_EDIT_PIN_TABLE_BASE::OnImportButtonClick ), NULL, this ); + m_btnImportFromClipboard->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_LIB_EDIT_PIN_TABLE_BASE::OnImportButtonClick ), NULL, this ); m_addButton->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_LIB_EDIT_PIN_TABLE_BASE::OnAddRow ), NULL, this ); m_deleteButton->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_LIB_EDIT_PIN_TABLE_BASE::OnDeleteRow ), NULL, this ); m_cbGroup->Connect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( DIALOG_LIB_EDIT_PIN_TABLE_BASE::OnRebuildRows ), NULL, this ); @@ -207,6 +289,10 @@ DIALOG_LIB_EDIT_PIN_TABLE_BASE::~DIALOG_LIB_EDIT_PIN_TABLE_BASE() m_grid->Disconnect( wxEVT_GRID_CELL_LEFT_CLICK, wxGridEventHandler( DIALOG_LIB_EDIT_PIN_TABLE_BASE::OnCellSelected ), NULL, this ); m_grid->Disconnect( wxEVT_GRID_EDITOR_SHOWN, wxGridEventHandler( DIALOG_LIB_EDIT_PIN_TABLE_BASE::OnCellSelected ), NULL, this ); m_grid->Disconnect( wxEVT_SIZE, wxSizeEventHandler( DIALOG_LIB_EDIT_PIN_TABLE_BASE::OnSize ), NULL, this ); + m_btnExportToFile->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_LIB_EDIT_PIN_TABLE_BASE::OnExportButtonClick ), NULL, this ); + m_btnExportToClipboard->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_LIB_EDIT_PIN_TABLE_BASE::OnExportButtonClick ), NULL, this ); + m_btnImportFromFile->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_LIB_EDIT_PIN_TABLE_BASE::OnImportButtonClick ), NULL, this ); + m_btnImportFromClipboard->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_LIB_EDIT_PIN_TABLE_BASE::OnImportButtonClick ), NULL, this ); m_addButton->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_LIB_EDIT_PIN_TABLE_BASE::OnAddRow ), NULL, this ); m_deleteButton->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( DIALOG_LIB_EDIT_PIN_TABLE_BASE::OnDeleteRow ), NULL, this ); m_cbGroup->Disconnect( wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler( DIALOG_LIB_EDIT_PIN_TABLE_BASE::OnRebuildRows ), NULL, this ); diff --git a/eeschema/dialogs/dialog_lib_edit_pin_table_base.fbp b/eeschema/dialogs/dialog_lib_edit_pin_table_base.fbp index d8b6b65486..cd062fa2a7 100644 --- a/eeschema/dialogs/dialog_lib_edit_pin_table_base.fbp +++ b/eeschema/dialogs/dialog_lib_edit_pin_table_base.fbp @@ -66,509 +66,1266 @@ top_sizer wxVERTICAL none - - 10 - wxEXPAND|wxTOP|wxRIGHT|wxLEFT - 0 - - - bSummarySizer - wxHORIZONTAL - none - - 5 - wxALIGN_CENTER_VERTICAL|wxLEFT - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Pin numbers: - 0 - - 0 - - - 0 - - 1 - m_staticTextPinNumbers - 1 - - - protected - 1 - - Resizable - 1 - - - ; forward_declare - 0 - - - - - -1 - - - - 5 - wxRIGHT|wxLEFT|wxALIGN_CENTER_VERTICAL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - 0 - 0 - - 0 - - - 0 - -1,-1 - 1 - m_pin_numbers_summary - 1 - - - protected - 1 - - Resizable - 1 - - - ; forward_declare - 0 - - - - - -1 - - - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 10 - wxALIGN_CENTER_VERTICAL|wxLEFT - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Pin count: - 0 - - 0 - - - 0 - - 1 - m_staticTextPinCount - 1 - - - protected - 1 - - Resizable - 1 - - - ; forward_declare - 0 - - - - - -1 - - - - 5 - wxRIGHT|wxLEFT|wxALIGN_CENTER_VERTICAL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - 0 - 0 - - 0 - - - 0 - -1,-1 - 1 - m_pin_count - 1 - - - protected - 1 - - Resizable - 1 - - - ; forward_declare - 0 - - - - - -1 - - - - 5 - wxEXPAND - 1 - - 0 - protected - 0 - - - - 10 - wxALIGN_CENTER_VERTICAL|wxLEFT - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - Duplicate pins: - 0 - - 0 - - - 0 - - 1 - m_staticTextDuplicatePins - 1 - - - protected - 1 - - Resizable - 1 - - - ; forward_declare - 0 - - - - - -1 - - - - 5 - wxRIGHT|wxLEFT|wxALIGN_CENTER_VERTICAL - 0 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 0 - 1 - - 1 - - 0 - 0 - wxID_ANY - 0 - 0 - - 0 - - - 0 - -1,-1 - 1 - m_duplicate_pins - 1 - - - protected - 1 - - Resizable - 1 - - - ; forward_declare - 0 - - - - - -1 - - - - 5 - wxEXPAND|wxRIGHT|wxLEFT + wxEXPAND 1 - bGridMarginsSizer - wxVERTICAL + bSizer6 + wxHORIZONTAL none - - 10 - wxEXPAND|wxLEFT|wxRIGHT|wxTOP + + 5 + wxEXPAND 1 - - 1 - 1 - 1 - 1 - 0 - - 0 - 0 - 0 - 0 - - - - 1 - - - wxALIGN_LEFT - - wxALIGN_CENTER - 0 - 1 - wxALIGN_CENTER - 24 - "Count" "Number" "Name" "Electrical Type" "Graphic Style" "Orientation" "Number Text Size" "Name Text Size" "Length" "X Position" "Y Position" "Visible" "Unit" "De Morgan" - wxALIGN_CENTER - 14 - 60,66,84,140,140,100,110,110,84,84,84,84,66 - - 1 - 0 - Dock - 0 - Left - 0 - 0 - 1 - 0 - 0 - 1 - 1 - - 1 - - - 1 - 0 - 0 - wxID_ANY - - - - 0 - 0 - - 0 - - - 0 - 690,200 - 1 - m_grid - 1 - - - protected - 1 - - Resizable - wxALIGN_CENTER - 0 - - wxALIGN_CENTER - - 5 - 1 - 800,400 - WX_GRID; widgets/wx_grid.h; forward_declare - 0 - - - - - OnCellEdited - OnCellSelected - OnCellSelected - OnSize + + + bLeftGridSizer + wxVERTICAL + none + + 10 + wxEXPAND|wxTOP|wxRIGHT|wxLEFT + 0 + + + bSummarySizer + wxHORIZONTAL + none + + 5 + wxALIGN_CENTER_VERTICAL|wxLEFT + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Pin numbers: + 0 + + 0 + + + 0 + + 1 + m_staticTextPinNumbers + 1 + + + protected + 1 + + Resizable + 1 + + + ; forward_declare + 0 + + + + + -1 + + + + 5 + wxRIGHT|wxLEFT|wxALIGN_CENTER_VERTICAL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + 0 + 0 + + 0 + + + 0 + -1,-1 + 1 + m_pin_numbers_summary + 1 + + + protected + 1 + + Resizable + 1 + + + ; forward_declare + 0 + + + + + -1 + + + + 5 + wxEXPAND + 1 + + 0 + protected + 0 + + + + 10 + wxALIGN_CENTER_VERTICAL|wxLEFT + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Pin count: + 0 + + 0 + + + 0 + + 1 + m_staticTextPinCount + 1 + + + protected + 1 + + Resizable + 1 + + + ; forward_declare + 0 + + + + + -1 + + + + 5 + wxRIGHT|wxLEFT|wxALIGN_CENTER_VERTICAL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + 0 + 0 + + 0 + + + 0 + -1,-1 + 1 + m_pin_count + 1 + + + protected + 1 + + Resizable + 1 + + + ; forward_declare + 0 + + + + + -1 + + + + 5 + wxEXPAND + 1 + + 0 + protected + 0 + + + + 10 + wxALIGN_CENTER_VERTICAL|wxLEFT + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Duplicate pins: + 0 + + 0 + + + 0 + + 1 + m_staticTextDuplicatePins + 1 + + + protected + 1 + + Resizable + 1 + + + ; forward_declare + 0 + + + + + -1 + + + + 5 + wxRIGHT|wxLEFT|wxALIGN_CENTER_VERTICAL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + 0 + 0 + + 0 + + + 0 + -1,-1 + 1 + m_duplicate_pins + 1 + + + protected + 1 + + Resizable + 1 + + + ; forward_declare + 0 + + + + + -1 + + + + + + 5 + wxEXPAND|wxRIGHT|wxLEFT + 1 + + + bGridMarginsSizer + wxVERTICAL + none + + 10 + wxEXPAND|wxLEFT|wxRIGHT|wxTOP + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + 0 + + + + 1 + + + wxALIGN_LEFT + + wxALIGN_CENTER + 0 + 1 + wxALIGN_CENTER + 24 + "Count" "Number" "Name" "Electrical Type" "Graphic Style" "Orientation" "Number Text Size" "Name Text Size" "Length" "X Position" "Y Position" "Visible" "Unit" "De Morgan" + wxALIGN_CENTER + 14 + 60,66,84,140,140,100,110,110,84,84,84,84,66 + + 1 + 0 + Dock + 0 + Left + 0 + 0 + 1 + 0 + 0 + 1 + 1 + + 1 + + + 1 + 0 + 0 + wxID_ANY + + + + 0 + 0 + + 0 + + + 0 + 690,200 + 1 + m_grid + 1 + + + protected + 1 + + Resizable + wxALIGN_CENTER + 0 + + wxALIGN_CENTER + + 5 + 1 + 800,400 + WX_GRID; widgets/wx_grid.h; forward_declare + 0 + + + + + OnCellEdited + OnCellSelected + OnCellSelected + OnSize + + + + + + + + 5 + wxEXPAND + 0 + + + bRightPaneSizer + wxVERTICAL + none + + 5 + wxEXPAND | wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + 0 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Export + + 0 + + + 0 + + 1 + m_exportPane + 1 + + + protected + 1 + + Resizable + 1 + + wxCP_DEFAULT_STYLE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + bExportSizer + wxVERTICAL + none + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Export all pins + + 0 + + + 0 + + 1 + m_rbExportAllPins + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + 0 + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Export only shown pins + + 0 + + + 0 + + 1 + m_rbExportOnlyShownPins + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + 0 + + + + + + + 5 + wxEXPAND + 1 + + + bExportBtnSizer + wxHORIZONTAL + none + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + To File... + + 0 + + 0 + + + 0 + + 1 + m_btnExportToFile + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + OnExportButtonClick + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + To Clipboard + + 0 + + 0 + + + 0 + + 1 + m_btnExportToClipboard + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + OnExportButtonClick + + + + + + + + + 5 + wxEXPAND | wxALL + 1 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + 0 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Import + + 0 + + + 0 + + 1 + m_importPane + 1 + + + protected + 1 + + Resizable + 1 + + wxCP_DEFAULT_STYLE + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + bImportSizer + wxVERTICAL + none + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Replace all existing pins + + 0 + + + 0 + + 1 + m_rbReplaceAll + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + 0 + + + + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Append to existing pins + + 0 + + + 0 + + 1 + m_radioBtn1 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + 0 + + + + + + + 5 + wxEXPAND + 1 + + + bImportBtnSizer + wxHORIZONTAL + none + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + From File... + + 0 + + 0 + + + 0 + + 1 + m_btnImportFromFile + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + OnImportButtonClick + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + 0 + + 0 + 0 + 0 + + + + + 1 + 0 + 1 + + 1 + + 0 + 0 + + Dock + 0 + Left + 0 + 1 + + 1 + + + 0 + 0 + wxID_ANY + From Clipboard + + 0 + + 0 + + + 0 + + 1 + m_btnImportFromClipboard + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + OnImportButtonClick + + + + + + + @@ -741,11 +1498,11 @@ OnDeleteRow - + 4 wxEXPAND|wxALL 0 - + 1 1 1 @@ -881,11 +1638,11 @@ OnRebuildRows - + 7 wxALL|wxALIGN_CENTER_VERTICAL 0 - + 1 1 1 @@ -1031,11 +1788,11 @@ OnRebuildRows - + 4 wxEXPAND|wxALL 0 - + 1 1 1 @@ -1105,11 +1862,11 @@ - + 5 wxALIGN_CENTER_VERTICAL|wxTOP|wxBOTTOM 0 - + 1 1 1 @@ -1171,11 +1928,11 @@ OnFilterCheckBox - + 5 wxALL|wxALIGN_CENTER_VERTICAL 0 - + 1 1 1 diff --git a/eeschema/dialogs/dialog_lib_edit_pin_table_base.h b/eeschema/dialogs/dialog_lib_edit_pin_table_base.h index bd6bda091a..c97f7567bb 100644 --- a/eeschema/dialogs/dialog_lib_edit_pin_table_base.h +++ b/eeschema/dialogs/dialog_lib_edit_pin_table_base.h @@ -23,11 +23,13 @@ class WX_GRID; #include #include #include -#include +#include +#include #include #include #include -#include +#include +#include #include #include #include @@ -49,6 +51,16 @@ class DIALOG_LIB_EDIT_PIN_TABLE_BASE : public DIALOG_SHIM wxStaticText* m_staticTextDuplicatePins; wxStaticText* m_duplicate_pins; WX_GRID* m_grid; + wxCollapsiblePane* m_exportPane; + wxRadioButton* m_rbExportAllPins; + wxRadioButton* m_rbExportOnlyShownPins; + wxButton* m_btnExportToFile; + wxButton* m_btnExportToClipboard; + wxCollapsiblePane* m_importPane; + wxRadioButton* m_rbReplaceAll; + wxRadioButton* m_radioBtn1; + wxButton* m_btnImportFromFile; + wxButton* m_btnImportFromClipboard; STD_BITMAP_BUTTON* m_addButton; STD_BITMAP_BUTTON* m_deleteButton; BITMAP_BUTTON* m_divider1; @@ -68,6 +80,8 @@ class DIALOG_LIB_EDIT_PIN_TABLE_BASE : public DIALOG_SHIM virtual void OnCellEdited( wxGridEvent& event ) = 0; virtual void OnCellSelected( wxGridEvent& event ) = 0; virtual void OnSize( wxSizeEvent& event ) = 0; + virtual void OnExportButtonClick( wxCommandEvent& event ) = 0; + virtual void OnImportButtonClick( wxCommandEvent& event ) = 0; virtual void OnAddRow( wxCommandEvent& event ) = 0; virtual void OnDeleteRow( wxCommandEvent& event ) = 0; virtual void OnRebuildRows( wxCommandEvent& event ) = 0; diff --git a/eeschema/dialogs/pin_table_data_model.h b/eeschema/dialogs/pin_table_data_model.h new file mode 100644 index 0000000000..6f70f09bee --- /dev/null +++ b/eeschema/dialogs/pin_table_data_model.h @@ -0,0 +1 @@ +#pragma once diff --git a/include/clipboard.h b/include/clipboard.h index 2377edcf45..adf7a6357e 100644 --- a/include/clipboard.h +++ b/include/clipboard.h @@ -25,8 +25,10 @@ #include #include +#include class wxImage; +class wxString; /** * Store information to the system clipboard. @@ -37,6 +39,16 @@ class wxImage; */ bool SaveClipboard( const std::string& aTextUTF8 ); +/** + * Store tabular data to the system clipboard. + */ +bool SaveTabularDataToClipboard( const std::vector>& aData ); + +/** + * Attempt to get tabular data from the clipboard. + */ +bool GetTabularDataFromClipboard( std::vector>& aData ); + /** * Return the information currently stored in the system clipboard. * @@ -52,4 +64,4 @@ std::string GetClipboardUTF8(); * * If there's a filename there, and it can be loaded as an image, do that. */ -std::unique_ptr GetImageFromClipboard(); \ No newline at end of file +std::unique_ptr GetImageFromClipboard(); diff --git a/include/string_utils.h b/include/string_utils.h index 7a618da90a..fcd07a0c66 100644 --- a/include/string_utils.h +++ b/include/string_utils.h @@ -358,6 +358,27 @@ inline void AccumulateDescription( wxString& aDesc, const wxString& aItem ) aDesc << aItem; } + +/** + * Build a comma-separated list from a collection of wxStrings. + * (e.g. std::vector, wxArrayString, etc). + */ +template +inline void AccumulateDescriptions( wxString& aDesc, const T& aItemCollection ) +{ + for( const auto& item : aItemCollection ) + AccumulateDescription( aDesc, item ); +} + + +template +inline wxString AccumulateDescriptions( const T& aItemCollection ) +{ + wxString desc; + AccumulateDescriptions( desc, aItemCollection ); + return desc; +} + /** * Split \a aString to a string list separated at \a aSplitter. * diff --git a/include/wildcards_and_files_ext.h b/include/wildcards_and_files_ext.h index be8f7d457f..6d2dd15e92 100644 --- a/include/wildcards_and_files_ext.h +++ b/include/wildcards_and_files_ext.h @@ -187,6 +187,7 @@ public: static const std::string TextFileExtension; static const std::string MarkdownFileExtension; static const std::string CsvFileExtension; + static const std::string TsvFileExtension; static const std::string XmlFileExtension; static const std::string JsonFileExtension; static const std::string PythonFileExtension; @@ -245,6 +246,7 @@ public: static wxString PADSNetlistFileWildcard(); static wxString HtmlFileWildcard(); static wxString CsvFileWildcard(); + static wxString CsvTsvFileWildcard(); static wxString PcbFileWildcard(); static wxString CadstarArchiveFilesWildcard(); static wxString AltiumProjectFilesWildcard(); diff --git a/pcbnew/dialogs/panel_edit_options.cpp b/pcbnew/dialogs/panel_edit_options.cpp index a7dc7d362d..d7a2dc52fd 100644 --- a/pcbnew/dialogs/panel_edit_options.cpp +++ b/pcbnew/dialogs/panel_edit_options.cpp @@ -93,6 +93,7 @@ static ARC_EDIT_MODE arcEditModeToEnum( int aIndex ) wxFAIL_MSG( wxString::Format( "Invalid index for ARC_EDIT_MODE: %d", aIndex ) ); break; } + return ARC_EDIT_MODE::KEEP_CENTER_ADJUST_ANGLE_RADIUS; }; diff --git a/pcbnew/toolbars_footprint_editor.cpp b/pcbnew/toolbars_footprint_editor.cpp index 7d8bff8c19..ed69c01560 100644 --- a/pcbnew/toolbars_footprint_editor.cpp +++ b/pcbnew/toolbars_footprint_editor.cpp @@ -120,9 +120,9 @@ std::optional FOOTPRINT_EDIT_TOOLBAR_SETTINGS::DefaultToo { std::unique_ptr arcMenu = std::make_unique( false, selTool ); - arcMenu->Add( PCB_ACTIONS::pointEditorArcKeepCenter, ACTION_MENU::CHECK ); - arcMenu->Add( PCB_ACTIONS::pointEditorArcKeepEndpoint, ACTION_MENU::CHECK ); - arcMenu->Add( PCB_ACTIONS::pointEditorArcKeepRadius, ACTION_MENU::CHECK ); + arcMenu->Add( ACTIONS::pointEditorArcKeepCenter, ACTION_MENU::CHECK ); + arcMenu->Add( ACTIONS::pointEditorArcKeepEndpoint, ACTION_MENU::CHECK ); + arcMenu->Add( ACTIONS::pointEditorArcKeepRadius, ACTION_MENU::CHECK ); return arcMenu; }; diff --git a/qa/tests/common/CMakeLists.txt b/qa/tests/common/CMakeLists.txt index 17440dd04f..d1cab3a65a 100644 --- a/qa/tests/common/CMakeLists.txt +++ b/qa/tests/common/CMakeLists.txt @@ -60,6 +60,7 @@ set( QA_COMMON_SRCS libeval/test_numeric_evaluator.cpp + io/test_csv.cpp io/altium/test_altium_parser.cpp io/altium/test_altium_parser_utils.cpp io/cadstar/test_cadstar_parts_parser.cpp @@ -117,4 +118,4 @@ endif() kicad_add_boost_test( qa_common qa_common ) -setup_qa_env( qa_common ) \ No newline at end of file +setup_qa_env( qa_common ) diff --git a/qa/tests/common/io/test_csv.cpp b/qa/tests/common/io/test_csv.cpp new file mode 100644 index 0000000000..a3494414c0 --- /dev/null +++ b/qa/tests/common/io/test_csv.cpp @@ -0,0 +1,160 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright The KiCad Developers, see AUTHORS.txt for contributors. + * + * 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 3 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, see . + */ + +#include + +#include + +#include + + +/** + * Define a stream function for logging this type. + */ +template +std::ostream& boost_test_print_type( std::ostream& os, const std::vector>& aTable ) +{ + os << "TABLE[ " << std::endl; + for( size_t i = 0; i < aTable.size(); ++i ) + { + const auto& row = aTable[i]; + os << " Row " << i << " [ "; + for( size_t j = 0; j < row.size(); ++j ) + { + os << row[j]; + if( j < row.size() - 1 ) + os << ", "; + } + os << "] " << std::endl; + } + os << " ]" << std::endl; + return os; +} + + +static bool TableDataEqual( const std::vector>& aExpected, + const std::vector>& aActual ) +{ + if( aExpected.size() != aActual.size() ) + { + BOOST_TEST_MESSAGE( "Row count mismatch: " << aExpected.size() << " != " << aActual.size() ); + return false; + } + + for( size_t i = 0; i < aExpected.size(); ++i ) + { + BOOST_TEST_INFO_SCOPE( "Row " << i ); + if( aExpected[i].size() != aActual[i].size() ) + return false; + + for( size_t j = 0; j < aExpected[i].size(); ++j ) + { + if( aExpected[i][j] != aActual[i][j] ) + return false; + } + } + + return true; +} + + +BOOST_AUTO_TEST_SUITE( CsvTests ) + + +struct CsvRoundTripCase +{ + wxString m_name; + std::vector> m_rows; + wxString m_expected; +}; + + +BOOST_AUTO_TEST_CASE( BasicRoundTrips ) +{ + // clang-format off + static const std::vector testCases = { + { + "Basic CSV, Double Quoted, Backslash escaped", + { + { "Head 1", "Head 2", "Head, \"3\"" }, + { "Row 1 Col 1", "Row 1 Col 2", "Row 1 Col 3" } + }, + "\"Head 1\",\"Head 2\",\"Head, \"\"3\"\"\"\n" + "\"Row 1 Col 1\",\"Row 1 Col 2\",\"Row 1 Col 3\"\n", + }, + }; + // clang-format on + + for( const auto& testCase : testCases ) + { + BOOST_TEST_INFO_SCOPE( testCase.m_name ); + + wxStringOutputStream os; + CSV_WRITER writer( os ); + writer.WriteLines( testCase.m_rows ); + + BOOST_CHECK_EQUAL( os.GetString(), testCase.m_expected ); + + std::vector> readRows; + + bool result = AutoDecodeCSV( os.GetString(), readRows ); + BOOST_CHECK( result ); + BOOST_CHECK_PREDICATE( TableDataEqual, ( testCase.m_rows )( readRows ) ); + } +} + + +struct CsvDecodeCase +{ + wxString m_name; + wxString m_input; + std::vector> m_expectedRows; +}; + + +BOOST_AUTO_TEST_CASE( BasicDecode ) +{ + // clang-format off + static const std::vector testCases = { + { + "Basic TSV, Double Quoted", + "\"Head 1\"\t\"Head 2\"\t\"Head, 3\"\n" + "\"Row 1 Col 1\"\t\"Row 1 Col 2\"\t\"Row 1 Col 3\"\n", + { + { "Head 1", "Head 2", "Head, 3" }, + { "Row 1 Col 1", "Row 1 Col 2", "Row 1 Col 3" } + }, + } + }; + // clang-format on + + for( const auto& testCase : testCases ) + { + BOOST_TEST_INFO_SCOPE( testCase.m_name ); + + std::vector> readRows; + + bool result = AutoDecodeCSV( testCase.m_input, readRows ); + BOOST_CHECK( result ); + BOOST_CHECK_PREDICATE( TableDataEqual, ( testCase.m_expectedRows )( readRows ) ); + } +} + + +BOOST_AUTO_TEST_SUITE_END() diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt index 950b325fdd..8d2e5a6d6d 100644 --- a/thirdparty/CMakeLists.txt +++ b/thirdparty/CMakeLists.txt @@ -68,5 +68,6 @@ add_subdirectory( picosha2 ) add_subdirectory( json_schema_validator ) add_subdirectory( pegtl ) add_subdirectory( 3dxware_sdk ) +add_subdirectory( rapidcsv ) add_subdirectory( turtle ) add_subdirectory( thread-pool ) diff --git a/thirdparty/rapidcsv/CMakeLists.txt b/thirdparty/rapidcsv/CMakeLists.txt new file mode 100644 index 0000000000..ab422ebe86 --- /dev/null +++ b/thirdparty/rapidcsv/CMakeLists.txt @@ -0,0 +1,7 @@ +add_library( rapidcsv INTERFACE ) + +target_include_directories( rapidcsv INTERFACE ${CMAKE_CURRENT_SOURCE_DIR} ) + +target_sources( rapidcsv INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR}/rapidcsv/rapidcsv.h +) diff --git a/thirdparty/rapidcsv/LICENSE.BSD3 b/thirdparty/rapidcsv/LICENSE.BSD3 new file mode 100644 index 0000000000..c96136d12f --- /dev/null +++ b/thirdparty/rapidcsv/LICENSE.BSD3 @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2017, Kristofer Berggren +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/thirdparty/rapidcsv/README.md b/thirdparty/rapidcsv/README.md new file mode 100644 index 0000000000..1a2d289800 --- /dev/null +++ b/thirdparty/rapidcsv/README.md @@ -0,0 +1,6 @@ +This directory contains the rapidcsv library, which is a C++ CSV parser and writer. +It is a header-only library, and released under the BSD 3-Clause License, a +copy of which is included in the LICENSE file. + +The files come from: +* https://github.com/d99kris/rapidcsv diff --git a/thirdparty/rapidcsv/rapidcsv/rapidcsv.h b/thirdparty/rapidcsv/rapidcsv/rapidcsv.h new file mode 100644 index 0000000000..19391b0155 --- /dev/null +++ b/thirdparty/rapidcsv/rapidcsv/rapidcsv.h @@ -0,0 +1,1943 @@ +/* + * rapidcsv.h + * + * URL: https://github.com/d99kris/rapidcsv + * Version: 8.87 + * + * Copyright (C) 2017-2025 Kristofer Berggren + * All rights reserved. + * + * rapidcsv is distributed under the BSD 3-Clause license, see LICENSE for details. + * + */ + + #pragma once + + #include + #include + #include + #ifdef HAS_CODECVT + #include + #include + #endif + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + + namespace rapidcsv + { + #if defined(_MSC_VER) + static const bool sPlatformHasCR = true; + #else + static const bool sPlatformHasCR = false; + #endif + static const std::vector s_Utf8BOM = { '\xef', '\xbb', '\xbf' }; + + /** + * @brief Datastructure holding parameters controlling how invalid numbers (including + * empty strings) should be handled. + */ + struct ConverterParams + { + /** + * @brief Constructor + * @param pHasDefaultConverter specifies if conversion of non-numerical strings shall be + * converted to a default numerical value, instead of causing + * an exception to be thrown (default). + * @param pDefaultFloat floating-point default value to represent invalid numbers. + * @param pDefaultInteger integer default value to represent invalid numbers. + * @param pNumericLocale specifies whether to honor LC_NUMERIC locale (default + * true). + */ + explicit ConverterParams(const bool pHasDefaultConverter = false, + const long double pDefaultFloat = std::numeric_limits::signaling_NaN(), + const long long pDefaultInteger = 0, + const bool pNumericLocale = true) + : mHasDefaultConverter(pHasDefaultConverter) + , mDefaultFloat(pDefaultFloat) + , mDefaultInteger(pDefaultInteger) + , mNumericLocale(pNumericLocale) + { + } + + /** + * @brief specifies if conversion of non-numerical strings shall be converted to a default + * numerical value, instead of causing an exception to be thrown (default). + */ + bool mHasDefaultConverter; + + /** + * @brief floating-point default value to represent invalid numbers. + */ + long double mDefaultFloat; + + /** + * @brief integer default value to represent invalid numbers. + */ + long long mDefaultInteger; + + /** + * @brief specifies whether to honor LC_NUMERIC locale. + */ + bool mNumericLocale; + }; + + /** + * @brief Exception thrown when attempting to access Document data in a datatype which + * is not supported by the Converter class. + */ + class no_converter : public std::exception + { + public: + /** + * @brief Provides details about the exception + * @returns an explanatory string + */ + const char* what() const throw() override + { + return "unsupported conversion datatype"; + } + }; + + /** + * @brief Class providing conversion to/from numerical datatypes and strings. Only + * intended for rapidcsv internal usage, but exposed externally to allow + * specialization for custom datatype conversions. + */ + template + class Converter + { + public: + /** + * @brief Constructor + * @param pConverterParams specifies how conversion of non-numerical values to + * numerical datatype shall be handled. + */ + Converter(const ConverterParams& pConverterParams) + : mConverterParams(pConverterParams) + { + } + + /** + * @brief Converts numerical value to string representation. + * @param pVal numerical value + * @param pStr output string + */ + void ToStr(const T& pVal, std::string& pStr) const + { + if (typeid(T) == typeid(int) || + typeid(T) == typeid(long) || + typeid(T) == typeid(long long) || + typeid(T) == typeid(unsigned) || + typeid(T) == typeid(unsigned long) || + typeid(T) == typeid(unsigned long long) || + typeid(T) == typeid(long double) || + typeid(T) == typeid(char)) + { + std::ostringstream out; + out << pVal; + pStr = out.str(); + } + else if (typeid(T) == typeid(float)) + { + std::ostringstream out; + out << std::setprecision(9) << pVal; + pStr = out.str(); + } + else if (typeid(T) == typeid(double)) + { + std::ostringstream out; + out << std::setprecision(17) << pVal; + pStr = out.str(); + } + else + { + throw no_converter(); + } + } + + /** + * @brief Converts string holding a numerical value to numerical datatype representation. + * @param pVal numerical value + * @param pStr output string + */ + void ToVal(const std::string& pStr, T& pVal) const + { + try + { + if (typeid(T) == typeid(int)) + { + pVal = static_cast(std::stoi(pStr)); + return; + } + else if (typeid(T) == typeid(long)) + { + pVal = static_cast(std::stol(pStr)); + return; + } + else if (typeid(T) == typeid(long long)) + { + pVal = static_cast(std::stoll(pStr)); + return; + } + else if (typeid(T) == typeid(unsigned)) + { + pVal = static_cast(std::stoul(pStr)); + return; + } + else if (typeid(T) == typeid(unsigned long)) + { + pVal = static_cast(std::stoul(pStr)); + return; + } + else if (typeid(T) == typeid(unsigned long long)) + { + pVal = static_cast(std::stoull(pStr)); + return; + } + } + catch (...) + { + if (!mConverterParams.mHasDefaultConverter) + { + throw; + } + else + { + pVal = static_cast(mConverterParams.mDefaultInteger); + return; + } + } + + try + { + if (mConverterParams.mNumericLocale) + { + if (typeid(T) == typeid(float)) + { + pVal = static_cast(std::stof(pStr)); + return; + } + else if (typeid(T) == typeid(double)) + { + pVal = static_cast(std::stod(pStr)); + return; + } + else if (typeid(T) == typeid(long double)) + { + pVal = static_cast(std::stold(pStr)); + return; + } + } + else + { + if ((typeid(T) == typeid(float)) || + (typeid(T) == typeid(double)) || + (typeid(T) == typeid(long double))) + { + std::istringstream iss(pStr); + iss.imbue(std::locale::classic()); + iss >> pVal; + if (iss.fail() || iss.bad() || !iss.eof()) + { + throw std::invalid_argument("istringstream: no conversion"); + } + return; + } + } + } + catch (...) + { + if (!mConverterParams.mHasDefaultConverter) + { + throw; + } + else + { + pVal = static_cast(mConverterParams.mDefaultFloat); + return; + } + } + + if (typeid(T) == typeid(char)) + { + pVal = static_cast(pStr[0]); + return; + } + else + { + throw no_converter(); + } + } + + private: + const ConverterParams& mConverterParams; + }; + + /** + * @brief Specialized implementation handling string to string conversion. + * @param pVal string + * @param pStr string + */ + template<> + inline void Converter::ToStr(const std::string& pVal, std::string& pStr) const + { + pStr = pVal; + } + + /** + * @brief Specialized implementation handling string to string conversion. + * @param pVal string + * @param pStr string + */ + template<> + inline void Converter::ToVal(const std::string& pStr, std::string& pVal) const + { + pVal = pStr; + } + + template + using ConvFunc = std::function; + + /** + * @brief Datastructure holding parameters controlling which row and column should be + * treated as labels. + */ + struct LabelParams + { + /** + * @brief Constructor + * @param pColumnNameIdx specifies the zero-based row index of the column labels, setting + * it to -1 prevents column lookup by label name, and gives access + * to all rows as document data. Default: 0 + * @param pRowNameIdx specifies the zero-based column index of the row labels, setting + * it to -1 prevents row lookup by label name, and gives access + * to all columns as document data. Default: -1 + */ + explicit LabelParams(const int pColumnNameIdx = 0, const int pRowNameIdx = -1) + : mColumnNameIdx(pColumnNameIdx) + , mRowNameIdx(pRowNameIdx) + { + if (mColumnNameIdx < -1) + { + const std::string errStr = "invalid column name index " + + std::to_string(mColumnNameIdx) + " < -1"; + throw std::out_of_range(errStr); + } + + if (mRowNameIdx < -1) + { + const std::string errStr = "invalid row name index " + + std::to_string(mRowNameIdx) + " < -1"; + throw std::out_of_range(errStr); + } + } + + /** + * @brief specifies the zero-based row index of the column labels. + */ + int mColumnNameIdx; + + /** + * @brief specifies the zero-based column index of the row labels. + */ + int mRowNameIdx; + }; + + /** + * @brief Datastructure holding parameters controlling how the CSV data fields are separated. + */ + struct SeparatorParams + { + /** + * @brief Constructor + * @param pSeparator specifies the column separator (default ','). + * @param pTrim specifies whether to trim leading and trailing spaces from + * cells read (default false). + * @param pHasCR specifies whether a new document (i.e. not an existing document read) + * should use CR/LF instead of only LF (default is to use standard + * behavior of underlying platforms - CR/LF for Win, and LF for others). + * @param pQuotedLinebreaks specifies whether to allow line breaks in quoted text (default false) + * @param pAutoQuote specifies whether to automatically dequote data during read, and add + * quotes during write (default true). + * @param pQuoteChar specifies the quote character (default '\"'). + */ + explicit SeparatorParams(const char pSeparator = ',', const bool pTrim = false, + const bool pHasCR = sPlatformHasCR, const bool pQuotedLinebreaks = false, + const bool pAutoQuote = true, const char pQuoteChar = '"') + : mSeparator(pSeparator) + , mTrim(pTrim) + , mHasCR(pHasCR) + , mQuotedLinebreaks(pQuotedLinebreaks) + , mAutoQuote(pAutoQuote) + , mQuoteChar(pQuoteChar) + { + } + + /** + * @brief specifies the column separator. + */ + char mSeparator; + + /** + * @brief specifies whether to trim leading and trailing spaces from cells read. + */ + bool mTrim; + + /** + * @brief specifies whether new documents should use CR/LF instead of LF. + */ + bool mHasCR; + + /** + * @brief specifies whether to allow line breaks in quoted text. + */ + bool mQuotedLinebreaks; + + /** + * @brief specifies whether to automatically dequote cell data. + */ + bool mAutoQuote; + + /** + * @brief specifies the quote character. + */ + char mQuoteChar; + }; + + /** + * @brief Datastructure holding parameters controlling how special line formats should be + * treated. + */ + struct LineReaderParams + { + /** + * @brief Constructor + * @param pSkipCommentLines specifies whether to skip lines prefixed with + * mCommentPrefix. Default: false + * @param pCommentPrefix specifies which prefix character to indicate a comment + * line. Default: # + * @param pSkipEmptyLines specifies whether to skip empty lines. Default: false + */ + explicit LineReaderParams(const bool pSkipCommentLines = false, + const char pCommentPrefix = '#', + const bool pSkipEmptyLines = false) + : mSkipCommentLines(pSkipCommentLines) + , mCommentPrefix(pCommentPrefix) + , mSkipEmptyLines(pSkipEmptyLines) + { + } + + /** + * @brief specifies whether to skip lines prefixed with mCommentPrefix. + */ + bool mSkipCommentLines; + + /** + * @brief specifies which prefix character to indicate a comment line. + */ + char mCommentPrefix; + + /** + * @brief specifies whether to skip empty lines. + */ + bool mSkipEmptyLines; + }; + + /** + * @brief Class representing a CSV document. + */ + class Document + { + public: + /** + * @brief Constructor + * @param pPath specifies the path of an existing CSV-file to populate the Document + * data with. + * @param pLabelParams specifies which row and column should be treated as labels. + * @param pSeparatorParams specifies which field and row separators should be used. + * @param pConverterParams specifies how invalid numbers (including empty strings) should be + * handled. + * @param pLineReaderParams specifies how special line formats should be treated. + */ + explicit Document(const std::string& pPath = std::string(), + const LabelParams& pLabelParams = LabelParams(), + const SeparatorParams& pSeparatorParams = SeparatorParams(), + const ConverterParams& pConverterParams = ConverterParams(), + const LineReaderParams& pLineReaderParams = LineReaderParams()) + : mPath(pPath) + , mLabelParams(pLabelParams) + , mSeparatorParams(pSeparatorParams) + , mConverterParams(pConverterParams) + , mLineReaderParams(pLineReaderParams) + , mData() + , mColumnNames() + , mRowNames() + { + if (!mPath.empty()) + { + ReadCsv(); + } + } + + /** + * @brief Constructor + * @param pStream specifies a binary input stream to read CSV data from. + * @param pLabelParams specifies which row and column should be treated as labels. + * @param pSeparatorParams specifies which field and row separators should be used. + * @param pConverterParams specifies how invalid numbers (including empty strings) should be + * handled. + * @param pLineReaderParams specifies how special line formats should be treated. + */ + explicit Document(std::istream& pStream, + const LabelParams& pLabelParams = LabelParams(), + const SeparatorParams& pSeparatorParams = SeparatorParams(), + const ConverterParams& pConverterParams = ConverterParams(), + const LineReaderParams& pLineReaderParams = LineReaderParams()) + : mPath() + , mLabelParams(pLabelParams) + , mSeparatorParams(pSeparatorParams) + , mConverterParams(pConverterParams) + , mLineReaderParams(pLineReaderParams) + , mData() + , mColumnNames() + , mRowNames() + { + ReadCsv(pStream); + } + + /** + * @brief Read Document data from file. + * @param pPath specifies the path of an existing CSV-file to populate the Document + * data with. + * @param pLabelParams specifies which row and column should be treated as labels. + * @param pSeparatorParams specifies which field and row separators should be used. + * @param pConverterParams specifies how invalid numbers (including empty strings) should be + * handled. + * @param pLineReaderParams specifies how special line formats should be treated. + */ + void Load(const std::string& pPath, + const LabelParams& pLabelParams = LabelParams(), + const SeparatorParams& pSeparatorParams = SeparatorParams(), + const ConverterParams& pConverterParams = ConverterParams(), + const LineReaderParams& pLineReaderParams = LineReaderParams()) + { + mPath = pPath; + mLabelParams = pLabelParams; + mSeparatorParams = pSeparatorParams; + mConverterParams = pConverterParams; + mLineReaderParams = pLineReaderParams; + ReadCsv(); + } + + /** + * @brief Read Document data from stream. + * @param pStream specifies a binary input stream to read CSV data from. + * @param pLabelParams specifies which row and column should be treated as labels. + * @param pSeparatorParams specifies which field and row separators should be used. + * @param pConverterParams specifies how invalid numbers (including empty strings) should be + * handled. + * @param pLineReaderParams specifies how special line formats should be treated. + */ + void Load(std::istream& pStream, + const LabelParams& pLabelParams = LabelParams(), + const SeparatorParams& pSeparatorParams = SeparatorParams(), + const ConverterParams& pConverterParams = ConverterParams(), + const LineReaderParams& pLineReaderParams = LineReaderParams()) + { + mPath = ""; + mLabelParams = pLabelParams; + mSeparatorParams = pSeparatorParams; + mConverterParams = pConverterParams; + mLineReaderParams = pLineReaderParams; + ReadCsv(pStream); + } + + /** + * @brief Write Document data to file. + * @param pPath optionally specifies the path where the CSV-file will be created + * (if not specified, the original path provided when creating or + * loading the Document data will be used). + */ + void Save(const std::string& pPath = std::string()) + { + if (!pPath.empty()) + { + mPath = pPath; + } + WriteCsv(); + } + + /** + * @brief Write Document data to stream. + * @param pStream specifies a binary output stream to write the data to. + */ + void Save(std::ostream& pStream) const + { + WriteCsv(pStream); + } + + /** + * @brief Clears loaded Document data. + * + */ + void Clear() + { + mData.clear(); + mColumnNames.clear(); + mRowNames.clear(); + #ifdef HAS_CODECVT + mIsUtf16 = false; + mIsLE = false; + #endif + mHasUtf8BOM = false; + } + + /** + * @brief Get column index by name. + * @param pColumnName column label name. + * @returns zero-based column index. + */ + int GetColumnIdx(const std::string& pColumnName) const + { + if (mLabelParams.mColumnNameIdx >= 0) + { + if (mColumnNames.find(pColumnName) != mColumnNames.end()) + { + return static_cast(mColumnNames.at(pColumnName)) - (mLabelParams.mRowNameIdx + 1); + } + } + return -1; + } + + /** + * @brief Get column by index. + * @param pColumnIdx zero-based column index. + * @returns vector of column data. + */ + template + std::vector GetColumn(const size_t pColumnIdx) const + { + const size_t dataColumnIdx = GetDataColumnIndex(pColumnIdx); + std::vector column; + Converter converter(mConverterParams); + for (auto itRow = mData.begin(); itRow != mData.end(); ++itRow) + { + if (std::distance(mData.begin(), itRow) > mLabelParams.mColumnNameIdx) + { + if (dataColumnIdx < itRow->size()) + { + T val; + converter.ToVal(itRow->at(dataColumnIdx), val); + column.push_back(val); + } + else + { + const std::string errStr = "requested column index " + + std::to_string(pColumnIdx) + " >= " + + std::to_string(itRow->size() - GetDataColumnIndex(0)) + + " (number of columns on row index " + + std::to_string(std::distance(mData.begin(), itRow) - + (mLabelParams.mColumnNameIdx + 1)) + ")"; + throw std::out_of_range(errStr); + } + } + } + return column; + } + + /** + * @brief Get column by index. + * @param pColumnIdx zero-based column index. + * @param pToVal conversion function. + * @returns vector of column data. + */ + template + std::vector GetColumn(const size_t pColumnIdx, ConvFunc pToVal) const + { + const size_t dataColumnIdx = GetDataColumnIndex(pColumnIdx); + std::vector column; + for (auto itRow = mData.begin(); itRow != mData.end(); ++itRow) + { + if (std::distance(mData.begin(), itRow) > mLabelParams.mColumnNameIdx) + { + T val; + pToVal(itRow->at(dataColumnIdx), val); + column.push_back(val); + } + } + return column; + } + + /** + * @brief Get column by name. + * @param pColumnName column label name. + * @returns vector of column data. + */ + template + std::vector GetColumn(const std::string& pColumnName) const + { + const int columnIdx = GetColumnIdx(pColumnName); + if (columnIdx < 0) + { + throw std::out_of_range("column not found: " + pColumnName); + } + return GetColumn(static_cast(columnIdx)); + } + + /** + * @brief Get column by name. + * @param pColumnName column label name. + * @param pToVal conversion function. + * @returns vector of column data. + */ + template + std::vector GetColumn(const std::string& pColumnName, ConvFunc pToVal) const + { + const int columnIdx = GetColumnIdx(pColumnName); + if (columnIdx < 0) + { + throw std::out_of_range("column not found: " + pColumnName); + } + return GetColumn(static_cast(columnIdx), pToVal); + } + + /** + * @brief Set column by index. + * @param pColumnIdx zero-based column index. + * @param pColumn vector of column data. + */ + template + void SetColumn(const size_t pColumnIdx, const std::vector& pColumn) + { + const size_t dataColumnIdx = GetDataColumnIndex(pColumnIdx); + + while (GetDataRowIndex(pColumn.size()) > GetDataRowCount()) + { + std::vector row; + row.resize(GetDataColumnCount()); + mData.push_back(row); + } + + if ((dataColumnIdx + 1) > GetDataColumnCount()) + { + for (auto itRow = mData.begin(); itRow != mData.end(); ++itRow) + { + if (std::distance(mData.begin(), itRow) >= mLabelParams.mColumnNameIdx) + { + itRow->resize(GetDataColumnIndex(dataColumnIdx + 1)); + } + } + } + + Converter converter(mConverterParams); + for (auto itRow = pColumn.begin(); itRow != pColumn.end(); ++itRow) + { + std::string str; + converter.ToStr(*itRow, str); + mData.at(static_cast(std::distance(pColumn.begin(), itRow) + mLabelParams.mColumnNameIdx + 1)).at( + dataColumnIdx) = str; + } + } + + /** + * @brief Set column by name. + * @param pColumnName column label name. + * @param pColumn vector of column data. + */ + template + void SetColumn(const std::string& pColumnName, const std::vector& pColumn) + { + const int columnIdx = GetColumnIdx(pColumnName); + if (columnIdx < 0) + { + throw std::out_of_range("column not found: " + pColumnName); + } + SetColumn(static_cast(columnIdx), pColumn); + } + + /** + * @brief Remove column by index. + * @param pColumnIdx zero-based column index. + */ + void RemoveColumn(const size_t pColumnIdx) + { + const size_t dataColumnIdx = GetDataColumnIndex(pColumnIdx); + for (auto itRow = mData.begin(); itRow != mData.end(); ++itRow) + { + if (std::distance(mData.begin(), itRow) >= mLabelParams.mColumnNameIdx) + { + if (dataColumnIdx < itRow->size()) + { + itRow->erase(itRow->begin() + static_cast(dataColumnIdx)); + } + else + { + const std::string errStr = "column out of range: " + + std::to_string(pColumnIdx) + " (on row " + + std::to_string(std::distance(mData.begin(), itRow)) + + ")"; + throw std::out_of_range(errStr); + } + } + } + + UpdateColumnNames(); + } + + /** + * @brief Remove column by name. + * @param pColumnName column label name. + */ + void RemoveColumn(const std::string& pColumnName) + { + int columnIdx = GetColumnIdx(pColumnName); + if (columnIdx < 0) + { + throw std::out_of_range("column not found: " + pColumnName); + } + + RemoveColumn(static_cast(columnIdx)); + } + + /** + * @brief Insert column at specified index. + * @param pColumnIdx zero-based column index. + * @param pColumn vector of column data (optional argument). + * @param pColumnName column label name (optional argument). + */ + template + void InsertColumn(const size_t pColumnIdx, const std::vector& pColumn = std::vector(), + const std::string& pColumnName = std::string()) + { + const size_t dataColumnIdx = GetDataColumnIndex(pColumnIdx); + + std::vector column; + if (pColumn.empty()) + { + column.resize(GetDataRowCount()); + } + else + { + column.resize(GetDataRowIndex(pColumn.size())); + Converter converter(mConverterParams); + for (auto itRow = pColumn.begin(); itRow != pColumn.end(); ++itRow) + { + std::string str; + converter.ToStr(*itRow, str); + const size_t rowIdx = + static_cast(std::distance(pColumn.begin(), itRow) + (mLabelParams.mColumnNameIdx + 1)); + column.at(rowIdx) = str; + } + } + + while (column.size() > GetDataRowCount()) + { + std::vector row; + const size_t columnCount = std::max(static_cast(mLabelParams.mColumnNameIdx + 1), + GetDataColumnCount()); + row.resize(columnCount); + mData.push_back(row); + } + + for (auto itRow = mData.begin(); itRow != mData.end(); ++itRow) + { + if (std::distance(mData.begin(), itRow) >= mLabelParams.mColumnNameIdx) + { + const size_t rowIdx = static_cast(std::distance(mData.begin(), itRow)); + if (dataColumnIdx <= itRow->size()) + { + itRow->insert(itRow->begin() + static_cast(dataColumnIdx), column.at(rowIdx)); + } + else + { + const std::string errStr = "column out of range: " + + std::to_string(pColumnIdx) + " (on row " + + std::to_string(std::distance(mData.begin(), itRow)) + + ")"; + throw std::out_of_range(errStr); + } + } + } + + if (!pColumnName.empty()) + { + SetColumnName(pColumnIdx, pColumnName); + } + + UpdateColumnNames(); + } + + /** + * @brief Get number of data columns (excluding label columns). + * @returns column count. + */ + size_t GetColumnCount() const + { + const size_t firstRow = static_cast((mLabelParams.mColumnNameIdx >= 0) ? mLabelParams.mColumnNameIdx : 0); + const int count = static_cast((mData.size() > firstRow) ? mData.at(firstRow).size() : 0) - + (mLabelParams.mRowNameIdx + 1); + return (count >= 0) ? static_cast(count) : 0; + } + + /** + * @brief Get row index by name. + * @param pRowName row label name. + * @returns zero-based row index. + */ + int GetRowIdx(const std::string& pRowName) const + { + if (mLabelParams.mRowNameIdx >= 0) + { + if (mRowNames.find(pRowName) != mRowNames.end()) + { + return static_cast(mRowNames.at(pRowName)) - (mLabelParams.mColumnNameIdx + 1); + } + } + return -1; + } + + /** + * @brief Get row by index. + * @param pRowIdx zero-based row index. + * @returns vector of row data. + */ + template + std::vector GetRow(const size_t pRowIdx) const + { + const size_t dataRowIdx = GetDataRowIndex(pRowIdx); + std::vector row; + Converter converter(mConverterParams); + for (auto itCol = mData.at(dataRowIdx).begin(); itCol != mData.at(dataRowIdx).end(); ++itCol) + { + if (std::distance(mData.at(dataRowIdx).begin(), itCol) > mLabelParams.mRowNameIdx) + { + T val; + converter.ToVal(*itCol, val); + row.push_back(val); + } + } + return row; + } + + /** + * @brief Get row by index. + * @param pRowIdx zero-based row index. + * @param pToVal conversion function. + * @returns vector of row data. + */ + template + std::vector GetRow(const size_t pRowIdx, ConvFunc pToVal) const + { + const size_t dataRowIdx = GetDataRowIndex(pRowIdx); + std::vector row; + Converter converter(mConverterParams); + for (auto itCol = mData.at(dataRowIdx).begin(); itCol != mData.at(dataRowIdx).end(); ++itCol) + { + if (std::distance(mData.at(dataRowIdx).begin(), itCol) > mLabelParams.mRowNameIdx) + { + T val; + pToVal(*itCol, val); + row.push_back(val); + } + } + return row; + } + + /** + * @brief Get row by name. + * @param pRowName row label name. + * @returns vector of row data. + */ + template + std::vector GetRow(const std::string& pRowName) const + { + int rowIdx = GetRowIdx(pRowName); + if (rowIdx < 0) + { + throw std::out_of_range("row not found: " + pRowName); + } + return GetRow(static_cast(rowIdx)); + } + + /** + * @brief Get row by name. + * @param pRowName row label name. + * @param pToVal conversion function. + * @returns vector of row data. + */ + template + std::vector GetRow(const std::string& pRowName, ConvFunc pToVal) const + { + int rowIdx = GetRowIdx(pRowName); + if (rowIdx < 0) + { + throw std::out_of_range("row not found: " + pRowName); + } + return GetRow(static_cast(rowIdx), pToVal); + } + + /** + * @brief Set row by index. + * @param pRowIdx zero-based row index. + * @param pRow vector of row data. + */ + template + void SetRow(const size_t pRowIdx, const std::vector& pRow) + { + const size_t dataRowIdx = GetDataRowIndex(pRowIdx); + + while ((dataRowIdx + 1) > GetDataRowCount()) + { + std::vector row; + row.resize(GetDataColumnCount()); + mData.push_back(row); + } + + if (pRow.size() > GetDataColumnCount()) + { + for (auto itRow = mData.begin(); itRow != mData.end(); ++itRow) + { + if (std::distance(mData.begin(), itRow) >= mLabelParams.mColumnNameIdx) + { + itRow->resize(GetDataColumnIndex(pRow.size())); + } + } + } + + Converter converter(mConverterParams); + for (auto itCol = pRow.begin(); itCol != pRow.end(); ++itCol) + { + std::string str; + converter.ToStr(*itCol, str); + mData.at(dataRowIdx).at(static_cast(std::distance(pRow.begin(), + itCol) + mLabelParams.mRowNameIdx + 1)) = str; + } + } + + /** + * @brief Set row by name. + * @param pRowName row label name. + * @param pRow vector of row data. + */ + template + void SetRow(const std::string& pRowName, const std::vector& pRow) + { + int rowIdx = GetRowIdx(pRowName); + if (rowIdx < 0) + { + throw std::out_of_range("row not found: " + pRowName); + } + return SetRow(static_cast(rowIdx), pRow); + } + + /** + * @brief Remove row by index. + * @param pRowIdx zero-based row index. + */ + void RemoveRow(const size_t pRowIdx) + { + const size_t dataRowIdx = GetDataRowIndex(pRowIdx); + mData.erase(mData.begin() + static_cast(dataRowIdx)); + UpdateRowNames(); + } + + /** + * @brief Remove row by name. + * @param pRowName row label name. + */ + void RemoveRow(const std::string& pRowName) + { + int rowIdx = GetRowIdx(pRowName); + if (rowIdx < 0) + { + throw std::out_of_range("row not found: " + pRowName); + } + + RemoveRow(static_cast(rowIdx)); + } + + /** + * @brief Insert row at specified index. + * @param pRowIdx zero-based row index. + * @param pRow vector of row data (optional argument). + * @param pRowName row label name (optional argument). + */ + template + void InsertRow(const size_t pRowIdx, const std::vector& pRow = std::vector(), + const std::string& pRowName = std::string()) + { + const size_t rowIdx = GetDataRowIndex(pRowIdx); + + std::vector row; + if (pRow.empty()) + { + row.resize(GetDataColumnCount()); + } + else + { + row.resize(GetDataColumnIndex(pRow.size())); + Converter converter(mConverterParams); + for (auto itCol = pRow.begin(); itCol != pRow.end(); ++itCol) + { + std::string str; + converter.ToStr(*itCol, str); + row.at(static_cast(std::distance(pRow.begin(), itCol) + mLabelParams.mRowNameIdx + 1)) = str; + } + } + + while (rowIdx > GetDataRowCount()) + { + std::vector tempRow; + tempRow.resize(GetDataColumnCount()); + mData.push_back(tempRow); + } + + mData.insert(mData.begin() + static_cast(rowIdx), row); + + if (!pRowName.empty()) + { + SetRowName(pRowIdx, pRowName); + } + + UpdateRowNames(); + } + + /** + * @brief Get number of data rows (excluding label rows). + * @returns row count. + */ + size_t GetRowCount() const + { + const int count = static_cast(mData.size()) - (mLabelParams.mColumnNameIdx + 1); + return (count >= 0) ? static_cast(count) : 0; + } + + /** + * @brief Get cell by index. + * @param pColumnIdx zero-based column index. + * @param pRowIdx zero-based row index. + * @returns cell data. + */ + template + T GetCell(const size_t pColumnIdx, const size_t pRowIdx) const + { + const size_t dataColumnIdx = GetDataColumnIndex(pColumnIdx); + const size_t dataRowIdx = GetDataRowIndex(pRowIdx); + + T val; + Converter converter(mConverterParams); + converter.ToVal(mData.at(dataRowIdx).at(dataColumnIdx), val); + return val; + } + + /** + * @brief Get cell by index. + * @param pColumnIdx zero-based column index. + * @param pRowIdx zero-based row index. + * @param pToVal conversion function. + * @returns cell data. + */ + template + T GetCell(const size_t pColumnIdx, const size_t pRowIdx, ConvFunc pToVal) const + { + const size_t dataColumnIdx = GetDataColumnIndex(pColumnIdx); + const size_t dataRowIdx = GetDataRowIndex(pRowIdx); + + T val; + pToVal(mData.at(dataRowIdx).at(dataColumnIdx), val); + return val; + } + + /** + * @brief Get cell by name. + * @param pColumnName column label name. + * @param pRowName row label name. + * @returns cell data. + */ + template + T GetCell(const std::string& pColumnName, const std::string& pRowName) const + { + const int columnIdx = GetColumnIdx(pColumnName); + if (columnIdx < 0) + { + throw std::out_of_range("column not found: " + pColumnName); + } + + const int rowIdx = GetRowIdx(pRowName); + if (rowIdx < 0) + { + throw std::out_of_range("row not found: " + pRowName); + } + + return GetCell(static_cast(columnIdx), static_cast(rowIdx)); + } + + /** + * @brief Get cell by name. + * @param pColumnName column label name. + * @param pRowName row label name. + * @param pToVal conversion function. + * @returns cell data. + */ + template + T GetCell(const std::string& pColumnName, const std::string& pRowName, ConvFunc pToVal) const + { + const int columnIdx = GetColumnIdx(pColumnName); + if (columnIdx < 0) + { + throw std::out_of_range("column not found: " + pColumnName); + } + + const int rowIdx = GetRowIdx(pRowName); + if (rowIdx < 0) + { + throw std::out_of_range("row not found: " + pRowName); + } + + return GetCell(static_cast(columnIdx), static_cast(rowIdx), pToVal); + } + + /** + * @brief Get cell by column name and row index. + * @param pColumnName column label name. + * @param pRowIdx zero-based row index. + * @returns cell data. + */ + template + T GetCell(const std::string& pColumnName, const size_t pRowIdx) const + { + const int columnIdx = GetColumnIdx(pColumnName); + if (columnIdx < 0) + { + throw std::out_of_range("column not found: " + pColumnName); + } + + return GetCell(static_cast(columnIdx), pRowIdx); + } + + /** + * @brief Get cell by column name and row index. + * @param pColumnName column label name. + * @param pRowIdx zero-based row index. + * @param pToVal conversion function. + * @returns cell data. + */ + template + T GetCell(const std::string& pColumnName, const size_t pRowIdx, ConvFunc pToVal) const + { + const int columnIdx = GetColumnIdx(pColumnName); + if (columnIdx < 0) + { + throw std::out_of_range("column not found: " + pColumnName); + } + + return GetCell(static_cast(columnIdx), pRowIdx, pToVal); + } + + /** + * @brief Get cell by column index and row name. + * @param pColumnIdx zero-based column index. + * @param pRowName row label name. + * @returns cell data. + */ + template + T GetCell(const size_t pColumnIdx, const std::string& pRowName) const + { + const int rowIdx = GetRowIdx(pRowName); + if (rowIdx < 0) + { + throw std::out_of_range("row not found: " + pRowName); + } + + return GetCell(pColumnIdx, static_cast(rowIdx)); + } + + /** + * @brief Get cell by column index and row name. + * @param pColumnIdx zero-based column index. + * @param pRowName row label name. + * @param pToVal conversion function. + * @returns cell data. + */ + template + T GetCell(const size_t pColumnIdx, const std::string& pRowName, ConvFunc pToVal) const + { + const int rowIdx = GetRowIdx(pRowName); + if (rowIdx < 0) + { + throw std::out_of_range("row not found: " + pRowName); + } + + return GetCell(pColumnIdx, static_cast(rowIdx), pToVal); + } + + /** + * @brief Set cell by index. + * @param pRowIdx zero-based row index. + * @param pColumnIdx zero-based column index. + * @param pCell cell data. + */ + template + void SetCell(const size_t pColumnIdx, const size_t pRowIdx, const T& pCell) + { + const size_t dataColumnIdx = GetDataColumnIndex(pColumnIdx); + const size_t dataRowIdx = GetDataRowIndex(pRowIdx); + + while ((dataRowIdx + 1) > GetDataRowCount()) + { + std::vector row; + row.resize(GetDataColumnCount()); + mData.push_back(row); + } + + if ((dataColumnIdx + 1) > GetDataColumnCount()) + { + for (auto itRow = mData.begin(); itRow != mData.end(); ++itRow) + { + if (std::distance(mData.begin(), itRow) >= mLabelParams.mColumnNameIdx) + { + itRow->resize(dataColumnIdx + 1); + } + } + } + + std::string str; + Converter converter(mConverterParams); + converter.ToStr(pCell, str); + mData.at(dataRowIdx).at(dataColumnIdx) = str; + } + + /** + * @brief Set cell by name. + * @param pColumnName column label name. + * @param pRowName row label name. + * @param pCell cell data. + */ + template + void SetCell(const std::string& pColumnName, const std::string& pRowName, const T& pCell) + { + const int columnIdx = GetColumnIdx(pColumnName); + if (columnIdx < 0) + { + throw std::out_of_range("column not found: " + pColumnName); + } + + const int rowIdx = GetRowIdx(pRowName); + if (rowIdx < 0) + { + throw std::out_of_range("row not found: " + pRowName); + } + + SetCell(static_cast(columnIdx), static_cast(rowIdx), pCell); + } + + /** + * @brief Set cell by column index and row name. + * @param pColumnIdx zero-based column index. + * @param pRowName row label name. + * @param pCell cell data. + */ + template + void SetCell(const size_t pColumnIdx, const std::string& pRowName, const T& pCell) + { + const int rowIdx = GetRowIdx(pRowName); + if (rowIdx < 0) + { + throw std::out_of_range("row not found: " + pRowName); + } + + SetCell(pColumnIdx, static_cast(rowIdx), pCell); + } + + /** + * @brief Set cell by column name and row index. + * @param pColumnName column label name. + * @param pRowIdx zero-based row index. + * @param pCell cell data. + */ + template + void SetCell(const std::string& pColumnName, const size_t pRowIdx, const T& pCell) + { + const int columnIdx = GetColumnIdx(pColumnName); + if (columnIdx < 0) + { + throw std::out_of_range("column not found: " + pColumnName); + } + + SetCell(static_cast(columnIdx), pRowIdx, pCell); + } + + /** + * @brief Get column name + * @param pColumnIdx zero-based column index. + * @returns column name. + */ + std::string GetColumnName(const size_t pColumnIdx) const + { + const size_t dataColumnIdx = GetDataColumnIndex(pColumnIdx); + if (mLabelParams.mColumnNameIdx < 0) + { + throw std::out_of_range("column name row index < 0: " + std::to_string(mLabelParams.mColumnNameIdx)); + } + + return mData.at(static_cast(mLabelParams.mColumnNameIdx)).at(dataColumnIdx); + } + + /** + * @brief Set column name + * @param pColumnIdx zero-based column index. + * @param pColumnName column name. + */ + void SetColumnName(size_t pColumnIdx, const std::string& pColumnName) + { + if (mLabelParams.mColumnNameIdx < 0) + { + throw std::out_of_range("column name row index < 0: " + std::to_string(mLabelParams.mColumnNameIdx)); + } + + const size_t dataColumnIdx = GetDataColumnIndex(pColumnIdx); + mColumnNames[pColumnName] = dataColumnIdx; + + // increase table size if necessary: + const size_t rowIdx = static_cast(mLabelParams.mColumnNameIdx); + if (rowIdx >= mData.size()) + { + mData.resize(rowIdx + 1); + } + auto& row = mData[rowIdx]; + if (dataColumnIdx >= row.size()) + { + row.resize(dataColumnIdx + 1); + } + + mData.at(static_cast(mLabelParams.mColumnNameIdx)).at(dataColumnIdx) = pColumnName; + } + + /** + * @brief Get column names + * @returns vector of column names. + */ + std::vector GetColumnNames() const + { + if (mLabelParams.mColumnNameIdx >= 0) + { + return std::vector(mData.at(static_cast(mLabelParams.mColumnNameIdx)).begin() + + (mLabelParams.mRowNameIdx + 1), + mData.at(static_cast(mLabelParams.mColumnNameIdx)).end()); + } + + return std::vector(); + } + + /** + * @brief Get row name + * @param pRowIdx zero-based column index. + * @returns row name. + */ + std::string GetRowName(const size_t pRowIdx) const + { + const size_t dataRowIdx = GetDataRowIndex(pRowIdx); + if (mLabelParams.mRowNameIdx < 0) + { + throw std::out_of_range("row name column index < 0: " + std::to_string(mLabelParams.mRowNameIdx)); + } + + return mData.at(dataRowIdx).at(static_cast(mLabelParams.mRowNameIdx)); + } + + /** + * @brief Set row name + * @param pRowIdx zero-based row index. + * @param pRowName row name. + */ + void SetRowName(size_t pRowIdx, const std::string& pRowName) + { + const size_t dataRowIdx = GetDataRowIndex(pRowIdx); + mRowNames[pRowName] = dataRowIdx; + if (mLabelParams.mRowNameIdx < 0) + { + throw std::out_of_range("row name column index < 0: " + std::to_string(mLabelParams.mRowNameIdx)); + } + + // increase table size if necessary: + if (dataRowIdx >= mData.size()) + { + mData.resize(dataRowIdx + 1); + } + auto& row = mData[dataRowIdx]; + if (mLabelParams.mRowNameIdx >= static_cast(row.size())) + { + row.resize(static_cast(mLabelParams.mRowNameIdx) + 1); + } + + mData.at(dataRowIdx).at(static_cast(mLabelParams.mRowNameIdx)) = pRowName; + } + + /** + * @brief Get row names + * @returns vector of row names. + */ + std::vector GetRowNames() const + { + std::vector rownames; + if (mLabelParams.mRowNameIdx >= 0) + { + for (auto itRow = mData.begin(); itRow != mData.end(); ++itRow) + { + if (std::distance(mData.begin(), itRow) > mLabelParams.mColumnNameIdx) + { + rownames.push_back(itRow->at(static_cast(mLabelParams.mRowNameIdx))); + } + } + } + return rownames; + } + + private: + void ReadCsv() + { + std::ifstream stream; + stream.exceptions(std::ifstream::failbit | std::ifstream::badbit); + stream.open(mPath, std::ios::binary); + ReadCsv(stream); + } + + void ReadCsv(std::istream& pStream) + { + Clear(); + pStream.seekg(0, std::ios::end); + std::streamsize length = pStream.tellg(); + pStream.seekg(0, std::ios::beg); + + #ifdef HAS_CODECVT + std::vector bom2b(2, '\0'); + if (length >= 2) + { + pStream.read(bom2b.data(), 2); + pStream.seekg(0, std::ios::beg); + } + + static const std::vector bomU16le = { '\xff', '\xfe' }; + static const std::vector bomU16be = { '\xfe', '\xff' }; + if ((bom2b == bomU16le) || (bom2b == bomU16be)) + { + mIsUtf16 = true; + mIsLE = (bom2b == bomU16le); + + std::wifstream wstream; + wstream.exceptions(std::wifstream::failbit | std::wifstream::badbit); + wstream.open(mPath, std::ios::binary); + if (mIsLE) + { + wstream.imbue(std::locale(wstream.getloc(), + new std::codecvt_utf16(std::consume_header | + std::little_endian)>)); + } + else + { + wstream.imbue(std::locale(wstream.getloc(), + new std::codecvt_utf16)); + } + std::wstringstream wss; + wss << wstream.rdbuf(); + std::string utf8 = ToString(wss.str()); + std::stringstream ss(utf8); + ParseCsv(ss, static_cast(utf8.size())); + } + else + #endif + { + // check for UTF-8 Byte order mark and skip it when found + if (length >= 3) + { + std::vector bom3b(3, '\0'); + pStream.read(bom3b.data(), 3); + + if (bom3b != s_Utf8BOM) + { + // file does not start with a UTF-8 Byte order mark + pStream.seekg(0, std::ios::beg); + } + else + { + // file did start with a UTF-8 Byte order mark, simply skip it + length -= 3; + mHasUtf8BOM = true; + } + } + + ParseCsv(pStream, length); + } + } + + void ParseCsv(std::istream& pStream, std::streamsize p_FileLength) + { + const std::streamsize bufLength = 64 * 1024; + std::vector buffer(bufLength); + std::vector row; + std::string cell; + bool quoted = false; + int cr = 0; + int lf = 0; + + while (p_FileLength > 0) + { + const std::streamsize toReadLength = std::min(p_FileLength, bufLength); + pStream.read(buffer.data(), toReadLength); + + // With user-specified istream opened in non-binary mode on windows, we may have a + // data length mismatch, so ensure we don't parse outside actual data length read. + const std::streamsize readLength = pStream.gcount(); + if (readLength <= 0) + { + break; + } + + for (size_t i = 0; i < static_cast(readLength); ++i) + { + if (buffer[i] == mSeparatorParams.mQuoteChar) + { + if (cell.empty() || (cell[0] == mSeparatorParams.mQuoteChar)) + { + quoted = !quoted; + } + else if (mSeparatorParams.mTrim) + { + // allow whitespace before first mQuoteChar + const auto firstQuote = std::find(cell.begin(), cell.end(), mSeparatorParams.mQuoteChar); + if (std::all_of(cell.begin(), firstQuote, [](int ch) { return isspace(ch); })) + { + quoted = !quoted; + } + } + cell += buffer[i]; + } + else if (buffer[i] == mSeparatorParams.mSeparator) + { + if (!quoted) + { + row.push_back(Unquote(Trim(cell))); + cell.clear(); + } + else + { + cell += buffer[i]; + } + } + else if (buffer[i] == '\r') + { + if (mSeparatorParams.mQuotedLinebreaks && quoted) + { + cell += buffer[i]; + } + else + { + ++cr; + } + } + else if (buffer[i] == '\n') + { + if (mSeparatorParams.mQuotedLinebreaks && quoted) + { + cell += buffer[i]; + } + else + { + ++lf; + if (mLineReaderParams.mSkipEmptyLines && row.empty() && cell.empty()) + { + // skip empty line + } + else + { + row.push_back(Unquote(Trim(cell))); + + if (mLineReaderParams.mSkipCommentLines && !row.at(0).empty() && + (row.at(0)[0] == mLineReaderParams.mCommentPrefix)) + { + // skip comment line + } + else + { + mData.push_back(row); + } + + cell.clear(); + row.clear(); + quoted = false; + } + } + } + else + { + cell += buffer[i]; + } + } + p_FileLength -= readLength; + } + + // Handle last row / cell without linebreak + if (row.empty() && cell.empty()) + { + // skip empty trailing line + } + else + { + row.push_back(Unquote(Trim(cell))); + + if (mLineReaderParams.mSkipCommentLines && !row.at(0).empty() && + (row.at(0)[0] == mLineReaderParams.mCommentPrefix)) + { + // skip comment line + } + else + { + mData.push_back(row); + } + + cell.clear(); + row.clear(); + quoted = false; + } + + // Assume CR/LF if at least half the linebreaks have CR + mSeparatorParams.mHasCR = (cr > (lf / 2)); + + // Set up column labels + UpdateColumnNames(); + + // Set up row labels + UpdateRowNames(); + } + + void WriteCsv() const + { + #ifdef HAS_CODECVT + if (mIsUtf16) + { + std::stringstream ss; + WriteCsv(ss); + std::string utf8 = ss.str(); + std::wstring wstr = ToWString(utf8); + + std::wofstream wstream; + wstream.exceptions(std::wofstream::failbit | std::wofstream::badbit); + wstream.open(mPath, std::ios::binary | std::ios::trunc); + + if (mIsLE) + { + wstream.imbue(std::locale(wstream.getloc(), + new std::codecvt_utf16(std::little_endian)>)); + } + else + { + wstream.imbue(std::locale(wstream.getloc(), + new std::codecvt_utf16)); + } + + wstream << static_cast(0xfeff); + wstream << wstr; + } + else + #endif + { + std::ofstream stream; + stream.exceptions(std::ofstream::failbit | std::ofstream::badbit); + stream.open(mPath, std::ios::binary | std::ios::trunc); + if (mHasUtf8BOM) + { + stream.write(s_Utf8BOM.data(), 3); + } + + WriteCsv(stream); + } + } + + void WriteCsv(std::ostream& pStream) const + { + for (auto itr = mData.begin(); itr != mData.end(); ++itr) + { + for (auto itc = itr->begin(); itc != itr->end(); ++itc) + { + if (mSeparatorParams.mAutoQuote && + ((itc->find(mSeparatorParams.mSeparator) != std::string::npos) || + (itc->find(' ') != std::string::npos) || + (itc->find('\n') != std::string::npos))) + { + // escape quotes in string + std::string str = *itc; + const std::string quoteCharStr = std::string(1, mSeparatorParams.mQuoteChar); + ReplaceString(str, quoteCharStr, quoteCharStr + quoteCharStr); + + pStream << quoteCharStr << str << quoteCharStr; + } + else + { + pStream << *itc; + } + + if (std::distance(itc, itr->end()) > 1) + { + pStream << mSeparatorParams.mSeparator; + } + } + pStream << (mSeparatorParams.mHasCR ? "\r\n" : "\n"); + } + } + + size_t GetDataRowCount() const + { + return mData.size(); + } + + size_t GetDataColumnCount() const + { + const size_t firstDataRow = + static_cast((mLabelParams.mColumnNameIdx >= 0) ? mLabelParams.mColumnNameIdx : 0); + return (mData.size() > firstDataRow) ? mData.at(firstDataRow).size() : 0; + } + + inline size_t GetDataRowIndex(const size_t pRowIdx) const + { + const size_t firstDataRow = + static_cast((mLabelParams.mColumnNameIdx + 1 >= 0) ? mLabelParams.mColumnNameIdx + 1 : 0); + return pRowIdx + firstDataRow; + } + + inline size_t GetDataColumnIndex(const size_t pColumnIdx) const + { + const size_t firstDataColumn = + static_cast((mLabelParams.mRowNameIdx + 1 >= 0) ? mLabelParams.mRowNameIdx + 1 : 0); + return pColumnIdx + firstDataColumn; + } + + std::string Trim(const std::string& pStr) const + { + if (mSeparatorParams.mTrim) + { + std::string str = pStr; + + // ltrim + str.erase(str.begin(), std::find_if(str.begin(), str.end(), [](int ch) { return !isspace(ch); })); + + // rtrim + str.erase(std::find_if(str.rbegin(), str.rend(), [](int ch) { return !isspace(ch); }).base(), str.end()); + + return str; + } + else + { + return pStr; + } + } + + std::string Unquote(const std::string& pStr) const + { + if (mSeparatorParams.mAutoQuote && (pStr.size() >= 2) && + (pStr.front() == mSeparatorParams.mQuoteChar) && + (pStr.back() == mSeparatorParams.mQuoteChar)) + { + // remove start/end quotes + std::string str = pStr.substr(1, pStr.size() - 2); + + // unescape quotes in string + const std::string quoteCharStr = std::string(1, mSeparatorParams.mQuoteChar); + ReplaceString(str, quoteCharStr + quoteCharStr, quoteCharStr); + + return str; + } + else + { + return pStr; + } + } + + void UpdateColumnNames() + { + mColumnNames.clear(); + if ((mLabelParams.mColumnNameIdx >= 0) && + (static_cast(mData.size()) > mLabelParams.mColumnNameIdx)) + { + size_t i = 0; + for (auto& columnName : mData[static_cast(mLabelParams.mColumnNameIdx)]) + { + mColumnNames[columnName] = i++; + } + } + } + + void UpdateRowNames() + { + mRowNames.clear(); + if ((mLabelParams.mRowNameIdx >= 0) && + (static_cast(mData.size()) > + (mLabelParams.mColumnNameIdx + 1))) + { + size_t i = 0; + for (auto& dataRow : mData) + { + if (static_cast(dataRow.size()) > mLabelParams.mRowNameIdx) + { + mRowNames[dataRow[static_cast(mLabelParams.mRowNameIdx)]] = i++; + } + } + } + } + + #ifdef HAS_CODECVT + #if defined(_MSC_VER) + #pragma warning (push) + #pragma warning (disable: 4996) + #endif + static std::string ToString(const std::wstring& pWStr) + { + return std::wstring_convert, wchar_t>{ }.to_bytes(pWStr); + } + + static std::wstring ToWString(const std::string& pStr) + { + return std::wstring_convert, wchar_t>{ }.from_bytes(pStr); + } + #if defined(_MSC_VER) + #pragma warning (pop) + #endif + #endif + + static void ReplaceString(std::string& pStr, const std::string& pSearch, const std::string& pReplace) + { + size_t pos = 0; + + while ((pos = pStr.find(pSearch, pos)) != std::string::npos) + { + pStr.replace(pos, pSearch.size(), pReplace); + pos += pReplace.size(); + } + } + + private: + std::string mPath; + LabelParams mLabelParams; + SeparatorParams mSeparatorParams; + ConverterParams mConverterParams; + LineReaderParams mLineReaderParams; + std::vector> mData; + std::map mColumnNames; + std::map mRowNames; + #ifdef HAS_CODECVT + bool mIsUtf16 = false; + bool mIsLE = false; + #endif + bool mHasUtf8BOM = false; + }; + }