Skip to content

Commit

Permalink
Added a publicly available function to parse a numeric range.
Browse files Browse the repository at this point in the history
This is quite a useful one, as I've seen at least 3 different implementations
of that. Rigorous tests for it were added as well.

Refs #7772
  • Loading branch information
arturbekasov committed Aug 21, 2013
1 parent 43de19f commit 3894f4a
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 0 deletions.
5 changes: 5 additions & 0 deletions Code/Mantid/Framework/Kernel/inc/MantidKernel/Strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@ namespace Mantid
/// checks if the candidate is the member of the group
MANTID_KERNEL_DLL int isMember(const std::vector<std::string> &group, const std::string &candidate);

/// Parses a number range, e.g. "1,4-9,54-111,3,10", to the vector containing all the elements
/// within the range
MANTID_KERNEL_DLL std::vector<int> parseRange(const std::string& str, const std::string& elemSep = ",",
const std::string& rangeSep = "-");

} // NAMESPACE Strings

} // NAMESPACE Kernel
Expand Down
80 changes: 80 additions & 0 deletions Code/Mantid/Framework/Kernel/src/Strings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
#include <Poco/StringTokenizer.h>
#include <Poco/Path.h>

#include <boost/lexical_cast.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/make_shared.hpp>

#include <cmath>
#include <fstream>
#include <iomanip>
Expand Down Expand Up @@ -1051,6 +1055,82 @@ namespace Mantid
return num;
}

/**
* Parses a number range, e.g. "1,4-9,54-111,3,10", to the vector containing all the elements
* within the range.
* @param str String to parse
* @param elemSep String with characters used to separate elements (',')
* @param rangeSep String with characters used to separate range start and end ('-')
* @return A vector with all the elements from the range
*/
std::vector<int> parseRange(const std::string& str, const std::string& elemSep,
const std::string& rangeSep)
{
typedef Poco::StringTokenizer Tokenizer;

boost::shared_ptr<Tokenizer> elements;

if(elemSep.find(' ') != std::string::npos)
{
// If element separator contains space character it's a special case, because in that case
// it is allowed to have element separator inside a range, e.g. "4 - 5", but not "4,-5"

// Space is added so that last empty element of the "1,2,3-" is not ignored and we can
// spot the error. Behaviour is changed in Poco 1.5 and this will not be needed.
Tokenizer ranges(str + " ", rangeSep, Tokenizer::TOK_TRIM);
std::string new_str = join(ranges.begin(), ranges.end(), rangeSep.substr(0,1));

elements = boost::make_shared<Tokenizer>(new_str, elemSep, Tokenizer::TOK_IGNORE_EMPTY | Tokenizer::TOK_TRIM);
}
else
{
elements = boost::make_shared<Tokenizer>(str, elemSep, Tokenizer::TOK_IGNORE_EMPTY | Tokenizer::TOK_TRIM);
}

std::vector<int> result;

// Estimation of the resulting number of elements
result.reserve(elements->count());

for (Tokenizer::Iterator it = elements->begin(); it != elements->end(); it++)
{
// See above for the reason space is added
Tokenizer rangeElements(*it + " ", rangeSep, Tokenizer::TOK_TRIM);

size_t noOfRangeElements = rangeElements.count();

// A single element
if(noOfRangeElements == 1)
{
int element;
if(convert(rangeElements[0], element) != 1)
throw std::invalid_argument("Invalid element: " + *it);
result.push_back(element);
}
// A pair
else if(noOfRangeElements == 2)
{
int start, end;

if(convert(rangeElements[0], start) != 1 || convert(rangeElements[1], end) != 1)
throw std::invalid_argument("Invalid range: " + *it);

if(start >= end)
throw std::invalid_argument("Range boundaries are reversed: " + *it);

for(int i = start; i <= end; i++)
result.push_back(i);
}
// Error - e.g. "--""
else
{
throw std::invalid_argument("Multiple range separators: " + *it);
}
}

return result;
}

/// \cond TEMPLATE
template MANTID_KERNEL_DLL int section(std::string&,double&);
template MANTID_KERNEL_DLL int section(std::string&,float&);
Expand Down
105 changes: 105 additions & 0 deletions Code/Mantid/Framework/Kernel/test/StringsTest.h
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,111 @@ class StringsTest : public CxxTest::TestSuite
TS_ASSERT_EQUALS(3,isMember(group,"C"));
}

