From af47348aa126c0b66b4f3f3eb3ab741bce9bd625 Mon Sep 17 00:00:00 2001 From: Hans Binderup Date: Tue, 17 Feb 2026 12:16:31 +0100 Subject: [PATCH 1/2] operations: correct order of missing json-logic strives at being deterministic. The operations missing and missing_some uses a std::set which is automatically ordered -> breaks the json order. Instead preserve the order of the json and return the same order as handled. To do this I've added a std::vector as container and kept the std::set (using unordered as we only care about lookups) for O(1) (most of the time) look up. Another solution would be to only use the vector but the look up complexity increases with bigger structures. Signed-off-by: Hans Binderup --- src/operations/data/missing.cpp | 12 ++++-- src/operations/data/missing_some.cpp | 14 +++++-- test/operations/data/missing.cpp | 52 ++++++++++++++++++++++--- test/operations/data/missing_some.cpp | 56 +++++++++++++++++++++++---- 4 files changed, 115 insertions(+), 19 deletions(-) diff --git a/src/operations/data/missing.cpp b/src/operations/data/missing.cpp index ba1ab2e..eca203d 100644 --- a/src/operations/data/missing.cpp +++ b/src/operations/data/missing.cpp @@ -1,7 +1,8 @@ #include "json_logic.h" -#include #include +#include +#include #include "nlohmann/json.hpp" @@ -17,7 +18,8 @@ namespace json_logic "Expected 1 or more arguments, but received " + std::to_string(values.size()) ); - std::set missing_keys{}; + std::vector missing_keys{}; + std::unordered_set seen{}; for (const auto& value : values) { @@ -34,7 +36,11 @@ namespace json_logic } catch (...) { - missing_keys.insert(key); + const auto [_, inserted] = seen.insert(key); + if (inserted) + { + missing_keys.push_back(key); + } } } diff --git a/src/operations/data/missing_some.cpp b/src/operations/data/missing_some.cpp index 01ef464..2b43826 100644 --- a/src/operations/data/missing_some.cpp +++ b/src/operations/data/missing_some.cpp @@ -1,7 +1,8 @@ #include "json_logic.h" -#include #include +#include +#include #include "nlohmann/json.hpp" @@ -23,8 +24,9 @@ namespace json_logic if (!search_values.is_array()) throw JsonLogicException(__FUNCTION__, "Second argument must be an array"); - std::set found_keys{}; - std::set missing_keys{}; + std::vector missing_keys{}; + std::unordered_set found_keys{}; + std::unordered_set seen_missing{}; for (const auto& value : search_values) { @@ -43,7 +45,11 @@ namespace json_logic } catch (...) { - missing_keys.insert(key); + const auto [_, inserted] = seen_missing.insert(key); + if (inserted) + { + missing_keys.push_back(key); + } } } diff --git a/test/operations/data/missing.cpp b/test/operations/data/missing.cpp index 658fadf..1e19409 100644 --- a/test/operations/data/missing.cpp +++ b/test/operations/data/missing.cpp @@ -1,4 +1,4 @@ -#include +#include #include "gtest/gtest.h" @@ -29,7 +29,7 @@ TEST_F(OperationDataMissing, SimpleKeys) const auto result = json_logic_->Apply(logic, data); - std::set expected_result{"b", "d"}; + std::vector expected_result{"b", "d"}; EXPECT_EQ(result, expected_result); } @@ -52,7 +52,7 @@ TEST_F(OperationDataMissing, SimpleKeysAllPresent) const auto result = json_logic_->Apply(logic, data); - std::set expected_result{}; + std::vector expected_result{}; EXPECT_EQ(result, expected_result); } @@ -73,7 +73,7 @@ TEST_F(OperationDataMissing, SimpleKeysRepeatedKeys) const auto result = json_logic_->Apply(logic, data); - std::set expected_result{"b", "d"}; + std::vector expected_result{"b", "d"}; EXPECT_EQ(result, expected_result); } @@ -100,6 +100,48 @@ TEST_F(OperationDataMissing, NestedKeys) const auto result = json_logic_->Apply(logic, data); - std::set expected_result{"a.y", "b.z"}; + std::vector expected_result{"a.y", "b.z"}; + EXPECT_EQ(result, expected_result); +} + +TEST_F(OperationDataMissing, OrderPreservationNonAlphabetic) +{ + const auto logic = R"( + { + "missing": ["zebra", "apple", "banana", "dog", "cat"] + } + )"_json; + + const auto data = R"( + { + "apple": 1, + "dog": 4 + } + )"_json; + + const auto result = json_logic_->Apply(logic, data); + + std::vector expected_result{"zebra", "banana", "cat"}; + EXPECT_EQ(result, expected_result); +} + +TEST_F(OperationDataMissing, OrderPreservationReversedKeys) +{ + const auto logic = R"( + { + "missing": ["z", "y", "x", "w", "v"] + } + )"_json; + + const auto data = R"( + { + "y": 2, + "w": 4 + } + )"_json; + + const auto result = json_logic_->Apply(logic, data); + + std::vector expected_result{"z", "x", "v"}; EXPECT_EQ(result, expected_result); } diff --git a/test/operations/data/missing_some.cpp b/test/operations/data/missing_some.cpp index 9773ca0..b81f292 100644 --- a/test/operations/data/missing_some.cpp +++ b/test/operations/data/missing_some.cpp @@ -1,4 +1,4 @@ -#include +#include #include "gtest/gtest.h" @@ -29,7 +29,7 @@ TEST_F(OperationDataMissingSome, SimpleKeysFoundMinimum) const auto result = json_logic_->Apply(logic, data); - std::set expected_result{}; + std::vector expected_result{}; EXPECT_EQ(result, expected_result); } @@ -50,7 +50,7 @@ TEST_F(OperationDataMissingSome, SimpleKeysNotFoundMinimum) const auto result = json_logic_->Apply(logic, data); - std::set expected_result{"b", "d"}; + std::vector expected_result{"b", "d"}; EXPECT_EQ(result, expected_result); } @@ -71,7 +71,7 @@ TEST_F(OperationDataMissingSome, SimpleRepeatedKeysFoundMinimum) const auto result = json_logic_->Apply(logic, data); - std::set expected_result{}; + std::vector expected_result{}; EXPECT_EQ(result, expected_result); } @@ -92,7 +92,7 @@ TEST_F(OperationDataMissingSome, SimpleRepeatedKeysNotFoundMinimum) const auto result = json_logic_->Apply(logic, data); - std::set expected_result{"b", "d"}; + std::vector expected_result{"b", "d"}; EXPECT_EQ(result, expected_result); } @@ -119,7 +119,7 @@ TEST_F(OperationDataMissingSome, NestedKeysFoundMinimum) const auto result = json_logic_->Apply(logic, data); - std::set expected_result{}; + std::vector expected_result{}; EXPECT_EQ(result, expected_result); } @@ -146,6 +146,48 @@ TEST_F(OperationDataMissingSome, NestedKeysNotFoundMinimum) const auto result = json_logic_->Apply(logic, data); - std::set expected_result{"a.y", "b.z"}; + std::vector expected_result{"a.y", "b.z"}; + EXPECT_EQ(result, expected_result); +} + +TEST_F(OperationDataMissingSome, OrderPreservationNonAlphabetic) +{ + const auto logic = R"( + { + "missing_some": [3, ["zebra", "dog", "apple", "banana", "cat"]] + } + )"_json; + + const auto data = R"( + { + "dog": 4, + "apple": 1 + } + )"_json; + + const auto result = json_logic_->Apply(logic, data); + + std::vector expected_result{"zebra", "banana", "cat"}; + EXPECT_EQ(result, expected_result); +} + +TEST_F(OperationDataMissingSome, OrderPreservationReversedKeys) +{ + const auto logic = R"( + { + "missing_some": [3, ["z", "y", "x", "w", "v"]] + } + )"_json; + + const auto data = R"( + { + "y": 2, + "w": 4 + } + )"_json; + + const auto result = json_logic_->Apply(logic, data); + + std::vector expected_result{"z", "x", "v"}; EXPECT_EQ(result, expected_result); } From a0e92264c46ebef1c61ca9c8fcbe3f5905a2778f Mon Sep 17 00:00:00 2001 From: Hans Binderup Date: Tue, 17 Feb 2026 12:28:06 +0100 Subject: [PATCH 2/2] gitignore: ignore test files Was previosuly not ignored. Workspace will now be clean. Signed-off-by: Hans Binderup --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 97d869a..49f0c5b 100644 --- a/.gitignore +++ b/.gitignore @@ -270,3 +270,7 @@ CPack* _CPack* .cache/ *.deb + +# test files +test/lib/googletest/googletest/generated/ +jsonlogic_test