diff --git a/cpp/roaring/roaring64map.hh b/cpp/roaring/roaring64map.hh index fd82b99b..60de8377 100644 --- a/cpp/roaring/roaring64map.hh +++ b/cpp/roaring/roaring64map.hh @@ -879,6 +879,138 @@ 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; + } + + /** + * 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. */ @@ -1702,6 +1834,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 a9592d1a..bb185f3d 100644 --- a/tests/cpp_unit.cpp +++ b/tests/cpp_unit.cpp @@ -2110,6 +2110,137 @@ 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_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 + 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); + } + } +} + 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 +2529,14 @@ 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_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 0f7e0bb4..4dfd0c16 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,52 @@ 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; + } + + 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);