From abda9a3cf03c16abdfff1c3209a384282e31bd2b Mon Sep 17 00:00:00 2001 From: kirkrodrigues <2454684+kirkrodrigues@users.noreply.github.com> Date: Sun, 31 Dec 2023 15:13:55 -0500 Subject: [PATCH] Restructure string_utils into a library. (#200) --- components/core/.clang-format | 6 +- components/core/CMakeLists.txt | 15 ++-- components/core/cmake/utils.cmake | 3 +- .../string_utils}/string_utils.hpp | 14 ++- components/core/src/CMakeLists.txt | 12 +++ components/core/src/DictionaryReader.hpp | 9 +- .../core/src/EncodedVariableInterpreter.cpp | 5 +- components/core/src/Grep.cpp | 7 +- components/core/src/Utils.cpp | 2 +- components/core/src/ffi/encoding_methods.inc | 21 +++-- .../src/ffi/search/CompositeWildcardToken.cpp | 4 +- .../core/src/ffi/search/WildcardToken.cpp | 7 +- .../core/src/ffi/search/query_methods.cpp | 7 +- .../core/src/ir/LogEventDeserializer.cpp | 3 +- components/core/src/ir/parsing.cpp | 7 +- components/core/src/string_utils.inc | 17 ---- .../src/{ => string_utils}/string_utils.cpp | 86 ++++++++++--------- .../make-dictionaries-readable.cpp | 5 +- components/core/tests/test-string_utils.cpp | 9 +- 19 files changed, 141 insertions(+), 98 deletions(-) rename components/core/{src => include/string_utils}/string_utils.hpp (90%) create mode 100644 components/core/src/CMakeLists.txt delete mode 100644 components/core/src/string_utils.inc rename components/core/src/{ => string_utils}/string_utils.cpp (97%) diff --git a/components/core/.clang-format b/components/core/.clang-format index 31ed8b57e..7735475bf 100644 --- a/components/core/.clang-format +++ b/components/core/.clang-format @@ -70,8 +70,10 @@ FixNamespaceComments: true IncludeBlocks: "Regroup" IncludeCategories: # NOTE: A header is grouped by first matching regex - # Third-party headers. Update when adding new third-party libraries. - - Regex: "^<(archive|boost|catch2|date|fmt|json|log_surgeon|mariadb|spdlog|sqlite3|yaml-cpp|zstd)" + # Library headers. Update when adding new libraries. + # NOTE: clang-format retains leading white-space on a line in violation of the YAML spec. + - Regex: "^<(archive|boost|catch2|date|fmt|json|log_surgeon|mariadb|spdlog|sqlite3|string_utils\ +|yaml-cpp|zstd)" Priority: 3 # C system headers - Regex: "^<.+\\.h>" diff --git a/components/core/CMakeLists.txt b/components/core/CMakeLists.txt index 630d2ad7f..c5910c4fb 100644 --- a/components/core/CMakeLists.txt +++ b/components/core/CMakeLists.txt @@ -302,8 +302,6 @@ set(SOURCE_FILES_clp src/streaming_compression/zstd/Constants.hpp src/streaming_compression/zstd/Decompressor.cpp src/streaming_compression/zstd/Decompressor.hpp - src/string_utils.cpp - src/string_utils.hpp src/StringReader.cpp src/StringReader.hpp src/TimestampPattern.cpp @@ -338,6 +336,7 @@ target_link_libraries(clp LibArchive::LibArchive MariaDBClient::MariaDBClient ${STD_FS_LIBS} + string_utils::string_utils yaml-cpp::yaml-cpp ZStd::ZStd ) @@ -449,8 +448,6 @@ set(SOURCE_FILES_clg src/streaming_compression/zstd/Constants.hpp src/streaming_compression/zstd/Decompressor.cpp src/streaming_compression/zstd/Decompressor.hpp - src/string_utils.cpp - src/string_utils.hpp src/StringReader.cpp src/StringReader.hpp src/TimestampPattern.cpp @@ -486,6 +483,7 @@ target_link_libraries(clg spdlog::spdlog ${sqlite_LIBRARY_DEPENDENCIES} ${STD_FS_LIBS} + string_utils::string_utils yaml-cpp::yaml-cpp ZStd::ZStd ) @@ -583,8 +581,6 @@ set(SOURCE_FILES_clo src/streaming_compression/zstd/Constants.hpp src/streaming_compression/zstd/Decompressor.cpp src/streaming_compression/zstd/Decompressor.hpp - src/string_utils.cpp - src/string_utils.hpp src/StringReader.cpp src/StringReader.hpp src/Thread.cpp @@ -621,6 +617,7 @@ target_link_libraries(clo spdlog::spdlog ${sqlite_LIBRARY_DEPENDENCIES} ${STD_FS_LIBS} + string_utils::string_utils ZStd::ZStd ) target_compile_features(clo @@ -774,9 +771,6 @@ set(SOURCE_FILES_unitTest src/streaming_compression/zstd/Constants.hpp src/streaming_compression/zstd/Decompressor.cpp src/streaming_compression/zstd/Decompressor.hpp - src/string_utils.cpp - src/string_utils.hpp - src/string_utils.inc src/StringReader.cpp src/StringReader.hpp src/TimestampPattern.cpp @@ -828,6 +822,7 @@ target_link_libraries(unitTest spdlog::spdlog ${sqlite_LIBRARY_DEPENDENCIES} ${STD_FS_LIBS} + string_utils::string_utils yaml-cpp::yaml-cpp ZStd::ZStd ) @@ -836,3 +831,5 @@ target_compile_features(unitTest ) include(cmake/utils.cmake) + +add_subdirectory(src) diff --git a/components/core/cmake/utils.cmake b/components/core/cmake/utils.cmake index e5aef15d4..b290ed376 100644 --- a/components/core/cmake/utils.cmake +++ b/components/core/cmake/utils.cmake @@ -22,8 +22,6 @@ set(SOURCE_FILES_make-dictionaries-readable ${CMAKE_CURRENT_SOURCE_DIR}/src/streaming_compression/passthrough/Decompressor.hpp ${CMAKE_CURRENT_SOURCE_DIR}/src/streaming_compression/zstd/Decompressor.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/streaming_compression/zstd/Decompressor.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/string_utils.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/string_utils.hpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Utils.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Utils.hpp ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/make_dictionaries_readable/CommandLineArguments.cpp @@ -46,6 +44,7 @@ target_link_libraries(make-dictionaries-readable Boost::filesystem Boost::iostreams Boost::program_options log_surgeon::log_surgeon spdlog::spdlog + string_utils::string_utils ZStd::ZStd ) target_compile_features(make-dictionaries-readable diff --git a/components/core/src/string_utils.hpp b/components/core/include/string_utils/string_utils.hpp similarity index 90% rename from components/core/src/string_utils.hpp rename to components/core/include/string_utils/string_utils.hpp index 477f258eb..f97e7fc96 100644 --- a/components/core/src/string_utils.hpp +++ b/components/core/include/string_utils/string_utils.hpp @@ -1,8 +1,10 @@ #ifndef STRING_UTILS_HPP #define STRING_UTILS_HPP +#include #include +namespace string_utils { /** * Checks if the given character is an alphabet * @param c @@ -122,6 +124,16 @@ bool wildcard_match_unsafe_case_sensitive(std::string_view tame, std::string_vie template bool convert_string_to_int(std::string_view raw, integer_t& converted); -#include "string_utils.inc" +template +bool convert_string_to_int(std::string_view raw, integer_t& converted) { + auto raw_end = raw.cend(); + auto result = std::from_chars(raw.cbegin(), raw_end, converted); + if (raw_end != result.ptr) { + return false; + } else { + return result.ec == std::errc(); + } +} +} // namespace string_utils #endif // STRING_UTILS_HPP diff --git a/components/core/src/CMakeLists.txt b/components/core/src/CMakeLists.txt new file mode 100644 index 000000000..d5ae5c7a6 --- /dev/null +++ b/components/core/src/CMakeLists.txt @@ -0,0 +1,12 @@ +set( + STRING_UTILS_HEADER_LIST + "${PROJECT_SOURCE_DIR}/include/string_utils/string_utils.hpp" +) +add_library( + string_utils + string_utils/string_utils.cpp + ${STRING_UTILS_HEADER_LIST} +) +add_library(string_utils::string_utils ALIAS string_utils) +target_include_directories(string_utils PUBLIC "${PROJECT_SOURCE_DIR}/include") +target_compile_features(string_utils PRIVATE cxx_std_17) diff --git a/components/core/src/DictionaryReader.hpp b/components/core/src/DictionaryReader.hpp index a86e92745..f0e59d7b1 100644 --- a/components/core/src/DictionaryReader.hpp +++ b/components/core/src/DictionaryReader.hpp @@ -5,13 +5,13 @@ #include #include +#include #include "dictionary_utils.hpp" #include "DictionaryEntry.hpp" #include "FileReader.hpp" #include "streaming_compression/passthrough/Decompressor.hpp" #include "streaming_compression/zstd/Decompressor.hpp" -#include "string_utils.hpp" #include "Utils.hpp" /** @@ -256,7 +256,12 @@ void DictionaryReader::get_entries_matching_wildcar std::unordered_set& entries ) const { for (auto const& entry : m_entries) { - if (wildcard_match_unsafe(entry.get_value(), wildcard_string, false == ignore_case)) { + if (string_utils::wildcard_match_unsafe( + entry.get_value(), + wildcard_string, + false == ignore_case + )) + { entries.insert(&entry); } } diff --git a/components/core/src/EncodedVariableInterpreter.cpp b/components/core/src/EncodedVariableInterpreter.cpp index 15f0fe498..3f668a0b0 100644 --- a/components/core/src/EncodedVariableInterpreter.cpp +++ b/components/core/src/EncodedVariableInterpreter.cpp @@ -3,12 +3,13 @@ #include #include +#include + #include "Defs.h" #include "ffi/ir_stream/decoding_methods.hpp" #include "ir/LogEvent.hpp" #include "ir/types.hpp" #include "spdlog_with_specializations.hpp" -#include "string_utils.hpp" #include "type_utils.hpp" using ffi::cEightByteEncodedFloatDigitsBitMask; @@ -55,7 +56,7 @@ bool EncodedVariableInterpreter::convert_string_to_representable_integer_var( } int64_t result; - if (false == convert_string_to_int(value, result)) { + if (false == string_utils::convert_string_to_int(value, result)) { // Conversion failed return false; } else { diff --git a/components/core/src/Grep.cpp b/components/core/src/Grep.cpp index a56aecbca..09e8184ca 100644 --- a/components/core/src/Grep.cpp +++ b/components/core/src/Grep.cpp @@ -3,6 +3,7 @@ #include #include +#include #include "EncodedVariableInterpreter.hpp" #include "ir/parsing.hpp" @@ -17,6 +18,10 @@ using std::vector; using streaming_archive::reader::Archive; using streaming_archive::reader::File; using streaming_archive::reader::Message; +using string_utils::clean_up_wildcard_search_string; +using string_utils::is_alphabet; +using string_utils::is_wildcard; +using string_utils::wildcard_match_unsafe; // Local types enum class SubQueryMatchabilityResult { @@ -693,7 +698,7 @@ bool Grep::get_bounds_of_next_potential_var( } } - if (is_decimal_digit(c)) { + if (string_utils::is_decimal_digit(c)) { contains_decimal_digit = true; } else if (is_alphabet(c)) { contains_alphabet = true; diff --git a/components/core/src/Utils.cpp b/components/core/src/Utils.cpp index 81b81995d..a9767a6d6 100644 --- a/components/core/src/Utils.cpp +++ b/components/core/src/Utils.cpp @@ -12,9 +12,9 @@ #include #include #include +#include #include "spdlog_with_specializations.hpp" -#include "string_utils.hpp" using std::list; using std::string; diff --git a/components/core/src/ffi/encoding_methods.inc b/components/core/src/ffi/encoding_methods.inc index dbbb45901..15f4b7d13 100644 --- a/components/core/src/ffi/encoding_methods.inc +++ b/components/core/src/ffi/encoding_methods.inc @@ -3,9 +3,10 @@ #include +#include + #include "../ir/parsing.hpp" #include "../ir/types.hpp" -#include "../string_utils.hpp" #include "../type_utils.hpp" namespace ffi { @@ -326,7 +327,7 @@ bool encode_integer_string(std::string_view str, encoded_variable_t& encoded_var } encoded_variable_t result; - if (false == convert_string_to_int(str, result)) { + if (false == string_utils::convert_string_to_int(str, result)) { // Conversion failed return false; } else { @@ -519,7 +520,7 @@ bool wildcard_query_matches_any_encoded_var( if constexpr (ir::VariablePlaceholder::Float == var_placeholder) { auto decoded_var = decode_float_var(encoded_vars[encoded_vars_ix]); - if (wildcard_match_unsafe(decoded_var, wildcard_query)) { + if (string_utils::wildcard_match_unsafe(decoded_var, wildcard_query)) { return true; } } @@ -537,7 +538,7 @@ bool wildcard_query_matches_any_encoded_var( if constexpr (ir::VariablePlaceholder::Integer == var_placeholder) { auto decoded_var = decode_integer_var(encoded_vars[encoded_vars_ix]); - if (wildcard_match_unsafe(decoded_var, wildcard_query)) { + if (string_utils::wildcard_match_unsafe(decoded_var, wildcard_query)) { return true; } } @@ -591,7 +592,11 @@ bool wildcard_match_encoded_vars( if (wildcard_var_placeholders[wildcard_var_ix] == c) { auto decoded_var = decode_float_var(encoded_vars[var_ix]); - if (wildcard_match_unsafe(decoded_var, wildcard_var_queries[wildcard_var_ix])) { + if (string_utils::wildcard_match_unsafe( + decoded_var, + wildcard_var_queries[wildcard_var_ix] + )) + { ++wildcard_var_ix; if (wildcard_var_ix == wildcard_var_queries_len) { break; @@ -612,7 +617,11 @@ bool wildcard_match_encoded_vars( if (wildcard_var_placeholders[wildcard_var_ix] == c) { auto decoded_var = decode_integer_var(encoded_vars[var_ix]); - if (wildcard_match_unsafe(decoded_var, wildcard_var_queries[wildcard_var_ix])) { + if (string_utils::wildcard_match_unsafe( + decoded_var, + wildcard_var_queries[wildcard_var_ix] + )) + { ++wildcard_var_ix; if (wildcard_var_ix == wildcard_var_queries_len) { break; diff --git a/components/core/src/ffi/search/CompositeWildcardToken.cpp b/components/core/src/ffi/search/CompositeWildcardToken.cpp index 4d821c41b..f9f8ebde5 100644 --- a/components/core/src/ffi/search/CompositeWildcardToken.cpp +++ b/components/core/src/ffi/search/CompositeWildcardToken.cpp @@ -1,5 +1,7 @@ #include "CompositeWildcardToken.hpp" +#include + #include "../../ir/parsing.hpp" #include "../../ir/types.hpp" @@ -28,7 +30,7 @@ CompositeWildcardToken::CompositeWildcardToken( is_escaped = false; } else if ('\\' == c) { is_escaped = true; - } else if (is_wildcard(c)) { + } else if (string_utils::is_wildcard(c)) { m_wildcards.emplace_back(c, i, begin_pos == i || end_pos - 1 == i); } } diff --git a/components/core/src/ffi/search/WildcardToken.cpp b/components/core/src/ffi/search/WildcardToken.cpp index 5f48ae838..6a4cee6e2 100644 --- a/components/core/src/ffi/search/WildcardToken.cpp +++ b/components/core/src/ffi/search/WildcardToken.cpp @@ -2,8 +2,9 @@ #include +#include + #include "../../ir/types.hpp" -#include "../../string_utils.hpp" #include "../../type_utils.hpp" #include "../encoding_methods.hpp" #include "QueryWildcard.hpp" @@ -122,9 +123,9 @@ static bool could_be_static_text(string_view query, size_t begin_pos, size_t end is_escaped = false; } else if ('\\' == c) { is_escaped = true; - } else if (is_decimal_digit(c)) { + } else if (string_utils::is_decimal_digit(c)) { return false; - } else if (is_alphabet(c)) { + } else if (string_utils::is_alphabet(c)) { contains_alphabet = true; } } diff --git a/components/core/src/ffi/search/query_methods.cpp b/components/core/src/ffi/search/query_methods.cpp index cffc5bcc1..d8e99ec96 100644 --- a/components/core/src/ffi/search/query_methods.cpp +++ b/components/core/src/ffi/search/query_methods.cpp @@ -1,5 +1,7 @@ #include "query_methods.hpp" +#include + #include "../../ir/parsing.hpp" #include "../../ir/types.hpp" #include "CompositeWildcardToken.hpp" @@ -13,6 +15,7 @@ using std::string; using std::string_view; using std::variant; using std::vector; +using string_utils::is_wildcard; namespace ffi::search { static auto TokenGetBeginPos = [](auto const& token) { return token.get_begin_pos(); }; @@ -251,9 +254,9 @@ static void find_delimiter( } } - if (is_decimal_digit(c)) { + if (string_utils::is_decimal_digit(c)) { contains_decimal_digit = true; - } else if (is_alphabet(c)) { + } else if (string_utils::is_alphabet(c)) { contains_alphabet = true; } } diff --git a/components/core/src/ir/LogEventDeserializer.cpp b/components/core/src/ir/LogEventDeserializer.cpp index d7fceafa3..daeb78cb6 100644 --- a/components/core/src/ir/LogEventDeserializer.cpp +++ b/components/core/src/ir/LogEventDeserializer.cpp @@ -3,6 +3,7 @@ #include #include +#include #include "../ffi/ir_stream/decoding_methods.hpp" #include "types.hpp" @@ -56,7 +57,7 @@ auto LogEventDeserializer::create(ReaderInterface& reader) } auto ref_timestamp_str = ref_timestamp_iter->get_ref(); epoch_time_ms_t ref_timestamp{}; - if (false == convert_string_to_int(ref_timestamp_str, ref_timestamp)) { + if (false == string_utils::convert_string_to_int(ref_timestamp_str, ref_timestamp)) { return std::errc::protocol_error; } diff --git a/components/core/src/ir/parsing.cpp b/components/core/src/ir/parsing.cpp index 50c738f5c..070f22bfb 100644 --- a/components/core/src/ir/parsing.cpp +++ b/components/core/src/ir/parsing.cpp @@ -1,6 +1,7 @@ #include "parsing.hpp" -#include "../string_utils.hpp" +#include + #include "../type_utils.hpp" #include "types.hpp" @@ -63,9 +64,9 @@ bool get_bounds_of_next_var(string_view const str, size_t& begin_pos, size_t& en end_pos = begin_pos; for (; end_pos < msg_length; ++end_pos) { auto c = str[end_pos]; - if (is_decimal_digit(c)) { + if (string_utils::is_decimal_digit(c)) { contains_decimal_digit = true; - } else if (is_alphabet(c)) { + } else if (string_utils::is_alphabet(c)) { contains_alphabet = true; } else if (is_delim(c)) { break; diff --git a/components/core/src/string_utils.inc b/components/core/src/string_utils.inc deleted file mode 100644 index 0135bc871..000000000 --- a/components/core/src/string_utils.inc +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef STRING_UTILS_TPP -#define STRING_UTILS_TPP - -#include - -template -bool convert_string_to_int(std::string_view raw, integer_t& converted) { - auto raw_end = raw.cend(); - auto result = std::from_chars(raw.cbegin(), raw_end, converted); - if (raw_end != result.ptr) { - return false; - } else { - return result.ec == std::errc(); - } -} - -#endif // STRING_UTILS_TPP diff --git a/components/core/src/string_utils.cpp b/components/core/src/string_utils/string_utils.cpp similarity index 97% rename from components/core/src/string_utils.cpp rename to components/core/src/string_utils/string_utils.cpp index 68e3e1d31..f1da3524e 100644 --- a/components/core/src/string_utils.cpp +++ b/components/core/src/string_utils/string_utils.cpp @@ -1,4 +1,4 @@ -#include "string_utils.hpp" +#include "string_utils/string_utils.hpp" #include #include @@ -7,6 +7,7 @@ using std::string; using std::string_view; +namespace { /** * Helper for ``wildcard_match_unsafe_case_sensitive`` to advance the pointer in * tame to the next character which matches wild. This method should be inlined @@ -18,13 +19,54 @@ using std::string_view; * @param wild_bookmark * @return true on success, false if wild cannot match tame */ -static inline bool advance_tame_to_next_match( +inline bool advance_tame_to_next_match( char const*& tame_current, char const*& tame_bookmark, char const* tame_end, char const*& wild_current ); +inline bool advance_tame_to_next_match( + char const*& tame_current, + char const*& tame_bookmark, + char const* tame_end, + char const*& wild_current +) { + auto w = *wild_current; + if ('?' != w) { + // No need to check for '*' since the caller ensures wild doesn't + // contain consecutive '*' + + // Handle escaped characters + if ('\\' == w) { + ++wild_current; + // This is safe without a bounds check since this the caller ensures + // there are no dangling escape characters + w = *wild_current; + } + + // Advance tame_current until it matches wild_current + while (true) { + if (tame_end == tame_current) { + // Wild group is longer than last group in tame, so can't match + // e.g. "*abc" doesn't match "zab" + return false; + } + auto t = *tame_current; + if (t == w) { + break; + } + ++tame_current; + } + } + + tame_bookmark = tame_current; + + return true; +} +} // namespace + +namespace string_utils { size_t find_first_of( string const& haystack, char const* needles, @@ -124,45 +166,6 @@ string clean_up_wildcard_search_string(string_view str) { return cleaned_str; } -static inline bool advance_tame_to_next_match( - char const*& tame_current, - char const*& tame_bookmark, - char const* tame_end, - char const*& wild_current -) { - auto w = *wild_current; - if ('?' != w) { - // No need to check for '*' since the caller ensures wild doesn't - // contain consecutive '*' - - // Handle escaped characters - if ('\\' == w) { - ++wild_current; - // This is safe without a bounds check since this the caller ensures - // there are no dangling escape characters - w = *wild_current; - } - - // Advance tame_current until it matches wild_current - while (true) { - if (tame_end == tame_current) { - // Wild group is longer than last group in tame, so can't match - // e.g. "*abc" doesn't match "zab" - return false; - } - auto t = *tame_current; - if (t == w) { - break; - } - ++tame_current; - } - } - - tame_bookmark = tame_current; - - return true; -} - bool wildcard_match_unsafe(string_view tame, string_view wild, bool case_sensitive_match) { if (case_sensitive_match) { return wildcard_match_unsafe_case_sensitive(tame, wild); @@ -291,3 +294,4 @@ bool wildcard_match_unsafe_case_sensitive(string_view tame, string_view wild) { } } } +} // namespace string_utils diff --git a/components/core/src/utils/make_dictionaries_readable/make-dictionaries-readable.cpp b/components/core/src/utils/make_dictionaries_readable/make-dictionaries-readable.cpp index c0cccd658..44319f99c 100644 --- a/components/core/src/utils/make_dictionaries_readable/make-dictionaries-readable.cpp +++ b/components/core/src/utils/make_dictionaries_readable/make-dictionaries-readable.cpp @@ -3,6 +3,7 @@ #include #include +#include #include "../../FileWriter.hpp" #include "../../ir/types.hpp" @@ -111,7 +112,9 @@ int main(int argc, char const* argv[]) { human_readable_value.append(value, constant_begin_pos, string::npos); } - file_writer.write_string(replace_characters("\n", "n", human_readable_value, true)); + file_writer.write_string( + string_utils::replace_characters("\n", "n", human_readable_value, true) + ); file_writer.write_char('\n'); std::set const& segment_ids = entry.get_ids_of_segments_containing_entry(); diff --git a/components/core/tests/test-string_utils.cpp b/components/core/tests/test-string_utils.cpp index b67df9cf1..397601758 100644 --- a/components/core/tests/test-string_utils.cpp +++ b/components/core/tests/test-string_utils.cpp @@ -3,17 +3,20 @@ #include #include #include - -#include "../src/string_utils.hpp" +#include using std::cout; using std::endl; using std::string; using std::vector; +using string_utils::clean_up_wildcard_search_string; +using string_utils::convert_string_to_int; +using string_utils::wildcard_match_unsafe; +using string_utils::wildcard_match_unsafe_case_sensitive; TEST_CASE("to_lower", "[to_lower]") { string str = "test123TEST"; - to_lower(str); + string_utils::to_lower(str); REQUIRE(str == "test123test"); }