#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
0 B
|👍
/👎