#include "pch.h" #include #include #include #include #include #include #include #include #include #include #include using namespace std::string_view_literals; using namespace std::string_literals; using std::string_view; using StringViews = std::vector; using Number = int; using Numbers = std::vector; constexpr auto DELIMITER = ","sv; constexpr auto COMMENT = "//"sv; const auto HELP_MSG_ON_NEGATIVE_INPUT = "negatives not allowed: "s; constexpr string_view WHITE_SPACE = " \f\n\r\t\v"sv; constexpr auto positives = [](auto n) noexcept { return n >= 0; }; constexpr auto negatives = [](auto n) noexcept { return n < 0; }; constexpr auto thousands = [](auto n) noexcept { return n > 999; }; [[nodiscard]] Number from_chars(string_view s) { Number value; auto res = std::from_chars(s.data(), s.data() + s.size(), value); // These two exceptions reflect the behavior of std::stoi. if (res.ec == std::errc::invalid_argument) { throw std::invalid_argument("invalid_argument"); } else if (res.ec == std::errc::result_out_of_range) { throw std::out_of_range("out_of_range"); } return value; }; //TODO: restrict to container types / ranges. [[nodiscard]] Numbers from_chars(auto v) { Numbers results; results.reserve(v.size()); for (auto &sv : v) { if (sv.empty()) { continue; } results.push_back(from_chars(sv)); } return results; } string_view trimLeft(string_view in, string_view delims = WHITE_SPACE) noexcept { const auto countFromTheLeft = in.find_first_not_of(delims); in.remove_prefix(countFromTheLeft); return in; }; string_view trimRight(string_view in, string_view delims = WHITE_SPACE) noexcept { const auto lastValidPos = in.find_last_not_of(delims); const auto countFromTheRight = in.size() - lastValidPos - 1; in.remove_suffix(countFromTheRight); return in; }; string_view trim(string_view in, string_view delims = WHITE_SPACE) noexcept { return trimRight(trimLeft(in)); } size_t count(std::string_view str, std::string_view sub) { if (sub.empty()) { return 0; } size_t count = 0; size_t pos = 0; while ((pos = str.find(sub, pos)) != std::string_view::npos) { ++count; pos += sub.length(); // Move past the current occurrence } return count; } /*[[nodiscard]] StringViews split(string_view source, string_view delim) { StringViews values; std::ranges::split_view outer_view{source, delim}; for (const auto &split : outer_view) { if (split.empty()) { continue; } values.emplace_back(split.begin(), split.end()); } return values; }*/ [[nodiscard]] StringViews split(std::string_view in, std::string_view sep) { std::vector result; if (sep.empty()) { result.emplace_back(in); return result; } result.reserve(count(in, sep) + 1); size_t startingFrom = 0; size_t end; while ((end = in.find(sep, startingFrom)) != std::string_view::npos) { result.emplace_back(in.substr(startingFrom, end - startingFrom)); startingFrom = end + sep.size(); } result.emplace_back(in.substr(startingFrom)); return result; } [[nodiscard]] Number sum(auto values) noexcept { return std::reduce(std::begin(values), std::end(values)); } [[nodiscard]] bool empty(auto begin, auto end) { return std::distance(begin, end) == 0; } [[nodiscard]] std::string replace(string_view haystack, string_view from, string_view to) { auto clean = std::string(haystack); if (from.empty()) { return clean; } size_t pos = 0; size_t offset = 0; while ((pos = clean.find(from, offset)) != std::string::npos) { clean.replace(pos, from.size(), to); offset = pos + to.size(); } return clean; } // overloads to let to_string handle non-numerics std::string to_string(const std::string &value) noexcept { return value; } std::string to_string(string_view value) noexcept { return std::string(value); } std::string join(std::input_iterator auto begin, std::input_iterator auto end, const std::string delim = ", "s) { if (empty(begin, end)) { return {}; } using namespace std; // std::accumulate, until we get std::experimental::make_ostream_joiner, or // std::format::join return std::accumulate(begin + 1, end, to_string(*begin), [&delim](const std::string &a, auto b) { return a + delim + to_string(b); }); } std::string join(auto container, std::string delim = ", "s) { return join(std::begin(container), std::end(container), delim); } void filter(Numbers &v, auto predicate) noexcept { std::erase_if(v, predicate); } bool contains(Numbers &v, auto predicate) { return std::ranges::find_if(v, predicate) != v.end(); } void throwBadArguments(Numbers &v) { auto subrange = std::ranges::stable_partition(v, positives); auto invalidValues = join(subrange.begin(), subrange.end()); throw std::invalid_argument(HELP_MSG_ON_NEGATIVE_INPUT + std::move(invalidValues)); } string_view findDelimeter(string_view &s) noexcept { if (s.starts_with(COMMENT) == false) { return DELIMITER; } s.remove_prefix(s.find_first_not_of(COMMENT)); const auto delimeterLength = s.find_first_of("0123456789"); return s.substr(0, delimeterLength); } struct StringCalculator { Numbers parseNumbers(string_view s) const { if (s.empty()) { return {}; } const auto delim = findDelimeter(s); const auto clean = replace(s, "\n", delim); const auto strings = split(clean, delim); return from_chars(strings); } Number add(string_view s) const { auto numbers = parseNumbers(s); if (contains(numbers, negatives)) { throwBadArguments(numbers); } filter(numbers, thousands); return sum(numbers); } }; TEST(StringCalculator, ReturnsZeroOnEmpty) { const StringCalculator c; EXPECT_EQ(c.add(""), 0); }; TEST(StringCalculator, ReturnsSameOnSingleNumber) { const StringCalculator c; EXPECT_EQ(c.add("1"), 1); EXPECT_EQ(c.add("9"), 9); EXPECT_EQ(c.add("878"), 878); }; TEST(StringCalculator, ThrowsOnBadInput) { const StringCalculator c; EXPECT_THROW(c.add("p"), std::invalid_argument); constexpr std::size_t largerThanNumber = std::numeric_limits::max() + std::size_t(1); EXPECT_THROW(c.add(std::to_string(largerThanNumber)), std::out_of_range); }; TEST(StringCalculator, AddsTwoCommaDelimitedNumbers) { const StringCalculator c; EXPECT_EQ(c.add(",1,2"), 3); EXPECT_EQ(c.add("1,2"), 3); EXPECT_EQ(c.add(",1,2,"), 3); }; TEST(StringCalculator, AddsTwoNewlineDelimitedNumbers) { const StringCalculator c; EXPECT_EQ(c.add("\n1\n2"), 3); EXPECT_EQ(c.add("\n1\n2\n"), 3); EXPECT_EQ(c.add("\n"), 0); }; TEST(StringCalculator, HandlesMultipleDelimiterTypes) { const StringCalculator c; EXPECT_EQ(c.add("\n1\n2,3,4"), 10); EXPECT_EQ(c.add("1,2\n3\n4"), 10); EXPECT_EQ(c.add("\n\n1,2,\n3\n,4"), 10); }; TEST(StringCalculator, IgnoresThousandOrHigher) { const StringCalculator c; EXPECT_EQ(c.add("1000"), 0); EXPECT_EQ(c.add("1001"), 0); EXPECT_EQ(c.add("100000"), 0); EXPECT_EQ(c.add("1,2\n3\n1000"), 6); }; TEST(StringCalculator, ThrowsOnNegatives) { const StringCalculator c; EXPECT_THROW(c.add("-1"), std::invalid_argument); try { c.add("-4,-5,7,9\n10,-4"sv); } catch (const std::invalid_argument &e) { std::string error(e.what()); EXPECT_EQ(HELP_MSG_ON_NEGATIVE_INPUT + "-4, -5, -4", error); } }; TEST(StringCalculator, AcceptsCustomDelimeter) { const StringCalculator c; EXPECT_EQ(c.add("//p1p5p6"), 12); }; TEST(StringCalculator, AcceptsCustomLongDelimeter) { const StringCalculator c; EXPECT_EQ(c.add("//po1po5po6"), 12); EXPECT_EQ(c.add("//lorem1lorem5lorem6"), 12); }; TEST(join, handlesEmptyRange) { Numbers n{}; const auto string = join(n); EXPECT_TRUE(string.empty()); } TEST(join, canJoinTwo) { Numbers n{1, 3}; const auto string = join(n, ", "); EXPECT_EQ(string, "1, 3"sv); } TEST(join, canJoinMany) { Numbers n{1, 3, 7, 9, 11}; const auto string = join(n, ", "); EXPECT_EQ(string, "1, 3, 7, 9, 11"sv); } TEST(join, canJoinStrings) { std::vector n{"1", "3", "7", "9", "11"}; const auto string = join(n, ", "); ; EXPECT_EQ(string, "1, 3, 7, 9, 11"); } TEST(join, canRoundTrip) { const auto values = "1, 3, 7, 9, 11"s; StringViews s = split(values, DELIMITER); const auto output = join(s.begin(), s.end(), ","); EXPECT_EQ(output, values); } TEST(trim, canTrimLeft) { string_view input = " 999"sv; EXPECT_EQ(trim(input), "999"sv); EXPECT_EQ(trimLeft("999 "sv), "999 "sv); // don't touch the right side } TEST(trim, canTrimEmpty) { string_view input = "999"sv; EXPECT_EQ(trim(input), "999"sv); } TEST(trim, canTrimPreTrimmed) { EXPECT_EQ(trimRight(""), ""sv); string_view input = "999"sv; EXPECT_EQ(trim(input), "999"sv); EXPECT_EQ(trimRight(input), "999"sv); EXPECT_EQ(trimLeft(input), "999"sv); } TEST(trim, canTrimRight) { string_view input = "999 "sv; EXPECT_EQ(trim(input), "999"sv); EXPECT_EQ(trimRight(" 999"sv), " 999"sv); // don't touch the left side } TEST(trim, canTrimBoth) { string_view input = " 999 "sv; EXPECT_EQ(trim(input), "999"sv); } TEST(trim, canTrimWithSpaceInMiddle) { string_view input = " 9 9 9 "sv; EXPECT_EQ(trim(input), "9 9 9"sv); string_view notrim = "9 9 9"sv; EXPECT_EQ(trim(notrim), "9 9 9"sv); } TEST(split, handlesNoDelimeter) { const auto values = split("123456", ","); EXPECT_EQ(values.size(), 1); } TEST(split, canSplitTwo) { const auto values = split("1,2", ","); EXPECT_EQ(values.size(), 2); } TEST(split, canSplitMany) { const auto values = split("1,2,3,4,5", ","); EXPECT_EQ(values.size(), 5); EXPECT_TRUE("4" == values[3]); } TEST(split, ignoresLeadingDelimeter) { const auto values = split(",12", ","); EXPECT_EQ(values.size(), 1); EXPECT_TRUE("12" == values[0]); } TEST(split, ignoresTrailingDelimeter) { const auto values = split("12,", ","); EXPECT_EQ(values.size(), 1); EXPECT_TRUE("12" == values[0]); } TEST(split, ignoresEmptyDelimeters) { auto values = split("1,,,,,2", ","); EXPECT_EQ(values.size(), 2); EXPECT_TRUE("2" == values[1]); values = split(",,,,,", ","); EXPECT_TRUE(values.empty()); } TEST(replace, canHandleMissingNeedle) { const auto goal = "em-em-em"sv; const auto haystack = "em-em-em"sv; const auto needle = "lor"sv; // is not in the haystack! const auto replacement = ""sv; const auto cleaned = replace(haystack, needle, replacement); EXPECT_EQ(cleaned, goal); } TEST(replace, canReplaceSingleChar) { const auto goal = "lorem-ipsum-dolorer"sv; const auto haystack = "lorem ipsum dolorer"sv; const auto needle = " "sv; const auto replacement = "-"sv; const auto cleaned = replace(haystack, needle, replacement); EXPECT_EQ(cleaned, goal); } TEST(replace, canReplaceNewline) { const auto goal = "em,em,em"sv; const auto haystack = "em\nem\nem"sv; const auto needle = "\n"sv; const auto replacement = ","sv; const auto cleaned = replace(haystack, needle, replacement); EXPECT_EQ(cleaned, goal); } TEST(replace, canReplaceSubstrings) { const auto goal = "em-em-em"sv; const auto haystack = "lorem-lorem-lorem"sv; const auto needle = "lor"sv; const auto replacement = ""sv; const auto cleaned = replace(haystack, needle, replacement); EXPECT_EQ(cleaned, goal); } TEST(replace, canReplaceWithLonger) { const auto goal = "ipsum-ipsum-ipsum"sv; const auto haystack = "em-em-em"sv; const auto needle = "em"sv; const auto replacement = "ipsum"sv; // replacement is longer than needle. const auto cleaned = replace(haystack, needle, replacement); EXPECT_EQ(cleaned, goal); } TEST(replace, handlesReplacemntContainsNeedle) { const auto goal = "lorem-lorem-lorem"sv; const auto haystack = "em-em-em"sv; const auto needle = "em"sv; const auto replacement = "lorem"sv; // the replacement contains the needle, "em" const auto cleaned = replace(haystack, needle, replacement); EXPECT_EQ(cleaned, std::string(goal)); }