diff --git a/BUILD.bazel b/BUILD.bazel index b0d81f975..58f03e1ba 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -67,6 +67,7 @@ cc_library( "include/nlohmann/detail/string_utils.hpp", "include/nlohmann/detail/value_t.hpp", "include/nlohmann/detail/view/builder.hpp", + "include/nlohmann/detail/view/compare.hpp", "include/nlohmann/detail/view/document_data.hpp", "include/nlohmann/detail/view/errors.hpp", "include/nlohmann/detail/view/input.hpp", diff --git a/include/nlohmann/detail/view/compare.hpp b/include/nlohmann/detail/view/compare.hpp new file mode 100644 index 000000000..551c4013e --- /dev/null +++ b/include/nlohmann/detail/view/compare.hpp @@ -0,0 +1,320 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include // sort, stable_sort +#include // size_t +#include // move, pair +#include // vector + +#include +#include + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +// Equality of views, and of views with basic_json values, with the semantics +// of basic_json's operator== applied to the values parse() would produce: +// numbers compare by value across their types, an object is compared by its +// members with duplicate keys resolved as parse() resolves them (the last +// value, at the position of the first occurrence), and in document order if +// the object type keeps an order (ordered_json), by key otherwise. + +/// one side of a comparison: a view +template +class view_side +{ + public: + using string_view_t = typename View::string_view_t; + + explicit view_side(const View& v) noexcept + : m_view(v) + {} + + value_t type() const noexcept + { + return m_view.type(); + } + + std::size_t size() const noexcept + { + return m_view.size(); + } + + string_view_t string() const + { + return m_view.get_string(); + } + + /// a number, boolean, or null as a basic_json value (no allocation) + BasicJsonType scalar() const + { + switch (m_view.type()) + { + case value_t::number_integer: + return BasicJsonType(m_view.template get()); + case value_t::number_unsigned: + return BasicJsonType(m_view.template get()); + case value_t::number_float: + return BasicJsonType(m_view.template get()); + case value_t::boolean: + return BasicJsonType(m_view.template get()); + case value_t::null: + case value_t::object: + case value_t::array: + case value_t::string: + case value_t::binary: + case value_t::discarded: + default: + return BasicJsonType(nullptr); + } + } + + void elements(std::vector& out) const + { + out.reserve(m_view.size()); + for (const View e : m_view) + { + out.emplace_back(e); + } + } + + /// the members as parse() keeps them: one per key, the last value at the + /// position of the first occurrence; in that order, or sorted by key + void members(std::vector>& out, bool ordered) const + { + struct member + { + string_view_t key; + View value; + std::size_t position; + }; + std::vector all; + all.reserve(m_view.size()); + std::size_t position = 0; + for (auto it = m_view.begin(); it != m_view.end(); ++it) + { + all.push_back(member{it.key(), it.value(), position++}); + } + std::stable_sort(all.begin(), all.end(), [](const member & a, const member & b) + { + return a.key < b.key; + }); + std::vector unique; + unique.reserve(all.size()); + for (std::size_t i = 0; i < all.size();) + { + std::size_t last = i; + while (last + 1 < all.size() && all[last + 1].key == all[i].key) + { + ++last; + } + unique.push_back(member{all[i].key, all[last].value, all[i].position}); + i = last + 1; + } + if (ordered) + { + std::sort(unique.begin(), unique.end(), [](const member & a, const member & b) + { + return a.position < b.position; + }); + } + out.reserve(unique.size()); + for (const member& m : unique) + { + out.emplace_back(m.key, view_side(m.value)); + } + } + + private: + View m_view; +}; + +/// the other side of a comparison: a basic_json value +template +class json_side +{ + public: + using string_view_t = StringView; + + explicit json_side(const BasicJsonType& j) noexcept + : m_json(&j) + {} + + value_t type() const noexcept + { + return m_json->type(); + } + + std::size_t size() const noexcept + { + return m_json->size(); + } + + string_view_t string() const + { + const auto& s = m_json->template get_ref(); + return string_view_t(s.data(), s.size()); + } + + BasicJsonType scalar() const + { + return *m_json; + } + + void elements(std::vector& out) const + { + out.reserve(m_json->size()); + for (const auto& e : *m_json) + { + out.emplace_back(e); + } + } + + void members(std::vector>& out, bool ordered) const + { + out.reserve(m_json->size()); + for (auto it = m_json->cbegin(); it != m_json->cend(); ++it) + { + out.emplace_back(string_view_t(it.key().data(), it.key().size()), json_side(it.value())); + } + if (!ordered) + { + std::sort(out.begin(), out.end(), [](const std::pair& a, const std::pair& b) + { + return a.first < b.first; + }); + } + } + + private: + const BasicJsonType* m_json; +}; + +/// whether two sides are equal; iterative, so that the nesting depth is +/// limited by memory only +template +bool equal(const A& a0, const B& b0) +{ + using string_view_t = typename A::string_view_t; + const bool ordered = is_ordered_map::value; + + struct frame + { + std::vector elements_a{}; + std::vector elements_b{}; + std::vector> members_a{}; + std::vector> members_b{}; + bool object = false; + std::size_t next = 0; + }; + std::vector stack; + A a = a0; + B b = b0; + for (;;) + { + const value_t ta = a.type(); + const value_t tb = b.type(); + const bool numbers = (ta == value_t::number_integer || ta == value_t::number_unsigned || ta == value_t::number_float) + && (tb == value_t::number_integer || tb == value_t::number_unsigned || tb == value_t::number_float); + if (ta == value_t::discarded || tb == value_t::discarded) + { + // basic_json decides (JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON) + if (ta != tb || !(BasicJsonType(value_t::discarded) == BasicJsonType(value_t::discarded))) + { + return false; + } + } + else if (numbers) + { + if (!(a.scalar() == b.scalar())) + { + return false; + } + } + else if (ta != tb) + { + return false; + } + else if (ta == value_t::string) + { + if (!(a.string() == b.string())) + { + return false; + } + } + else if (ta == value_t::array || ta == value_t::object) + { + if (a.size() != b.size() && ta == value_t::array) + { + return false; + } + frame f; + f.object = ta == value_t::object; + if (f.object) + { + a.members(f.members_a, ordered); + b.members(f.members_b, ordered); + if (f.members_a.size() != f.members_b.size()) + { + return false; + } + } + else + { + a.elements(f.elements_a); + b.elements(f.elements_b); + } + stack.push_back(std::move(f)); + } + else if (!(a.scalar() == b.scalar())) // null, boolean + { + return false; + } + + // the next pair of values + for (;;) + { + if (stack.empty()) + { + return true; + } + frame& f = stack.back(); + const std::size_t count = f.object ? f.members_a.size() : f.elements_a.size(); + if (f.next == count) + { + stack.pop_back(); + continue; + } + if (f.object) + { + if (!(f.members_a[f.next].first == f.members_b[f.next].first)) + { + return false; + } + a = f.members_a[f.next].second; + b = f.members_b[f.next].second; + } + else + { + a = f.elements_a[f.next]; + b = f.elements_b[f.next]; + } + ++f.next; + break; + } + } +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp index 8ef0fc8bb..5ced50c55 100644 --- a/include/nlohmann/json_view.hpp +++ b/include/nlohmann/json_view.hpp @@ -47,6 +47,7 @@ #endif #include +#include #include #include #include @@ -603,6 +604,44 @@ class basic_json_view } #endif + //////////////// + // comparison // + //////////////// + + /// whether the values parse() would produce for two views are equal, as + /// by BasicJsonType's operator== (numbers by value, objects by their + /// members with duplicate keys resolved as parse() resolves them) + friend bool operator==(const basic_json_view& a, const basic_json_view& b) + { + return detail::view::equal(side(a), side(b)); + } + + friend bool operator!=(const basic_json_view& a, const basic_json_view& b) + { + return !(a == b); + } + + /// whether the value parse() would produce for a view equals a value + friend bool operator==(const basic_json_view& a, const BasicJsonType& j) + { + return detail::view::equal(side(a), json_side_t(j)); + } + + friend bool operator==(const BasicJsonType& j, const basic_json_view& a) + { + return a == j; + } + + friend bool operator!=(const basic_json_view& a, const BasicJsonType& j) + { + return !(a == j); + } + + friend bool operator!=(const BasicJsonType& j, const basic_json_view& a) + { + return !(a == j); + } + ///////////////// // materialize // ///////////////// @@ -635,6 +674,13 @@ class basic_json_view : m_doc(d), m_node(n) {} + using json_side_t = detail::view::json_side; + + static detail::view::view_side side(const basic_json_view& v) noexcept + { + return detail::view::view_side(v); + } + /// the number of source bytes of this value (estimated for values with /// decoded strings) std::size_t source_extent() const noexcept diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index c18252304..6928b7b7e 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -1580,6 +1580,329 @@ inline bool build(document_data& d, const char* src, std::size_t size, bool comm } // namespace detail NLOHMANN_JSON_NAMESPACE_END +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // sort, stable_sort +#include // size_t +#include // move, pair +#include // vector + +// #include +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +// Equality of views, and of views with basic_json values, with the semantics +// of basic_json's operator== applied to the values parse() would produce: +// numbers compare by value across their types, an object is compared by its +// members with duplicate keys resolved as parse() resolves them (the last +// value, at the position of the first occurrence), and in document order if +// the object type keeps an order (ordered_json), by key otherwise. + +/// one side of a comparison: a view +template +class view_side +{ + public: + using string_view_t = typename View::string_view_t; + + explicit view_side(const View& v) noexcept + : m_view(v) + {} + + value_t type() const noexcept + { + return m_view.type(); + } + + std::size_t size() const noexcept + { + return m_view.size(); + } + + string_view_t string() const + { + return m_view.get_string(); + } + + /// a number, boolean, or null as a basic_json value (no allocation) + BasicJsonType scalar() const + { + switch (m_view.type()) + { + case value_t::number_integer: + return BasicJsonType(m_view.template get()); + case value_t::number_unsigned: + return BasicJsonType(m_view.template get()); + case value_t::number_float: + return BasicJsonType(m_view.template get()); + case value_t::boolean: + return BasicJsonType(m_view.template get()); + case value_t::null: + case value_t::object: + case value_t::array: + case value_t::string: + case value_t::binary: + case value_t::discarded: + default: + return BasicJsonType(nullptr); + } + } + + void elements(std::vector& out) const + { + out.reserve(m_view.size()); + for (const View e : m_view) + { + out.emplace_back(e); + } + } + + /// the members as parse() keeps them: one per key, the last value at the + /// position of the first occurrence; in that order, or sorted by key + void members(std::vector>& out, bool ordered) const + { + struct member + { + string_view_t key; + View value; + std::size_t position; + }; + std::vector all; + all.reserve(m_view.size()); + std::size_t position = 0; + for (auto it = m_view.begin(); it != m_view.end(); ++it) + { + all.push_back(member{it.key(), it.value(), position++}); + } + std::stable_sort(all.begin(), all.end(), [](const member & a, const member & b) + { + return a.key < b.key; + }); + std::vector unique; + unique.reserve(all.size()); + for (std::size_t i = 0; i < all.size();) + { + std::size_t last = i; + while (last + 1 < all.size() && all[last + 1].key == all[i].key) + { + ++last; + } + unique.push_back(member{all[i].key, all[last].value, all[i].position}); + i = last + 1; + } + if (ordered) + { + std::sort(unique.begin(), unique.end(), [](const member & a, const member & b) + { + return a.position < b.position; + }); + } + out.reserve(unique.size()); + for (const member& m : unique) + { + out.emplace_back(m.key, view_side(m.value)); + } + } + + private: + View m_view; +}; + +/// the other side of a comparison: a basic_json value +template +class json_side +{ + public: + using string_view_t = StringView; + + explicit json_side(const BasicJsonType& j) noexcept + : m_json(&j) + {} + + value_t type() const noexcept + { + return m_json->type(); + } + + std::size_t size() const noexcept + { + return m_json->size(); + } + + string_view_t string() const + { + const auto& s = m_json->template get_ref(); + return string_view_t(s.data(), s.size()); + } + + BasicJsonType scalar() const + { + return *m_json; + } + + void elements(std::vector& out) const + { + out.reserve(m_json->size()); + for (const auto& e : *m_json) + { + out.emplace_back(e); + } + } + + void members(std::vector>& out, bool ordered) const + { + out.reserve(m_json->size()); + for (auto it = m_json->cbegin(); it != m_json->cend(); ++it) + { + out.emplace_back(string_view_t(it.key().data(), it.key().size()), json_side(it.value())); + } + if (!ordered) + { + std::sort(out.begin(), out.end(), [](const std::pair& a, const std::pair& b) + { + return a.first < b.first; + }); + } + } + + private: + const BasicJsonType* m_json; +}; + +/// whether two sides are equal; iterative, so that the nesting depth is +/// limited by memory only +template +bool equal(const A& a0, const B& b0) +{ + using string_view_t = typename A::string_view_t; + const bool ordered = is_ordered_map::value; + + struct frame + { + std::vector elements_a{}; + std::vector elements_b{}; + std::vector> members_a{}; + std::vector> members_b{}; + bool object = false; + std::size_t next = 0; + }; + std::vector stack; + A a = a0; + B b = b0; + for (;;) + { + const value_t ta = a.type(); + const value_t tb = b.type(); + const bool numbers = (ta == value_t::number_integer || ta == value_t::number_unsigned || ta == value_t::number_float) + && (tb == value_t::number_integer || tb == value_t::number_unsigned || tb == value_t::number_float); + if (ta == value_t::discarded || tb == value_t::discarded) + { + // basic_json decides (JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON) + if (ta != tb || !(BasicJsonType(value_t::discarded) == BasicJsonType(value_t::discarded))) + { + return false; + } + } + else if (numbers) + { + if (!(a.scalar() == b.scalar())) + { + return false; + } + } + else if (ta != tb) + { + return false; + } + else if (ta == value_t::string) + { + if (!(a.string() == b.string())) + { + return false; + } + } + else if (ta == value_t::array || ta == value_t::object) + { + if (a.size() != b.size() && ta == value_t::array) + { + return false; + } + frame f; + f.object = ta == value_t::object; + if (f.object) + { + a.members(f.members_a, ordered); + b.members(f.members_b, ordered); + if (f.members_a.size() != f.members_b.size()) + { + return false; + } + } + else + { + a.elements(f.elements_a); + b.elements(f.elements_b); + } + stack.push_back(std::move(f)); + } + else if (!(a.scalar() == b.scalar())) // null, boolean + { + return false; + } + + // the next pair of values + for (;;) + { + if (stack.empty()) + { + return true; + } + frame& f = stack.back(); + const std::size_t count = f.object ? f.members_a.size() : f.elements_a.size(); + if (f.next == count) + { + stack.pop_back(); + continue; + } + if (f.object) + { + if (!(f.members_a[f.next].first == f.members_b[f.next].first)) + { + return false; + } + a = f.members_a[f.next].second; + b = f.members_b[f.next].second; + } + else + { + a = f.elements_a[f.next]; + b = f.elements_b[f.next]; + } + ++f.next; + break; + } + } +} + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + // #include // #include @@ -3764,6 +4087,44 @@ class basic_json_view } #endif + //////////////// + // comparison // + //////////////// + + /// whether the values parse() would produce for two views are equal, as + /// by BasicJsonType's operator== (numbers by value, objects by their + /// members with duplicate keys resolved as parse() resolves them) + friend bool operator==(const basic_json_view& a, const basic_json_view& b) + { + return detail::view::equal(side(a), side(b)); + } + + friend bool operator!=(const basic_json_view& a, const basic_json_view& b) + { + return !(a == b); + } + + /// whether the value parse() would produce for a view equals a value + friend bool operator==(const basic_json_view& a, const BasicJsonType& j) + { + return detail::view::equal(side(a), json_side_t(j)); + } + + friend bool operator==(const BasicJsonType& j, const basic_json_view& a) + { + return a == j; + } + + friend bool operator!=(const basic_json_view& a, const BasicJsonType& j) + { + return !(a == j); + } + + friend bool operator!=(const BasicJsonType& j, const basic_json_view& a) + { + return !(a == j); + } + ///////////////// // materialize // ///////////////// @@ -3796,6 +4157,13 @@ class basic_json_view : m_doc(d), m_node(n) {} + using json_side_t = detail::view::json_side; + + static detail::view::view_side side(const basic_json_view& v) noexcept + { + return detail::view::view_side(v); + } + /// the number of source bytes of this value (estimated for values with /// decoded strings) std::size_t source_extent() const noexcept diff --git a/tests/src/unit-json_view.cpp b/tests/src/unit-json_view.cpp index 2c0076486..5f752fffe 100644 --- a/tests/src/unit-json_view.cpp +++ b/tests/src/unit-json_view.cpp @@ -1185,3 +1185,87 @@ TEST_CASE("json_view dump") CHECK(json_view().dump() == json(json::value_t::discarded).dump()); } } + +TEST_CASE("json_view comparison") +{ + SECTION("equality of the values parse() produces") + { + generator g; + std::vector texts; + for (int i = 0; i < 600; ++i) + { + std::string text; + g.value(text, 0); + texts.push_back(text); + // the same value written differently: sorted keys, canonical numbers + texts.push_back(json::parse(text).dump(1)); + } + for (std::size_t i = 0; i + 2 < texts.size(); ++i) + { + for (std::size_t k = i; k < i + 3; ++k) + { + CAPTURE(texts[i]); + CAPTURE(texts[k]); + const json_document a = json_document::parse(texts[i]); + const json_document b = json_document::parse(texts[k]); + const json ja = json::parse(texts[i]); + const json jb = json::parse(texts[k]); + CHECK((a.root() == b.root()) == (ja == jb)); + CHECK((a.root() != b.root()) == (ja != jb)); + CHECK((a.root() == jb) == (ja == jb)); + CHECK((jb == a.root()) == (ja == jb)); + CHECK((a.root() != jb) == (ja != jb)); + CHECK((jb != a.root()) == (ja != jb)); + + // ordered_json compares members in order + const ordered_json_document oa = ordered_json_document::parse(texts[i]); + const ordered_json_document ob = ordered_json_document::parse(texts[k]); + const ordered_json oja = ordered_json::parse(texts[i]); + const ordered_json ojb = ordered_json::parse(texts[k]); + CHECK((oa.root() == ob.root()) == (oja == ojb)); + CHECK((oa.root() == ojb) == (oja == ojb)); + } + } + } + + SECTION("numbers, duplicate keys, member order") + { + const auto same = [](const char* x, const char* y) + { + return json_document::parse(x).root() == json_document::parse(y).root(); + }; + CHECK(same("1", "1.0")); + CHECK(same("[1, -1, 2.5]", "[1.0, -1.0, 25e-1]")); + CHECK(!same("1", "1.5")); + CHECK(same("18446744073709551615", "18446744073709551615")); + CHECK(same(R"({"a": 1, "a": 2})", R"({"a": 2})")); + CHECK(!same(R"({"a": 1, "a": 2})", R"({"a": 1})")); + CHECK(same(R"({"a": 1, "b": 2})", R"({"b": 2, "a": 1})")); + CHECK(!same(R"({"a": 1})", R"({"a": 1, "b": 2})")); + CHECK(!same("[1, 2]", "[2, 1]")); + CHECK(!same("\"a\"", "\"b\"")); + CHECK(same("\"\\u00e9\"", "\"\xc3\xa9\"")); + CHECK(!same("null", "false")); + CHECK(!same("[]", "{}")); + CHECK(ordered_json_document::parse(R"({"a": 1, "b": 2, "a": 3})").root() == ordered_json_document::parse(R"({"a": 3, "b": 2})").root()); + CHECK(ordered_json_document::parse(R"({"a": 1, "b": 2})").root() != ordered_json_document::parse(R"({"b": 2, "a": 1})").root()); + + // discarded values compare as basic_json's do + const json discarded(json::value_t::discarded); + CHECK((json_view() == json_view()) == (discarded == discarded)); + CHECK((json_view() == discarded) == (discarded == discarded)); + CHECK(!(json_view() == json_document::parse("null").root())); + CHECK(!(json_document::parse("null").root() == discarded)); + } + + SECTION("deep nesting") + { + const std::string deep = std::string(100000, '[') + std::string(100000, ']'); + const json_document a = json_document::parse(deep); + const json_document b = json_document::parse(deep); + CHECK(a.root() == b.root()); + CHECK(a.root() == json::parse(deep)); + const std::string other = std::string(100000, '[') + "1" + std::string(100000, ']'); + CHECK(a.root() != json_document::parse(other).root()); + } +}