From e34f3daa1f2ab7b87c9c8331a8e80ff63904c2f4 Mon Sep 17 00:00:00 2001 From: Gang Wu Date: Wed, 23 Apr 2025 15:02:58 +0800 Subject: [PATCH 1/2] feat: add formatter specialization --- src/iceberg/statistics_file.cc | 38 ++------ src/iceberg/util/formatter.h | 102 +++++++++++++++++++++ test/CMakeLists.txt | 10 +- test/formatter_test.cc | 163 +++++++++++++++++++++++++++++++++ 4 files changed, 278 insertions(+), 35 deletions(-) create mode 100644 test/formatter_test.cc diff --git a/src/iceberg/statistics_file.cc b/src/iceberg/statistics_file.cc index 5ae753a69..b8b8139d7 100644 --- a/src/iceberg/statistics_file.cc +++ b/src/iceberg/statistics_file.cc @@ -21,6 +21,8 @@ #include +#include "iceberg/util/formatter.h" + namespace iceberg { std::string ToString(const BlobMetadata& blob_metadata) { @@ -29,26 +31,9 @@ std::string ToString(const BlobMetadata& blob_metadata) { "type='{}',sourceSnapshotId={},sourceSnapshotSequenceNumber={},", blob_metadata.type, blob_metadata.source_snapshot_id, blob_metadata.source_snapshot_sequence_number); - std::format_to(std::back_inserter(repr), "fields=["); - for (auto iter = blob_metadata.fields.cbegin(); iter != blob_metadata.fields.cend(); - ++iter) { - if (iter != blob_metadata.fields.cbegin()) { - std::format_to(std::back_inserter(repr), ",{}", *iter); - } else { - std::format_to(std::back_inserter(repr), "{}", *iter); - } - } - std::format_to(std::back_inserter(repr), "],properties=["); - for (auto iter = blob_metadata.properties.cbegin(); - iter != blob_metadata.properties.cend(); ++iter) { - const auto& [key, value] = *iter; - if (iter != blob_metadata.properties.cbegin()) { - std::format_to(std::back_inserter(repr), ",{}:{}", key, value); - } else { - std::format_to(std::back_inserter(repr), "{}:{}", key, value); - } - } - repr += "]]"; + std::format_to(std::back_inserter(repr), "fields={},", blob_metadata.fields); + std::format_to(std::back_inserter(repr), "properties={}", blob_metadata.properties); + std::format_to(std::back_inserter(repr), "]"); return repr; } @@ -59,16 +44,9 @@ std::string ToString(const StatisticsFile& statistics_file) { statistics_file.snapshot_id, statistics_file.path, statistics_file.file_size_in_bytes, statistics_file.file_footer_size_in_bytes); - std::format_to(std::back_inserter(repr), "blobMetadata=["); - for (auto iter = statistics_file.blob_metadata.cbegin(); - iter != statistics_file.blob_metadata.cend(); ++iter) { - if (iter != statistics_file.blob_metadata.cbegin()) { - std::format_to(std::back_inserter(repr), ",{}", ToString(*iter)); - } else { - std::format_to(std::back_inserter(repr), "{}", ToString(*iter)); - } - } - repr += "]]"; + std::format_to(std::back_inserter(repr), "blobMetadata={}", + statistics_file.blob_metadata); + std::format_to(std::back_inserter(repr), "]"); return repr; } diff --git a/src/iceberg/util/formatter.h b/src/iceberg/util/formatter.h index d9a67415f..810798a8b 100644 --- a/src/iceberg/util/formatter.h +++ b/src/iceberg/util/formatter.h @@ -27,7 +27,10 @@ #include #include +#include #include +#include +#include #include "iceberg/util/formattable.h" @@ -40,3 +43,102 @@ struct std::formatter : std::formatter { return std::formatter::format(obj.ToString(), ctx); } }; + +/// \brief std::formatter specialization for std::vector +template +struct std::formatter> : std::formatter { + template + auto format(const std::vector& vec, FormatContext& ctx) const { + std::string result = "["; + + bool first = true; + for (const auto& item : vec) { + if (!first) { + std::format_to(std::back_inserter(result), ", "); + } + if constexpr (requires { *item; }) { + if (item) { + std::format_to(std::back_inserter(result), "{}", *item); + } else { + std::format_to(std::back_inserter(result), "null"); + } + } else { + std::format_to(std::back_inserter(result), "{}", item); + } + first = false; + } + + std::format_to(std::back_inserter(result), "]"); + return std::formatter::format(result, ctx); + } +}; + +/// \brief Helper template for formatting map-like containers +template +std::string FormatMap(const MapType& map) { + std::string result = "{"; + + bool first = true; + for (const auto& [key, value] : map) { + if (!first) { + std::format_to(std::back_inserter(result), ", "); + } + + // Format key (handle if it's a smart pointer) + if constexpr (requires { *key; }) { + if (key) { + std::format_to(std::back_inserter(result), "{}: ", *key); + } else { + std::format_to(std::back_inserter(result), "null: "); + } + } else { + std::format_to(std::back_inserter(result), "{}: ", key); + } + + // Format value (handle if it's a smart pointer) + if constexpr (requires { *value; }) { + if (value) { + std::format_to(std::back_inserter(result), "{}", *value); + } else { + std::format_to(std::back_inserter(result), "null"); + } + } else { + std::format_to(std::back_inserter(result), "{}", value); + } + + first = false; + } + + std::format_to(std::back_inserter(result), "}}"); + return result; +} + +/// \brief std::formatter specialization for std::map +template +struct std::formatter> : std::formatter { + template + auto format(const std::map& map, FormatContext& ctx) const { + return std::formatter::format(FormatMap(map), ctx); + } +}; + +/// \brief std::formatter specialization for std::unordered_map +template +struct std::formatter> : std::formatter { + template + auto format(const std::unordered_map& map, FormatContext& ctx) const { + return std::formatter::format(FormatMap(map), ctx); + } +}; + +/// \brief std::formatter specialization for any type that has a ToString function +template + requires requires(const T& t) { + { ToString(t) } -> std::convertible_to; + } +struct std::formatter : std::formatter { + template + auto format(const T& value, FormatContext& ctx) const { + return std::formatter::format(ToString(value), ctx); + } +}; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index c31c6e0e8..41786402e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -42,11 +42,6 @@ target_sources(schema_test target_link_libraries(schema_test PRIVATE iceberg_static GTest::gtest_main GTest::gmock) add_test(NAME schema_test COMMAND schema_test) -add_executable(expected_test) -target_sources(expected_test PRIVATE expected_test.cc) -target_link_libraries(expected_test PRIVATE iceberg_static GTest::gtest_main GTest::gmock) -add_test(NAME expected_test COMMAND expected_test) - add_executable(expression_test) target_sources(expression_test PRIVATE expression_test.cc) target_link_libraries(expression_test PRIVATE iceberg_static GTest::gtest_main @@ -61,6 +56,11 @@ target_link_libraries(json_serde_test PRIVATE iceberg_static GTest::gtest_main GTest::gmock) add_test(NAME json_serde_test COMMAND json_serde_test) +add_executable(util_test) +target_sources(util_test PRIVATE expected_test.cc formatter_test.cc) +target_link_libraries(util_test PRIVATE iceberg_static GTest::gtest_main GTest::gmock) +add_test(NAME util_test COMMAND util_test) + if(ICEBERG_BUILD_BUNDLE) add_executable(avro_test) target_sources(avro_test PRIVATE avro_test.cc) diff --git a/test/formatter_test.cc b/test/formatter_test.cc new file mode 100644 index 000000000..78e00c086 --- /dev/null +++ b/test/formatter_test.cc @@ -0,0 +1,163 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include "iceberg/util/formatter.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include "iceberg/statistics_file.h" + +namespace iceberg { + +// Tests for the std::format specializations +TEST(FormatterTest, VectorFormat) { + std::vector empty; + EXPECT_EQ("[]", std::format("{}", empty)); + + std::vector nums = {1, 2, 3, 4, 5}; + EXPECT_EQ("[1, 2, 3, 4, 5]", std::format("{}", nums)); + + std::vector names = {"Alice", "Bob", "Charlie"}; + EXPECT_EQ("[Alice, Bob, Charlie]", std::format("{}", names)); +} + +TEST(FormatterTest, MapFormat) { + std::map empty; + EXPECT_EQ("{}", std::format("{}", empty)); + + std::map ages = {{"Alice", 30}, {"Bob", 25}, {"Charlie", 35}}; + EXPECT_EQ("{Alice: 30, Bob: 25, Charlie: 35}", std::format("{}", ages)); +} + +TEST(FormatterTest, UnorderedMapFormat) { + std::unordered_map empty; + EXPECT_EQ("{}", std::format("{}", empty)); + + std::unordered_map scores = { + {"Alice", 95.5}, {"Bob", 87.0}, {"Charlie", 92.3}}; + std::string str = std::format("{}", scores); + EXPECT_TRUE(str.find("Alice: 95.5") != std::string::npos); + EXPECT_TRUE(str.find("Bob: 87") != std::string::npos); + EXPECT_TRUE(str.find("Charlie: 92.3") != std::string::npos); +} + +TEST(FormatterTest, NestedContainersFormat) { + std::vector> nested = {{{"a", 1}, {"b", 2}}, + {{"c", 3}, {"d", 4}}}; + + EXPECT_EQ("[{a: 1, b: 2}, {c: 3, d: 4}]", std::format("{}", nested)); + + std::map> nested_map = { + {"primes", {2, 3, 5, 7, 11}}, {"fibonacci", {1, 1, 2, 3, 5, 8, 13}}}; + std::string result = std::format("{}", nested_map); + EXPECT_TRUE(result.find("primes") != std::string::npos); + EXPECT_TRUE(result.find("fibonacci") != std::string::npos); + EXPECT_TRUE(result.find("[2, 3, 5, 7, 11]") != std::string::npos); + EXPECT_TRUE(result.find("[1, 1, 2, 3, 5, 8, 13]") != std::string::npos); +} + +TEST(FormatterTest, EdgeCasesFormat) { + std::vector single_vec = {42}; + EXPECT_EQ("[42]", std::format("{}", single_vec)); + + std::map single_map = {{"key", 42}}; + EXPECT_EQ("{key: 42}", std::format("{}", single_map)); + + std::vector> nested_empty = {{}, {1, 2}, {}}; + EXPECT_EQ("[[], [1, 2], []]", std::format("{}", nested_empty)); +} + +TEST(FormatterTest, SmartPointerFormat) { + std::vector> int_ptrs; + int_ptrs.push_back(std::make_shared(42)); + int_ptrs.push_back(std::make_shared(123)); + int_ptrs.push_back(nullptr); + EXPECT_EQ("[42, 123, null]", std::format("{}", int_ptrs)); + + std::vector> str_ptrs; + str_ptrs.push_back(std::make_shared("hello")); + str_ptrs.push_back(std::make_shared("world")); + str_ptrs.push_back(nullptr); + EXPECT_EQ("[hello, world, null]", std::format("{}", str_ptrs)); + + std::map> map_with_ptr_values; + map_with_ptr_values["one"] = std::make_shared(1); + map_with_ptr_values["two"] = std::make_shared(2); + map_with_ptr_values["null"] = nullptr; + EXPECT_EQ("{null: null, one: 1, two: 2}", std::format("{}", map_with_ptr_values)); + + std::unordered_map> scores; + scores["Alice"] = std::make_shared(95.5); + scores["Bob"] = std::make_shared(87.0); + scores["Charlie"] = nullptr; + std::string str = std::format("{}", scores); + EXPECT_TRUE(str.find("Alice: 95.5") != std::string::npos); + EXPECT_TRUE(str.find("Bob: 87") != std::string::npos); + EXPECT_TRUE(str.find("Charlie: null") != std::string::npos); + + std::vector>> nested; + std::map> map1; + map1["a"] = std::make_shared(1); + map1["b"] = std::make_shared(2); + std::map> map2; + map2["c"] = std::make_shared(3); + map2["d"] = nullptr; + nested.push_back(std::move(map1)); + nested.push_back(std::move(map2)); + EXPECT_EQ("[{a: 1, b: 2}, {c: 3, d: null}]", std::format("{}", nested)); +} + +TEST(FormatterTest, StatisticsFileFormat) { + StatisticsFile statistics_file{ + .snapshot_id = 123, + .path = "test_path", + .file_size_in_bytes = 100, + .file_footer_size_in_bytes = 20, + .blob_metadata = {BlobMetadata{.type = "type1", + .source_snapshot_id = 1, + .source_snapshot_sequence_number = 1, + .fields = {1, 2, 3}, + .properties = {{"key1", "value1"}}}, + BlobMetadata{.type = "type2", + .source_snapshot_id = 2, + .source_snapshot_sequence_number = 2, + .fields = {4, 5, 6}, + .properties = {}}}}; + + const std::string expected = + "StatisticsFile[" + "snapshotId=123,path=test_path,fileSizeInBytes=100,fileFooterSizeInBytes=20," + "blobMetadata=[" + "BlobMetadata[type='type1',sourceSnapshotId=1,sourceSnapshotSequenceNumber=1," + "fields=[1, 2, 3],properties={key1: value1}], " + "BlobMetadata[type='type2',sourceSnapshotId=2,sourceSnapshotSequenceNumber=2," + "fields=[4, 5, 6],properties={}]" + "]" + "]"; + EXPECT_EQ(expected, std::format("{}", statistics_file)); +} + +} // namespace iceberg From 903c414dc4cf9dde857099cee486f389531e2ef9 Mon Sep 17 00:00:00 2001 From: Gang Wu Date: Wed, 23 Apr 2025 16:11:17 +0800 Subject: [PATCH 2/2] use matchers and switch to internal header --- src/iceberg/statistics_file.cc | 2 +- src/iceberg/util/formatter.h | 90 ------------------- src/iceberg/util/formatter_internal.h | 121 ++++++++++++++++++++++++++ test/formatter_test.cc | 76 ++++++++-------- 4 files changed, 160 insertions(+), 129 deletions(-) create mode 100644 src/iceberg/util/formatter_internal.h diff --git a/src/iceberg/statistics_file.cc b/src/iceberg/statistics_file.cc index b8b8139d7..acff3c69e 100644 --- a/src/iceberg/statistics_file.cc +++ b/src/iceberg/statistics_file.cc @@ -21,7 +21,7 @@ #include -#include "iceberg/util/formatter.h" +#include "iceberg/util/formatter_internal.h" namespace iceberg { diff --git a/src/iceberg/util/formatter.h b/src/iceberg/util/formatter.h index 810798a8b..be2e246cf 100644 --- a/src/iceberg/util/formatter.h +++ b/src/iceberg/util/formatter.h @@ -27,10 +27,7 @@ #include #include -#include #include -#include -#include #include "iceberg/util/formattable.h" @@ -44,93 +41,6 @@ struct std::formatter : std::formatter { } }; -/// \brief std::formatter specialization for std::vector -template -struct std::formatter> : std::formatter { - template - auto format(const std::vector& vec, FormatContext& ctx) const { - std::string result = "["; - - bool first = true; - for (const auto& item : vec) { - if (!first) { - std::format_to(std::back_inserter(result), ", "); - } - if constexpr (requires { *item; }) { - if (item) { - std::format_to(std::back_inserter(result), "{}", *item); - } else { - std::format_to(std::back_inserter(result), "null"); - } - } else { - std::format_to(std::back_inserter(result), "{}", item); - } - first = false; - } - - std::format_to(std::back_inserter(result), "]"); - return std::formatter::format(result, ctx); - } -}; - -/// \brief Helper template for formatting map-like containers -template -std::string FormatMap(const MapType& map) { - std::string result = "{"; - - bool first = true; - for (const auto& [key, value] : map) { - if (!first) { - std::format_to(std::back_inserter(result), ", "); - } - - // Format key (handle if it's a smart pointer) - if constexpr (requires { *key; }) { - if (key) { - std::format_to(std::back_inserter(result), "{}: ", *key); - } else { - std::format_to(std::back_inserter(result), "null: "); - } - } else { - std::format_to(std::back_inserter(result), "{}: ", key); - } - - // Format value (handle if it's a smart pointer) - if constexpr (requires { *value; }) { - if (value) { - std::format_to(std::back_inserter(result), "{}", *value); - } else { - std::format_to(std::back_inserter(result), "null"); - } - } else { - std::format_to(std::back_inserter(result), "{}", value); - } - - first = false; - } - - std::format_to(std::back_inserter(result), "}}"); - return result; -} - -/// \brief std::formatter specialization for std::map -template -struct std::formatter> : std::formatter { - template - auto format(const std::map& map, FormatContext& ctx) const { - return std::formatter::format(FormatMap(map), ctx); - } -}; - -/// \brief std::formatter specialization for std::unordered_map -template -struct std::formatter> : std::formatter { - template - auto format(const std::unordered_map& map, FormatContext& ctx) const { - return std::formatter::format(FormatMap(map), ctx); - } -}; - /// \brief std::formatter specialization for any type that has a ToString function template requires requires(const T& t) { diff --git a/src/iceberg/util/formatter_internal.h b/src/iceberg/util/formatter_internal.h new file mode 100644 index 000000000..f802bbecd --- /dev/null +++ b/src/iceberg/util/formatter_internal.h @@ -0,0 +1,121 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "iceberg/util/formatter.h" + +/// \brief Concept for smart pointer types +template +concept SmartPointerType = requires(T t) { + { t.operator->() } -> std::same_as; + { *t } -> std::convertible_to; + { static_cast(t) } -> std::same_as; + typename T::element_type; +}; + +/// \brief Helper function to format an item using concepts to differentiate types +template +std::string FormatItem(const T& item) { + if constexpr (SmartPointerType) { + if (item) { + return std::format("{}", *item); + } else { + return "null"; + } + } else { + return std::format("{}", item); + } +} + +/// \brief Generic function to join a range of elements with a separator and wrap with +/// delimiters +template +std::string FormatRange(const Range& range, std::string_view separator, + std::string_view prefix, std::string_view suffix) { + if (std::ranges::empty(range)) { + return std::format("{}{}", prefix, suffix); + } + + std::stringstream ss; + ss << prefix; + + bool first = true; + for (const auto& element : range) { + if (!first) { + ss << separator; + } + ss << element; + first = false; + } + + ss << suffix; + return ss.str(); +} + +/// \brief Helper template for formatting map-like containers +template +std::string FormatMap(const MapType& map) { + // Transform the map items into formatted key-value pairs + std::ranges::transform_view formatted_range = + map | std::views::transform([](const auto& pair) -> std::string { + const auto& [key, value] = pair; + return std::format("{}: {}", FormatItem(key), FormatItem(value)); + }); + return FormatRange(formatted_range, ", ", "{", "}"); +} + +/// \brief std::formatter specialization for std::map +template +struct std::formatter> : std::formatter { + template + auto format(const std::map& map, FormatContext& ctx) const { + return std::formatter::format(FormatMap(map), ctx); + } +}; + +/// \brief std::formatter specialization for std::unordered_map +template +struct std::formatter> : std::formatter { + template + auto format(const std::unordered_map& map, FormatContext& ctx) const { + return std::formatter::format(FormatMap(map), ctx); + } +}; + +/// \brief std::formatter specialization for std::vector +template +struct std::formatter> : std::formatter { + template + auto format(const std::vector& vec, FormatContext& ctx) const { + auto formatted_range = + vec | std::views::transform([](const auto& item) { return FormatItem(item); }); + return std::formatter::format( + FormatRange(formatted_range, ", ", "[", "]"), ctx); + } +}; diff --git a/test/formatter_test.cc b/test/formatter_test.cc index 78e00c086..767020b2e 100644 --- a/test/formatter_test.cc +++ b/test/formatter_test.cc @@ -17,8 +17,6 @@ * under the License. */ -#include "iceberg/util/formatter.h" - #include #include #include @@ -26,9 +24,12 @@ #include #include +#include +#include #include #include "iceberg/statistics_file.h" +#include "iceberg/util/formatter_internal.h" namespace iceberg { @@ -59,9 +60,9 @@ TEST(FormatterTest, UnorderedMapFormat) { std::unordered_map scores = { {"Alice", 95.5}, {"Bob", 87.0}, {"Charlie", 92.3}}; std::string str = std::format("{}", scores); - EXPECT_TRUE(str.find("Alice: 95.5") != std::string::npos); - EXPECT_TRUE(str.find("Bob: 87") != std::string::npos); - EXPECT_TRUE(str.find("Charlie: 92.3") != std::string::npos); + EXPECT_THAT(str, ::testing::HasSubstr("Alice: 95.5")); + EXPECT_THAT(str, ::testing::HasSubstr("Bob: 87")); + EXPECT_THAT(str, ::testing::HasSubstr("Charlie: 92.3")); } TEST(FormatterTest, NestedContainersFormat) { @@ -73,10 +74,10 @@ TEST(FormatterTest, NestedContainersFormat) { std::map> nested_map = { {"primes", {2, 3, 5, 7, 11}}, {"fibonacci", {1, 1, 2, 3, 5, 8, 13}}}; std::string result = std::format("{}", nested_map); - EXPECT_TRUE(result.find("primes") != std::string::npos); - EXPECT_TRUE(result.find("fibonacci") != std::string::npos); - EXPECT_TRUE(result.find("[2, 3, 5, 7, 11]") != std::string::npos); - EXPECT_TRUE(result.find("[1, 1, 2, 3, 5, 8, 13]") != std::string::npos); + EXPECT_THAT(result, ::testing::HasSubstr("primes")); + EXPECT_THAT(result, ::testing::HasSubstr("fibonacci")); + EXPECT_THAT(result, ::testing::HasSubstr("[2, 3, 5, 7, 11]")); + EXPECT_THAT(result, ::testing::HasSubstr("[1, 1, 2, 3, 5, 8, 13]")); } TEST(FormatterTest, EdgeCasesFormat) { @@ -91,42 +92,41 @@ TEST(FormatterTest, EdgeCasesFormat) { } TEST(FormatterTest, SmartPointerFormat) { - std::vector> int_ptrs; - int_ptrs.push_back(std::make_shared(42)); - int_ptrs.push_back(std::make_shared(123)); - int_ptrs.push_back(nullptr); + std::vector> int_ptrs = { + std::make_shared(42), + std::make_shared(123), + nullptr, + }; EXPECT_EQ("[42, 123, null]", std::format("{}", int_ptrs)); - std::vector> str_ptrs; - str_ptrs.push_back(std::make_shared("hello")); - str_ptrs.push_back(std::make_shared("world")); - str_ptrs.push_back(nullptr); + std::vector> str_ptrs = { + std::make_shared("hello"), + std::make_shared("world"), + nullptr, + }; EXPECT_EQ("[hello, world, null]", std::format("{}", str_ptrs)); - std::map> map_with_ptr_values; - map_with_ptr_values["one"] = std::make_shared(1); - map_with_ptr_values["two"] = std::make_shared(2); - map_with_ptr_values["null"] = nullptr; + std::map> map_with_ptr_values = { + {"one", std::make_shared(1)}, + {"two", std::make_shared(2)}, + {"null", nullptr}, + }; EXPECT_EQ("{null: null, one: 1, two: 2}", std::format("{}", map_with_ptr_values)); - std::unordered_map> scores; - scores["Alice"] = std::make_shared(95.5); - scores["Bob"] = std::make_shared(87.0); - scores["Charlie"] = nullptr; + std::unordered_map> scores = { + {"Alice", std::make_shared(95.5)}, + {"Bob", std::make_shared(87.0)}, + {"Charlie", nullptr}, + }; std::string str = std::format("{}", scores); - EXPECT_TRUE(str.find("Alice: 95.5") != std::string::npos); - EXPECT_TRUE(str.find("Bob: 87") != std::string::npos); - EXPECT_TRUE(str.find("Charlie: null") != std::string::npos); - - std::vector>> nested; - std::map> map1; - map1["a"] = std::make_shared(1); - map1["b"] = std::make_shared(2); - std::map> map2; - map2["c"] = std::make_shared(3); - map2["d"] = nullptr; - nested.push_back(std::move(map1)); - nested.push_back(std::move(map2)); + EXPECT_THAT(str, ::testing::HasSubstr("Alice: 95.5")); + EXPECT_THAT(str, ::testing::HasSubstr("Bob: 87")); + EXPECT_THAT(str, ::testing::HasSubstr("Charlie: null")); + + std::vector>> nested = { + {{"a", std::make_shared(1)}, {"b", std::make_shared(2)}}, + {{"c", std::make_shared(3)}, {"d", nullptr}}, + }; EXPECT_EQ("[{a: 1, b: 2}, {c: 3, d: null}]", std::format("{}", nested)); }