diff --git a/cpp/include/cudf/strings/combine.hpp b/cpp/include/cudf/strings/combine.hpp index 8382e616a64..024ebb6250c 100644 --- a/cpp/include/cudf/strings/combine.hpp +++ b/cpp/include/cudf/strings/combine.hpp @@ -98,6 +98,64 @@ std::unique_ptr join_strings( string_scalar const& narep = string_scalar("", false), rmm::mr::device_memory_resource* mr = rmm::mr::get_default_resource()); +/** + * @brief Concatenates a list of strings columns using separators for each row + * and returns the result as a string column. + * + * Each new string is created by concatenating the strings from the same + * row delimited by the row separator provided for that row. The following rules + * are applicable: + * + * - If row separator for a given row is null, output column for that row is null, unless + * there is a valid @p separator_narep + * - If all column values for a given row is null, output column for that row is null, unless + * there is a valid @p col_narep + * - null column values for a given row are skipped, if the column replacement isn't valid + * - The separator is only applied between two valid column values + * - If valid @p separator_narep and @p col_narep are provided, the output column is always + * non nullable + * + * @code{.pseudo} + * Example: + * c0 = ['aa', null, '', 'ee', null, 'ff'] + * c1 = [null, 'cc', 'dd', null, null, 'gg'] + * c2 = ['bb', '', null, null, null, 'hh'] + * sep = ['::', '%%', '^^', '!', '*', null] + * out0 = concatenate([c0, c1, c2], sep) + * out0 is ['aa::bb', 'cc%%', '^^dd', 'ee', null, null] + * + * sep_rep = '+' + * out1 = concatenate([c0, c1, c2], sep, sep_rep) + * out1 is ['aa::bb', 'cc%%', '^^dd', 'ee', null, 'ff+gg+hh'] + * + * col_rep = '-' + * out2 = concatenate([c0, c1, c2], sep, invalid_sep_rep, col_rep) + * out2 is ['aa::-::bb', '-%%cc%%', '^^dd^^-', 'ee!-!-', '-*-*-', null] + * @endcode + * + * @throw cudf::logic_error if no input columns are specified - table view is empty + * @throw cudf::logic_error if input columns are not all strings columns. + * @throw cudf::logic_error if the number of rows from @p separators and @p strings_columns + * do not match + * + * @param strings_columns List of string columns to concatenate. + * @param separators String column that provides the separator for a given row + * @param separator_narep String that should be used in place of a null separator for a given + * row. Default of invalid-scalar means no row separator value replacements. + * Default is an invalid string. + * @param col_narep String that should be used in place of any null strings + * found in any column. Default of invalid-scalar means no null column value replacements. + * Default is an invalid string. + * @param mr Resource for allocating device memory. + * @return New column with concatenated results. + */ +std::unique_ptr concatenate( + table_view const& strings_columns, + strings_column_view const& separators, + string_scalar const& separator_narep = string_scalar("", false), + string_scalar const& col_narep = string_scalar("", false), + rmm::mr::device_memory_resource* mr = rmm::mr::get_default_resource()); + /** @} */ // end of doxygen group } // namespace strings } // namespace cudf diff --git a/cpp/src/strings/combine.cu b/cpp/src/strings/combine.cu index 2de4e0b3977..0b9322aa637 100644 --- a/cpp/src/strings/combine.cu +++ b/cpp/src/strings/combine.cu @@ -245,6 +245,195 @@ std::unique_ptr join_strings( mr); } +// +std::unique_ptr concatenate(table_view const& strings_columns, + strings_column_view const& separators, + string_scalar const& separator_narep, + string_scalar const& col_narep, + rmm::mr::device_memory_resource* mr, + cudaStream_t stream) +{ + auto num_columns = strings_columns.num_columns(); + CUDF_EXPECTS(num_columns > 0, "At least one column must be specified"); + // Check if all columns are of type string + CUDF_EXPECTS(std::all_of(strings_columns.begin(), + strings_columns.end(), + [](auto c) { return c.type().id() == STRING; }), + "All columns must be of type string"); + + auto strings_count = strings_columns.num_rows(); + CUDF_EXPECTS(strings_count == separators.size(), + "Separators column should be of the same size of string columns"); + if (strings_count == 0) // Empty begets empty + return detail::make_empty_strings_column(mr, stream); + + // Invalid output column strings - null rows + string_view const invalid_str{nullptr, 0}; + auto const separator_rep = get_scalar_device_view(const_cast(separator_narep)); + auto const col_rep = get_scalar_device_view(const_cast(col_narep)); + auto const separator_col_view_ptr = column_device_view::create(separators.parent(), stream); + auto const separator_col_view = *separator_col_view_ptr; + + if (num_columns == 1) { + // Shallow copy of the resultant strings + rmm::device_vector out_col_strings(strings_count); + + // Device view of the only column in the table view + auto const col0_ptr = column_device_view::create(strings_columns.column(0), stream); + auto const col0 = *col0_ptr; + + // Execute it on every element + thrust::transform( + rmm::exec_policy(stream)->on(stream), + thrust::make_counting_iterator(0), + thrust::make_counting_iterator(strings_count), + out_col_strings.data().get(), + // Output depends on the separator + [col0, invalid_str, separator_col_view, separator_rep, col_rep] __device__(auto ridx) { + if (!separator_col_view.is_valid(ridx) && !separator_rep.is_valid()) return invalid_str; + return (!col0.is_valid(ridx)) ? (col_rep.is_valid() ? col_rep.value() : invalid_str) + : col0.element(ridx); + }); + + return make_strings_column(out_col_strings, invalid_str, stream, mr); + } + + // Create device views from the strings columns. + auto table = table_device_view::create(strings_columns, stream); + auto d_table = *table; + + // Create resulting null mask + auto valid_mask = cudf::experimental::detail::valid_if( + thrust::make_counting_iterator(0), + thrust::make_counting_iterator(strings_count), + [d_table, separator_col_view, separator_rep, col_rep] __device__(size_type ridx) { + if (!separator_col_view.is_valid(ridx) && !separator_rep.is_valid()) return false; + bool all_nulls = + thrust::all_of(thrust::seq, d_table.begin(), d_table.end(), [ridx](auto const& col) { + return col.is_null(ridx); + }); + return all_nulls ? col_rep.is_valid() : true; + }, + stream, + mr); + + auto null_count = valid_mask.second; + + // Build offsets column by computing sizes of each string in the output + auto offsets_transformer = [d_table, separator_col_view, separator_rep, col_rep] __device__( + size_type ridx) { + // If the separator value for the row is null and if there aren't global separator + // replacements, this row does not have any value - null row + if (!separator_col_view.is_valid(ridx) && !separator_rep.is_valid()) return 0; + + // For this row (idx), iterate over each column and add up the bytes + bool all_nulls = + thrust::all_of(thrust::seq, d_table.begin(), d_table.end(), [ridx](auto const& d_column) { + return d_column.is_null(ridx); + }); + // If all column values are null and there isn't a global column replacement value, this row + // is a null row + if (all_nulls && !col_rep.is_valid()) return 0; + + // There is at least one non-null column value (it can still be empty though) + auto separator_str = separator_col_view.is_valid(ridx) + ? separator_col_view.element(ridx) + : separator_rep.value(); + + size_type bytes = thrust::transform_reduce( + thrust::seq, + d_table.begin(), + d_table.end(), + [ridx, separator_str, col_rep] __device__(column_device_view const& d_column) { + // If column is null and there isn't a valid column replacement, this isn't used in + // final string concatenate + if (d_column.is_null(ridx) && !col_rep.is_valid()) return 0; + return separator_str.size_bytes() + (d_column.is_null(ridx) + ? col_rep.size() + : d_column.element(ridx).size_bytes()); + }, + 0, + thrust::plus()); + + // Null/empty separator and columns doesn't produce a non-empty string + if (bytes == 0) assert(separator_str.size_bytes() == 0); + + // Separator goes only in between elements + return bytes - separator_str.size_bytes(); + }; + auto offsets_transformer_itr = thrust::make_transform_iterator( + thrust::make_counting_iterator(0), offsets_transformer); + auto offsets_column = detail::make_offsets_child_column( + offsets_transformer_itr, offsets_transformer_itr + strings_count, mr, stream); + auto d_results_offsets = offsets_column->view().data(); + + // Create the chars column + size_type bytes = thrust::device_pointer_cast(d_results_offsets)[strings_count]; + auto chars_column = + strings::detail::create_chars_child_column(strings_count, null_count, bytes, mr, stream); + + // Fill the chars column + auto d_results_chars = chars_column->mutable_view().data(); + thrust::for_each_n(rmm::exec_policy(stream)->on(stream), + thrust::make_counting_iterator(0), + strings_count, + [d_table, + num_columns, + d_results_offsets, + d_results_chars, + separator_col_view, + separator_rep, + col_rep] __device__(size_type ridx) { + // If the separator for this row is null and if there isn't a valid separator + // to replace, do not write anything for this row + if (!separator_col_view.is_valid(ridx) && !separator_rep.is_valid()) return; + + bool all_nulls = thrust::all_of( + thrust::seq, d_table.begin(), d_table.end(), [ridx](auto const& col) { + return col.is_null(ridx); + }); + + // If all column values are null and there isn't a valid column replacement, + // skip this row + if (all_nulls && !col_rep.is_valid()) return; + + size_type offset = d_results_offsets[ridx]; + char* d_buffer = d_results_chars + offset; + bool colval_written = false; + + // There is at least one non-null column value (it can still be empty though) + auto separator_str = separator_col_view.is_valid(ridx) + ? separator_col_view.element(ridx) + : separator_rep.value(); + + // Write out each column's entry for this row + for (size_type col_idx = 0; col_idx < num_columns; ++col_idx) { + auto d_column = d_table.column(col_idx); + // If the column isn't valid and if there isn't a replacement for it, skip + // it + if (d_column.is_null(ridx) && !col_rep.is_valid()) continue; + + // Separator goes only in between elements + if (colval_written) + d_buffer = detail::copy_string(d_buffer, separator_str); + + string_view d_str = d_column.is_null(ridx) + ? col_rep.value() + : d_column.element(ridx); + d_buffer = detail::copy_string(d_buffer, d_str); + colval_written = true; + } + }); + + return make_strings_column(strings_count, + std::move(offsets_column), + std::move(chars_column), + null_count, + (null_count) ? std::move(valid_mask.first) : rmm::device_buffer{}, + stream, + mr); +} + } // namespace detail // APIs @@ -267,5 +456,15 @@ std::unique_ptr join_strings(strings_column_view const& strings, return detail::join_strings(strings, separator, narep, mr); } +std::unique_ptr concatenate(table_view const& strings_columns, + strings_column_view const& separators, + string_scalar const& separator_narep, + string_scalar const& col_narep, + rmm::mr::device_memory_resource* mr) +{ + CUDF_FUNC_RANGE(); + return detail::concatenate(strings_columns, separators, separator_narep, col_narep, mr, 0); +} + } // namespace strings } // namespace cudf diff --git a/cpp/tests/strings/combine_tests.cpp b/cpp/tests/strings/combine_tests.cpp index 7fdd02036b2..6c3f39f9dfa 100644 --- a/cpp/tests/strings/combine_tests.cpp +++ b/cpp/tests/strings/combine_tests.cpp @@ -162,3 +162,296 @@ TEST_F(StringsCombineTest, JoinAllNullStringsColumn) cudf::test::strings_column_wrapper expected3({"*-*-*"}); cudf::test::expect_columns_equal(*results, expected3); } + +struct StringsConcatenateWithColSeparatorTest : public cudf::test::BaseFixture { +}; + +TEST_F(StringsConcatenateWithColSeparatorTest, ExceptionTests) +{ + // Exception tests + // 0. 0 columns passed + // 1. > 0 columns passed; some using non string data types + // 2. separator column of different size to column size + { + EXPECT_THROW(cudf::strings::concatenate(cudf::table_view{}, + cudf::strings_column_view{cudf::column_view{}}), + cudf::logic_error); + } + + { + cudf::column_view col0(cudf::data_type{cudf::STRING}, 0, nullptr, nullptr, 0); + cudf::test::fixed_width_column_wrapper col1{{1}}; + + EXPECT_THROW( + cudf::strings::concatenate(cudf::table_view{{col0, col1}}, cudf::strings_column_view(col0)), + cudf::logic_error); + } + + { + auto col0 = cudf::test::strings_column_wrapper({"", "", "", ""}, {false, true, true, false}); + auto sep_col = cudf::test::strings_column_wrapper({"", ""}, {true, false}); + + EXPECT_THROW( + cudf::strings::concatenate(cudf::table_view{{col0}}, cudf::strings_column_view(sep_col)), + cudf::logic_error); + } +} + +TEST_F(StringsConcatenateWithColSeparatorTest, ZeroSizedColumns) +{ + cudf::column_view col0(cudf::data_type{cudf::STRING}, 0, nullptr, nullptr, 0); + + auto results = + cudf::strings::concatenate(cudf::table_view{{col0}}, cudf::strings_column_view(col0)); + cudf::test::expect_strings_empty(results->view()); +} + +TEST_F(StringsConcatenateWithColSeparatorTest, SingleColumnEmptyAndNullStringsNoReplacements) +{ + auto col0 = cudf::test::strings_column_wrapper({"", "", "", ""}, {false, true, true, false}); + auto sep_col = cudf::test::strings_column_wrapper({"", "", "", ""}, {true, false, false, true}); + + auto exp_results = + cudf::test::strings_column_wrapper({"", "", "", ""}, {false, false, false, false}); + + auto results = + cudf::strings::concatenate(cudf::table_view{{col0}}, cudf::strings_column_view(sep_col)); + cudf::test::expect_columns_equal(*results, exp_results, true); +} + +TEST_F(StringsConcatenateWithColSeparatorTest, SingleColumnStringMixNoReplacements) +{ + auto col0 = cudf::test::strings_column_wrapper( + {"eeexyz", "", "", "bbabc", "invalid", "d", "éa", "invalid", "bbb", "éééf"}, + {true, false, true, true, false, true, true, false, true, true}); + auto sep_col = cudf::test::strings_column_wrapper( + {"", "~", "!", "@", "#", "$", "%", "^", "&", "*"}, + {false, false, true, true, true, true, true, false, true, true}); + + auto exp_results = cudf::test::strings_column_wrapper( + {"", "", "", "bbabc", "", "d", "éa", "", "bbb", "éééf"}, + {false, false, true, true, false, true, true, false, true, true}); + + auto results = + cudf::strings::concatenate(cudf::table_view{{col0}}, cudf::strings_column_view(sep_col)); + cudf::test::expect_columns_equal(*results, exp_results, true); +} + +TEST_F(StringsConcatenateWithColSeparatorTest, SingleColumnStringMixSeparatorReplacement) +{ + auto col0 = cudf::test::strings_column_wrapper( + {"eeexyz", "", "", "bbabc", "invalid", "d", "éa", "invalid", "bbb", "éééf"}, + {true, false, true, true, false, true, true, false, true, true}); + auto sep_col = cudf::test::strings_column_wrapper( + {"", "~", "!", "@", "#", "$", "%", "^", "&", "*"}, + {false, false, false, true, true, true, true, false, true, true}); + auto sep_rep = cudf::string_scalar("-"); + + auto exp_results = cudf::test::strings_column_wrapper( + {"eeexyz", "", "", "bbabc", "", "d", "éa", "", "bbb", "éééf"}, + {true, false, true, true, false, true, true, false, true, true}); + + auto results = cudf::strings::concatenate( + cudf::table_view{{col0}}, cudf::strings_column_view(sep_col), sep_rep); + cudf::test::expect_columns_equal(*results, exp_results, true); +} + +TEST_F(StringsConcatenateWithColSeparatorTest, SingleColumnStringMixColumnReplacement) +{ + auto col0 = cudf::test::strings_column_wrapper( + {"eeexyz", "", "", "bbabc", "invalid", "d", "éa", "invalid", "bbb", "éééf"}, + {true, false, true, true, false, true, true, false, true, true}); + auto sep_col = cudf::test::strings_column_wrapper( + {"", "~", "!", "@", "#", "$", "%", "^", "&", "*"}, + {false, false, false, true, true, true, true, false, true, true}); + auto col_rep = cudf::string_scalar("goobly"); + + auto exp_results = cudf::test::strings_column_wrapper( + {"", "", "", "bbabc", "goobly", "d", "éa", "", "bbb", "éééf"}, + {false, false, false, true, true, true, true, false, true, true}); + + auto results = cudf::strings::concatenate(cudf::table_view{{col0}}, + cudf::strings_column_view(sep_col), + cudf::string_scalar("", false), + col_rep); + cudf::test::expect_columns_equal(*results, exp_results, true); +} + +TEST_F(StringsConcatenateWithColSeparatorTest, SingleColumnStringMixSeparatorAndColumnReplacement) +{ + auto col0 = cudf::test::strings_column_wrapper( + {"eeexyz", "", "", "bbabc", "invalid", "d", "éa", "invalid", "bbb", "éééf"}, + {true, false, true, true, false, true, true, false, true, true}); + auto sep_col = cudf::test::strings_column_wrapper( + {"", "~", "!", "@", "#", "$", "%", "^", "&", "*"}, + {false, false, false, true, true, true, true, false, true, true}); + auto sep_rep = cudf::string_scalar("-"); + auto col_rep = cudf::string_scalar("goobly"); + + // All valid, as every invalid element is replaced - a non nullable column + auto exp_results = cudf::test::strings_column_wrapper( + {"eeexyz", "goobly", "", "bbabc", "goobly", "d", "éa", "goobly", "bbb", "éééf"}); + + auto results = cudf::strings::concatenate( + cudf::table_view{{col0}}, cudf::strings_column_view(sep_col), sep_rep, col_rep); + cudf::test::expect_columns_equal(*results, exp_results, true); +} + +TEST_F(StringsConcatenateWithColSeparatorTest, MultiColumnEmptyAndNullStringsNoReplacements) +{ + auto col0 = cudf::test::strings_column_wrapper( + {"", "", "", "", "", "", "", ""}, {false, false, true, true, true, true, false, false}); + auto col1 = cudf::test::strings_column_wrapper( + {"", "", "", "", "", "", "", ""}, {false, false, true, true, false, false, true, true}); + auto sep_col = cudf::test::strings_column_wrapper( + {"", "", "", "", "", "", "", ""}, {true, false, true, false, true, false, true, false}); + + auto exp_results = cudf::test::strings_column_wrapper( + {"", "", "", "", "", "", "", ""}, {false, false, true, false, true, false, true, false}); + + auto results = + cudf::strings::concatenate(cudf::table_view{{col0, col1}}, cudf::strings_column_view(sep_col)); + cudf::test::expect_columns_equal(*results, exp_results, true); +} + +TEST_F(StringsConcatenateWithColSeparatorTest, MultiColumnStringMixNoReplacements) +{ + auto col0 = cudf::test::strings_column_wrapper( + {"eeexyz", "", "", "éééf", "éa", "", "", "invalid", "null", "NULL", "-1", ""}, + {true, true, true, true, true, true, false, false, false, false, false, false}); + auto col1 = cudf::test::strings_column_wrapper( + {"foo", "", "éaff", "", "invalid", "NULL", "éaff", "valid", "doo", "", "", "-1"}, + {true, true, true, false, false, false, true, true, true, false, false, false}); + auto sep_col = cudf::test::strings_column_wrapper( + {"", "~~~", "", "@", "", "", "", "^^^^", "", "--", "*****", "######"}, + {true, true, false, true, false, true, false, true, true, true, true, true}); + + auto exp_results = cudf::test::strings_column_wrapper( + {"eeexyzfoo", "~~~", "", "éééf", "", "", "", "valid", "doo", "", "", ""}, + {true, true, false, true, false, true, false, true, true, false, false, false}); + + auto results = + cudf::strings::concatenate(cudf::table_view{{col0, col1}}, cudf::strings_column_view(sep_col)); + cudf::test::expect_columns_equal(*results, exp_results, true); +} + +TEST_F(StringsConcatenateWithColSeparatorTest, MultiColumnStringMixSeparatorReplacement) +{ + auto col0 = cudf::test::strings_column_wrapper( + {"eeexyz", "", "", "éééf", "éa", "", "", "invalid", "null", "NULL", "-1", ""}, + {true, true, true, true, true, true, false, false, false, false, false, false}); + auto col1 = cudf::test::strings_column_wrapper( + {"foo", "", "éaff", "", "invalid", "NULL", "éaff", "valid", "doo", "", "", "-1"}, + {true, true, true, false, false, false, true, true, true, false, false, false}); + auto sep_col = cudf::test::strings_column_wrapper( + {"", "~~~", "", "@", "", "", "", "^^^^", "", "--", "*****", "######"}, + {true, true, false, true, false, true, false, true, true, true, true, true}); + + auto sep_rep = cudf::string_scalar("!!!!!!!!!!"); + + auto exp_results = cudf::test::strings_column_wrapper( + {"eeexyzfoo", + "~~~", + "!!!!!!!!!!éaff", + "éééf", + "éa", + "", + "éaff", + "valid", + "doo", + "", + "", + ""}, + {true, true, true, true, true, true, true, true, true, false, false, false}); + + auto results = cudf::strings::concatenate( + cudf::table_view{{col0, col1}}, cudf::strings_column_view(sep_col), sep_rep); + cudf::test::expect_columns_equal(*results, exp_results, true); +} + +TEST_F(StringsConcatenateWithColSeparatorTest, MultiColumnStringMixColumnReplacement) +{ + auto col0 = cudf::test::strings_column_wrapper( + {"eeexyz", "", "", "éééf", "éa", "", "", "invalid", "null", "NULL", "-1", ""}, + {true, true, true, true, true, true, false, false, false, false, false, false}); + auto col1 = cudf::test::strings_column_wrapper( + {"foo", "", "éaff", "", "invalid", "NULL", "éaff", "valid", "doo", "", "", "-1"}, + {true, true, true, false, false, false, true, true, true, false, false, false}); + auto sep_col = cudf::test::strings_column_wrapper( + {"", "~~~", "", "@", "", "", "", "^^^^", "", "--", "*****", "######"}, + {true, true, false, true, false, true, false, true, true, true, true, true}); + + auto col_rep = cudf::string_scalar("_col_replacement_"); + + auto exp_results = cudf::test::strings_column_wrapper( + {"eeexyzfoo", + "~~~", + "", + "éééf@_col_replacement_", + "", + "_col_replacement_", + "", + "_col_replacement_^^^^valid", + "_col_replacement_doo", + "_col_replacement_--_col_replacement_", + "_col_replacement_*****_col_replacement_", + "_col_replacement_######_col_replacement_"}, + {true, true, false, true, false, true, false, true, true, true, true, true}); + + auto results = cudf::strings::concatenate(cudf::table_view{{col0, col1}}, + cudf::strings_column_view(sep_col), + cudf::string_scalar("", false), + col_rep); + cudf::test::expect_columns_equal(*results, exp_results, true); +} + +TEST_F(StringsConcatenateWithColSeparatorTest, MultiColumnStringMixSeparatorAndColumnReplacement) +{ + auto col0 = cudf::test::strings_column_wrapper( + {"eeexyz", "", "", "éééf", "éa", "", "", "invalid", "null", "NULL", "-1", ""}, + {true, true, true, true, true, true, false, false, false, false, false, false}); + auto col1 = cudf::test::strings_column_wrapper( + {"foo", "", "éaff", "", "invalid", "NULL", "éaff", "valid", "doo", "", "", "-1"}, + {true, true, true, false, false, false, true, true, true, false, false, false}); + auto sep_col = cudf::test::strings_column_wrapper( + {"", "~~~", "", "@", "", "", "", "^^^^", "", "--", "*****", "######"}, + {true, true, false, true, false, true, false, true, true, true, true, true}); + + auto sep_rep = cudf::string_scalar("!!!!!!!!!!"); + auto col_rep = cudf::string_scalar("_col_replacement_"); + + // Every null item (separator/column) is replaced - a non nullable column + auto exp_results = + cudf::test::strings_column_wrapper({"eeexyzfoo", + "~~~", + "!!!!!!!!!!éaff", + "éééf@_col_replacement_", + "éa!!!!!!!!!!_col_replacement_", + "_col_replacement_", + "_col_replacement_!!!!!!!!!!éaff", + "_col_replacement_^^^^valid", + "_col_replacement_doo", + "_col_replacement_--_col_replacement_", + "_col_replacement_*****_col_replacement_", + "_col_replacement_######_col_replacement_"}); + + auto results = cudf::strings::concatenate( + cudf::table_view{{col0, col1}}, cudf::strings_column_view(sep_col), sep_rep, col_rep); + cudf::test::expect_columns_equal(*results, exp_results, true); +} + +TEST_F(StringsConcatenateWithColSeparatorTest, MultiColumnNonNullableStrings) +{ + auto col0 = + cudf::test::strings_column_wrapper({"eeexyz", "", "éaff", "éééf", "", "", "", ""}); + auto col1 = cudf::test::strings_column_wrapper({"foo", "nan", "", "", "NULL", "éaff", "", ""}); + auto sep_col = cudf::test::strings_column_wrapper({"", "~~~", "", "@", "", "+++", "", "^^^^"}); + + // Every item (separator/column) is used, as everything is valid producing a non nullable column + auto exp_results = cudf::test::strings_column_wrapper( + {"eeexyzfoo", "~~~nan", "éaff", "éééf@", "NULL", "+++éaff", "", "^^^^"}); + + auto results = + cudf::strings::concatenate(cudf::table_view{{col0, col1}}, cudf::strings_column_view(sep_col)); + cudf::test::expect_columns_equal(*results, exp_results, true); +}