diff --git a/onnxruntime/core/providers/cpu/text/string_normalizer.cc b/onnxruntime/core/providers/cpu/text/string_normalizer.cc index 27407e9999..32de3105d6 100644 --- a/onnxruntime/core/providers/cpu/text/string_normalizer.cc +++ b/onnxruntime/core/providers/cpu/text/string_normalizer.cc @@ -4,27 +4,19 @@ #include "string_normalizer.h" #include "core/common/common.h" #include "core/framework/tensor.h" +// Used below HAS_DEPRECATED_DECLARATIONS #include "onnxruntime_config.h" #ifdef _MSC_VER -#include #include -#elif defined(__APPLE__) || defined(__ANDROID__) -#include -#else -#include -#include - #endif // _MSC_VER +#include #include #include -#include #if defined(__GNUC__) -// Allow deprecated-declarations warning - std::wstring_convert is deprecated. -// TODO find a suitable replacement -// Note: GNU libiconv (e.g., on Apple platforms) is not suitable due to its LGPL license. +// Allow deprecated-declarations warning - std::codecvt_utf8 is deprecatedd #if defined(HAS_DEPRECATED_DECLARATIONS) #pragma GCC diagnostic warning "-Wdeprecated-declarations" #endif // defined(HAS_DEPRECATED_DECLARATIONS) @@ -40,8 +32,196 @@ ONNX_CPU_OPERATOR_KERNEL( StringNormalizer); namespace string_normalizer { -const std::string conv_error("Conversion Error"); -const std::wstring wconv_error(L"Conversion Error"); + +// codecvt_utf8 is deprecated, we will want to replace it with our class +class Utf8ConverterGeneric { + public: + size_t ComputeRequiredSizeToUtf8(const std::wstring& wstr) const { + if (wstr.empty()) { + return 0; + } + + size_t result = 0; + std::mbstate_t state = std::mbstate_t(); + + const wchar_t* src = wstr.data(); + const wchar_t* src_end = src + wstr.length(); + + char dummy_dest[128] = {0}; + + char* char_next = dummy_dest; + const wchar_t* wchar_next = src; + + size_t converted = 0; + + std::codecvt_base::result ret_code = std::codecvt_base::ok; + + // Continue while we exhaust the sequence + while (converted < wstr.length()) { + ret_code = converter_.out(state, + wchar_next, + src_end, + wchar_next, + std::begin(dummy_dest), + std::end(dummy_dest), + char_next); + result += (char_next - dummy_dest); + converted = (wchar_next - src); + + if (ret_code != std::codecvt_base::partial && + ret_code != std::codecvt_base::ok) { + break; + } + } + + ORT_ENFORCE(ret_code != std::codecvt_base::noconv, "Conversion is expected"); + + if (ret_code != std::codecvt_base::ok) { + ORT_THROW("Failed to compute size for UTF-8. Converted only first: ", + converted, " codepoints out of: ", wstr.length()); + } + + return result; + } + + // We assume the caller pre-allocated the correct length + Status ConvertToUtf8(const std::wstring& wstr, std::string& str) const { + if (wstr.empty()) { + str.clear(); + return Status::OK(); + } + + std::mbstate_t state = std::mbstate_t(); + + const wchar_t* src = wstr.data(); + const wchar_t* src_end = src + wstr.length(); + + char* dest = str.data(); + char* dest_end = dest + str.length(); + + char* char_next = dest; + const wchar_t* wchar_next = src; + + std::codecvt_base::result ret_code = converter_.out(state, + src, + src_end, + wchar_next, + dest, + dest_end, + char_next); + + if (ret_code != std::codecvt_base::ok) { + size_t converted = narrow(wchar_next - wstr.data()); + return ORT_MAKE_STATUS(ONNXRUNTIME, FAIL, "Failed to convert to UTF-8. Converted only first: ", + converted, " codepoints out of: ", wstr.length()); + } + + str.resize(char_next - dest); + + return Status::OK(); + } + + Status ComputeRequiredSizeToWideChar(const std::string& str, size_t& wchars) { + if (str.empty()) { + wchars = 0; + return Status::OK(); + } + + size_t result = 0; + std::mbstate_t state = std::mbstate_t(); + + const char* src = str.data(); + const char* src_end = src + str.length(); + + wchar_t dummy_dest[128] = {0}; + const char* char_next = src; + wchar_t* wchar_next = dummy_dest; + + size_t converted = 0; + + std::codecvt_base::result ret_code = std::codecvt_base::ok; + while (converted < str.length()) { + ret_code = converter_.in(state, + char_next, + src_end, + char_next, + std::begin(dummy_dest), + std::end(dummy_dest), + wchar_next); + result += (wchar_next - dummy_dest); + converted = (char_next - src); + + if (ret_code != std::codecvt_base::partial && + ret_code != std::codecvt_base::ok) { + break; + } + } + + ORT_ENFORCE(ret_code != std::codecvt_base::noconv, "Conversion is expected"); + + if (ret_code != std::codecvt_base::ok) { + return ORT_MAKE_STATUS(ONNXRUNTIME, FAIL, + "Failed to compute buffer size for wchar_t. Converted only first: ", + converted, " bytes out of: ", str.length(), + " Source: ", src); + } + + wchars = result; + return Status::OK(); + } + + // We assume the destination buffer is preallocated correctly + Status ConvertToWideChar(const std::string& str, std::wstring& wstr) { + if (str.empty()) { + // Preserve the buffer for re-use, just set size to 0 + wstr.clear(); + return Status::OK(); + } + + std::mbstate_t state = std::mbstate_t(); + const char* src = str.data(); + const char* src_end = src + str.length(); + + wchar_t* dest = wstr.data(); + wchar_t* dest_end = dest + wstr.length(); + + const char* char_next = src; + wchar_t* wchar_next = dest; + + std::codecvt_base::result ret_code = converter_.in(state, + src, + src_end, + char_next, + dest, + dest_end, + wchar_next); + + if (ret_code != std::codecvt_base::ok) { + size_t converted = narrow(char_next - str.data()); + return ORT_MAKE_STATUS(ONNXRUNTIME, FAIL, "Failed to convert to wchar_t. Converted only first: ", + converted, " bytes out of: ", str.length(), + " Source: ", src); + } + + wstr.resize(wchar_next - dest); + + return Status::OK(); + } + + std::wstring from_bytes(const std::string& s) { + std::wstring result; + + size_t wchars = 0; + ORT_THROW_IF_ERROR(ComputeRequiredSizeToWideChar(s, wchars)); + + result.resize(wchars); + ORT_THROW_IF_ERROR(ConvertToWideChar(s, result)); + return result; + } + + private: + std::codecvt_utf8 converter_; +}; // We need to specialize for MS as there is // a std::locale creation bug that affects different @@ -83,10 +263,122 @@ class Locale { _locale_t loc_; }; -using Utf8Converter = std::wstring_convert>; +class Utf8ConverterWindows { + public: + size_t ComputeRequiredSizeToUtf8(const std::wstring& wstr) const { + if (wstr.empty()) { + return 0; + } + + int ret = WideCharToMultiByte(CP_UTF8, + 0, + wstr.data(), + narrow(wstr.length()), // We specify the length so no trailing zero terminator + NULL, + 0, // indicates we need the buffer size. + NULL, // Must be NULL for UTF-8 + NULL); // Must be NULL for UTF-8 + + // Failed. This is unlikely since the original UTF-8 to wchar_t succeeded. + // So we throw. + if (ret == 0) { + const auto error_code = GetLastError(); + ORT_THROW("WideCharToMultiByte failed errcode = ", + error_code, " - ", + std::system_category().message(error_code)); + } + + return narrow(ret); + } + + Status ConvertToUtf8(const std::wstring& wstr, std::string& dest) const { + if (wstr.empty()) { + dest.clear(); + return Status::OK(); + } + + const int ret = WideCharToMultiByte(CP_UTF8, 0, + wstr.data(), + narrow(wstr.length()), + dest.data(), + narrow(dest.length()), + nullptr, + nullptr); + + if (ret == 0) { + const auto error_code = GetLastError(); + return ORT_MAKE_STATUS(ONNXRUNTIME, FAIL, "WideCharToMultiByte failed errcode = ", + error_code, " - ", + std::system_category().message(error_code)); + } + + dest.resize(narrow(ret)); + return Status::OK(); + } + + Status ComputeRequiredSizeToWideChar(const std::string& str, size_t& wchars) { + if (str.empty()) { + wchars = 0; + return Status::OK(); + } + + const int ret = MultiByteToWideChar(CP_UTF8, + MB_ERR_INVALID_CHARS, + str.data(), + narrow(str.length()), + nullptr, + 0); + + if (ret == 0) { + const auto error_code = GetLastError(); + return ORT_MAKE_STATUS(ONNXRUNTIME, FAIL, "MultiByteToWideChar failed errcode = ", + error_code, " - ", + std::system_category().message(error_code)); + } + + wchars = narrow(ret); + return Status::OK(); + } + + Status ConvertToWideChar(const std::string& str, std::wstring& wstr) { + if (str.empty()) { + // Preserve the buffer for re-use, just set size to 0 + wstr.clear(); + return Status::OK(); + } + + const int ret = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + str.data(), + narrow(str.length()), + wstr.data(), + narrow(wstr.length())); + + if (ret == 0) { + const auto error_code = GetLastError(); + return ORT_MAKE_STATUS(ONNXRUNTIME, FAIL, "MultiByteToWideChar failed errcode = ", + error_code, " - ", + std::system_category().message(error_code)); + } + + wstr.resize(narrow(ret)); + return Status::OK(); + } + + // Used only in the constructor to initialize stop_words + std::wstring from_bytes(const std::string& s) { + size_t size_required = 0; + ORT_THROW_IF_ERROR(ComputeRequiredSizeToWideChar(s, size_required)); + std::wstring result; + result.resize(size_required); + ORT_THROW_IF_ERROR(ConvertToWideChar(s, result)); + return result; + } +}; const std::string default_locale("en-US"); +using Utf8Converter = Utf8ConverterWindows; + #else // _MSC_VER class Locale { @@ -123,81 +415,11 @@ class Locale { #if defined(__APPLE__) || defined(__ANDROID__) -using Utf8Converter = std::wstring_convert>; +using Utf8Converter = Utf8ConverterGeneric; #else -// All others (not Windows, Apple, or Android) -class Utf8Converter { - public: - Utf8Converter(const std::string&, const std::wstring&) {} - - std::wstring from_bytes(const std::string& s) const { - std::wstring result; - if (s.empty()) { - return result; - } - // Order of arguments is to, from - auto icvt = iconv_open("WCHAR_T", "UTF-8"); - // CentOS is not happy with -1 - if (std::numeric_limits::max() == icvt) { - return wconv_error; - } - - char* iconv_in = const_cast(s.c_str()); - size_t iconv_in_bytes = s.length(); - // Temporary buffer assumes 1 byte to 1 wchar_t - // to make sure it is enough. - const size_t buffer_len = iconv_in_bytes * sizeof(wchar_t); - auto buffer = std::make_unique(buffer_len); - char* iconv_out = buffer.get(); - size_t iconv_out_bytes = buffer_len; - auto ret = iconv(icvt, &iconv_in, &iconv_in_bytes, &iconv_out, &iconv_out_bytes); - if (static_cast(-1) == ret) { - result = wconv_error; - } else { - size_t converted_bytes = buffer_len - iconv_out_bytes; - assert((converted_bytes % sizeof(wchar_t)) == 0); - result.assign(reinterpret_cast(buffer.get()), converted_bytes / sizeof(wchar_t)); - } - iconv_close(icvt); - return result; - } - - std::string to_bytes(const std::wstring& wstr) const { - std::string result; - if (wstr.empty()) { - return result; - } - // Order of arguments is to, from - auto icvt = iconv_open("UTF-8", "WCHAR_T"); - // CentOS is not happy with -1 - if (std::numeric_limits::max() == icvt) { - return conv_error; - } - - // I hope this does not modify the incoming buffer - wchar_t* non_const_in = const_cast(wstr.c_str()); - char* iconv_in = reinterpret_cast(non_const_in); - size_t iconv_in_bytes = wstr.length() * sizeof(wchar_t); - // Temp buffer, assume every code point converts into 3 bytes, this should be enough - // We do not convert terminating zeros - const size_t buffer_len = wstr.length() * 3; - auto buffer = std::make_unique(buffer_len); - - char* iconv_out = buffer.get(); - size_t iconv_out_bytes = buffer_len; - auto ret = iconv(icvt, &iconv_in, &iconv_in_bytes, &iconv_out, &iconv_out_bytes); - if (static_cast(-1) == ret) { - result = conv_error; - } else { - size_t converted_len = buffer_len - iconv_out_bytes; - result.assign(buffer.get(), converted_len); - } - iconv_close(icvt); - return result; - } -}; +using Utf8Converter = Utf8ConverterGeneric; #endif @@ -213,65 +435,11 @@ const std::string default_locale("en_US.UTF-8"); // All non-MS and not Apple #endif #endif // _MSC_VER - -template -Status CopyCaseAction(ForwardIter first, ForwardIter end, OpKernelContext* ctx, - const Locale& loc, - Utf8Converter& converter, - size_t N, size_t C, - StringNormalizer::CaseAction caseaction) { - std::vector output_dims; - if (N == 1) { - output_dims.push_back(1); - } - - // Empty output case - if (C == 0) { - output_dims.push_back(1); - TensorShape output_shape(output_dims); - // This will create one empty string - ctx->Output(0, output_shape); - return Status::OK(); - } - - output_dims.push_back(C); - - TensorShape output_shape(output_dims); - auto output_tensor = ctx->Output(0, output_shape); - auto const output_data = output_tensor->MutableData(); - - size_t output_idx = 0; - while (first != end) { - auto& s = *first; - if (caseaction == StringNormalizer::LOWER || caseaction == StringNormalizer::UPPER) { - std::wstring wstr = converter.from_bytes(s); - if (wstr == wconv_error) { - // Please do not include the input text in the error message as it could - // be deemed as a compliance violation by teams using this operator - return Status(common::ONNXRUNTIME, common::INVALID_ARGUMENT, - "Input contains invalid utf8 chars"); - } - // In place transform - loc.ChangeCase(caseaction, wstr); - *(output_data + output_idx) = converter.to_bytes(wstr); - } else { - assert(caseaction == StringNormalizer::NONE); - // Simple copy or move if the iterator points to a non-const string - *(output_data + output_idx) = std::move(s); - } - ++output_idx; - ++first; - } - return Status::OK(); -} } // namespace string_normalizer using namespace string_normalizer; -StringNormalizer::StringNormalizer(const OpKernelInfo& info) : OpKernel(info), - is_case_sensitive_(true), - case_change_action_(NONE), - compare_caseaction_(NONE) { +StringNormalizer::StringNormalizer(const OpKernelInfo& info) : OpKernel(info) { int64_t iscasesensitive = 0; Status status = info.GetAttr("is_case_sensitive", &iscasesensitive); ORT_ENFORCE(status.IsOK(), "attribute is_case_sensitive is not set"); @@ -290,27 +458,22 @@ StringNormalizer::StringNormalizer(const OpKernelInfo& info) : OpKernel(info), ORT_ENFORCE(false, "attribute case_change_action has invalid value"); } - if (!is_case_sensitive_) { - // Convert stop words to a case which can help us preserve the case of filtered strings - compare_caseaction_ = (case_change_action_ == UPPER) ? UPPER : LOWER; - } - locale_name_ = info.GetAttrOrDefault("locale", default_locale); - Locale locale(locale_name_); - Utf8Converter converter(conv_error, wconv_error); - std::vector swords = info.GetAttrsOrDefault("stopwords"); - for (auto& sw : swords) { - ORT_ENFORCE(!sw.empty(), "Empty stopwords not allowed"); - if (is_case_sensitive_) { - auto p = stopwords_.insert(std::move(sw)); - ORT_ENFORCE(p.second, "Duplicate stopwords not allowed"); - } else { - std::wstring wstr = converter.from_bytes(sw); - ORT_ENFORCE(wstr != wconv_error, "Stopword contains invalid utf8 chars"); + std::vector stop_words = info.GetAttrsOrDefault("stopwords"); + if (is_case_sensitive_) { + stopwords_.reserve(stop_words.size()); + for (std::string& s : stop_words) { + stopwords_.insert(std::move(s)); + } + } else { + Locale locale(locale_name_); + Utf8Converter converter; + wstopwords_.reserve(stop_words.size()); + for (std::string& s : stop_words) { + std::wstring wstr = converter.from_bytes(s); locale.ChangeCase(compare_caseaction_, wstr); - auto p = wstopwords_.insert(std::move(wstr)); - ORT_ENFORCE(p.second, "Duplicate stopwords not allowed"); + wstopwords_.insert(std::move(wstr)); } } } @@ -319,95 +482,155 @@ Status StringNormalizer::Compute(OpKernelContext* ctx) const { using namespace string_normalizer; auto X = ctx->Input(0); - if (X == nullptr) return Status(common::ONNXRUNTIME, common::FAIL, "input count mismatch"); auto input_dims = X->Shape().GetDims(); + auto input_span = X->DataAsSpan(); - size_t N = 0; - size_t C = 0; + TensorShapeVector output_shape; + int64_t C = 0; if (input_dims.size() == 1) { if (input_dims[0] < 1) { return Status(common::ONNXRUNTIME, common::INVALID_ARGUMENT, "Single dimension value must be greater than 0"); } - C = onnxruntime::narrow(input_dims[0]); + C = input_dims[0]; } else if (input_dims.size() == 2) { if (input_dims[0] != 1 || input_dims[1] < 1) { return Status(common::ONNXRUNTIME, common::INVALID_ARGUMENT, "Input dimensions are either[C > 0] or [1][C > 0] allowed"); } - N = 1; - C = onnxruntime::narrow(input_dims[1]); + output_shape.push_back(1); + C = input_dims[1]; } else { return Status(common::ONNXRUNTIME, common::INVALID_ARGUMENT, "Input dimensions are either[C > 0] or [1][C > 0] allowed"); } - Status status; + // Special case, no filtering and no case change + if (case_change_action_ == NONE && + ((is_case_sensitive_ && stopwords_.empty()) || + (!is_case_sensitive_ && wstopwords_.empty()))) { + output_shape.push_back(C); + auto output_tensor = ctx->Output(0, output_shape); + auto const output_data = output_tensor->MutableData(); + std::copy(input_span.begin(), input_span.end(), output_data); + return Status::OK(); + } + + // We need to know the result dimension, and for that we need to filter + // the words first. If comparison mode is case sensitive, we just go ahead + // and compare with the original strings. Otherwise, we need to convert the string + // to widechar, lowercase it and then compare. Case-insensitive comparison is complicated + // for UTF-8 and requires additional dependency. + Locale locale(locale_name_); - Utf8Converter converter(conv_error, wconv_error); - auto* const input_data = X->Data(); - using StrRef = std::reference_wrapper; - if (is_case_sensitive_) { - if (!stopwords_.empty()) { - InlinedVector filtered_strings; - filtered_strings.reserve(C); - auto first = input_data; - auto const last = input_data + C; - while (first != last) { - const std::string& s = *first; - if (0 == stopwords_.count(s)) { - filtered_strings.push_back(std::cref(s)); - } - ++first; + Utf8Converter converter; + + // Compute the largest widestring buffer needed. + size_t max_wide_buffer_len = 0; + for (const auto& s : input_span) { + size_t wchars = 0; + // Checks for invalid UTF-8 characters on Windows + ORT_RETURN_IF_ERROR(converter.ComputeRequiredSizeToWideChar(s, wchars)); + max_wide_buffer_len = std::max(max_wide_buffer_len, wchars); + } + + // Reuse reserved space + std::wstring wchar_buffer; + wchar_buffer.reserve(max_wide_buffer_len); + + // Output everything and change case as required + auto output_no_filtering = [&](const TensorShape& output_shape) { + auto output_tensor = ctx->Output(0, output_shape); + auto const output_data = output_tensor->MutableData(); + for (size_t i = 0, lim = input_span.size(); i < lim; ++i) { + const std::string& s = input_span[i]; + wchar_buffer.resize(max_wide_buffer_len); + ORT_RETURN_IF_ERROR(converter.ConvertToWideChar(s, wchar_buffer)); + locale.ChangeCase(case_change_action_, wchar_buffer); + + auto& dest = output_data[i]; + size_t utf8_buffer_len = converter.ComputeRequiredSizeToUtf8(wchar_buffer); + dest.resize(utf8_buffer_len); + ORT_RETURN_IF_ERROR(converter.ConvertToUtf8(wchar_buffer, dest)); + } + return Status::OK(); + }; + + auto output_filtered = [&](const TensorShape& output_shape, gsl::span filtered_indices) { + auto output_tensor = ctx->Output(0, output_shape); + auto output_data = output_tensor->MutableData(); + for (size_t i : filtered_indices) { + const std::string& s = input_span[i]; + if (case_change_action_ != NONE) { + wchar_buffer.resize(max_wide_buffer_len); + ORT_RETURN_IF_ERROR(converter.ConvertToWideChar(s, wchar_buffer)); + locale.ChangeCase(case_change_action_, wchar_buffer); + + auto& dest = *output_data++; + size_t utf8_buffer_len = converter.ComputeRequiredSizeToUtf8(wchar_buffer); + dest.resize(utf8_buffer_len); + ORT_RETURN_IF_ERROR(converter.ConvertToUtf8(wchar_buffer, dest)); + } else { + *output_data++ = s; } - status = CopyCaseAction(filtered_strings.cbegin(), filtered_strings.cend(), ctx, locale, converter, - N, filtered_strings.size(), case_change_action_); + } + return Status::OK(); + }; + + Status status; + + if (is_case_sensitive_) { + if (stopwords_.empty()) { + assert(case_change_action_ != NONE); + output_shape.push_back(C); + status = output_no_filtering(output_shape); } else { - // Nothing to filter. Copy input to output and change case if needed - status = CopyCaseAction(input_data, input_data + C, ctx, locale, converter, N, C, case_change_action_); + // we need to filter + InlinedVector filtered_strings_indices; + filtered_strings_indices.reserve(input_span.size()); + + for (size_t i = 0, lim = input_span.size(); i < lim; ++i) { + const std::string& s = input_span[i]; + if (stopwords_.count(s) == 0) { + filtered_strings_indices.push_back(i); + } + } + + // According to the spec, if all strings are filtered out + // the output must have a shape of {1} with a single empty string. + const int64_t filtered_count = std::max(1, narrow(filtered_strings_indices.size())); + output_shape.push_back(filtered_count); + status = output_filtered(output_shape, filtered_strings_indices); } } else { - if (!wstopwords_.empty()) { - // Filter input. When no case action is required - // we simply store original string references. - // Otherwise, we store converted strings. - InlinedVector filtered_orignal_strings; - InlinedVector filtered_cased_strings; - filtered_orignal_strings.reserve(C); - filtered_cased_strings.reserve(C); - auto first = input_data; - auto const last = input_data + C; - while (first != last) { - const std::string& s = *first; - std::wstring wstr = converter.from_bytes(s); - if (wstr == wconv_error) { - // Please do not include the input text in the error message as it could - // be deemed as a compliance violation by teams using this operator - return Status(common::ONNXRUNTIME, common::INVALID_ARGUMENT, - "Input contains invalid utf8 chars"); - } - locale.ChangeCase(compare_caseaction_, wstr); - if (0 == wstopwords_.count(wstr)) { - if (case_change_action_ == NONE) { - filtered_orignal_strings.push_back(std::cref(s)); - } else { - filtered_cased_strings.push_back(converter.to_bytes(wstr)); - } - } - ++first; - } - if (case_change_action_ == NONE) { - status = CopyCaseAction(filtered_orignal_strings.cbegin(), filtered_orignal_strings.cend(), ctx, locale, converter, - N, filtered_orignal_strings.size(), NONE); - } else { - status = CopyCaseAction(filtered_cased_strings.begin(), filtered_cased_strings.end(), ctx, locale, converter, - N, filtered_cased_strings.size(), NONE); - } + if (wstopwords_.empty()) { + assert(case_change_action_ != NONE); + output_shape.push_back(C); + status = output_no_filtering(output_shape); } else { - // Nothing to filter. Copy input to output and change case if needed - status = CopyCaseAction(input_data, input_data + C, ctx, locale, converter, N, C, case_change_action_); + // Case insensitive filtering is performed by converting the input strings + // to compare_caseaction_. For that we convert to wchar_t UNICODE. + // Otherwise, we need to pull ICU library on all platforms. + InlinedVector filtered_strings_indices; + filtered_strings_indices.reserve(input_span.size()); + for (size_t i = 0, lim = input_span.size(); i < lim; ++i) { + const std::string& s = input_span[i]; + wchar_buffer.resize(max_wide_buffer_len); + ORT_RETURN_IF_ERROR(converter.ConvertToWideChar(s, wchar_buffer)); + locale.ChangeCase(compare_caseaction_, wchar_buffer); + if (wstopwords_.count(wchar_buffer) == 0) { + filtered_strings_indices.push_back(i); + } + } + + // According to the spec, if all strings are filtered out + // the output must have a shape of {1} with a single empty string. + const int64_t filtered_count = std::max(1, narrow(filtered_strings_indices.size())); + output_shape.push_back(filtered_count); + status = output_filtered(output_shape, filtered_strings_indices); } } + return status; } } // namespace onnxruntime diff --git a/onnxruntime/core/providers/cpu/text/string_normalizer.h b/onnxruntime/core/providers/cpu/text/string_normalizer.h index 50a4afd8d6..750c59ec21 100644 --- a/onnxruntime/core/providers/cpu/text/string_normalizer.h +++ b/onnxruntime/core/providers/cpu/text/string_normalizer.h @@ -25,9 +25,11 @@ class StringNormalizer : public OpKernel { Status Compute(OpKernelContext* ctx) const override; private: - bool is_case_sensitive_; - CaseAction case_change_action_; - CaseAction compare_caseaction_; // used for case-insensitive compare + bool is_case_sensitive_{true}; + CaseAction case_change_action_{NONE}; + // Set this to lower because some characters do not have capital case. + // used for case-insensitive compare + CaseAction compare_caseaction_{LOWER}; std::string locale_name_; // Either if these are populated but not both InlinedHashSet stopwords_; diff --git a/onnxruntime/test/providers/cpu/text/string_normalizer_test.cc b/onnxruntime/test/providers/cpu/text/string_normalizer_test.cc index 5279a0ef2b..724fdb078e 100644 --- a/onnxruntime/test/providers/cpu/text/string_normalizer_test.cc +++ b/onnxruntime/test/providers/cpu/text/string_normalizer_test.cc @@ -1,11 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -#if ((__cplusplus >= 201703L) || (defined(_MSVC_LANG) && (_MSVC_LANG >= 201703L))) -// TODO: handle the u8string. -#else + #include "gtest/gtest.h" #include "test/providers/provider_test_utils.h" +#ifdef __APPLE__ +#include +#if TARGET_OS_IOS +#define ORT_IOS +#endif +#endif + namespace onnxruntime { namespace test { @@ -19,10 +24,10 @@ const std::string test_locale("en-US"); const std::string test_locale("en_US.UTF-8"); #endif -void InitTestAttr(OpTester& test, const std::string& case_change_action, - bool is_case_sensitive, - const std::vector& stopwords, - const std::string& locale) { +static void InitTestAttr(OpTester& test, const std::string& case_change_action, + bool is_case_sensitive, + const std::vector& stopwords, + const std::string& locale) { if (!case_change_action.empty()) { test.AddAttribute("case_change_action", case_change_action); } @@ -38,170 +43,194 @@ void InitTestAttr(OpTester& test, const std::string& case_change_action, using namespace str_normalizer_test; -TEST(ContribOpTest, StringNormalizerTest) { +TEST(ContribOpTest, StringNormalizerSensitiveNoCase) { // - casesensitive approach // - no stopwords. // - No change case action, expecting default to take over - { - OpTester test("StringNormalizer", opset_ver, domain); - InitTestAttr(test, "", true, {}, test_locale); - std::vector dims{4}; - std::vector input = {std::string("monday"), std::string("tuesday"), - std::string("wednesday"), std::string("thursday")}; - test.AddInput("T", dims, input); - std::vector output(input); // do the same for now - test.AddOutput("Y", dims, output); - test.Run(OpTester::ExpectResult::kExpectSuccess); - } + OpTester test("StringNormalizer", opset_ver, domain); + InitTestAttr(test, "", true, {}, test_locale); + std::vector dims{4}; + std::vector input = {std::string("monday"), std::string("tuesday"), + std::string("wednesday"), std::string("thursday")}; + test.AddInput("T", dims, input); + std::vector output(input); // do the same for now + test.AddOutput("Y", dims, output); + test.Run(OpTester::ExpectResult::kExpectSuccess); +} + +TEST(ContribOpTest, StringNormalizerSensitiveFilterOutNoCase) { // - casesensitive approach // - filter out monday // - No change case action - { - OpTester test("StringNormalizer", opset_ver, domain); - InitTestAttr(test, "NONE", true, {"monday"}, test_locale); - std::vector dims{4}; - std::vector input = {std::string("monday"), std::string("tuesday"), - std::string("wednesday"), std::string("thursday")}; - test.AddInput("T", dims, input); - std::vector output = {std::string("tuesday"), - std::string("wednesday"), std::string("thursday")}; - test.AddOutput("Y", {3}, output); - test.Run(OpTester::ExpectResult::kExpectSuccess); - } + OpTester test("StringNormalizer", opset_ver, domain); + InitTestAttr(test, "NONE", true, {"monday"}, test_locale); + std::vector dims{4}; + std::vector input = {std::string("monday"), std::string("tuesday"), + std::string("wednesday"), std::string("thursday")}; + test.AddInput("T", dims, input); + + std::vector output = {std::string("tuesday"), + std::string("wednesday"), std::string("thursday")}; + test.AddOutput("Y", {3}, output); + test.Run(OpTester::ExpectResult::kExpectSuccess); +} + +TEST(ContribOpTest, StringNormalizerSensitiveFilterOutLower) { // - casesensitive approach // - filter out monday // - LOWER should produce the same output as they are all lower. - { - OpTester test("StringNormalizer", opset_ver, domain); - InitTestAttr(test, "LOWER", true, {"monday"}, test_locale); - std::vector dims{4}; - std::vector input = {std::string("monday"), std::string("tuesday"), - std::string("wednesday"), std::string("thursday")}; - test.AddInput("T", dims, input); + OpTester test("StringNormalizer", opset_ver, domain); + InitTestAttr(test, "LOWER", true, {"monday"}, test_locale); + std::vector dims{4}; + std::vector input = {std::string("monday"), std::string("tuesday"), + std::string("wednesday"), std::string("thursday")}; + test.AddInput("T", dims, input); - std::vector output = {std::string("tuesday"), - std::string("wednesday"), std::string("thursday")}; - test.AddOutput("Y", {3}, output); - test.Run(OpTester::ExpectResult::kExpectSuccess); - } - // - casesensitive approach - // - filter out monday - // - UPPER should produce the same output as they are all lower. - { - OpTester test("StringNormalizer", opset_ver, domain); - InitTestAttr(test, "UPPER", true, {"monday"}, test_locale); - std::vector dims{4}; - std::vector input = {std::string("monday"), std::string("tuesday"), - std::string("wednesday"), std::string("thursday")}; - test.AddInput("T", dims, input); - - std::vector output = {std::string("TUESDAY"), - std::string("WEDNESDAY"), std::string("THURSDAY")}; - test.AddOutput("Y", {3}, output); - test.Run(OpTester::ExpectResult::kExpectSuccess); - } - // - case-SENSETIVE approach en_US locale - // - we test the behavior of a mix of english, french, german, russian and chinese - // with en_US locale - // - filter out monday - // - UPPER should produce the same output as they are all lower. - { - OpTester test("StringNormalizer", opset_ver, domain); - InitTestAttr(test, "UPPER", true, {u8"monday"}, test_locale); - std::vector dims{7}; - std::vector input = {std::string(u8"monday"), - std::string(u8"tuesday"), - std::string(u8"Besançon"), - std::string(u8"École élémentaire"), - std::string(u8"Понедельник"), - std::string(u8"mit freundlichen grüßen"), - std::string(u8"中文")}; - test.AddInput("T", dims, input); - - // en_US results (default) - std::vector output = {std::string(u8"TUESDAY"), - // It does upper case cecedille, accented E - // and german umlaut but fails - // with german eszett - std::string(u8"BESANÇON"), - std::string(u8"ÉCOLE ÉLÉMENTAIRE"), - // No issues with Cyrllic - std::string(u8"ПОНЕДЕЛЬНИК"), - std::string(u8"MIT FREUNDLICHEN GRÜßEN"), - // Chinese do not have cases - std::string(u8"中文")}; - test.AddOutput("Y", {6}, output); - test.Run(OpTester::ExpectResult::kExpectSuccess); - } - // - case-INSENSETIVE approach en_US locale - // - we test the behavior of a mix of english, french, german, russian and chinese - // with en_US locale - // - filter out monday - // - UPPER should produce the same output as they are all lower. - { - OpTester test("StringNormalizer", opset_ver, domain); - InitTestAttr(test, "UPPER", false, {u8"monday"}, test_locale); - std::vector dims{7}; - std::vector input = {std::string(u8"monday"), - std::string(u8"tuesday"), - std::string(u8"Besançon"), - std::string(u8"École élémentaire"), - std::string(u8"Понедельник"), - std::string(u8"mit freundlichen grüßen"), - std::string(u8"中文")}; - test.AddInput("T", dims, input); - - // en_US results (default) - std::vector output = {std::string(u8"TUESDAY"), - // It does upper case cecedille, accented E - // and german umlaut but fails - // with german eszett - std::string(u8"BESANÇON"), - std::string(u8"ÉCOLE ÉLÉMENTAIRE"), - // No issues with Cyrllic - std::string(u8"ПОНЕДЕЛЬНИК"), - std::string(u8"MIT FREUNDLICHEN GRÜßEN"), - // Chinese do not have cases - std::string(u8"中文")}; - test.AddOutput("Y", {6}, output); - test.Run(OpTester::ExpectResult::kExpectSuccess); - } - - // Empty output case - // - casesensitive approach - // - filter out monday - // - UPPER should produce the same output as they are all lower. - { - OpTester test("StringNormalizer", opset_ver, domain); - InitTestAttr(test, "UPPER", true, {"monday"}, test_locale); - std::vector dims{2}; - std::vector input = {std::string("monday"), - std::string("monday")}; - test.AddInput("T", dims, input); - - std::vector output{""}; // One empty string - test.AddOutput("Y", {1}, output); - test.Run(OpTester::ExpectResult::kExpectSuccess); - } - // Empty output case - // - casesensitive approach - // - filter out monday - // - UPPER should produce the same output as they are all lower. - { - OpTester test("StringNormalizer", opset_ver, domain); - InitTestAttr(test, "UPPER", true, {"monday"}, ""); - std::vector dims{1, 2}; - std::vector input = {std::string("monday"), - std::string("monday")}; - test.AddInput("T", dims, input); - - std::vector output{""}; // One empty string - test.AddOutput("Y", {1, 1}, output); - test.Run(OpTester::ExpectResult::kExpectSuccess); - } + std::vector output = {std::string("tuesday"), + std::string("wednesday"), std::string("thursday")}; + test.AddOutput("Y", {3}, output); + test.Run(OpTester::ExpectResult::kExpectSuccess); } +TEST(ContribOpTest, StringNormalizerSensitiveFilterOutUpper) { + // - casesensitive approach + // - filter out monday + // - UPPER should produce the same output as they are all lower. + + OpTester test("StringNormalizer", opset_ver, domain); + InitTestAttr(test, "UPPER", true, {"monday"}, test_locale); + std::vector dims{4}; + std::vector input = {std::string("monday"), std::string("tuesday"), + std::string("wednesday"), std::string("thursday")}; + test.AddInput("T", dims, input); + + std::vector output = {std::string("TUESDAY"), + std::string("WEDNESDAY"), std::string("THURSDAY")}; + test.AddOutput("Y", {3}, output); + test.Run(OpTester::ExpectResult::kExpectSuccess); +} + +TEST(ContribOpTest, StringNormalizerSensitiveFilterOutUpperWithLocale) { + // - case-SENSITIVE approach en_US locale + // - we test the behavior of a mix of english, french, german, russian and chinese + // with en_US locale + // - filter out monday + // - UPPER should produce the same output as they are all lower. + + OpTester test("StringNormalizer", opset_ver, domain); + InitTestAttr(test, "UPPER", true, {"monday"}, test_locale); + std::vector dims{7}; + std::vector input = {"monday", + "tuesday", + "Besançon", + "École élémentaire", + "Понедельник", + "mit freundlichen grüßen", + "中文"}; + test.AddInput("T", dims, input); + + // en_US results (default) + std::vector output = {"TUESDAY", + // It does upper case cecedille, accented E + "BESANÇON", + "ÉCOLE ÉLÉMENTAIRE", + // No issues with Cyrllic + "ПОНЕДЕЛЬНИК", + // Works with german umlaut but fails + // with german Eszett. Reason being, capital case for Eszett + // was introduced only recently into encodings + // and some platforms produce it, but others do not +#ifdef __wasm__ + "MIT FREUNDLICHEN GRÜẞEN", +#else + "MIT FREUNDLICHEN GRÜßEN", +#endif + // Chinese do not have cases + "中文"}; + test.AddOutput("Y", {6}, output); + test.Run(OpTester::ExpectResult::kExpectSuccess); +} + +TEST(ContribOpTest, StringNormalizerInsensitiveFilterOutUpperWithLocale) { + // - case-INSENSITIVE approach en_US locale + // - we test the behavior of a mix of english, french, german, russian and chinese + // with en_US locale + // - filter out monday + // - UPPER should produce the same output as they are all lower. + + OpTester test("StringNormalizer", opset_ver, domain); + InitTestAttr(test, "UPPER", false, {"monday"}, test_locale); + std::vector dims{7}; + std::vector input = {"monday", + "tuesday", + "Besançon", + "École élémentaire", + "Понедельник", + "mit freundlichen grüßen", + "中文"}; + test.AddInput("T", dims, input); + + // en_US results (default) + std::vector output = {"TUESDAY", + // It does upper case cecedille, and accented E + "BESANÇON", + "ÉCOLE ÉLÉMENTAIRE", + // No issues with Cyrllic + "ПОНЕДЕЛЬНИК", + // Works with german umlaut but fails + // with german Eszett (ß). Reason being, capital case for Eszett + // was introduced only recently into encodings + // and some platforms produce it, but others do not +#ifdef __wasm__ + "MIT FREUNDLICHEN GRÜẞEN", +#else + "MIT FREUNDLICHEN GRÜßEN", +#endif + + // Chinese do not have cases + "中文"}; + test.AddOutput("Y", {6}, output); + test.Run(OpTester::ExpectResult::kExpectSuccess); +} + +TEST(ContribOpTest, StringNormalizerSensitiveFilterOutUpperEmptyCase) { + // Empty output case + // - casesensitive approach + // - filter out monday + // - UPPER should produce the same output as they are all lower. + + OpTester test("StringNormalizer", opset_ver, domain); + InitTestAttr(test, "UPPER", true, {"monday"}, test_locale); + std::vector dims{2}; + std::vector input = {"monday", "monday"}; + test.AddInput("T", dims, input); + + std::vector output{""}; // One empty string + test.AddOutput("Y", {1}, output); + test.Run(OpTester::ExpectResult::kExpectSuccess); +} + +// Fails on iOS because necessary locales are not installed +// MacOS runs fine. +#ifndef ORT_IOS +TEST(ContribOpTest, StringNormalizerSensitiveFilterOutUpperSameOutput) { + // Empty output case + // - casesensitive approach + // - filter out monday + // - UPPER should produce the same output as they are all lower. + OpTester test("StringNormalizer", opset_ver, domain); + InitTestAttr(test, "UPPER", true, {"monday"}, ""); + std::vector dims{1, 2}; + std::vector input = {std::string("monday"), + std::string("monday")}; + test.AddInput("T", dims, input); + + std::vector output{""}; // One empty string + test.AddOutput("Y", {1, 1}, output); + test.Run(OpTester::ExpectResult::kExpectSuccess); +} +#endif + } // namespace test } // namespace onnxruntime -#endif \ No newline at end of file