From 7b7616b57cd5eb6e99d1e8b7a914423ada111760 Mon Sep 17 00:00:00 2001 From: Anand Kumar Shaw Date: Wed, 2 Sep 2026 15:18:05 +0530 Subject: [PATCH 1/3] Add Roaring64Map::and_cardinality and Roaring64Map::intersect Roaring64Map had no way to ask for the size of an intersection, or whether two bitmaps intersect at all, without materializing the intersection via operator&. The 32-bit Roaring class has provided both for a long time. Walk the two ordered maps of 32-bit bitmaps in tandem and delegate to the 32-bit routines on shared high keys. intersect() stops at the first shared value. Cardinality sums go through an overflow-checked accumulator that mirrors what cardinality() does when the true result is 2^64. --- cpp/roaring/roaring64map.hh | 68 +++++++++++++++++++++++++++++++++++++ tests/cpp_unit.cpp | 54 +++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/cpp/roaring/roaring64map.hh b/cpp/roaring/roaring64map.hh index fd82b99b3..c232bba9c 100644 --- a/cpp/roaring/roaring64map.hh +++ b/cpp/roaring/roaring64map.hh @@ -879,6 +879,52 @@ class Roaring64Map { #endif } + /** + * Computes the size of the intersection between two bitmaps. + * Throws std::length_error in the special case where the result is 2^64. + */ + uint64_t and_cardinality(const Roaring64Map &r) const { + uint64_t card = 0; + auto lhs = roarings.cbegin(); + auto rhs = r.roarings.cbegin(); + while (lhs != roarings.cend() && rhs != r.roarings.cend()) { + if (lhs->first < rhs->first) { + ++lhs; + } else if (lhs->first > rhs->first) { + ++rhs; + } else { + card = addCardinalities( + card, lhs->second.and_cardinality(rhs->second)); + ++lhs; + ++rhs; + } + } + return card; + } + + /** + * Check whether the two bitmaps intersect. Stops at the first shared + * value rather than computing the whole intersection. + */ + bool intersect(const Roaring64Map &r) const { + auto lhs = roarings.cbegin(); + auto rhs = r.roarings.cbegin(); + while (lhs != roarings.cend() && rhs != r.roarings.cend()) { + if (lhs->first < rhs->first) { + ++lhs; + } else if (lhs->first > rhs->first) { + ++rhs; + } else { + if (lhs->second.intersect(rhs->second)) { + return true; + } + ++lhs; + ++rhs; + } + } + return false; + } + /** * Returns true if the bitmap is subset of the other. */ @@ -1702,6 +1748,28 @@ class Roaring64Map { static constexpr uint32_t lowBytes(const uint64_t in) { return uint32_t(in); } + /** + * Adds two cardinalities. A 64-bit bitmap can hold 2^64 values, which is + * not representable in a uint64_t, so this mirrors what cardinality() + * does in that case. Each addend is at most 2^32 and there are at most + * 2^32 keys, so the true sum never exceeds 2^64 and a wraparound check is + * exact. + */ + static uint64_t addCardinalities(uint64_t lhs, uint64_t rhs) { + uint64_t sum = lhs + rhs; + if (sum < lhs) { +#if ROARING_EXCEPTIONS + throw std::length_error( + "cardinality is 2^64, " + "unable to represent in a 64-bit integer"); +#else + ROARING_TERMINATE( + "cardinality is 2^64, " + "unable to represent in a 64-bit integer"); +#endif + } + return sum; + } static constexpr uint64_t uniteBytes(const uint32_t highBytes, const uint32_t lowBytes) { return (uint64_t(highBytes) << 32) | uint64_t(lowBytes); diff --git a/tests/cpp_unit.cpp b/tests/cpp_unit.cpp index a9592d1af..9ebc2b3fa 100644 --- a/tests/cpp_unit.cpp +++ b/tests/cpp_unit.cpp @@ -2110,6 +2110,56 @@ DEFINE_TEST(test_combinatoric_flip_many_64) { } } +DEFINE_TEST(test_cpp_and_cardinality_64_basic) { + Roaring64Map r1, r2; + for (uint64_t i = 0; i < 100; ++i) { + r1.add(i); + r1.add((uint64_t(1) << 32) + i); + } + for (uint64_t i = 50; i < 150; ++i) { + r2.add(i); + r2.add((uint64_t(1) << 32) + i); + } + assert_true(r1.and_cardinality(r2) == 100); +} + +DEFINE_TEST(test_cpp_and_cardinality_64_disjoint_keys) { + Roaring64Map r1, r2; + r1.add(uint64_t(1)); + r1.add((uint64_t(2) << 32) + 5); + r2.add((uint64_t(7) << 32) + 1); + r2.add((uint64_t(9) << 32) + 5); + assert_true(r1.and_cardinality(r2) == 0); + assert_false(r1.intersect(r2)); +} + +DEFINE_TEST(test_cpp_intersect_predicate_64) { + Roaring64Map r1, r2, empty; + r1.add(uint64_t(1)); + r1.add((uint64_t(3) << 32) + 8); + r2.add((uint64_t(3) << 32) + 8); + assert_true(r1.intersect(r2)); + assert_true(r2.intersect(r1)); + assert_false(r1.intersect(empty)); + assert_false(empty.intersect(r1)); + assert_false(empty.intersect(empty)); + assert_true(r1.intersect(r1)); +} + +DEFINE_TEST(test_cpp_and_cardinality_64_matches_materialized) { + Roaring64Map r1, r2; + for (uint64_t k = 0; k < 4; ++k) { + for (uint64_t i = 0; i < 200; i += 3) { + r1.add((k << 32) + i); + } + for (uint64_t i = 0; i < 200; i += 2) { + r2.add(((k + 1) << 32) + i); + } + } + assert_true(r1.and_cardinality(r2) == (r1 & r2).cardinality()); + assert_true(r1.intersect(r2) == ((r1 & r2).cardinality() > 0)); +} + DEFINE_TEST(test_cpp_is_subset_64) { Roaring64Map r1 = Roaring64Map::bitmapOf(1, uint64_t(1)); Roaring64Map r2 = Roaring64Map::bitmapOf(1, uint64_t(1) << 32); @@ -2398,6 +2448,10 @@ int main() { cmocka_unit_test(test_issue304), cmocka_unit_test(issue_336), cmocka_unit_test(issue_372), + cmocka_unit_test(test_cpp_and_cardinality_64_basic), + cmocka_unit_test(test_cpp_and_cardinality_64_disjoint_keys), + cmocka_unit_test(test_cpp_intersect_predicate_64), + cmocka_unit_test(test_cpp_and_cardinality_64_matches_materialized), cmocka_unit_test(test_cpp_is_subset_64), cmocka_unit_test(test_cpp_fast_union_64), cmocka_unit_test(test_cpp_to_string), From 75eacc8da4c888a3e7b5905be385bc9a1b52832f Mon Sep 17 00:00:00 2001 From: Anand Kumar Shaw Date: Wed, 2 Sep 2026 15:19:08 +0530 Subject: [PATCH 2/3] Cover Roaring64Map and_cardinality and intersect in the checked harness --- tests/cpp_unit.cpp | 15 +++++++++++++++ tests/roaring64map_checked.hh | 20 ++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/tests/cpp_unit.cpp b/tests/cpp_unit.cpp index 9ebc2b3fa..4fe25ab33 100644 --- a/tests/cpp_unit.cpp +++ b/tests/cpp_unit.cpp @@ -2160,6 +2160,20 @@ DEFINE_TEST(test_cpp_and_cardinality_64_matches_materialized) { assert_true(r1.intersect(r2) == ((r1 & r2).cardinality() > 0)); } +DEFINE_TEST(test_cpp_and_cardinality_64_checked) { + doublechecked::Roaring64Map r1, r2; + for (uint64_t k = 0; k < 3; ++k) { + for (uint64_t i = 0; i < 300; i += 7) { + r1.add((k << 32) + i); + } + for (uint64_t i = 0; i < 300; i += 5) { + r2.add((k << 32) + i); + } + } + r1.and_cardinality(r2); + r1.intersect(r2); +} + DEFINE_TEST(test_cpp_is_subset_64) { Roaring64Map r1 = Roaring64Map::bitmapOf(1, uint64_t(1)); Roaring64Map r2 = Roaring64Map::bitmapOf(1, uint64_t(1) << 32); @@ -2452,6 +2466,7 @@ int main() { cmocka_unit_test(test_cpp_and_cardinality_64_disjoint_keys), cmocka_unit_test(test_cpp_intersect_predicate_64), cmocka_unit_test(test_cpp_and_cardinality_64_matches_materialized), + cmocka_unit_test(test_cpp_and_cardinality_64_checked), cmocka_unit_test(test_cpp_is_subset_64), cmocka_unit_test(test_cpp_fast_union_64), cmocka_unit_test(test_cpp_to_string), diff --git a/tests/roaring64map_checked.hh b/tests/roaring64map_checked.hh index 0f7e0bb4e..522c6d5a1 100644 --- a/tests/roaring64map_checked.hh +++ b/tests/roaring64map_checked.hh @@ -29,11 +29,13 @@ #define INCLUDE_ROARING_64_MAP_CHECKED_HH_ #include +#include #include #include // sorted set, typically a red-black tree implementation #include #include #include +#include #include "test.h" @@ -415,6 +417,24 @@ class Roaring64Map { return ans; } + uint64_t and_cardinality(const Roaring64Map &r) const { + uint64_t ans = plain.and_cardinality(r.plain); + std::vector expected; + std::set_intersection(check.begin(), check.end(), r.check.begin(), + r.check.end(), std::back_inserter(expected)); + assert_true(ans == expected.size()); + return ans; + } + + bool intersect(const Roaring64Map &r) const { + bool ans = plain.intersect(r.plain); + std::vector expected; + std::set_intersection(check.begin(), check.end(), r.check.begin(), + r.check.end(), std::back_inserter(expected)); + assert_true(ans == !expected.empty()); + return ans; + } + bool isStrictSubset( const Roaring64Map &r) const { // is `this` subset of `r`? bool ans = plain.isStrictSubset(r.plain); From 340ba9609b397ff05c6b2f89a52fe93112e94b3f Mon Sep 17 00:00:00 2001 From: Anand Kumar Shaw Date: Wed, 2 Sep 2026 15:24:38 +0530 Subject: [PATCH 3/3] Add Roaring64Map or_cardinality, xor_cardinality and andnot_cardinality Completes the set-cardinality methods that the 32-bit Roaring class already provides, so callers can size a union, symmetric difference or difference without materializing the result. Union and symmetric difference must account for keys held by only one side, so unmatched keys contribute their full cardinality and the leftover tail of either map is drained. Difference is asymmetric: only keys present in the left map contribute, and a key missing from the right map contributes in full. All sums go through the overflow-checked accumulator. The checked test harness shadows each method against the equivalent std::set algorithm. --- cpp/roaring/roaring64map.hh | 86 +++++++++++++++++++++++++++++++++++ tests/cpp_unit.cpp | 82 ++++++++++++++++++++++++++++++--- tests/roaring64map_checked.hh | 28 ++++++++++++ 3 files changed, 190 insertions(+), 6 deletions(-) diff --git a/cpp/roaring/roaring64map.hh b/cpp/roaring/roaring64map.hh index c232bba9c..60de8377e 100644 --- a/cpp/roaring/roaring64map.hh +++ b/cpp/roaring/roaring64map.hh @@ -925,6 +925,92 @@ class Roaring64Map { return false; } + /** + * Computes the size of the union between two bitmaps. + * Throws std::length_error in the special case where the result is 2^64. + */ + uint64_t or_cardinality(const Roaring64Map &r) const { + uint64_t card = 0; + auto lhs = roarings.cbegin(); + auto rhs = r.roarings.cbegin(); + while (lhs != roarings.cend() && rhs != r.roarings.cend()) { + if (lhs->first < rhs->first) { + card = addCardinalities(card, lhs->second.cardinality()); + ++lhs; + } else if (lhs->first > rhs->first) { + card = addCardinalities(card, rhs->second.cardinality()); + ++rhs; + } else { + card = addCardinalities( + card, lhs->second.or_cardinality(rhs->second)); + ++lhs; + ++rhs; + } + } + for (; lhs != roarings.cend(); ++lhs) { + card = addCardinalities(card, lhs->second.cardinality()); + } + for (; rhs != r.roarings.cend(); ++rhs) { + card = addCardinalities(card, rhs->second.cardinality()); + } + return card; + } + + /** + * Computes the size of the symmetric difference between two bitmaps. + * Throws std::length_error in the special case where the result is 2^64. + */ + uint64_t xor_cardinality(const Roaring64Map &r) const { + uint64_t card = 0; + auto lhs = roarings.cbegin(); + auto rhs = r.roarings.cbegin(); + while (lhs != roarings.cend() && rhs != r.roarings.cend()) { + if (lhs->first < rhs->first) { + card = addCardinalities(card, lhs->second.cardinality()); + ++lhs; + } else if (lhs->first > rhs->first) { + card = addCardinalities(card, rhs->second.cardinality()); + ++rhs; + } else { + card = addCardinalities( + card, lhs->second.xor_cardinality(rhs->second)); + ++lhs; + ++rhs; + } + } + for (; lhs != roarings.cend(); ++lhs) { + card = addCardinalities(card, lhs->second.cardinality()); + } + for (; rhs != r.roarings.cend(); ++rhs) { + card = addCardinalities(card, rhs->second.cardinality()); + } + return card; + } + + /** + * Computes the size of the difference (andnot) between two bitmaps. + * Throws std::length_error in the special case where the result is 2^64. + */ + uint64_t andnot_cardinality(const Roaring64Map &r) const { + uint64_t card = 0; + auto lhs = roarings.cbegin(); + auto rhs = r.roarings.cbegin(); + while (lhs != roarings.cend()) { + if (rhs == r.roarings.cend() || lhs->first < rhs->first) { + card = addCardinalities(card, lhs->second.cardinality()); + ++lhs; + } else if (lhs->first > rhs->first) { + ++rhs; + } else { + card = addCardinalities( + card, lhs->second.andnot_cardinality(rhs->second)); + ++lhs; + ++rhs; + } + } + return card; + } + /** * Returns true if the bitmap is subset of the other. */ diff --git a/tests/cpp_unit.cpp b/tests/cpp_unit.cpp index 4fe25ab33..bb185f3d6 100644 --- a/tests/cpp_unit.cpp +++ b/tests/cpp_unit.cpp @@ -2160,18 +2160,85 @@ DEFINE_TEST(test_cpp_and_cardinality_64_matches_materialized) { assert_true(r1.intersect(r2) == ((r1 & r2).cardinality() > 0)); } -DEFINE_TEST(test_cpp_and_cardinality_64_checked) { - doublechecked::Roaring64Map r1, r2; +DEFINE_TEST(test_cpp_or_cardinality_64) { + Roaring64Map r1, r2, empty; + for (uint64_t i = 0; i < 100; ++i) { + r1.add(i); + r1.add((uint64_t(2) << 32) + i); + } + for (uint64_t i = 50; i < 150; ++i) { + r2.add(i); + r2.add((uint64_t(8) << 32) + i); + } + assert_true(r1.or_cardinality(r2) == (r1 | r2).cardinality()); + assert_true(r2.or_cardinality(r1) == (r1 | r2).cardinality()); + assert_true(r1.or_cardinality(empty) == r1.cardinality()); + assert_true(empty.or_cardinality(r1) == r1.cardinality()); + assert_true(r1.or_cardinality(r1) == r1.cardinality()); +} + +DEFINE_TEST(test_cpp_xor_cardinality_64) { + Roaring64Map r1, r2, empty; + for (uint64_t i = 0; i < 100; ++i) { + r1.add(i); + r1.add((uint64_t(4) << 32) + i); + } + for (uint64_t i = 50; i < 150; ++i) { + r2.add(i); + r2.add((uint64_t(6) << 32) + i); + } + assert_true(r1.xor_cardinality(r2) == (r1 ^ r2).cardinality()); + assert_true(r2.xor_cardinality(r1) == (r1 ^ r2).cardinality()); + assert_true(r1.xor_cardinality(empty) == r1.cardinality()); + assert_true(r1.xor_cardinality(r1) == 0); +} + +DEFINE_TEST(test_cpp_andnot_cardinality_64) { + Roaring64Map r1, r2, empty; + for (uint64_t i = 0; i < 100; ++i) { + r1.add(i); + r1.add((uint64_t(3) << 32) + i); + } + for (uint64_t i = 50; i < 150; ++i) { + r2.add(i); + r2.add((uint64_t(5) << 32) + i); + } + // A key held only by r1, so the two differences have distinct sizes and + // the asymmetry check below is meaningful. + r1.add((uint64_t(9) << 32) + 1); + assert_true(r1.andnot_cardinality(r2) == (r1 - r2).cardinality()); + assert_true(r2.andnot_cardinality(r1) == (r2 - r1).cardinality()); + assert_true(r1.andnot_cardinality(r2) != r2.andnot_cardinality(r1)); + assert_true(r1.andnot_cardinality(empty) == r1.cardinality()); + assert_true(empty.andnot_cardinality(r1) == 0); + assert_true(r1.andnot_cardinality(r1) == 0); +} + +DEFINE_TEST(test_cpp_set_cardinality_64_checked) { + // The checked wrappers assert against std::set, so simply calling each + // method on operands with shared, disjoint and one-sided keys is the test. + doublechecked::Roaring64Map r1, r2, empty; for (uint64_t k = 0; k < 3; ++k) { for (uint64_t i = 0; i < 300; i += 7) { r1.add((k << 32) + i); } for (uint64_t i = 0; i < 300; i += 5) { - r2.add((k << 32) + i); + r2.add(((k + 1) << 32) + i); + } + } + r1.add((uint64_t(90) << 32) + 3); + r2.add((uint64_t(91) << 32) + 4); + + const doublechecked::Roaring64Map *operands[3] = {&r1, &r2, &empty}; + for (auto *a : operands) { + for (auto *b : operands) { + a->and_cardinality(*b); + a->intersect(*b); + a->or_cardinality(*b); + a->xor_cardinality(*b); + a->andnot_cardinality(*b); } } - r1.and_cardinality(r2); - r1.intersect(r2); } DEFINE_TEST(test_cpp_is_subset_64) { @@ -2466,7 +2533,10 @@ int main() { cmocka_unit_test(test_cpp_and_cardinality_64_disjoint_keys), cmocka_unit_test(test_cpp_intersect_predicate_64), cmocka_unit_test(test_cpp_and_cardinality_64_matches_materialized), - cmocka_unit_test(test_cpp_and_cardinality_64_checked), + cmocka_unit_test(test_cpp_or_cardinality_64), + cmocka_unit_test(test_cpp_xor_cardinality_64), + cmocka_unit_test(test_cpp_andnot_cardinality_64), + cmocka_unit_test(test_cpp_set_cardinality_64_checked), cmocka_unit_test(test_cpp_is_subset_64), cmocka_unit_test(test_cpp_fast_union_64), cmocka_unit_test(test_cpp_to_string), diff --git a/tests/roaring64map_checked.hh b/tests/roaring64map_checked.hh index 522c6d5a1..4dfd0c16e 100644 --- a/tests/roaring64map_checked.hh +++ b/tests/roaring64map_checked.hh @@ -435,6 +435,34 @@ class Roaring64Map { return ans; } + uint64_t or_cardinality(const Roaring64Map &r) const { + uint64_t ans = plain.or_cardinality(r.plain); + std::vector expected; + std::set_union(check.begin(), check.end(), r.check.begin(), + r.check.end(), std::back_inserter(expected)); + assert_true(ans == expected.size()); + return ans; + } + + uint64_t xor_cardinality(const Roaring64Map &r) const { + uint64_t ans = plain.xor_cardinality(r.plain); + std::vector expected; + std::set_symmetric_difference(check.begin(), check.end(), + r.check.begin(), r.check.end(), + std::back_inserter(expected)); + assert_true(ans == expected.size()); + return ans; + } + + uint64_t andnot_cardinality(const Roaring64Map &r) const { + uint64_t ans = plain.andnot_cardinality(r.plain); + std::vector expected; + std::set_difference(check.begin(), check.end(), r.check.begin(), + r.check.end(), std::back_inserter(expected)); + assert_true(ans == expected.size()); + return ans; + } + bool isStrictSubset( const Roaring64Map &r) const { // is `this` subset of `r`? bool ans = plain.isStrictSubset(r.plain);