void test_parseRange_defaultSimple()
{
std::vector<int> result;
TS_ASSERT_THROWS_NOTHING(result = parseRange("3,1,4,0,2,5"));
std::vector<int> expected = {3,1,4,0,2,5};
TS_ASSERT_EQUALS(result, expected);
}

void test_parseRange_defaultRanges()
{
std::vector<int> result;
TS_ASSERT_THROWS_NOTHING(result = parseRange(" 1, 2 - 5, 6 ,7,8, 9,10-12"));
std::vector<int> expected = {1,2,3,4,5,6,7,8,9,10,11,12};
TS_ASSERT_EQUALS(result, expected);
}

void test_parseRange_emptyElements()
{
std::vector<int> expected = {1,2,3};

std::vector<int> result1;
TS_ASSERT_THROWS_NOTHING(result1 = parseRange(",1,2,3"));
TS_ASSERT_EQUALS(result1, expected);

std::vector<int> result2;
TS_ASSERT_THROWS_NOTHING(result2 = parseRange("1,2,3,"));
TS_ASSERT_EQUALS(result2, expected);

std::vector<int> result3;
TS_ASSERT_THROWS_NOTHING(result3 = parseRange("1,2,,,,3"));
TS_ASSERT_EQUALS(result3, expected);
}

void test_parseRange_mapStyleSimple()
{
std::vector<int> result;
TS_ASSERT_THROWS_NOTHING(result = parseRange(" 52 53 54 55 56 57 58 192", " "));
std::vector<int> expected = {52,53,54,55,56,57,58,192};
TS_ASSERT_EQUALS(result, expected);
}

void test_parseRange_mapStyleRanges()
{
std::vector<int> result;
TS_ASSERT_THROWS_NOTHING(result = parseRange(" 1- 3 4 5 - 7 8 -10 ", " "));
std::vector<int> expected = {1,2,3,4,5,6,7,8,9,10};
TS_ASSERT_EQUALS(result, expected);
}

void test_parseRange_customRangeSep()
{
std::vector<int> result;
TS_ASSERT_THROWS_NOTHING(result = parseRange("1-2,3:5,6-7,8:10", ",","-:"));
std::vector<int> expected = {1,2,3,4,5,6,7,8,9,10};
TS_ASSERT_EQUALS(result, expected);
}

void test_parseRange_emptyString()
{
std::vector<int> result;
TS_ASSERT_THROWS_NOTHING(result = parseRange(""));
TS_ASSERT(result.empty());
}

void test_parseRange_invalidElement()
{
TS_ASSERT_THROWS_EQUALS(parseRange("1,2,3,a,5"), const std::invalid_argument& e,
e.what(), std::string("Invalid element: a"));

TS_ASSERT_THROWS_EQUALS(parseRange("1|,|3|4", "|"), const std::invalid_argument& e,
e.what(), std::string("Invalid element: ,"));
}

void test_parseRange_invalidRange()
{
TS_ASSERT_THROWS_EQUALS(parseRange("1,2,3,-5,6"), const std::invalid_argument& e,
e.what(), std::string("Invalid range: -5"));

TS_ASSERT_THROWS_EQUALS(parseRange("-3 4", " "), const std::invalid_argument& e,
e.what(), std::string("Invalid range: -3"));

TS_ASSERT_THROWS_EQUALS(parseRange("1,2,a-4,5"), const std::invalid_argument& e,
e.what(), std::string("Invalid range: a-4"));

TS_ASSERT_THROWS_EQUALS(parseRange("1,2-,5,6"), const std::invalid_argument& e,
e.what(), std::string("Invalid range: 2-"));

TS_ASSERT_THROWS_EQUALS(parseRange("1 5-", " "), const std::invalid_argument& e,
e.what(), std::string("Invalid range: 5-"));
}

void test_parseRange_multipleRangeSep()
{
TS_ASSERT_THROWS_EQUALS(parseRange("1--5 6 7", " "), const std::invalid_argument& e,
e.what(), std::string("Multiple range separators: 1--5"));

TS_ASSERT_THROWS_EQUALS(parseRange("----"), const std::invalid_argument& e,
e.what(), std::string("Multiple range separators: ----"));
}

void test_parseRange_reversedRange()
{
TS_ASSERT_THROWS_EQUALS(parseRange("5-1,6,7"), const std::invalid_argument& e,
e.what(), std::string("Range boundaries are reversed: 5-1"));
}

};

Expand Down

0 comments on commit 3894f4a

Please sign in to comment.