Advertisement
ulfben

StringCalculator

Nov 19th, 2022 (edited)
731
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C++ 11.57 KB | None | 0 0
  1. #include "pch.h"
  2. #include <algorithm>
  3. #include <charconv>
  4. #include <iostream>
  5. #include <iterator>
  6. #include <limits>
  7. #include <numeric>
  8. #include <ranges>
  9. #include <span>
  10. #include <stdexcept>
  11. #include <string>
  12. #include <string_view>
  13.  
  14. using namespace std::string_view_literals;
  15. using namespace std::string_literals;
  16. using std::string_view;
  17. using StringViews = std::vector<string_view>;
  18. using Number = int;
  19. using Numbers = std::vector<Number>;
  20.  
  21. constexpr auto DELIMITER = ","sv;
  22. constexpr auto COMMENT = "//"sv;
  23. const auto HELP_MSG_ON_NEGATIVE_INPUT = "negatives not allowed: "s;
  24. constexpr string_view WHITE_SPACE = " \f\n\r\t\v"sv;
  25. constexpr auto positives = [](auto n) noexcept { return n >= 0; };
  26. constexpr auto negatives = [](auto n) noexcept { return n < 0; };
  27. constexpr auto thousands = [](auto n) noexcept { return n > 999; };
  28.  
  29. [[nodiscard]] Number from_chars(string_view s) {
  30.   Number value;
  31.   auto res = std::from_chars(s.data(), s.data() + s.size(), value);
  32.   // These two exceptions reflect the behavior of std::stoi.
  33.   if (res.ec == std::errc::invalid_argument) {
  34.     throw std::invalid_argument("invalid_argument");
  35.   } else if (res.ec == std::errc::result_out_of_range) {
  36.     throw std::out_of_range("out_of_range");
  37.   }
  38.   return value;
  39. };
  40.  
  41. //TODO: restrict to container types / ranges.
  42. [[nodiscard]] Numbers from_chars(auto v) {
  43.   Numbers results;
  44.   results.reserve(v.size());
  45.   for (auto &sv : v) {
  46.     if (sv.empty()) {
  47.       continue;
  48.     }
  49.     results.push_back(from_chars(sv));
  50.   }
  51.   return results;
  52. }
  53.  
  54. string_view trimLeft(string_view in, string_view delims = WHITE_SPACE) noexcept {
  55.   const auto countFromTheLeft = in.find_first_not_of(delims);
  56.   in.remove_prefix(countFromTheLeft);
  57.   return in;
  58. };
  59. string_view trimRight(string_view in, string_view delims = WHITE_SPACE) noexcept {
  60.   const auto lastValidPos = in.find_last_not_of(delims);
  61.   const auto countFromTheRight = in.size() - lastValidPos - 1;
  62.   in.remove_suffix(countFromTheRight);
  63.   return in;
  64. };
  65.  
  66. string_view trim(string_view in, string_view delims = WHITE_SPACE) noexcept {
  67.   return trimRight(trimLeft(in));
  68. }
  69.  
  70. [[nodiscard]] StringViews split(string_view source, string_view delim) {
  71.   StringViews values;
  72.   std::ranges::split_view outer_view{source, delim};
  73.   for (const auto &split : outer_view) {
  74.     if (split.empty()) {
  75.       continue;
  76.     }
  77.     values.emplace_back(split.begin(), split.end());
  78.   }
  79.   return values;
  80. }
  81.  
  82. [[nodiscard]] Number sum(auto values) noexcept {
  83.   return std::reduce(std::begin(values), std::end(values));
  84. }
  85.  
  86. [[nodiscard]] bool empty(auto begin, auto end) {
  87.   return std::distance(begin, end) == 0;
  88. }
  89.  
  90. [[nodiscard]] std::string replace(string_view haystack, string_view from,
  91.                                   string_view to) {
  92.   auto clean = std::string(haystack);
  93.   if (from.empty()) {
  94.     return clean;
  95.   }
  96.   size_t pos = 0;
  97.   size_t offset = 0;
  98.   while ((pos = clean.find(from, offset)) != std::string::npos) {
  99.     clean.replace(pos, from.size(), to);
  100.     offset = pos + to.size();
  101.   }
  102.   return clean;
  103. }
  104.  
  105. // overloads to let to_string handle non-numerics
  106. std::string to_string(const std::string &value) noexcept { return value; }
  107. std::string to_string(string_view value) noexcept { return std::string(value); }
  108.  
  109. std::string join(std::input_iterator auto begin, std::input_iterator auto end,
  110.                  const std::string delim = ", "s) {
  111.   if (empty(begin, end)) {
  112.     return {};
  113.   }
  114.   using namespace std;
  115.   // std::accumulate, until we get std::experimental::make_ostream_joiner, or
  116.   // std::format::join
  117.   return std::accumulate(begin + 1, end, to_string(*begin),
  118.                          [&delim](const std::string &a, auto b) {
  119.                            return a + delim + to_string(b);
  120.                          });
  121. }
  122.  
  123. std::string join(auto container, std::string delim = ", "s) {
  124.   return join(std::begin(container), std::end(container), delim);
  125. }
  126.  
  127. void filter(Numbers &v, auto predicate) noexcept {
  128.   std::erase_if(v, predicate);
  129. }
  130.  
  131. bool contains(Numbers &v, auto predicate) {
  132.   return std::ranges::find_if(v, predicate) != v.end();
  133. }
  134.  
  135. void throwBadArguments(Numbers &v) {
  136.   auto subrange =
  137.       std::ranges::stable_partition(v, positives);
  138.   auto invalidValues = join(subrange.begin(), subrange.end());
  139.   throw std::invalid_argument(HELP_MSG_ON_NEGATIVE_INPUT +
  140.                               std::move(invalidValues));
  141. }
  142.  
  143. string_view findDelimeter(string_view &s) noexcept {
  144.   if (s.starts_with(COMMENT) == false) {
  145.     return DELIMITER;
  146.   }
  147.   s.remove_prefix(s.find_first_not_of(COMMENT));
  148.   const auto delimeterLength = s.find_first_of("0123456789");
  149.   return s.substr(0, delimeterLength);
  150. }
  151.  
  152. struct StringCalculator {
  153.   Numbers parseNumbers(string_view s) const {
  154.     if (s.empty()) {
  155.       return {};
  156.     }
  157.     const auto delim = findDelimeter(s);
  158.     const auto clean = replace(s, "\n", delim);
  159.     const auto strings = split(clean, delim);
  160.     return from_chars(strings);
  161.   }
  162.  
  163.   Number add(string_view s) const {
  164.     auto numbers = parseNumbers(s);
  165.     if (contains(numbers, negatives)) {
  166.       throwBadArguments(numbers);
  167.     }
  168.     filter(numbers, thousands);
  169.     return sum(numbers);
  170.   }
  171. };
  172.  
  173. TEST(StringCalculator, ReturnsZeroOnEmpty) {
  174.   const StringCalculator c;
  175.   EXPECT_EQ(c.add(""), 0);
  176. };
  177.  
  178. TEST(StringCalculator, ReturnsSameOnSingleNumber) {
  179.   const StringCalculator c;
  180.   EXPECT_EQ(c.add("1"), 1);
  181.   EXPECT_EQ(c.add("9"), 9);
  182.   EXPECT_EQ(c.add("878"), 878);
  183. };
  184.  
  185. TEST(StringCalculator, ThrowsOnBadInput) {
  186.   const StringCalculator c;
  187.   EXPECT_THROW(c.add("p"), std::invalid_argument);
  188.   constexpr std::size_t largerThanNumber =
  189.       std::numeric_limits<Number>::max() + std::size_t(1);
  190.   EXPECT_THROW(c.add(std::to_string(largerThanNumber)), std::out_of_range);
  191. };
  192.  
  193. TEST(StringCalculator, AddsTwoCommaDelimitedNumbers) {
  194.   const StringCalculator c;
  195.   EXPECT_EQ(c.add(",1,2"), 3);
  196.   EXPECT_EQ(c.add("1,2"), 3);
  197.   EXPECT_EQ(c.add(",1,2,"), 3);
  198. };
  199.  
  200. TEST(StringCalculator, AddsTwoNewlineDelimitedNumbers) {
  201.   const StringCalculator c;
  202.   EXPECT_EQ(c.add("\n1\n2"), 3);
  203.   EXPECT_EQ(c.add("\n1\n2\n"), 3);
  204.   EXPECT_EQ(c.add("\n"), 0);
  205. };
  206.  
  207. TEST(StringCalculator, HandlesMultipleDelimiterTypes) {
  208.   const StringCalculator c;
  209.   EXPECT_EQ(c.add("\n1\n2,3,4"), 10);
  210.   EXPECT_EQ(c.add("1,2\n3\n4"), 10);
  211.   EXPECT_EQ(c.add("\n\n1,2,\n3\n,4"), 10);
  212. };
  213.  
  214. TEST(StringCalculator, IgnoresThousandOrHigher) {
  215.   const StringCalculator c;
  216.   EXPECT_EQ(c.add("1000"), 0);
  217.   EXPECT_EQ(c.add("1001"), 0);
  218.   EXPECT_EQ(c.add("100000"), 0);
  219.   EXPECT_EQ(c.add("1,2\n3\n1000"), 6);
  220. };
  221.  
  222. TEST(StringCalculator, ThrowsOnNegatives) {
  223.   const StringCalculator c;
  224.   EXPECT_THROW(c.add("-1"), std::invalid_argument);
  225.   try {
  226.     c.add("-4,-5,7,9\n10,-4"sv);
  227.   } catch (const std::invalid_argument &e) {
  228.     std::string error(e.what());
  229.     EXPECT_EQ(HELP_MSG_ON_NEGATIVE_INPUT + "-4, -5, -4", error);
  230.   }
  231. };
  232.  
  233. TEST(StringCalculator, AcceptsCustomDelimeter) {
  234.   const StringCalculator c;
  235.   EXPECT_EQ(c.add("//p1p5p6"), 12);
  236. };
  237.  
  238. TEST(StringCalculator, AcceptsCustomLongDelimeter) {
  239.   const StringCalculator c;
  240.   EXPECT_EQ(c.add("//po1po5po6"), 12);
  241.   EXPECT_EQ(c.add("//lorem1lorem5lorem6"), 12);
  242. };
  243. TEST(join, handlesEmptyRange) {
  244.   Numbers n{};
  245.   const auto string = join(n);
  246.   EXPECT_TRUE(string.empty());
  247. }
  248. TEST(join, canJoinTwo) {
  249.   Numbers n{1, 3};
  250.   const auto string = join(n, ", ");
  251.   EXPECT_EQ(string, "1, 3"sv);
  252. }
  253. TEST(join, canJoinMany) {
  254.   Numbers n{1, 3, 7, 9, 11};
  255.   const auto string = join(n, ", ");
  256.   EXPECT_EQ(string, "1, 3, 7, 9, 11"sv);
  257. }
  258. TEST(join, canJoinStrings) {
  259.   std::vector<std::string> n{"1", "3", "7", "9", "11"};
  260.   const auto string = join(n, ", ");
  261.   ;
  262.   EXPECT_EQ(string, "1, 3, 7, 9, 11");
  263. }
  264. TEST(join, canRoundTrip) {
  265.   const auto values = "1, 3, 7, 9, 11"s;
  266.   StringViews s = split(values, DELIMITER);
  267.   const auto output = join(s.begin(), s.end(), ",");
  268.   EXPECT_EQ(output, values);
  269. }
  270.  
  271. TEST(trim, canTrimLeft) {
  272.   string_view input = "     999"sv;
  273.   EXPECT_EQ(trim(input), "999"sv);
  274.   EXPECT_EQ(trimLeft("999     "sv), "999     "sv); // don't touch the right side
  275. }
  276. TEST(trim, canTrimEmpty) {
  277.   string_view input = "999"sv;
  278.   EXPECT_EQ(trim(input), "999"sv);
  279. }
  280.  
  281. TEST(trim, canTrimPreTrimmed) {
  282.   EXPECT_EQ(trimRight(""), ""sv);
  283.  
  284.   string_view input = "999"sv;
  285.   EXPECT_EQ(trim(input), "999"sv);
  286.   EXPECT_EQ(trimRight(input), "999"sv);
  287.   EXPECT_EQ(trimLeft(input), "999"sv);
  288. }
  289. TEST(trim, canTrimRight) {
  290.   string_view input = "999     "sv;
  291.   EXPECT_EQ(trim(input), "999"sv);
  292.   EXPECT_EQ(trimRight("    999"sv), "    999"sv); // don't touch the left side
  293. }
  294.  
  295. TEST(trim, canTrimBoth) {
  296.   string_view input = "      999     "sv;
  297.   EXPECT_EQ(trim(input), "999"sv);
  298. }
  299.  
  300. TEST(trim, canTrimWithSpaceInMiddle) {
  301.   string_view input = "      9 9 9     "sv;
  302.   EXPECT_EQ(trim(input), "9 9 9"sv);
  303.  
  304.   string_view notrim = "9 9 9"sv;
  305.   EXPECT_EQ(trim(notrim), "9 9 9"sv);
  306. }
  307.  
  308. TEST(split, handlesNoDelimeter) {
  309.   const auto values = split("123456", ",");
  310.   EXPECT_EQ(values.size(), 1);
  311. }
  312. TEST(split, canSplitTwo) {
  313.   const auto values = split("1,2", ",");
  314.   EXPECT_EQ(values.size(), 2);
  315. }
  316. TEST(split, canSplitMany) {
  317.   const auto values = split("1,2,3,4,5", ",");
  318.   EXPECT_EQ(values.size(), 5);
  319.   EXPECT_TRUE("4" == values[3]);
  320. }
  321. TEST(split, ignoresLeadingDelimeter) {
  322.   const auto values = split(",12", ",");
  323.   EXPECT_EQ(values.size(), 1);
  324.   EXPECT_TRUE("12" == values[0]);
  325. }
  326. TEST(split, ignoresTrailingDelimeter) {
  327.   const auto values = split("12,", ",");
  328.   EXPECT_EQ(values.size(), 1);
  329.   EXPECT_TRUE("12" == values[0]);
  330. }
  331. TEST(split, ignoresEmptyDelimeters) {
  332.   auto values = split("1,,,,,2", ",");
  333.   EXPECT_EQ(values.size(), 2);
  334.   EXPECT_TRUE("2" == values[1]);
  335.  
  336.   values = split(",,,,,", ",");
  337.   EXPECT_TRUE(values.empty());
  338. }
  339.  
  340. TEST(replace, canHandleMissingNeedle) {
  341.   const auto goal = "em-em-em"sv;
  342.   const auto haystack = "em-em-em"sv;
  343.   const auto needle = "lor"sv; // is not in the haystack!
  344.   const auto replacement = ""sv;
  345.   const auto cleaned = replace(haystack, needle, replacement);
  346.   EXPECT_EQ(cleaned, goal);
  347. }
  348. TEST(replace, canReplaceSingleChar) {
  349.   const auto goal = "lorem-ipsum-dolorer"sv;
  350.   const auto haystack = "lorem ipsum dolorer"sv;
  351.   const auto needle = " "sv;
  352.   const auto replacement = "-"sv;
  353.   const auto cleaned = replace(haystack, needle, replacement);
  354.   EXPECT_EQ(cleaned, goal);
  355. }
  356.  
  357. TEST(replace, canReplaceNewline) {
  358.   const auto goal = "em,em,em"sv;
  359.   const auto haystack = "em\nem\nem"sv;
  360.   const auto needle = "\n"sv;
  361.   const auto replacement = ","sv;
  362.   const auto cleaned = replace(haystack, needle, replacement);
  363.   EXPECT_EQ(cleaned, goal);
  364. }
  365. TEST(replace, canReplaceSubstrings) {
  366.   const auto goal = "em-em-em"sv;
  367.   const auto haystack = "lorem-lorem-lorem"sv;
  368.   const auto needle = "lor"sv;
  369.   const auto replacement = ""sv;
  370.   const auto cleaned = replace(haystack, needle, replacement);
  371.   EXPECT_EQ(cleaned, goal);
  372. }
  373.  
  374. TEST(replace, canReplaceWithLonger) {
  375.   const auto goal = "ipsum-ipsum-ipsum"sv;
  376.   const auto haystack = "em-em-em"sv;
  377.   const auto needle = "em"sv;
  378.   const auto replacement = "ipsum"sv; // replacement is longer than needle.
  379.   const auto cleaned = replace(haystack, needle, replacement);
  380.   EXPECT_EQ(cleaned, goal);
  381. }
  382.  
  383. TEST(replace, handlesReplacemntContainsNeedle) {
  384.   const auto goal = "lorem-lorem-lorem"sv;
  385.   const auto haystack = "em-em-em"sv;
  386.   const auto needle = "em"sv;
  387.   const auto replacement =
  388.       "lorem"sv; // the replacement contains the needle, "em"
  389.   const auto cleaned = replace(haystack, needle, replacement);
  390.   EXPECT_EQ(cleaned, std::string(goal));
  391. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement