mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-26 15:10:22 -05:00
#14878: Add RifCsvHtmlTableTools for CSV content preview
Generic helper to render CSV file content as an HTML table, used to preview well formations.csv content in the UI.
This commit is contained in:
1 parent
cf36012cd2
commit
fe887270e5
5 files changed
+227
-1
No files matched your search
@@ -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
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "RifCsvHtmlTableTools.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// 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<QString, QString> 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<QString, QString> 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<QStringList> 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<QStringList>& rows )
|
||||
{
|
||||
QString html( "<table border=1 cellspacing=0 cellpadding=3>"
|
||||
" <thead>"
|
||||
" <tr bgcolor=lightblue>" );
|
||||
|
||||
for ( const QString& column : header )
|
||||
{
|
||||
html += QString( "<th>%1</th>" ).arg( column.trimmed().toHtmlEscaped() );
|
||||
}
|
||||
html += "</tr></thead><tbody>";
|
||||
|
||||
for ( const QStringList& row : rows )
|
||||
{
|
||||
html += "<tr>";
|
||||
for ( const QString& cell : row )
|
||||
{
|
||||
const QString trimmedCell = cell.trimmed();
|
||||
|
||||
bool isNumber = false;
|
||||
trimmedCell.toDouble( &isNumber );
|
||||
const QString align = isNumber ? " align=right" : "";
|
||||
|
||||
html += QString( "<td%1>%2</td>" ).arg( align ).arg( trimmedCell.toHtmlEscaped() );
|
||||
}
|
||||
html += "</tr>";
|
||||
}
|
||||
|
||||
html += "</tbody></table>";
|
||||
|
||||
return html;
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#include <expected>
|
||||
#include <vector>
|
||||
|
||||
//==================================================================================================
|
||||
/// 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<QString, QString> 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<QString, QString> 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<QStringList>& rows );
|
||||
|
||||
static QChar detectDelimiter( const QString& line );
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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( "<th>Well</th>" ) );
|
||||
EXPECT_TRUE( result->contains( "<th>Zone</th>" ) );
|
||||
EXPECT_TRUE( result->contains( "<td>A-1</td>" ) );
|
||||
EXPECT_TRUE( result->contains( "<td>Upper</td>" ) );
|
||||
EXPECT_TRUE( result->contains( "<td align=right>100.0</td>" ) );
|
||||
}
|
||||
|
||||
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( "<th>Well</th>" ) );
|
||||
EXPECT_TRUE( result->contains( "<td>A-1</td>" ) );
|
||||
}
|
||||
|
||||
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( "<td align=right>1</td>" ) );
|
||||
EXPECT_FALSE( result->contains( "<td align=right>3</td>" ) );
|
||||
}
|
||||
Reference in new issue
Block a user