From 629a608133907eb438109546f6c5a43cdd357269 Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Mon, 10 Jun 2024 17:55:23 +0800 Subject: [PATCH] feat: finish console input. - finish console input function. add test for it. - add Replace function in string helper. --- src/ConsoleHelper.cpp | 121 +++++++++++++++++++++++++++++++++++++++--- src/StringHelper.cpp | 32 +++++++++++ src/StringHelper.hpp | 25 +++++---- testbench/main.cpp | 38 +++++++++++-- 4 files changed, 195 insertions(+), 21 deletions(-) diff --git a/src/ConsoleHelper.cpp b/src/ConsoleHelper.cpp index eb27309..7cc7be5 100644 --- a/src/ConsoleHelper.cpp +++ b/src/ConsoleHelper.cpp @@ -32,10 +32,116 @@ namespace YYCC::ConsoleHelper { return true; } + //template || std::is_same_v<_Ty, wchar_t>, int> = 0> + //static bool FetchEOL(std::basic_string<_Ty>& internal_buffer, std::basic_string<_Ty>& result_buffer) { + // // try finding EOL in internal buffer + // size_t pos; + // if constexpr (std::is_same_v<_Ty, char>) internal_buffer.find_first_of('\n'); + // else internal_buffer.find_first_of(L'\n'); + + // // check finding result + // if (pos == std::wstring::npos) { + // // the whole string do not include EOL, fully appended to return value + // result_buffer += internal_buffer; + // internal_buffer.clear(); + // // return false mean need more data + // return false; + // } else { + // // split result + // // push into result and remain some in internal buffer. + // result_buffer.append(internal_buffer, 0u, pos); + // internal_buffer.erase(0u, pos + 1u); // +1 because EOL take one place. + // // return true mean success finding + // return true; + // } + //} + + template + static std::string PlainRead(HANDLE hStdIn) { + using _TChar = std::conditional_t<_bIsConsole, wchar_t, char>; + + // Prepare an internal buffer because the read data may not be fully used. + // For example, we may read x\ny in a single calling but after processing \n, this function will return + // so y will temporarily stored in this internal buffer for next using. + // Thus this function is not thread safe. + static std::basic_string<_TChar> internal_buffer; + // create return value buffer + std::basic_string<_TChar> return_buffer; + + // Prepare some variables + DWORD dwReadNumberOfChars; + _TChar szReadChars[64]; + size_t eol_pos; + + // try fetching EOL + while (true) { + // if internal buffer is empty, + // try fetching it. + if (internal_buffer.empty()) { + // console and non-console use different method to read. + if constexpr (_bIsConsole) { + // console handle, use ReadConsoleW. + // read from console, the read data is wchar based + if (!ReadConsoleW(hStdIn, szReadChars, sizeof(szReadChars) / sizeof(_TChar), &dwReadNumberOfChars, NULL)) + break; + } else { + // anything else, use ReadFile instead. + // the read data is utf8 based + if (!ReadFile(hStdIn, szReadChars, sizeof(szReadChars), &dwReadNumberOfChars, NULL)) + break; + } + + // send to internal buffer + if (dwReadNumberOfChars == 0) break; + internal_buffer.append(szReadChars, dwReadNumberOfChars); + } + + // try finding EOL in internal buffer + if constexpr (std::is_same_v<_TChar, char>) eol_pos = internal_buffer.find_first_of('\n'); + else eol_pos = internal_buffer.find_first_of(L'\n'); + // check finding result + if (eol_pos == std::wstring::npos) { + // the whole string do not include EOL, fully appended to return value + return_buffer += internal_buffer; + internal_buffer.clear(); + // need more data, continue while + } else { + // split result + // push into result and remain some in internal buffer. + return_buffer.append(internal_buffer, 0u, eol_pos); + internal_buffer.erase(0u, eol_pos + 1u); // +1 because EOL take one place. + // break while mean success finding + break; + } + } + + // post-process for return value + std::string real_return_buffer; + if constexpr (_bIsConsole) { + // console mode need convert wchar to utf8 + YYCC::EncodingHelper::WcharToUTF8(return_buffer.c_str(), real_return_buffer); + } else { + // non-console just copt the result + real_return_buffer = return_buffer; + } + // every mode need delete \r words + YYCC::StringHelper::Replace(real_return_buffer, "\r", ""); + // return value + return real_return_buffer; + } + std::string ReadLine() { #if YYCC_OS == YYCC_OS_WINDOWS - return std::string(); + // get stdin mode + HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE); + // use different method to get according to whether stdin is redirected + DWORD dwConsoleMode; + if (GetConsoleMode(hStdIn, &dwConsoleMode)) { + return PlainRead(hStdIn); + } else { + return PlainRead(hStdIn); + } #elif YYCC_OS == YYCC_OS_LINUX @@ -52,19 +158,22 @@ namespace YYCC::ConsoleHelper { static void PlainWrite(const std::string& strl) { #if YYCC_OS == YYCC_OS_WINDOWS + // Prepare some Win32 variables // fetch stdout handle first - HANDLE hstdout = GetStdHandle(STD_OUTPUT_HANDLE); + HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); + DWORD dwConsoleMode; + DWORD dwWrittenNumberOfChars; + // if stdout was redirected, this handle may point to a file handle or anything else, // WriteConsoleW can not write data into such scenario, so we need check whether this handle is console handle - DWORD console_mode; - if (GetConsoleMode(hstdout, &console_mode)) { + if (GetConsoleMode(hStdOut, &dwConsoleMode)) { // console handle, use WriteConsoleW. // convert utf8 string to wide char first std::wstring wstrl(YYCC::EncodingHelper::UTF8ToWchar(strl.c_str())); size_t wstrl_size = wstrl.size(); // write string with size check if (wstrl_size <= std::numeric_limits::max()) { - WriteConsoleW(hstdout, wstrl.c_str(), static_cast(wstrl_size), NULL, NULL); + WriteConsoleW(hStdOut, wstrl.c_str(), static_cast(wstrl_size), &dwWrittenNumberOfChars, NULL); } } else { // anything else, use WriteFile instead. @@ -73,7 +182,7 @@ namespace YYCC::ConsoleHelper { size_t strl_size = strl.size() * sizeof(std::string::value_type); // write string with size check if (strl_size <= std::numeric_limits::max()) { - WriteFile(hstdout, strl.c_str(), static_cast(strl_size), NULL, NULL); + WriteFile(hStdOut, strl.c_str(), static_cast(strl_size), &dwWrittenNumberOfChars, NULL); } } diff --git a/src/StringHelper.cpp b/src/StringHelper.cpp index 7703e25..6c9750d 100644 --- a/src/StringHelper.cpp +++ b/src/StringHelper.cpp @@ -65,6 +65,37 @@ namespace YYCC::StringHelper { return ret; } + void Replace(std::string& strl, const char* _from_strl, const char* _to_strl) { + // Reference: https://stackoverflow.com/questions/3418231/replace-part-of-a-string-with-another-string + + // check requirements + // from string and to string should not be nullptr. + if (_from_strl == nullptr || _to_strl == nullptr) return; + // from string should not be empty + std::string from_strl(_from_strl); + std::string to_strl(_to_strl); + if (from_strl.empty()) return; + + // start replace one by one + size_t start_pos = 0; + while ((start_pos = strl.find(from_strl, start_pos)) != std::string::npos) { + strl.replace(start_pos, from_strl.size(), to_strl); + start_pos += to_strl.size(); // In case 'to' contains 'from', like replacing 'x' with 'yx' + } + } + + std::string Replace(const char* _strl, const char* _from_strl, const char* _to_strl) { + // prepare result + std::string strl; + // if given string is not nullptr, assign it and process it. + if (_strl != nullptr) { + strl = _strl; + Replace(strl, _from_strl, _to_strl); + } + // return value + return strl; + } + std::string Join(JoinDataProvider fct_data, const char* decilmer) { std::string ret; bool is_first = true; @@ -116,6 +147,7 @@ namespace YYCC::StringHelper { template void GeneralStringLowerUpper(std::string& strl) { + // References: // https://en.cppreference.com/w/cpp/algorithm/transform // https://en.cppreference.com/w/cpp/string/byte/tolower std::transform( diff --git a/src/StringHelper.hpp b/src/StringHelper.hpp index caebb91..e0b86ac 100644 --- a/src/StringHelper.hpp +++ b/src/StringHelper.hpp @@ -7,19 +7,22 @@ #include namespace YYCC::StringHelper { - + bool Printf(std::string& strl, const char* format, ...); bool VPrintf(std::string& strl, const char* format, va_list argptr); std::string Printf(const char* format, ...); std::string VPrintf(const char* format, va_list argptr); + void Replace(std::string& strl, const char* _from_strl, const char* _to_strl); + std::string Replace(const char* _strl, const char* _from_strl, const char* _to_strl); + /** * @brief The data provider of general Join function. * This function pointer return non-null string pointer to represent a element of joined series. * otherwise return nullptr to terminate the joining process. */ - using JoinDataProvider = std::function; + using JoinDataProvider = std::function; /** * @brief General Join function. * @details This function use function pointer as a general data provider interface, @@ -31,24 +34,24 @@ namespace YYCC::StringHelper { std::string Join(JoinDataProvider fct_data, const char* decilmer); /** * @brief Specialized Join function for common used container. - * @param data - * @param decilmer - * @param reversed - * @return + * @param data + * @param decilmer + * @param reversed + * @return */ std::string Join(const std::vector& data, const char* decilmer, bool reversed = false); /** * @brief Transform string to lower. - * @param strl - * @return + * @param strl + * @return */ std::string Lower(const char* strl); void Lower(std::string& strl); /** * @brief Transform string to upper. - * @param strl - * @return + * @param strl + * @return */ std::string Upper(const char* strl); void Upper(std::string& strl); @@ -59,7 +62,7 @@ namespace YYCC::StringHelper { * If this is nullptr, the result will be empty. * @param _decilmer[in] The decilmer for splitting. * If decilmer is nullptr or zero length, the result will only have 1 element which is original string. - * @return + * @return * @remarks This function may be low performance because it just a homebrew Split functon. * It can works in most toy cases but not suit for high performance scenario. * Also, this function will produce a copy of original string because it is not zero copy. diff --git a/testbench/main.cpp b/testbench/main.cpp index e767746..048f796 100644 --- a/testbench/main.cpp +++ b/testbench/main.cpp @@ -19,8 +19,8 @@ namespace YYCCTestbench { Console::EnableColorfulConsole(stdout); Console::WriteLine("Color Test:"); - // U+2588 is full block #define TEST_MACRO(col) Console::WriteLine("\t" YYCC_COLOR_ ## col ("\u2588\u2588") YYCC_COLOR_LIGHT_ ## col("\u2588\u2588") " " #col " / LIGHT " #col ); + // U+2588 is full block TEST_MACRO(BLACK); TEST_MACRO(RED); @@ -35,7 +35,6 @@ namespace YYCCTestbench { // UTF8 Output Test // Ref: https://stackoverflow.com/questions/478201/how-to-test-an-application-for-correct-encoding-e-g-utf-8 - //Console::EnableUTF8Console(stdout); Console::WriteLine("UTF8 Output Test:"); static std::vector c_TestStrings { "\u30E6\u30FC\u30B6\u30FC\u5225\u30B5\u30A4\u30C8", // JAPAN @@ -55,18 +54,48 @@ namespace YYCCTestbench { Console::WriteLine("\t%s", ptr); } + // UTF8 Input Test + Console::WriteLine("UTF8 Input Test:"); + for (const auto* ptr : c_TestStrings) { + Console::WriteLine("\tPlease type: %s", ptr); + Console::Write("\t> "); + + std::string gotten(Console::ReadLine()); + Assert(gotten == ptr, YYCC::StringHelper::Printf("Got: %s", gotten.c_str()).c_str()); + } + } static void StringTestbench() { + // Test Printf auto test_printf = YYCC::StringHelper::Printf("%s == %s", "Hello World", "Hello, world"); Assert(test_printf == "Hello World == Hello, world", "YYCC::StringHelper::Printf"); - + + // Test Replace + auto test_replace = YYCC::StringHelper::Replace("aabbcc", "bb", "dd"); // normal case + Assert(test_replace == "aaddcc", "YYCC::StringHelper::Replace"); + test_replace = YYCC::StringHelper::Replace("aabbcc", "zz", "yy"); // no replace + Assert(test_replace == "aabbcc", "YYCC::StringHelper::Replace"); + test_replace = YYCC::StringHelper::Replace("aabbcc", "", "zz"); // empty finding + Assert(test_replace == "aabbcc", "YYCC::StringHelper::Replace"); + test_replace = YYCC::StringHelper::Replace("aabbcc", nullptr, "zz"); // nullptr finding + Assert(test_replace == "aabbcc", "YYCC::StringHelper::Replace"); + test_replace = YYCC::StringHelper::Replace("aaaabbaa", "aa", ""); // no replaced string + Assert(test_replace == "bb", "YYCC::StringHelper::Replace"); + test_replace = YYCC::StringHelper::Replace("aaxcc", "x", "yx"); // nested replacing + Assert(test_replace == "aayxcc", "YYCC::StringHelper::Replace"); + test_replace = YYCC::StringHelper::Replace("", "", "xy"); // empty source string + Assert(test_replace == "", "YYCC::StringHelper::Replace"); + test_replace = YYCC::StringHelper::Replace(nullptr, "", "xy"); // nullptr source string + Assert(test_replace == "", "YYCC::StringHelper::Replace"); + + // Test Upper / Lower auto test_lower = YYCC::StringHelper::Lower("LOWER"); Assert(test_lower == "lower", "YYCC::StringHelper::Lower"); auto test_upper = YYCC::StringHelper::Upper("upper"); Assert(test_upper == "UPPER", "YYCC::StringHelper::Upper"); - + // Test Join std::vector test_join_container { "", "1", "2", "" }; @@ -75,6 +104,7 @@ namespace YYCCTestbench { test_join = YYCC::StringHelper::Join(test_join_container, ", ", true); Assert(test_join == ", 2, 1, ", "YYCC::StringHelper::Join"); + // Test Split auto test_split = YYCC::StringHelper::Split(", 1, 2, ", ", "); Assert(test_split.size() == 4u, "YYCC::StringHelper::Split"); Assert(test_split[0] == "", "YYCC::StringHelper::Split");