ulfben icon

StringCalculator

ulfben | PRO | 11/22/23 09:29:08 AM UTC (Edited) | 0 ⭐ | 1478 👁️ | Never ⏰ | []
C++ |

12.47 KB

|

None

|

0 👍

/

0 👎

#include "pch.h"
#include <algorithm>
#include <charconv>
#include <iostream>
#include <iterator>
#include <limits>
#include <numeric>
#include <ranges>
#include <span>
#include <stdexcept>
#include <string>
#include <string_view>
 
using namespace std::string_view_literals;
using namespace std::string_literals;
using std::string_view;
using StringViews = std::vector<string_view>;
using Number = int;
using Numbers = std::vector<Number>;
 
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<std::string_view> 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<Number>::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<std::string> 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));
}

Comments

  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