#13510 Add exclusion support to range selection parsing

Range selection functions now support exclusion tokens (e.g., "!4", "!5-7") for omitting values or ranges. UI placeholder texts and documentation updated to reflect new syntax. Comprehensive tests added for exclusion cases, including edge scenarios. Open-ended exclusion ranges are safely ignored in min/max variant. This enhances flexibility and precision for value selection.
This commit is contained in:
Magne Sjaastad committed 2026-02-04 18:07:28 +01:00
1 parent 71eb461f13
commit f1f25ca760
5 files changed
+269 -7

No files matched your search

@@ -387,6 +387,7 @@ std::set<int> RiaStdStringTools::valuesFromRangeSelection( const std::string& s
try
{
std::set<int> result;
std::set<int> exclusions;
std::istringstream stringStream( s );
std::string token;
@@ -394,6 +395,18 @@ std::set<int> RiaStdStringTools::valuesFromRangeSelection( const std::string& s
{
token = RiaStdStringTools::trimString( token );
// Check if this is an exclusion token
bool isExclusion = false;
if ( !token.empty() && token[0] == '!' )
{
isExclusion = true;
token = token.substr( 1 ); // Remove the '!' prefix
token = RiaStdStringTools::trimString( token );
}
// Skip empty tokens
if ( token.empty() ) continue;
std::istringstream tokenStream( token );
int startIndex, endIndex;
char dash, colon;
@@ -421,16 +434,36 @@ std::set<int> RiaStdStringTools::valuesFromRangeSelection( const std::string& s
for ( int i = startIndex; i <= endIndex; i += step )
{
result.insert( i );
if ( isExclusion )
{
exclusions.insert( i );
}
else
{
result.insert( i );
}
}
}
else
{
result.insert( startIndex );
if ( isExclusion )
{
exclusions.insert( startIndex );
}
else
{
result.insert( startIndex );
}
}
}
}
// Remove excluded values from result
for ( int excluded : exclusions )
{
result.erase( excluded );
}
return result;
}
catch ( const std::exception& e )
@@ -457,6 +490,7 @@ std::set<int> RiaStdStringTools::valuesFromRangeSelection( const std::string& s,
try
{
std::set<int> result;
std::set<int> exclusions;
std::stringstream stringStream( s );
std::string token;
@@ -464,10 +498,28 @@ std::set<int> RiaStdStringTools::valuesFromRangeSelection( const std::string& s,
{
token = RiaStdStringTools::trimString( token );
// Check if this is an exclusion token
bool isExclusion = false;
if ( !token.empty() && token[0] == '!' )
{
isExclusion = true;
token = token.substr( 1 ); // Remove the '!' prefix
token = RiaStdStringTools::trimString( token );
}
// Skip empty tokens
if ( token.empty() ) continue;
// Check for range
size_t dashPos = token.find( '-' );
if ( dashPos != std::string::npos )
{
// For exclusions, don't allow open-ended ranges
if ( isExclusion && ( dashPos == 0 || dashPos == token.size() - 1 ) )
{
continue; // Skip invalid exclusion ranges
}
int startIndex = ( dashPos == 0 ) ? minVal : std::stoi( token.substr( 0, dashPos ) );
int endIndex = ( dashPos == token.size() - 1 ) ? maxVal : std::stoi( token.substr( dashPos + 1 ) );
if ( startIndex > endIndex )
@@ -477,16 +529,36 @@ std::set<int> RiaStdStringTools::valuesFromRangeSelection( const std::string& s,
}
for ( int i = startIndex; i <= endIndex; ++i )
{
result.insert( i );
if ( isExclusion )
{
exclusions.insert( i );
}
else
{
result.insert( i );
}
}
}
else
{
// Check for individual numbers
result.insert( std::stoi( token ) );
if ( isExclusion )
{
exclusions.insert( std::stoi( token ) );
}
else
{
result.insert( std::stoi( token ) );
}
}
}
// Remove excluded values from result
for ( int excluded : exclusions )
{
result.erase( excluded );
}
return result;
}
catch ( const std::exception& e )
@@ -66,10 +66,14 @@ public:
static std::string removeHtmlTags( const std::string& s );
// Convert the string "1,2,5-8,10" to {1, 2, 5, 6, 7, 8, 10}
// Supports exclusion with "!" prefix: "1-10, !4" to {1, 2, 3, 5, 6, 7, 8, 9, 10}
// Supports exclusion of ranges: "1-10, !5-7" to {1, 2, 3, 4, 8, 9, 10}
static std::set<int> valuesFromRangeSelection( const std::string& s );
// Convert the range string with support for open ended expressions. minimum and maximum value will be used to limit the ranges.
// The input "-3,5-8,10-", min:1, max:12 will produce {1, 2, 3, 5, 6, 7, 8, 10, 11, 12}
// Supports exclusion with "!" prefix: "-10, !4", min:1, max:20 will produce {1, 2, 3, 5, 6, 7, 8, 9, 10}
// Supports exclusion of ranges: "-10, !5-7", min:1, max:20 will produce {1, 2, 3, 4, 8, 9, 10}
static std::set<int> valuesFromRangeSelection( const std::string& s, int minimumValue, int maximumValue );
// Create a string from a set of values. {1, 2, 3, 5, 6, 7, 8, 10, 11, 12} will be converted to "1, 2, 3, 5-8, 10-12"
@@ -337,7 +337,7 @@ RicRecursiveFileSearchDialog::RicRecursiveFileSearchDialog( QWidget* parent, con
connect( m_treeViewFilterLineEdit, &QLineEdit::returnPressed, m_treeViewFilterButton, &QPushButton::click );
connect( m_treeViewFilterLineEdit, &QLineEdit::textEdited, m_treeViewFilterButton, &QPushButton::click );
m_treeViewFilterLineEdit->setPlaceholderText( "Select Realizations: 1, 5-7, 9-18:3" );
m_treeViewFilterLineEdit->setPlaceholderText( "Select Realizations: 1, 5-7, !4, 9-18:3" );
m_treeViewFilterLabel->setVisible( false );
m_treeViewFilterButton->setVisible( false );
@@ -61,7 +61,12 @@ RimEnsembleFileSet::RimEnsembleFileSet()
CAF_PDM_InitObject( "Ensemble", ":/SummaryEnsemble.svg", "", "" );
CAF_PDM_InitField( &m_pathPattern, "PathPattern", QString(), "Path Pattern", "", "", "" );
CAF_PDM_InitField( &m_realizationSubSet, "RealizationSubSet", QString(), "Realization Filter", "", "", "" );
CAF_PDM_InitField( &m_realizationSubSet,
"RealizationSubSet",
QString(),
"Realization Filter",
"",
"Specify realization numbers. Example: 1-5, 8, 11-20, !4 will include 1, 2, 3, 5, 8, 11-20" );
CAF_PDM_InitFieldNoDefault( &m_ensembleInfo, "EnsembleInfo", "Info" );
m_ensembleInfo.registerGetMethod( this, &RimEnsembleFileSet::ensembleInfo );
@@ -280,7 +285,7 @@ void RimEnsembleFileSet::defineEditorAttribute( const caf::PdmFieldHandle* field
{
if ( auto lineEdAttr = dynamic_cast<caf::PdmUiLineEditorAttribute*>( attribute ) )
{
lineEdAttr->placeholderText = "E.g. 0,1,4-6. Use '*' for all.";
lineEdAttr->placeholderText = "E.g. 0,1,4-10,!6. Use '*' for all.";
}
}
else if ( field == &m_ensembleInfo )
@@ -318,6 +318,187 @@ TEST( RiaStdStringToolsTest, TestInvalidRangeStrings )
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RiaStdStringToolsTest, ValuesFromRangeSelectionWithExclusion )
{
{
// Test basic exclusion with single value
std::string testString = "4-5, 10";
std::set<int> expectedValues = { 4, 5, 10 };
auto actualValues = RiaStdStringTools::valuesFromRangeSelection( testString );
ASSERT_EQ( expectedValues, actualValues );
}
{
// Test exclusion with range
std::string testString = "1-10, !5-7";
std::set<int> expectedValues = { 1, 2, 3, 4, 8, 9, 10 };
auto actualValues = RiaStdStringTools::valuesFromRangeSelection( testString );
ASSERT_EQ( expectedValues, actualValues );
}
{
// Test exclusion with single values
std::string testString = "1-10, !5, !7";
std::set<int> expectedValues = { 1, 2, 3, 4, 6, 8, 9, 10 };
auto actualValues = RiaStdStringTools::valuesFromRangeSelection( testString );
ASSERT_EQ( expectedValues, actualValues );
}
{
// Test exclusion with spaces
std::string testString = "1-10, !5-7";
std::set<int> expectedValues = { 1, 2, 3, 4, 8, 9, 10 };
auto actualValues = RiaStdStringTools::valuesFromRangeSelection( testString );
ASSERT_EQ( expectedValues, actualValues );
}
{
// Test exclusion with step values
std::string testString = "1-10:2, !5";
std::set<int> expectedValues = { 1, 3, 7, 9 };
auto actualValues = RiaStdStringTools::valuesFromRangeSelection( testString );
ASSERT_EQ( expectedValues, actualValues );
}
{
// Test exclusion when value is not in the set
std::string testString = "1-5, !10";
std::set<int> expectedValues = { 1, 2, 3, 4, 5 };
auto actualValues = RiaStdStringTools::valuesFromRangeSelection( testString );
ASSERT_EQ( expectedValues, actualValues );
}
{
// Test multiple exclusion ranges
std::string testString = "1-20, !3-5, !15-17";
std::set<int> expectedValues = { 1, 2, 6, 7, 8, 9, 10, 11, 12, 13, 14, 18, 19, 20 };
auto actualValues = RiaStdStringTools::valuesFromRangeSelection( testString );
ASSERT_EQ( expectedValues, actualValues );
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RiaStdStringToolsTest, ValuesFromRangeSelectionWithExclusionMinMax )
{
int minimumValue = 1;
int maximumValue = 20;
{
// Test exclusion with open ended ranges
std::string testString = "-10, !5-7";
std::set<int> expectedValues = { 1, 2, 3, 4, 8, 9, 10 };
auto actualValues = RiaStdStringTools::valuesFromRangeSelection( testString, minimumValue, maximumValue );
ASSERT_EQ( expectedValues, actualValues );
}
{
// Test exclusion with multiple ranges
std::string testString = "1-20, !5-7, !15-17";
std::set<int> expectedValues = { 1, 2, 3, 4, 8, 9, 10, 11, 12, 13, 14, 18, 19, 20 };
auto actualValues = RiaStdStringTools::valuesFromRangeSelection( testString, minimumValue, maximumValue );
ASSERT_EQ( expectedValues, actualValues );
}
{
// Test exclusion with open-ended start range
std::string testString = "10-, !15-17";
std::set<int> expectedValues = { 10, 11, 12, 13, 14, 18, 19, 20 };
auto actualValues = RiaStdStringTools::valuesFromRangeSelection( testString, minimumValue, maximumValue );
ASSERT_EQ( expectedValues, actualValues );
}
{
// Test invalid open-ended exclusion ranges (should be ignored)
std::string testString = "1-20, !-5, !15-";
std::set<int> expectedValues = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 };
auto actualValues = RiaStdStringTools::valuesFromRangeSelection( testString, minimumValue, maximumValue );
ASSERT_EQ( expectedValues, actualValues );
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RiaStdStringToolsTest, ValuesFromRangeSelectionEdgeCases )
{
{
// Test empty exclusion token (should be ignored)
std::string testString = "1-10, !";
std::set<int> expectedValues = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
auto actualValues = RiaStdStringTools::valuesFromRangeSelection( testString );
ASSERT_EQ( expectedValues, actualValues );
}
{
// Test empty exclusion token with spaces (should be ignored)
std::string testString = "1-10, ! ";
std::set<int> expectedValues = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
auto actualValues = RiaStdStringTools::valuesFromRangeSelection( testString );
ASSERT_EQ( expectedValues, actualValues );
}
{
// Test exclusion-only input (should return empty set)
std::string testString = "!5-7";
std::set<int> expectedValues = {};
auto actualValues = RiaStdStringTools::valuesFromRangeSelection( testString );
ASSERT_EQ( expectedValues, actualValues );
}
{
// Test overlapping exclusions
std::string testString = "1-10, !3-7, !5-9";
std::set<int> expectedValues = { 1, 2, 10 };
auto actualValues = RiaStdStringTools::valuesFromRangeSelection( testString );
ASSERT_EQ( expectedValues, actualValues );
}
{
// Test exclusion with step values in the exclusion range
std::string testString = "1-20, !5-15:2";
std::set<int> expectedValues = { 1, 2, 3, 4, 6, 8, 10, 12, 14, 16, 17, 18, 19, 20 };
auto actualValues = RiaStdStringTools::valuesFromRangeSelection( testString );
ASSERT_EQ( expectedValues, actualValues );
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------