diff --git a/ApplicationLibCode/FileInterface/CMakeLists_files.cmake b/ApplicationLibCode/FileInterface/CMakeLists_files.cmake index ae5529023f..4247e879a6 100644 --- a/ApplicationLibCode/FileInterface/CMakeLists_files.cmake +++ b/ApplicationLibCode/FileInterface/CMakeLists_files.cmake @@ -34,7 +34,7 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RifCsvUserDataParser.cpp ${CMAKE_CURRENT_LIST_DIR}/RifWellMeasurementReader.cpp ${CMAKE_CURRENT_LIST_DIR}/RifWellPathFormationReader.cpp - ${CMAKE_CURRENT_LIST_DIR}/RifWellPathFormationsImporter.cpp + ${CMAKE_CURRENT_LIST_DIR}/RifCsvHtmlTableTools.cpp ${CMAKE_CURRENT_LIST_DIR}/RifElementPropertyTableReader.cpp ${CMAKE_CURRENT_LIST_DIR}/RifElementPropertyReader.cpp ${CMAKE_CURRENT_LIST_DIR}/RifStimPlanXmlReader.cpp diff --git a/ApplicationLibCode/FileInterface/RifCsvHtmlTableTools.cpp b/ApplicationLibCode/FileInterface/RifCsvHtmlTableTools.cpp new file mode 100644 index 0000000000..48c52c077f --- /dev/null +++ b/ApplicationLibCode/FileInterface/RifCsvHtmlTableTools.cpp @@ -0,0 +1,121 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2026- Equinor ASA +// +// ResInsight 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. +// +// ResInsight 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 at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "RifCsvHtmlTableTools.h" + +#include +#include + +//-------------------------------------------------------------------------------------------------- +/// Picks the most likely delimiter by checking for the presence of common CSV separators, in order +/// of specificity (semicolon and tab are less likely to appear incidentally than comma). +//-------------------------------------------------------------------------------------------------- +QChar RifCsvHtmlTableTools::detectDelimiter( const QString& line ) +{ + if ( line.contains( ';' ) ) return ';'; + if ( line.contains( '\t' ) ) return '\t'; + return ','; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::expected RifCsvHtmlTableTools::generateHtmlTableFromFile( const QString& filePath, int maxRowCount ) +{ + QFile file( filePath ); + if ( !file.open( QIODevice::ReadOnly | QIODevice::Text ) ) + { + return std::unexpected( QString( "Could not open file: %1" ).arg( filePath ) ); + } + + QTextStream stream( &file ); + return generateHtmlTableFromText( stream.readAll(), maxRowCount ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::expected RifCsvHtmlTableTools::generateHtmlTableFromText( const QString& content, int maxRowCount ) +{ + QStringList lines = content.split( '\n' ); + while ( !lines.isEmpty() && lines.last().trimmed().isEmpty() ) + { + lines.removeLast(); + } + + if ( lines.isEmpty() ) + { + return std::unexpected( QString( "Content is empty" ) ); + } + + const QString rawHeader = lines.takeFirst(); + const QString headerLine = rawHeader.endsWith( '\r' ) ? rawHeader.chopped( 1 ) : rawHeader; + const QChar delimiter = detectDelimiter( headerLine ); + const QStringList header = headerLine.split( delimiter ); + + std::vector rows; + int rowCount = 0; + for ( const QString& rawLine : lines ) + { + if ( maxRowCount >= 0 && rowCount >= maxRowCount ) break; + + const QString line = rawLine.endsWith( '\r' ) ? rawLine.chopped( 1 ) : rawLine; + if ( line.trimmed().isEmpty() ) continue; + + rows.push_back( line.split( delimiter ) ); + rowCount++; + } + + return generateHtmlTable( header, rows ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RifCsvHtmlTableTools::generateHtmlTable( const QStringList& header, const std::vector& rows ) +{ + QString html( "" + " " + " " ); + + for ( const QString& column : header ) + { + html += QString( "" ).arg( column.trimmed().toHtmlEscaped() ); + } + html += ""; + + for ( const QStringList& row : rows ) + { + html += ""; + for ( const QString& cell : row ) + { + const QString trimmedCell = cell.trimmed(); + + bool isNumber = false; + trimmedCell.toDouble( &isNumber ); + const QString align = isNumber ? " align=right" : ""; + + html += QString( "%2" ).arg( align ).arg( trimmedCell.toHtmlEscaped() ); + } + html += ""; + } + + html += "
%1
"; + + return html; +} diff --git a/ApplicationLibCode/FileInterface/RifCsvHtmlTableTools.h b/ApplicationLibCode/FileInterface/RifCsvHtmlTableTools.h new file mode 100644 index 0000000000..95603c895b --- /dev/null +++ b/ApplicationLibCode/FileInterface/RifCsvHtmlTableTools.h @@ -0,0 +1,50 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2026- Equinor ASA +// +// ResInsight 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. +// +// ResInsight 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 at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include +#include + +//================================================================================================== +/// Generates a simple HTML preview table from CSV data with a header row, for display in a +/// read-only field using the caf::PdmUiTextEditor HTML text mode (e.g. a quick content preview in +/// the property editor). Not a full CSV import framework: quoted fields and embedded delimiters are +/// not supported, only plain comma/semicolon/tab separated values. +//================================================================================================== +class RifCsvHtmlTableTools +{ +public: + // Reads a CSV file, auto-detects the delimiter from the header line, and returns an HTML table + // of its content, or an error message if the file could not be read. If maxRowCount is >= 0, + // only the first maxRowCount data rows (after the header) are included. + static std::expected generateHtmlTableFromFile( const QString& filePath, int maxRowCount = -1 ); + + // As generateHtmlTableFromFile(), but operates on already-read text content (e.g. for testing, + // or content read through some other mechanism than a local file). + static std::expected generateHtmlTableFromText( const QString& content, int maxRowCount = -1 ); + + // Builds an HTML table string from an already parsed header and set of data rows. Rows with a + // different number of columns than the header are rendered as-is. + static QString generateHtmlTable( const QStringList& header, const std::vector& rows ); + + static QChar detectDelimiter( const QString& line ); +}; diff --git a/ApplicationLibCode/UnitTests/CMakeLists.txt b/ApplicationLibCode/UnitTests/CMakeLists.txt index aa38aa7f9b..6b7ac91088 100644 --- a/ApplicationLibCode/UnitTests/CMakeLists.txt +++ b/ApplicationLibCode/UnitTests/CMakeLists.txt @@ -124,6 +124,7 @@ set(SOURCE_UNITTEST_FILES ${CMAKE_CURRENT_LIST_DIR}/RifFormationNamesReader-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RifWellPathFormationReader-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RigWellPathFormations-Test.cpp + ${CMAKE_CURRENT_LIST_DIR}/RifCsvHtmlTableTools-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RifRoffReader-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RifRoffWriter-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RifRoffFileTools-Test.cpp diff --git a/ApplicationLibCode/UnitTests/RifCsvHtmlTableTools-Test.cpp b/ApplicationLibCode/UnitTests/RifCsvHtmlTableTools-Test.cpp new file mode 100644 index 0000000000..bd6371997c --- /dev/null +++ b/ApplicationLibCode/UnitTests/RifCsvHtmlTableTools-Test.cpp @@ -0,0 +1,54 @@ +#include "gtest/gtest.h" + +#include "RifCsvHtmlTableTools.h" + +TEST( RifCsvHtmlTableTools, GeneratesTableFromCommaSeparatedText ) +{ + const QString content = "Well,Zone,Top,Base\n" + "A-1,Upper,100.0,110.0\n" + "A-1,Lower,110.0,120.0\n"; + + auto result = RifCsvHtmlTableTools::generateHtmlTableFromText( content ); + ASSERT_TRUE( result.has_value() ); + + EXPECT_TRUE( result->contains( "Well" ) ); + EXPECT_TRUE( result->contains( "Zone" ) ); + EXPECT_TRUE( result->contains( "A-1" ) ); + EXPECT_TRUE( result->contains( "Upper" ) ); + EXPECT_TRUE( result->contains( "100.0" ) ); +} + +TEST( RifCsvHtmlTableTools, GeneratesTableFromSemicolonSeparatedText ) +{ + const QString content = "Well;Zone;Top;Base\n" + "A-1;Upper;100.0;110.0\n"; + + auto result = RifCsvHtmlTableTools::generateHtmlTableFromText( content ); + ASSERT_TRUE( result.has_value() ); + EXPECT_TRUE( result->contains( "Well" ) ); + EXPECT_TRUE( result->contains( "A-1" ) ); +} + +TEST( RifCsvHtmlTableTools, DetectsDelimiter ) +{ + EXPECT_EQ( QChar( ',' ), RifCsvHtmlTableTools::detectDelimiter( "a,b,c" ) ); + EXPECT_EQ( QChar( ';' ), RifCsvHtmlTableTools::detectDelimiter( "a;b;c" ) ); + EXPECT_EQ( QChar( '\t' ), RifCsvHtmlTableTools::detectDelimiter( "a\tb\tc" ) ); +} + +TEST( RifCsvHtmlTableTools, EmptyContentGivesError ) +{ + auto result = RifCsvHtmlTableTools::generateHtmlTableFromText( "" ); + EXPECT_FALSE( result.has_value() ); +} + +TEST( RifCsvHtmlTableTools, RespectsMaxRowCount ) +{ + const QString content = "A,B\n1,2\n3,4\n5,6\n"; + + auto result = RifCsvHtmlTableTools::generateHtmlTableFromText( content, 1 ); + ASSERT_TRUE( result.has_value() ); + + EXPECT_TRUE( result->contains( "1" ) ); + EXPECT_FALSE( result->contains( "3" ) ); +}