summaryrefslogtreecommitdiff
path: root/include/fud_string_convert.hpp
diff options
context:
space:
mode:
authorDominick Allen <djallen@librehumanitas.org>2024-10-23 13:21:10 -0500
committerDominick Allen <djallen@librehumanitas.org>2024-10-23 13:21:10 -0500
commit5cc7cbc3704ec255eb5d0ac53b2cc0fcb1221d63 (patch)
tree169d4d2d8dffe014851712e31a55036deb0c7c0c /include/fud_string_convert.hpp
parentb2dbcb55e2832c373fecb4033a3ed77e5dbc77aa (diff)
String conversion and parsing format spec.
Diffstat (limited to 'include/fud_string_convert.hpp')
-rw-r--r--include/fud_string_convert.hpp456
1 files changed, 456 insertions, 0 deletions
diff --git a/include/fud_string_convert.hpp b/include/fud_string_convert.hpp
new file mode 100644
index 0000000..597c6a9
--- /dev/null
+++ b/include/fud_string_convert.hpp
@@ -0,0 +1,456 @@
+/*
+ * libfud
+ * Copyright 2024 Dominick Allen
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef FUD_STRING_CONVERT_HPP
+#define FUD_STRING_CONVERT_HPP
+
+#include "fud_algorithm.hpp"
+#include "fud_option.hpp"
+#include "fud_result.hpp"
+#include "fud_status.hpp"
+#include "fud_string.hpp"
+#include "fud_string_view.hpp"
+
+#include <cmath>
+#include <cstdint>
+
+namespace fud {
+
+enum class Radix : uint8_t
+{
+ Binary = 2,
+ Octal = 8,
+ Decimal = 10,
+ Hexadecimal = 16,
+};
+
+constexpr uint8_t RadixMax = 36;
+
+template <typename T>
+struct ConvertValue {
+ size_t nextIndex;
+ T value;
+};
+
+template <typename T>
+using StringConvertResult = Result<ConvertValue<T>, FudStatus>;
+
+template <typename T>
+StringConvertResult<T> fromString(StringView inputView, Option<uint8_t> specifiedRadixOption = NullOpt);
+
+template <typename T>
+StringConvertResult<T> fromString(StringView inputView, Radix specifiedRadixOption);
+
+template <typename T>
+StringConvertResult<T> fromString(const String& inputView, Option<uint8_t> specifiedRadixOption = NullOpt);
+
+namespace impl {
+constexpr Array<int8_t, 256> AsciiLookup{
+ {-1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
+ -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
+ -2, -2, -2, -2, -2, -2, -2, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
+ 32, 33, 34, 35, -2, -2, -2, -2, -2, -2, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28,
+ 29, 30, 31, 32, 33, 34, 35, -2, -2, -2, -2, -2, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3,
+ -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3,
+ -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3,
+ -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3,
+ -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3}};
+
+// NOLINTBEGIN(readability-magic-numbers)
+static_assert(AsciiLookup[static_cast<size_t>('0')] == 0);
+static_assert(AsciiLookup[static_cast<size_t>('1')] == 1);
+static_assert(AsciiLookup[static_cast<size_t>('2')] == 2);
+static_assert(AsciiLookup[static_cast<size_t>('3')] == 3);
+static_assert(AsciiLookup[static_cast<size_t>('4')] == 4);
+static_assert(AsciiLookup[static_cast<size_t>('5')] == 5);
+static_assert(AsciiLookup[static_cast<size_t>('6')] == 6);
+static_assert(AsciiLookup[static_cast<size_t>('7')] == 7);
+static_assert(AsciiLookup[static_cast<size_t>('8')] == 8);
+static_assert(AsciiLookup[static_cast<size_t>('9')] == 9);
+static_assert(AsciiLookup[static_cast<size_t>('a')] == 10);
+static_assert(AsciiLookup[static_cast<size_t>('b')] == 11);
+static_assert(AsciiLookup[static_cast<size_t>('c')] == 12);
+static_assert(AsciiLookup[static_cast<size_t>('d')] == 13);
+static_assert(AsciiLookup[static_cast<size_t>('e')] == 14);
+static_assert(AsciiLookup[static_cast<size_t>('f')] == 15);
+static_assert(AsciiLookup[static_cast<size_t>('A')] == 10);
+static_assert(AsciiLookup[static_cast<size_t>('B')] == 11);
+static_assert(AsciiLookup[static_cast<size_t>('C')] == 12);
+static_assert(AsciiLookup[static_cast<size_t>('D')] == 13);
+static_assert(AsciiLookup[static_cast<size_t>('E')] == 14);
+static_assert(AsciiLookup[static_cast<size_t>('F')] == 15);
+static_assert(AsciiLookup[127] == -2);
+static_assert(AsciiLookup[128] == -3);
+// NOLINTEND(readability-magic-numbers)
+
+FudStatus checkPlusSigned(StringView& view, size_t& skipIndex);
+
+Result<Radix, FudStatus> determineRadix(StringView input, size_t& index);
+
+Result<bool, FudStatus> checkNegative(StringView& view, size_t& skipIndex);
+
+Result<uint8_t, FudStatus> getRadix(StringView& view, size_t& skipIndex, Option<uint8_t> specifiedRadixOption);
+
+template <typename T>
+StringConvertResult<T> unsignedFromString(StringView nextView, size_t skipIndex, Option<uint8_t> specifiedRadixOption)
+{
+ static_assert(std::is_unsigned_v<T> && std::is_integral_v<T>);
+ auto status = checkPlusSigned(nextView, skipIndex);
+ if (status != FudStatus::Success) {
+ return FudStatus::ArgumentInvalid;
+ }
+
+ auto radixResult = impl::getRadix(nextView, skipIndex, specifiedRadixOption);
+ if (radixResult.isError()) {
+ return radixResult.takeError();
+ }
+ auto radix = radixResult.takeOkay();
+
+ T num = 0;
+ size_t digitIndex = 0;
+
+ while (digitIndex < nextView.length()) {
+ auto digitResult = impl::AsciiLookup[nextView.data()[digitIndex]];
+ if (digitResult >= radix || digitResult < 0) {
+ break;
+ }
+
+ auto digit = static_cast<uint8_t>(digitResult);
+ if (std::numeric_limits<T>::max() / radix < num) {
+ return FudStatus::RangeError;
+ }
+ num *= radix;
+ if (std::numeric_limits<T>::max() - digit < num) {
+ return FudStatus::RangeError;
+ }
+ num += digit;
+ digitIndex++;
+ }
+ if (digitIndex < 1) {
+ return FudStatus::ArgumentInvalid;
+ }
+
+ return ConvertValue{skipIndex + digitIndex, num};
+}
+
+template <typename T>
+FudStatus signedPositiveFromString(StringView view, uint8_t radix, size_t& digitIndex, T& num)
+{
+ digitIndex = 0;
+ while (digitIndex < view.length()) {
+ int8_t digitResult = impl::AsciiLookup[view.data()[digitIndex]];
+ if (digitResult >= radix || digitResult < 0) {
+ break;
+ }
+
+ auto digit = static_cast<uint8_t>(digitResult);
+ if (std::numeric_limits<T>::max() / radix < num) {
+ return FudStatus::RangeError;
+ }
+ num = static_cast<T>(num * radix);
+ if (std::numeric_limits<T>::max() - digit < num) {
+ return FudStatus::RangeError;
+ }
+ num = static_cast<T>(num + digit);
+ digitIndex++;
+ }
+
+ return FudStatus::Success;
+}
+
+template <typename T>
+FudStatus signedNegativeFromString(StringView view, uint8_t radix, size_t& digitIndex, T& num)
+{
+ digitIndex = 0;
+ while (digitIndex < view.length()) {
+ int8_t digitResult = impl::AsciiLookup[view.data()[digitIndex]];
+ if (digitResult >= radix || digitResult < 0) {
+ break;
+ }
+
+ auto digit = static_cast<uint8_t>(digitResult);
+ if ((std::numeric_limits<T>::min() / radix > num)) {
+ return FudStatus::RangeError;
+ }
+ num = static_cast<T>(num * radix);
+ if (std::numeric_limits<T>::min() + digit > num) {
+ return FudStatus::RangeError;
+ }
+ num = static_cast<T>(num - digit);
+ digitIndex++;
+ }
+
+ return FudStatus::Success;
+}
+
+template <typename T>
+StringConvertResult<T> signedFromString(StringView nextView, size_t skipIndex, Option<uint8_t> specifiedRadixOption)
+{
+ static_assert(std::is_signed_v<T> && std::is_integral_v<T>);
+ auto status = impl::checkPlusSigned(nextView, skipIndex);
+ if (status != FudStatus::Success) {
+ return FudStatus::ArgumentInvalid;
+ }
+
+ auto radixResult = impl::getRadix(nextView, skipIndex, specifiedRadixOption);
+ if (radixResult.isError()) {
+ return radixResult.takeError();
+ }
+ auto radix = radixResult.takeOkay();
+
+ T num = 0;
+ size_t digitIndex = 0;
+
+ auto isNegativeResult = checkNegative(nextView, skipIndex);
+ if (isNegativeResult.isError()) {
+ return isNegativeResult.takeError();
+ }
+ const auto isNegative = isNegativeResult.takeOkay();
+
+ if (isNegative) {
+ status = signedNegativeFromString(nextView, radix, digitIndex, num);
+ } else {
+ status = signedPositiveFromString(nextView, radix, digitIndex, num);
+ }
+
+ if (status != FudStatus::Success) {
+ return status;
+ }
+
+ if (digitIndex < 1) {
+ return FudStatus::ArgumentInvalid;
+ }
+
+ return ConvertValue{skipIndex + digitIndex, num};
+}
+
+template <typename T>
+bool isNanOrInf(T& num, StringView& view, T& sign, size_t& digitIndex)
+{
+ if (view.length() >= 3) {
+ Array<uint8_t, 3> letters{{view.data()[0], view.data()[1], view.data()[2]}};
+ forEach(letters.span(), charToLower);
+ if (letters[0] == 'i' && letters[1] == 'n' && letters[2] == 'f') {
+ num = sign * std::numeric_limits<T>::infinity();
+ digitIndex = 3;
+ return true;
+ }
+ if (letters[0] == 'n' && letters[1] == 'a' && letters[2] == 'n') {
+ num = std::numeric_limits<T>::quiet_NaN();
+ digitIndex = 3;
+ return true;
+ }
+ }
+ return false;
+}
+
+template <typename T>
+FudStatus getWhole(
+ const StringView view,
+ size_t& digitIndex,
+ T& num,
+ T sign,
+ uint8_t radix,
+ bool& foundDecimal,
+ bool& foundExponent)
+{
+ while (digitIndex < view.length()) {
+ auto nextChar = view.data()[digitIndex];
+ if (nextChar == '.') {
+ foundDecimal = true;
+ digitIndex++;
+ break;
+ }
+
+ if (radix == static_cast<uint8_t>(Radix::Decimal) && (nextChar == 'e' || nextChar == 'E')) {
+ foundExponent = true;
+ digitIndex++;
+ break;
+ }
+
+ auto digitResult = impl::AsciiLookup[nextChar];
+ if (digitResult >= radix) {
+ return FudStatus::ArgumentInvalid;
+ }
+ if (digitResult < 0) {
+ break;
+ }
+ auto digit = static_cast<T>(digitResult) * sign;
+ num *= static_cast<T>(radix);
+
+ num += digit;
+ digitIndex++;
+ }
+ return FudStatus::Success;
+}
+
+template <typename T>
+FudStatus getExponent(StringView view, size_t& digitIndex, T& num, uint8_t radix)
+{
+ StringView tempView{view.length() - digitIndex, view.data() + digitIndex};
+ size_t exponentLength{};
+ auto convertResult = signedFromString<int32_t>(tempView, exponentLength, static_cast<uint8_t>(Radix::Decimal));
+ if (convertResult.isError()) {
+ return convertResult.takeError();
+ }
+ auto convertValue = convertResult.takeOkay();
+ digitIndex += convertValue.nextIndex + exponentLength;
+ num = num * std::pow(static_cast<T>(radix), static_cast<T>(convertValue.value));
+ return FudStatus::Success;
+}
+
+template <typename T>
+FudStatus getFraction(const StringView view, size_t& digitIndex, T& num, T sign, uint8_t radix, bool& foundExponent)
+{
+ auto radixDiv = static_cast<T>(1) / static_cast<T>(radix);
+ while (digitIndex < view.length()) {
+ auto nextChar = view.data()[digitIndex];
+ if (radix == static_cast<uint8_t>(Radix::Decimal) && (nextChar == 'e' || nextChar == 'E')) {
+ foundExponent = true;
+ digitIndex++;
+ break;
+ }
+
+ auto digitResult = impl::AsciiLookup[nextChar];
+ if (digitResult >= radix) {
+ return FudStatus::ArgumentInvalid;
+ }
+ if (digitResult < 0) {
+ break;
+ }
+ auto digit = static_cast<T>(digitResult) * sign;
+ num += digit * radixDiv;
+ radixDiv /= static_cast<T>(radix);
+ digitIndex++;
+ }
+ return FudStatus::Success;
+}
+
+template <typename T>
+StringConvertResult<T> floatFromString(StringView nextView, size_t skipIndex, Option<uint8_t> specifiedRadixOption)
+{
+ static_assert(std::is_floating_point_v<T>);
+ if (nextView.length() < 1) {
+ return FudStatus::ArgumentInvalid;
+ }
+
+ auto isNegativeResult = checkNegative(nextView, skipIndex);
+ if (isNegativeResult.isError()) {
+ return isNegativeResult.takeError();
+ }
+ const auto isNegative = isNegativeResult.takeOkay();
+
+ if (!isNegative) {
+ auto status = checkPlusSigned(nextView, skipIndex);
+ if (status != FudStatus::Success) {
+ return FudStatus::ArgumentInvalid;
+ }
+ }
+ T sign = isNegative ? -1.0 : 1.0;
+
+ T num = 0;
+ size_t digitIndex = 0;
+
+ auto retSuccess = [&]() { return ConvertValue{skipIndex + digitIndex, num}; };
+
+ if (impl::isNanOrInf(num, nextView, sign, digitIndex)) {
+ return retSuccess();
+ }
+
+ auto radixResult = impl::getRadix(nextView, skipIndex, specifiedRadixOption);
+ if (radixResult.isError()) {
+ return radixResult.takeError();
+ }
+ auto radix = radixResult.takeOkay();
+
+ bool foundDecimal = false;
+ bool foundExponent = false;
+ auto status = getWhole(nextView, digitIndex, num, sign, radix, foundDecimal, foundExponent);
+
+ if (status == FudStatus::Success && foundExponent) {
+ status = getExponent(nextView, digitIndex, num, radix);
+ }
+
+ if (status != FudStatus::Success) {
+ return status;
+ }
+
+ if (!foundDecimal) {
+ if (digitIndex < 1) {
+ return FudStatus::ArgumentInvalid;
+ }
+
+ return retSuccess();
+ }
+
+ status = getFraction(nextView, digitIndex, num, sign, radix, foundExponent);
+
+ if (foundExponent) {
+ status = getExponent(nextView, digitIndex, num, radix);
+ if (status != FudStatus::Success) {
+ return status;
+ }
+ }
+
+ if (digitIndex < 1) {
+ return FudStatus::ArgumentInvalid;
+ }
+
+ if (std::isinf(num) || std::isnan(num)) // isnan is dubious here - likely unreachable
+ {
+ return FudStatus::RangeError;
+ }
+
+ return retSuccess();
+}
+
+} // namespace impl
+
+template <typename T>
+StringConvertResult<T> fromString(StringView inputView, Radix specifiedRadixOption)
+{
+ return fromString<T>(inputView, static_cast<uint8_t>(specifiedRadixOption));
+}
+
+template <typename T>
+StringConvertResult<T> fromString(StringView inputView, Option<uint8_t> specifiedRadixOption)
+{
+ if (inputView.data() == nullptr) {
+ return FudStatus::NullPointer;
+ }
+
+ StringView nextView{inputView};
+ auto skipResult = nextView.skipWhitespace();
+ if (skipResult.isError()) {
+ return skipResult.takeError();
+ }
+ size_t skipIndex = skipResult.takeOkay();
+
+ if constexpr (std::is_unsigned_v<T> && std::is_integral_v<T>) {
+ return impl::unsignedFromString<T>(nextView, skipIndex, specifiedRadixOption);
+ } else if constexpr (std::is_signed_v<T> && std::is_integral_v<T>) {
+ return impl::signedFromString<T>(nextView, skipIndex, specifiedRadixOption);
+ } else if constexpr (std::is_floating_point_v<T>) {
+ return impl::floatFromString<T>(nextView, skipIndex, specifiedRadixOption);
+ } else {
+ return FudStatus::NotImplemented;
+ }
+}
+
+} // namespace fud
+
+#endif