From d8803880f165825a9dec955ba079a1acbe64ece5 Mon Sep 17 00:00:00 2001 From: Redstone1024 <2824517378@qq.com> Date: Fri, 8 Nov 2024 20:46:57 +0800 Subject: [PATCH] feat(string): add parsing of arithmetic types from strings --- .../Source/Private/Testing/StringTesting.cpp | 107 ++++++++++ .../Source/Public/String/Conversion.h.inl | 191 ++++++++++++++++++ .../Source/Public/Testing/StringTesting.h | 1 + 3 files changed, 299 insertions(+) diff --git a/Redcraft.Utility/Source/Private/Testing/StringTesting.cpp b/Redcraft.Utility/Source/Private/Testing/StringTesting.cpp index 8621392..6bcb1fb 100644 --- a/Redcraft.Utility/Source/Private/Testing/StringTesting.cpp +++ b/Redcraft.Utility/Source/Private/Testing/StringTesting.cpp @@ -6,6 +6,8 @@ #include "String/StringView.h" #include "Miscellaneous/AssertionMacros.h" +#include + NAMESPACE_REDCRAFT_BEGIN NAMESPACE_MODULE_BEGIN(Redcraft) NAMESPACE_MODULE_BEGIN(Utility) @@ -17,6 +19,7 @@ void TestString() TestChar(); TestStringView(); TestTemplateString(); + TestStringConversion(); } void TestChar() @@ -553,6 +556,110 @@ void TestTemplateString() Test(InPlaceType); } +void TestStringConversion() +{ + auto Test = [](TInPlaceType) + { + auto CheckParseArithmetic = [](TStringView View, U Result) + { + U Object; + + always_check(View.Parse(LITERAL(T, "{0:}"), Object) == 1); + + if constexpr (CFloatingPoint) + { + always_check(NAMESPACE_STD::isinf(Result) == NAMESPACE_STD::isinf(Object)); + always_check(NAMESPACE_STD::isnan(Result) == NAMESPACE_STD::isnan(Object)); + + always_check(NAMESPACE_STD::signbit(Result) == NAMESPACE_STD::signbit(Object)); + + if (NAMESPACE_STD::isinf(Result) || NAMESPACE_STD::isnan(Result)) return; + + constexpr auto Epsilon = 1e-3; + + always_check(NAMESPACE_STD::abs(Object - Result) < Epsilon); + } + else always_check(Object == Result); + }; + + CheckParseArithmetic(LITERAL(T, "true" ), true ); + CheckParseArithmetic(LITERAL(T, "false"), false); + + auto CheckParseInt = [&](TInPlaceType) + { + CheckParseArithmetic(LITERAL(T, "+0"), static_cast(+0.0)); + CheckParseArithmetic(LITERAL(T, " 0"), static_cast( 0.0)); + CheckParseArithmetic(LITERAL(T, "-0"), static_cast(-0.0)); + + CheckParseArithmetic(LITERAL(T, "+42"), static_cast( +42)); + CheckParseArithmetic(LITERAL(T, "+052"), static_cast( +052)); + CheckParseArithmetic(LITERAL(T, "+0x2A"), static_cast( +0x2A)); + CheckParseArithmetic(LITERAL(T, "+0b101010"), static_cast(+0b101010)); + + CheckParseArithmetic(LITERAL(T, "42"), static_cast( 42)); + CheckParseArithmetic(LITERAL(T, "052"), static_cast( 052)); + CheckParseArithmetic(LITERAL(T, "0x2A"), static_cast( 0x2A)); + CheckParseArithmetic(LITERAL(T, "0b101010"), static_cast(0b101010)); + + CheckParseArithmetic(LITERAL(T, "-42"), static_cast( -42)); + CheckParseArithmetic(LITERAL(T, "-052"), static_cast( -052)); + CheckParseArithmetic(LITERAL(T, "-0x2A"), static_cast( -0x2A)); + CheckParseArithmetic(LITERAL(T, "-0b101010"), static_cast(-0b101010)); + }; + + CheckParseInt(InPlaceType); + + CheckParseInt(InPlaceType); + CheckParseInt(InPlaceType); + CheckParseInt(InPlaceType); + CheckParseInt(InPlaceType); + + CheckParseInt(InPlaceType); + CheckParseInt(InPlaceType); + CheckParseInt(InPlaceType); + CheckParseInt(InPlaceType); + + auto CheckParseFloat = [&](TInPlaceType) + { + CheckParseInt(InPlaceType); + + CheckParseArithmetic(LITERAL(T, "+3.14"), static_cast( +3.14)); + CheckParseArithmetic(LITERAL(T, "+3.14e2"), static_cast( +3.14e2)); + CheckParseArithmetic(LITERAL(T, "+3.14e-2"), static_cast( +3.14e-2)); + CheckParseArithmetic(LITERAL(T, "+0x1.91eb86p1"), static_cast(+0x1.91eb86p1)); + + CheckParseArithmetic(LITERAL(T, "3.14"), static_cast( 3.14)); + CheckParseArithmetic(LITERAL(T, "3.14e2"), static_cast( 3.14e2)); + CheckParseArithmetic(LITERAL(T, "3.14e-2"), static_cast( 3.14e-2)); + CheckParseArithmetic(LITERAL(T, "0x1.91eb86p1"), static_cast(0x1.91eb86p1)); + + CheckParseArithmetic(LITERAL(T, "-3.14"), static_cast( -3.14)); + CheckParseArithmetic(LITERAL(T, "-3.14e2"), static_cast( -3.14e2)); + CheckParseArithmetic(LITERAL(T, "-3.14e-2"), static_cast( -3.14e-2)); + CheckParseArithmetic(LITERAL(T, "-0x1.91eb86p1"), static_cast(-0x1.91eb86p1)); + + CheckParseArithmetic(LITERAL(T, "+Infinity"), +NAMESPACE_STD::numeric_limits::infinity()); + CheckParseArithmetic(LITERAL(T, " Infinity"), +NAMESPACE_STD::numeric_limits::infinity()); + CheckParseArithmetic(LITERAL(T, "-Infinity"), -NAMESPACE_STD::numeric_limits::infinity()); + + CheckParseArithmetic(LITERAL(T, "+NaN"), +NAMESPACE_STD::numeric_limits::quiet_NaN()); + CheckParseArithmetic(LITERAL(T, " NaN"), +NAMESPACE_STD::numeric_limits::quiet_NaN()); + CheckParseArithmetic(LITERAL(T, "-NaN"), -NAMESPACE_STD::numeric_limits::quiet_NaN()); + }; + + CheckParseFloat(InPlaceType); + CheckParseFloat(InPlaceType); + CheckParseFloat(InPlaceType); + }; + + Test(InPlaceType); + Test(InPlaceType); + Test(InPlaceType); + Test(InPlaceType); + Test(InPlaceType); + Test(InPlaceType); +} + NAMESPACE_END(Testing) NAMESPACE_MODULE_END(Utility) diff --git a/Redcraft.Utility/Source/Public/String/Conversion.h.inl b/Redcraft.Utility/Source/Public/String/Conversion.h.inl index 48a2e5e..252e569 100644 --- a/Redcraft.Utility/Source/Public/String/Conversion.h.inl +++ b/Redcraft.Utility/Source/Public/String/Conversion.h.inl @@ -58,6 +58,197 @@ struct TStringHelper return false; } + else if constexpr (CArithmetic) + { + checkf(Fmt.IsEmpty(), TEXT("Formatted parsing of arithmetic types not implemented.")); + + // Skip leading white spaces. + while (!View.IsEmpty() && TChar::IsSpace(View.Front())) View.RemovePrefix(1); + + if (View.IsEmpty()) return false; + + bool bNegative = false; + + // Handle optional sign. + if (View.Front() == LITERAL(T, '+')) + { + View.RemovePrefix(1); + } + else if (View.Front() == LITERAL(T, '-')) + { + bNegative = true; + View.RemovePrefix(1); + } + + // Handle boolean conversion. + else if constexpr (CSameAs) + { + bool bIsTrue = false; + bool bIsFalse = false; + + bIsTrue |= View.StartsWith(LITERAL(T, "true")) || View.StartsWith(LITERAL(T, "True")) || View.StartsWith(LITERAL(T, "TRUE")); + bIsFalse |= View.StartsWith(LITERAL(T, "false")) || View.StartsWith(LITERAL(T, "False")) || View.StartsWith(LITERAL(T, "FALSE")); + + if (bIsTrue) { View.RemovePrefix(4); Object = true; return true; } + if (bIsFalse) { View.RemovePrefix(5); Object = false; return true; } + } + + // Handle floating-point conversion. + if constexpr (CFloatingPoint) + { + bool bIsInfinity = false; + bool bIsNaN = false; + + bIsInfinity |= View.StartsWith(LITERAL(T, "infinity")) || View.StartsWith(LITERAL(T, "Infinity")) || View.StartsWith(LITERAL(T, "INFINITY")); + bIsNaN |= View.StartsWith(LITERAL(T, "nan")) || View.StartsWith(LITERAL(T, "NaN")) || View.StartsWith(LITERAL(T, "NAN")); + + if (bIsInfinity) { View.RemovePrefix(8); Object = bNegative ? -NAMESPACE_STD::numeric_limits::infinity() : NAMESPACE_STD::numeric_limits::infinity(); return true; } + if (bIsNaN) { View.RemovePrefix(3); Object = bNegative ? -NAMESPACE_STD::numeric_limits::quiet_NaN() : NAMESPACE_STD::numeric_limits::quiet_NaN(); return true; } + } + + unsigned Base = 0; + + // Auto detect base. + { + if (View.Num() >= 2 && View.Front() == LITERAL(T, '0')) + { + if (View[1] == LITERAL(T, 'x') || View[1] == LITERAL(T, 'X')) + { + Base = 16; + View.RemovePrefix(2); + } + else if (View[1] == LITERAL(T, 'b') || View[1] == LITERAL(T, 'B')) + { + Base = 2; + View.RemovePrefix(2); + } + else if (TChar::IsDigit(View.Front(), 8)) + { + Base = 8; + View.RemovePrefix(1); + } + else Base = 10; + } + else Base = 10; + } + + // Parse the number. + auto ToNumber = [&View](TInPlaceType, unsigned Base, NumberType Init = static_cast(0)) -> NumberType + { + NumberType Result = Init; + + while (!View.IsEmpty() && (Base == 10 ? TChar::IsDigit(View.Front()) : TChar::IsDigit(View.Front(), Base))) + { + Result = static_cast(Result * Base + *TChar::ToDigit(View.Front())); + + View.RemovePrefix(1); + } + + return Result; + }; + + // Handle integral conversion. + if constexpr (CIntegral) + { + using UnsignedU = TMakeUnsigned, U, int>>; + + if (View.IsEmpty()) return false; + + // The integral number must start with a digit. + if (!TChar::IsDigit(View.Front(), Base)) return false; + + // Parse the integral number. + UnsignedU Number = ToNumber(InPlaceType, Base); + + Object = static_cast(bNegative ? -Number : Number); + + return true; + } + + // Handle floating-point conversion. + else if constexpr (CFloatingPoint) + { + if (View.IsEmpty()) return false; + + // The floating-point number must start with a digit or a dot. + if (!(TChar::IsDigit(View.Front(), Base) || View.Front() == LITERAL(T, '.'))) return false; + + size_t IntegralBeginNum = View.Num(); + + // Parse the integral number. + Object = ToNumber(InPlaceType, Base); + + size_t IntegralLength = IntegralBeginNum - View.Num(); + + // Parse the fractional number. + if (!View.IsEmpty() && View.Front() == LITERAL(T, '.')) + { + View.RemovePrefix(1); + + U InvBase = 1 / static_cast(Base); + + size_t FractionBeginNum = View.Num(); + + Object = ToNumber(InPlaceType, Base, Object); + + size_t FractionLength = FractionBeginNum - View.Num(); + + Object *= NAMESPACE_STD::pow(InvBase, static_cast(FractionLength)); + } + else if (IntegralLength == 0) return false; + + // For floating point numbers apply the symbols directly + Object = static_cast(bNegative ? -Object : Object); + + if (View.IsEmpty()) return true; + + if (Base != 10 && Base != 16) return true; + + bool bHasExponent = false; + + bHasExponent |= Base == 10 && View.Front() == LITERAL(T, 'e'); + bHasExponent |= Base == 10 && View.Front() == LITERAL(T, 'E'); + bHasExponent |= Base == 16 && View.Front() == LITERAL(T, 'p'); + bHasExponent |= Base == 16 && View.Front() == LITERAL(T, 'P'); + + if (!bHasExponent) return true; + + View.RemovePrefix(1); + + if (View.IsEmpty()) return false; + + // Parse the exponent number. + { + bool bNegativeExponent = false; + + if (View.Front() == LITERAL(T, '+')) + { + View.RemovePrefix(1); + } + else if (View.Front() == LITERAL(T, '-')) + { + bNegativeExponent = true; + View.RemovePrefix(1); + } + + // The exponent number must start with a digit. + if (!TChar::IsDigit(View.Front())) return false; + + U Exponent = ToNumber(InPlaceType, 10); + + Exponent = bNegativeExponent ? -Exponent : Exponent; + + Object *= static_cast(NAMESPACE_STD::pow(static_cast(Base == 16 ? 2 : 10), Exponent)); + } + + return true; + } + + else static_assert(sizeof(U) == -1, "Unsupported arithmetic type"); + + return false; + } + return false; } diff --git a/Redcraft.Utility/Source/Public/Testing/StringTesting.h b/Redcraft.Utility/Source/Public/Testing/StringTesting.h index 87d3b82..42af043 100644 --- a/Redcraft.Utility/Source/Public/Testing/StringTesting.h +++ b/Redcraft.Utility/Source/Public/Testing/StringTesting.h @@ -12,6 +12,7 @@ REDCRAFTUTILITY_API void TestString(); REDCRAFTUTILITY_API void TestChar(); REDCRAFTUTILITY_API void TestStringView(); REDCRAFTUTILITY_API void TestTemplateString(); +REDCRAFTUTILITY_API void TestStringConversion(); NAMESPACE_END(Testing)