diff --git a/Framework/Kernel/inc/MantidKernel/Strings.h b/Framework/Kernel/inc/MantidKernel/Strings.h index 3a346ee494cb414299327765c591f2ee638ecab7..ce9e578c9587f7bf550df00fdd96ca89d60832cc 100644 --- a/Framework/Kernel/inc/MantidKernel/Strings.h +++ b/Framework/Kernel/inc/MantidKernel/Strings.h @@ -5,10 +5,12 @@ // Includes //---------------------------------------------------------------------- #include "MantidKernel/DllConfig.h" +#include "MantidKernel/StringTokenizer.h" #include "MantidKernel/System.h" -#include <map> +#include <boost/lexical_cast.hpp> #include <iosfwd> +#include <map> #include <set> #include <sstream> #include <string> @@ -249,12 +251,96 @@ 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 +/// 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 = "-"); +/// Parses unsigned integer groups, e.g. "1+2,4-7,9,11" to a nested vector +/// structure. +template <typename Integer> +std::vector<std::vector<Integer>> parseGroups(const std::string &str) { + std::vector<std::vector<Integer>> groups; + + // Local helper functions. + auto translateAdd = [&groups](const std::string &str) { + const auto tokens = Kernel::StringTokenizer( + str, "+", Kernel::StringTokenizer::TOK_TRIM | + Kernel::StringTokenizer::TOK_IGNORE_EMPTY); + std::vector<Integer> group; + group.reserve(tokens.count()); + for (const auto &t : tokens) { + // add this number to the group we're about to add + group.emplace_back(boost::lexical_cast<Integer>(t)); + } + groups.emplace_back(std::move(group)); + }; + + auto translateSumRange = [&groups](const std::string &str) { + // add a group with the numbers in the range + const auto tokens = Kernel::StringTokenizer( + str, "-", Kernel::StringTokenizer::TOK_TRIM | + Kernel::StringTokenizer::TOK_IGNORE_EMPTY); + if (tokens.count() != 2) + throw std::runtime_error("Malformed range (-) operation."); + Integer first = boost::lexical_cast<Integer>(tokens[0]); + Integer last = boost::lexical_cast<Integer>(tokens[1]); + if (first > last) + std::swap(first, last); + // add all the numbers in the range to the output group + std::vector<Integer> group; + group.reserve(last - first + 1); + for (Integer i = first; i <= last; ++i) + group.emplace_back(i); + if (!group.empty()) + groups.emplace_back(std::move(group)); + }; + + auto translateRange = [&groups](const std::string &str) { + // add a group per number + const auto tokens = Kernel::StringTokenizer( + str, ":", Kernel::StringTokenizer::TOK_TRIM | + Kernel::StringTokenizer::TOK_IGNORE_EMPTY); + if (tokens.count() != 2) + throw std::runtime_error("Malformed range (:) operation."); + Integer first = boost::lexical_cast<Integer>(tokens[0]); + Integer last = boost::lexical_cast<Integer>(tokens[1]); + if (first > last) + std::swap(first, last); + // add all the numbers in the range to separate output groups + for (Integer i = first; i <= last; ++i) { + groups.emplace_back(1, i); + } + }; + + try { + // split into comma separated groups, each group potentially containing + // an operation (+-:) that produces even more groups. + const auto tokens = StringTokenizer( + str, ",", + StringTokenizer::TOK_TRIM | StringTokenizer::TOK_IGNORE_EMPTY); + for (const auto &token : tokens) { + // Look for the various operators in the string. If one is found then + // do the necessary translation into groupings. + if (token.find('+') != std::string::npos) { + translateAdd(token); + } else if (token.find('-') != std::string::npos) { + translateSumRange(token); + } else if (token.find(':') != std::string::npos) { + translateRange(token); + } else if (!token.empty()) { + // contains a single number, just add it as a new group + groups.emplace_back(1, boost::lexical_cast<Integer>(token)); + } + } + } catch (boost::bad_lexical_cast &) { + throw std::runtime_error("Cannot parse numbers from string: '" + + str + "'"); + } + + return groups; +} + /// Extract a line from input stream, discarding any EOL characters encountered MANTID_KERNEL_DLL std::istream &extractToEOL(std::istream &is, std::string &str); diff --git a/Framework/Kernel/test/StringsTest.h b/Framework/Kernel/test/StringsTest.h index b1fcb44db56539ec59eacfa3d3035e8b242ec5bd..71b19fe68d4e4f79400aa4e76a396e3da4a957b1 100644 --- a/Framework/Kernel/test/StringsTest.h +++ b/Framework/Kernel/test/StringsTest.h @@ -487,6 +487,82 @@ public: std::string("Range boundaries are reversed: 5-1")); } + void test_parseGroups_emptyString() { + std::vector<std::vector<int>> result; + TS_ASSERT_THROWS_NOTHING(result = parseGroups<int>("")) + TS_ASSERT(result.empty()); + } + + void test_parseGroups_comma() { + std::vector<std::vector<int>> result; + TS_ASSERT_THROWS_NOTHING(result = parseGroups<int>("7,13")) + std::vector<std::vector<int>> expected{{std::vector<int>(1, 7), std::vector<int>(1, 13)}}; + TS_ASSERT_EQUALS(result, expected) + } + + void test_parseGroups_plus() { + std::vector<std::vector<int>> result; + TS_ASSERT_THROWS_NOTHING(result = parseGroups<int>("7+13")) + std::vector<std::vector<int>> expected{{std::vector<int>()}}; + expected.front().emplace_back(7); + expected.front().emplace_back(13); + TS_ASSERT_EQUALS(result, expected) + + } + + void test_parseGroups_dash() { + std::vector<std::vector<int>> result; + TS_ASSERT_THROWS_NOTHING(result = parseGroups<int>("7-13")) + std::vector<std::vector<int>> expected{{std::vector<int>()}}; + for (int i = 7; i <= 13; ++i) { + expected.front().emplace_back(i); + } + TS_ASSERT_EQUALS(result, expected) + } + + void test_parseGroups_complexExpression() { + std::vector<std::vector<int>> result; + TS_ASSERT_THROWS_NOTHING(result = parseGroups<int>("1,4+5+8,7-13,1")) + std::vector<std::vector<int>> expected; + expected.emplace_back(1, 1); + expected.emplace_back(); + expected.back().emplace_back(4); + expected.back().emplace_back(5); + expected.back().emplace_back(8); + expected.emplace_back(); + for (int i = 7; i <= 13; ++i) { + expected.back().emplace_back(i); + } + expected.emplace_back(1, 1); + TS_ASSERT_EQUALS(result, expected) + } + + void test_parseGroups_acceptsWhitespace() { + std::vector<std::vector<int>> result; + TS_ASSERT_THROWS_NOTHING(result = parseGroups<int>(" 1\t, 4 + 5\t+ 8 , 7\t- 13 ,\t1 ")) + std::vector<std::vector<int>> expected; + expected.emplace_back(1, 1); + expected.emplace_back(); + expected.back().emplace_back(4); + expected.back().emplace_back(5); + expected.back().emplace_back(8); + expected.emplace_back(); + for (int i = 7; i <= 13; ++i) { + expected.back().emplace_back(i); + } + expected.emplace_back(1, 1); + TS_ASSERT_EQUALS(result, expected) + } + + void test_parseGroups_throwsWhenInputContainsNonnumericCharacters() { + TS_ASSERT_THROWS_EQUALS(parseGroups<int>("a"), const std::runtime_error &e, e.what(), std::string("Cannot parse numbers from string: 'a'")) + } + + void test_parseGroups_throwsWhenOperationsAreInvalid() { + TS_ASSERT_THROWS_EQUALS(parseGroups<int>("-1"), const std::runtime_error &e, e.what(), std::string("Malformed range (-) operation.")) + TS_ASSERT_THROWS_EQUALS(parseGroups<int>(":1"), const std::runtime_error &e, e.what(), std::string("Malformed range (:) operation.")) + } + void test_toString_vector_of_ints() { std::vector<int> sortedInts{1, 2, 3, 5, 6, 8}; auto result = toString(sortedInts);