Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions cpp/roaring/roaring64map.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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);
Expand Down
139 changes: 139 additions & 0 deletions tests/cpp_unit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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),
Expand Down
48 changes: 48 additions & 0 deletions tests/roaring64map_checked.hh
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@
#define INCLUDE_ROARING_64_MAP_CHECKED_HH_

#include <algorithm>
#include <iterator>
#include <new>
#include <set> // sorted set, typically a red-black tree implementation
#include <stdarg.h>
#include <stdexcept>
#include <string>
#include <vector>

#include "test.h"

Expand Down Expand Up @@ -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<uint64_t> 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<uint64_t> 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<uint64_t> 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<uint64_t> 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<uint64_t> 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);
Expand Down
Loading