diff --git a/solver/src/main/java/org/chocosolver/memory/structure/SparseBitSet.java b/solver/src/main/java/org/chocosolver/memory/structure/SparseBitSet.java new file mode 100644 index 0000000000..92e974057d --- /dev/null +++ b/solver/src/main/java/org/chocosolver/memory/structure/SparseBitSet.java @@ -0,0 +1,489 @@ +package org.chocosolver.memory.structure; + +import org.chocosolver.memory.IEnvironment; +import org.chocosolver.memory.IStateBitSet; + +import java.util.Arrays; + +/** + * A backtrackable bitset optimised for compaction. This implementation saves + * memory when the content is sparse, typically when it contains a few bit + * sets or clustered bitsets. The implementation relies on an allocation per + * block: setting a bit on a high index will only lead to the allocation of + * the corresponding block, contrary to {@link S64BitSet} that will allocate + * memory up to the desired index whatever the actual need. + *

+ * In terms of memory efficiency, the benefits will depend on the density of the + * bit sets and the block size. When some memory regions are set then + * cleared, the memory is not reclaimed. Accordingly, if the bitset is full of 1 + * early and cleared over the time, then no savings are possible. + * {@link #equals(Object)} and {@link #hashCode()} supports with any kind of + * {@link IStateBitSet} implementation but they must not be used in any + * performance sensitive context. + */ +public class SparseBitSet implements IStateBitSet { + + /** Block size in bits. */ + private final int blockSize; + + /** + * The index declares the opened blocks in the current world. This is not + * required for correctness but speed up iterations to only browse + * meaningful blocks. + */ + private final S64BitSet index; + + /** The blocks. */ + private IStateBitSet[] blocks; + + /** + * The environment to use to create internal backtrackable variables. + */ + private final IEnvironment env; + + /** + * @param env backtracking environment. + * @param blockSize block size in bits. + */ + public SparseBitSet(final IEnvironment env, final int blockSize) { + this.env = env; + if (blockSize <= 0) { + throw new IllegalArgumentException( + "Block size must be > 0. Got " + blockSize); + } + this.blockSize = blockSize; + blocks = new IStateBitSet[0]; + index = new S64BitSet(env); + } + + /** + * Check that the given index is strictly positive. + * + * @param index the index + * @throws IndexOutOfBoundsException if the index is negative + */ + private static void requirePositiveIndex(final int index) { + if (index < 0) { + throw new IndexOutOfBoundsException( + "Positive index expected. Got " + index); + } + } + + /** + * Check the validity of a range of indices. + * Both indices must be strictly positive and the second one must be greater + * than the first one. + * + * @param from lower bound. + * @param to upper bound. + * @throws IndexOutOfBoundsException if the range is invalid. + */ + private static void validIndexRange(final int from, final int to) { + requirePositiveIndex(from); + requirePositiveIndex(to); + if (from > to) { + throw new IndexOutOfBoundsException( + "Invalid range: [" + from + ", " + to + ")"); + } + } + + /** + * Get the block index for a given bit. + * This does not ensure that the block exists or that the index is big enough. + * + * @param bit the bit + * @return the block index. + */ + private int blockIndex(final int bit) { + return bit / blockSize; + } + + /** + * Get the bitset-level index value from a block-level index. + * + * @param blockIdx the block index. + * @param localIndex the block-level index. + * @return the absolute index. + */ + private int absIndex(final int blockIdx, final int localIndex) { + return blockIdx * blockSize + localIndex; + } + + /** + * Get the block-level index from a bitset-level one. This does + * not indicate which block must be used (see {@link #blockIndex(int)}). + * + * @param absIndex the bit. + * @return the block-level index to use. + */ + private int localIndex(final int absIndex) { + return absIndex % blockSize; + } + + /** + * Ensure that the index is big enough. + * The index is growth to the given value if needed but no blocks are however + * created. + * + * @param size the desired index size. + */ + private void ensureIndexCapacity(final int size) { + if (size >= blocks.length) { + blocks = Arrays.copyOf(blocks, size + 1); + } + } + + /** + * If needed, create the block at the given block index. + * The index is considered to be big enough. + * + * @param blockIndex the block index. + * @return the block at this index. + */ + private IStateBitSet ensureBlock(final int blockIndex) { + if (blocks[blockIndex] == null) { + // Create the block and register it. + blocks[blockIndex] = new S64BitSet(env, 64); + } + index.set(blockIndex); + return blocks[blockIndex]; + } + + @Override + public void set(final int bit) { + requirePositiveIndex(bit); + // Block index. + final int bIdx = blockIndex(bit); + // Ensure the index is big enough. + ensureIndexCapacity(bIdx); + // Set the right offset in the block. + ensureBlock(bIdx).set(localIndex(bit)); + } + + @Override + public void clear(final int bit) { + requirePositiveIndex(bit); + // Which block. + final int bIdx = blockIndex(bit); + if (!index.get(bIdx)) { + // The block is not registered in this world. Nothing to clear. + return; + } + // The block exists and is registered. Clear at the offset. + blocks[bIdx].clear(localIndex(bit)); + } + + @Override + public void set(final int bit, final boolean flag) { + requirePositiveIndex(bit); + if (flag) { + set(bit); + } else { + clear(bit); + } + } + + @Override + public boolean get(final int bit) { + requirePositiveIndex(bit); + // Which block. + final int bIdx = blockIndex(bit); + if (!index.get(bIdx)) { + // Un-registered block. + return false; + } + return blocks[bIdx].get(localIndex(bit)); + } + + @Override + public int size() { + return index.size() * blockSize; + } + + /** + * Get the number of bits actually used to store data. + * This accounts for the blocks and the index sizes. + * @return a positive amount + */ + public long memorySize() { + long size = index.size(); + for (int bIdx = index.nextSetBit(0); bIdx >= 0; + bIdx = index.nextSetBit(bIdx + 1)) { + final IStateBitSet bs = blocks[bIdx]; + assert bs != null; + size += bs.size(); + } + return size; + } + + @Override + public int cardinality() { + int sum = 0; + for (int bIdx = index.nextSetBit(0); bIdx >= 0; + bIdx = index.nextSetBit(bIdx + 1)) { + final IStateBitSet bs = blocks[bIdx]; + assert bs != null; + sum += bs.cardinality(); + } + return sum; + } + + @Override + public void clear() { + // Clear the content and the index. + for (int bIdx = index.nextSetBit(0); bIdx >= 0; + bIdx = index.nextSetBit(bIdx + 1)) { + blocks[bIdx].clear(); + } + index.clear(); + } + + @Override + public boolean isEmpty() { + if (index.isEmpty()) { + // No blocks so for sure an empty bitset. + return true; + } + // One non-empty block is sufficient. + for (int bIdx = index.nextSetBit(0); bIdx >= 0; + bIdx = index.nextSetBit(bIdx + 1)) { + if (!blocks[bIdx].isEmpty()) { + return false; + } + } + return true; + } + + @Override + public void clear(final int from, final int to) { + validIndexRange(from, to); + if (from == to) { + return; + } + // Go over every impacted blocks. The first and the last may be partially + // set. + for (int bIdx = blockIndex(from); bIdx <= blockIndex(to); bIdx++) { + if (bIdx >= blocks.length) { + // The block is passed the index size. Thus for sure everything here is + // cleared. + return; + } + if (!this.index.get(bIdx)) { + // No block allocated here, nothing to clear; + continue; + } + final int st; + if (bIdx == blockIndex(from)) { + st = from; + } else { + st = 0; + } + // local end is the block end for all the block, except the last one + final int ed; + if (bIdx == blockIndex(to)) { + ed = localIndex(to); + } else { + ed = blockSize; + } + ensureBlock(bIdx).clear(st, ed); + } + } + + @Override + public void set(final int from, final int to) { + validIndexRange(from, to); + if (from == to) { + return; + } + // Grows the index. + ensureIndexCapacity(blockIndex(to)); + // Go over every impacted blocks. The first and the last may be partially + // set. + final int firstBlock = blockIndex(from); + final int lastBlock = blockIndex(to); + for (int bIdx = firstBlock; bIdx <= lastBlock; bIdx++) { + // local start is offset(from) for the first block only, otherwise 0. + final int st; + if (bIdx == firstBlock) { + st = localIndex(from); + } else { + st = 0; + } + // local end is the block end for all the block, except the last one + final int ed; + if (bIdx == lastBlock) { + ed = localIndex(to); + } else { + ed = blockSize; + } + ensureBlock(bIdx).set(st, ed); + } + } + + @Override + public int nextSetBit(final int fromIndex) { + requirePositiveIndex(fromIndex); + final int startingBlock = blockIndex(fromIndex); + + // Iterate over all the blocks starting from the current index to pick the + // first bit set. + for (int bIdx = index.nextSetBit(startingBlock); bIdx >= 0; + bIdx = index.nextSetBit(bIdx + 1)) { + // For the current block, the offset is the one associated to fromIndex + // but at the moment the next blocks are browsed, the offset is 0 to grab + // the first bit set. + final int offset; + if (bIdx > startingBlock) { + offset = 0; + } else { + offset = localIndex(fromIndex); + } + int bit = blocks[bIdx].nextSetBit(offset); + if (bit >= 0) { + // Found it. + return absIndex(bIdx, bit); + } + } + return -1; + } + + @Override + public int prevSetBit(final int fromIndex) { + requirePositiveIndex(fromIndex); + final int lastBlockIdx = blockIndex(fromIndex); + // Iterate over all the blocks backward, starting from the current index to + // pick the first bit set. + for (int bIdx = index.prevSetBit(lastBlockIdx); bIdx >= 0; + bIdx = index.prevSetBit(bIdx - 1)) { + // For the current block, the offset is the one associated to fromIndex + // but at the moment the previous blocks are browsed, the offset is + // 'blockSize' to grab the first bit set from the end. + int offset = localIndex(fromIndex); + if (bIdx < lastBlockIdx) { + offset = blockSize; + } + int bit = blocks[bIdx].prevSetBit(offset); + if (bit >= 0) { + // Found it. + return absIndex(bIdx, bit); + } + } + return -1; + } + + @Override + public int nextClearBit(final int fromIndex) { + requirePositiveIndex(fromIndex); + final int fromBlock = blockIndex(fromIndex); + int curBlock = fromBlock; + while (curBlock < blocks.length) { + if (blocks[curBlock] == null || !index.get(curBlock)) { + // null block. fromIndex is then clear for sure. + // In case the block is not null, check the index in case we have set + // bits but a cleared block that only clear the index. + return fromIndex; + } + // local offset depending on the block under inspection. + final int localOff; + if (curBlock > fromBlock) { + // Intermediate block, start at 0. + localOff = 0; + } else { + // First block, start from fromIndex. + localOff = localIndex(fromIndex); + } + final int nextClear = blocks[curBlock].nextClearBit(localOff); + if (nextClear != blockSize) { + // Not all the bits are set, the first clear bit is here. + return absIndex(curBlock, nextClear); + } + // All the bits are set, check the next block. + curBlock++; + } + return fromIndex; + } + + @Override + public int prevClearBit(final int fromIndex) { + requirePositiveIndex(fromIndex); + final int fromBlock = blockIndex(fromIndex); + if (fromBlock >= index.length()) { + // Outside the current index. For sure there is a cleared bit at fromIndex. + return fromIndex; + } + int curBlock = fromBlock; + while (curBlock >= 0) { + if (!index.get(curBlock)) { + // null block. fromIndex is then clear for sure. Possibly also a cleared + // block. + return fromIndex; + } + // local offset depending on the block under inspection. + final int localOff; + if (curBlock < fromBlock) { + // Intermediate block, start at blockSize. + localOff = blockSize - 1; + } else { + // First block, start from fromIndex. + localOff = localIndex(fromIndex); + } + final int prevClear = blocks[curBlock].prevClearBit(localOff); + if (prevClear >= 0) { + // Not all the bits are set, the first clear bit is here. + return absIndex(curBlock, prevClear); + } + // All the bits are set, check the previous block. + curBlock--; + } + // No cleared bit in any block. + return -1; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!(o instanceof IStateBitSet)) { + return false; + } + + final IStateBitSet that = (IStateBitSet) o; + // Fail fast. + if (this.cardinality() != that.cardinality()) { + return false; + } + // Same cardinality. Iterate over the bit sets. Those must be sets in 'that' + // as well. + for (int i = nextSetBit(0); i >= 0; i = nextSetBit(i + 1)) { + if (!that.get(i)) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + int hashCode = 1; + for (int i = nextSetBit(0); i >= 0; i = nextSetBit(i + 1)) { + hashCode = hashCode * 31 + i; + } + return hashCode; + } + + @Override + public String toString() { + final StringBuilder b = new StringBuilder(); + b.append('{'); + int i = nextSetBit(0); + if (i != -1) { + b.append(i); + for (i = nextSetBit(i + 1); i >= 0; i = nextSetBit(i + 1)) { + b.append(", ").append(i); + } + } + b.append('}'); + return b.toString(); + } +} diff --git a/solver/src/test/java/org/chocosolver/memory/structure/SparseBitSetTest.java b/solver/src/test/java/org/chocosolver/memory/structure/SparseBitSetTest.java new file mode 100644 index 0000000000..c37168d780 --- /dev/null +++ b/solver/src/test/java/org/chocosolver/memory/structure/SparseBitSetTest.java @@ -0,0 +1,401 @@ +package org.chocosolver.memory.structure; + +import org.chocosolver.memory.IEnvironment; +import org.chocosolver.memory.IStateBitSet; +import org.chocosolver.memory.trailing.EnvironmentTrailing; +import org.chocosolver.solver.Model; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.util.Random; + +/** + * Unit tests for {@link org.chocosolver.memory.structure.SparseBitSet}. + */ +public class SparseBitSetTest { + + @Test + public void testNew() { + IEnvironment env = new EnvironmentTrailing(); + IStateBitSet bs = new SparseBitSet(env, 64); + Assert.assertTrue(bs.isEmpty()); + Assert.assertEquals(0, bs.cardinality()); + Assert.assertEquals(-1, bs.nextSetBit(0)); + } + + @Test + public void testSetGet() { + IEnvironment env = new EnvironmentTrailing(); + IStateBitSet bs = new SparseBitSet(env, 64); + Assert.assertFalse(bs.get(72904)); + bs.set(65); + Assert.assertTrue(bs.get(65)); + Assert.assertFalse(bs.get(64)); + Assert.assertEquals(1, bs.cardinality()); + bs.set(0); + Assert.assertEquals(2, bs.cardinality()); + Assert.assertTrue(bs.get(0)); + + // Idempotence. + bs.set(0, true); + Assert.assertEquals(2, bs.cardinality()); + + bs.set(0, false); + Assert.assertEquals(1, bs.cardinality()); + Assert.assertFalse(bs.get(0)); + } + + @Test + public void testNextSetBit() { + IEnvironment env = new EnvironmentTrailing(); + IStateBitSet bs = new SparseBitSet(env, 64); + // Set inside the first block. + bs.set(1); + // Other consecutive bit sets on different blocks. + bs.set(255); + bs.set(256); + bs.set(257); + + // Right on the spot + Assert.assertEquals(bs.nextSetBit(1), 1); + Assert.assertEquals(bs.nextSetBit(255), 255); + Assert.assertEquals(bs.nextSetBit(256), 256); + Assert.assertEquals(bs.nextSetBit(257), 257); + + // Before, same block. + Assert.assertEquals(bs.nextSetBit(0), 1); + // Before previous blocks. + Assert.assertEquals(bs.nextSetBit(128), 255); + // After, used block. + Assert.assertEquals(bs.nextSetBit(258), -1); + // After, unused block. + Assert.assertEquals(bs.nextSetBit(65445), -1); + } + + @Test + public void testNextClearBit() { + final IEnvironment env = new EnvironmentTrailing(); + IStateBitSet bs = new SparseBitSet(env, 64); + Assert.assertEquals(0, bs.nextClearBit(0)); + bs.set(1); + // Before the set bit, same block. + Assert.assertEquals(0, bs.nextClearBit(0)); + // Right on the set bit. Expect the next one on this block. + Assert.assertEquals(2, bs.nextClearBit(1)); + + // Last clear bit on the same block. + Assert.assertEquals(63, bs.nextClearBit(63)); + + // On a non-existing block. + Assert.assertEquals(135, bs.nextClearBit(135)); + + // Create a range of set bits over multiple blocks. + bs.set(32, 257); + bs.clear(38, 72); // clear From 38 to 71 (incl) + Assert.assertEquals(bs.nextClearBit(37), 38); + Assert.assertEquals(bs.nextClearBit(71), 71); + Assert.assertEquals(bs.nextClearBit(72), 257); + Assert.assertEquals(bs.nextClearBit(65456), 65456); + } + + @Test + public void testPrevClearBit() { + final IEnvironment env = new EnvironmentTrailing(); + final IStateBitSet bs = new SparseBitSet(env, 64); + Assert.assertEquals(bs.prevClearBit(0), 0); + bs.set(0); + Assert.assertEquals(bs.prevClearBit(0), -1); + Assert.assertEquals(bs.prevClearBit(192), 192); + bs.set(32, 257); + Assert.assertEquals(bs.prevClearBit(256), 31); + } + + @Test + public void testPrevSetBit() { + IEnvironment env = new EnvironmentTrailing(); + IStateBitSet bs = new SparseBitSet(env, 64); + // Set inside the first block. + bs.set(1); + // Other consecutive bit sets on different blocks + bs.set(255); + bs.set(256); + bs.set(257); + + // Right on the spot + Assert.assertEquals(bs.prevSetBit(1), 1); + Assert.assertEquals(bs.prevSetBit(255), 255); + Assert.assertEquals(bs.prevSetBit(256), 256); + Assert.assertEquals(bs.prevSetBit(257), 257); + + // After, same block. + Assert.assertEquals(bs.prevSetBit(2), 1); + // After next blocks. + Assert.assertEquals(bs.prevSetBit(260), 257); + // After, unused block. + Assert.assertEquals(bs.prevSetBit(65445), 257); + + Assert.assertEquals(-1, bs.prevSetBit(0)); + } + + @Test + public void testClear() { + IEnvironment env = new EnvironmentTrailing(); + IStateBitSet bs = new SparseBitSet(env, 64); + bs.set(1); + bs.set(128); + bs.set(8212); + bs.clear(54); + Assert.assertEquals(3, bs.cardinality()); + bs.clear(8212); + Assert.assertEquals(2, bs.cardinality()); + Assert.assertFalse(bs.get(8212)); + Assert.assertEquals(-1, bs.nextSetBit(8000)); + + // Clear all. + bs.clear(); + Assert.assertEquals(bs.cardinality(), 0); + Assert.assertEquals(bs.nextSetBit(0), -1); + } + + @DataProvider(name = "invalidRanges") + public Object[][] invalidRanges() { + return new Object[][]{{-1, 3}, {5, 4}, {-1, -1}}; + } + + @Test(dataProvider = "invalidRanges", + expectedExceptions = IndexOutOfBoundsException.class) + public void testBadSetRanges(final int from, final int to) { + final IEnvironment env = new EnvironmentTrailing(); + final IStateBitSet bs = new SparseBitSet(env, 64); + bs.set(from, to); + } + + @Test(dataProvider = "invalidRanges", + expectedExceptions = IndexOutOfBoundsException.class) + public void testBadClearRanges(final int from, final int to) { + final IEnvironment env = new EnvironmentTrailing(); + final IStateBitSet bs = new SparseBitSet(env, 64); + bs.clear(from, to); + } + + @Test + public void testSetRange() { + IEnvironment env = new EnvironmentTrailing(); + IStateBitSet bs = new SparseBitSet(env, 64); + IStateBitSet ref = new S64BitSet(env); + + bs.set(3, 135); + ref.set(3, 135); + Assert.assertEquals(bs.cardinality(), 132); + int prev = 3; + for (int bit = bs.nextSetBit(4); bit >= 0; bit = bs.nextSetBit(bit + 1)) { + Assert.assertEquals(bit, prev + 1); + prev = bit; + } + Assert.assertEquals(134, prev); + bs.set(0, 4); + // Expand + prev = 0; + for (int bit = bs.nextSetBit(1); bit >= 0; bit = bs.nextSetBit(bit + 1)) { + Assert.assertEquals(bit, prev + 1); + prev = bit; + } + Assert.assertEquals(134, prev); + + bs = new SparseBitSet(env, 64); + bs.set(38, 72); + for (int idx = 38; idx < 72; idx++) { + Assert.assertEquals(idx, bs.nextSetBit(idx)); + } + Assert.assertEquals(-1, bs.nextSetBit(72)); + + // UB and LB are the same, that is a nop. + bs.set(155, 155); + Assert.assertFalse(bs.get(155)); + } + + @Test + public void testMemorySize() { + final IEnvironment env = new EnvironmentTrailing(); + final IStateBitSet ref = new S64BitSet(env); + final SparseBitSet sparse = new SparseBitSet(env, 64); + ref.set(0); + sparse.set(0); + Assert.assertEquals(ref.size(), 64); + // One long for the index, one for the block. There is an overhead compared + // to S64BitSet. + Assert.assertEquals(sparse.memorySize(), 2 * 64); + + sparse.set(128 * 64 - 1); + ref.set(128 * 64 - 1); + Assert.assertEquals(ref.size(), 128 * 64); + // 2 longs for the index, 2 longs for bit 0 and 128*64. Compared to size(), + // we see the compaction. + Assert.assertEquals(sparse.memorySize(), 4 * 64); + Assert.assertEquals(sparse.size(), 128 * 64); + } + + @Test + public void testSize() { + final IEnvironment env = new EnvironmentTrailing(); + final IStateBitSet sparse = new SparseBitSet(env, 64); + sparse.set(0); + // 64 blocks of 64 bits each. + Assert.assertEquals(sparse.size(), 64 * 64); + sparse.set(128 * 64 - 1); + // 128 blocks of 128 bits each. + Assert.assertEquals(sparse.size(), 128 * 64); + } + + @Test + public void testClearRange() { + IEnvironment env = new EnvironmentTrailing(); + IStateBitSet bs = new SparseBitSet(env, 64); + bs.clear(0, 187); + Assert.assertEquals(bs.cardinality(), 0); + bs.set(10, 237); + bs.clear(12, 239); + Assert.assertEquals(2, bs.cardinality()); + Assert.assertTrue(bs.get(10) && bs.get(11)); + bs.clear(10, 12); + Assert.assertEquals(0, bs.cardinality()); + + bs = new SparseBitSet(env, 64); + bs.set(32, 257); + bs.clear(38, 72); + Assert.assertEquals(bs.nextSetBit(38), 72); + + // Same LB than UB. This is a nop. + bs.clear(250, 250); + Assert.assertTrue(bs.get(250)); + + bs = new SparseBitSet(env, 32); + bs.set(0, 32); + bs.set(64, 70); + // Clear where there is no blocks. + bs.clear(32, 64); + Assert.assertEquals(bs.nextSetBit(64), 64); + } + + @Test + public void testEmpty() { + IEnvironment env = new EnvironmentTrailing(); + IStateBitSet bs = new SparseBitSet(env, 64); + Assert.assertTrue(bs.isEmpty()); + bs.set(10, 237); + Assert.assertFalse(bs.isEmpty()); + bs.clear(10, 237); + Assert.assertTrue(bs.isEmpty()); + } + + @Test + public void testBacktracking() { + final Model mo = new Model(new EnvironmentTrailing(), "foo"); + final IEnvironment env = mo.getEnvironment(); + + final IStateBitSet ref = env.makeBitSet(1000); + final SparseBitSet test = new SparseBitSet(env, 64); + Random rnd = new Random(); + // Set some. + for (int i = 0; i < 100; i++) { + int bit = rnd.nextInt(1010); + ref.set(bit); + test.set(bit); + } + env.worldPush(); + checkEquivalence(ref, test, 1010); + for (int i = 0; i < 100; i++) { + int bit = rnd.nextInt(1010); + ref.clear(bit); + test.clear(bit); + } + checkEquivalence(ref, test, 1010); + env.worldPop(); + checkEquivalence(ref, test, 1010); + + ref.clear(); + test.clear(); + checkEquivalence(ref, test, 1010); + env.worldPush(); + ref.set(0, 100); + test.set(0, 100); + checkEquivalence(ref, test, 1010); + env.worldPop(); + checkEquivalence(ref, test, 1010); + } + + public void checkEquivalence(final IStateBitSet ref, final SparseBitSet got, + final int max) { + // Check that the two bitsets are equivalent. This consists in checking + // every public APIs. + Assert.assertEquals(ref.cardinality(), got.cardinality()); + Assert.assertEquals(ref.isEmpty(), got.isEmpty()); + // Iteration capabilities. + for (int i = max; i >= 0; i--) { + Assert.assertEquals(ref.prevSetBit(i), got.prevSetBit(i)); + Assert.assertEquals(ref.nextSetBit(i), got.nextSetBit(i)); + Assert.assertEquals(ref.nextClearBit(i), got.nextClearBit(i)); + Assert.assertEquals(ref.prevClearBit(i), got.prevClearBit(i)); + } + Assert.assertEquals(ref.toString(), got.toString()); + } + + @Test + public void testEquals() { + final IEnvironment env = new EnvironmentTrailing(); + final IStateBitSet ref = new S64BitSet(env, 64); + final IStateBitSet sparse = new SparseBitSet(env, 128); + + // S64Bitset.equals is not correct, cannot use Assert.assertEquals() + Assert.assertTrue(sparse.equals(ref)); + ref.set(7, 32); + sparse.set(7, 32); + + Assert.assertTrue(sparse.equals(ref)); + ref.set(278, 317); + sparse.set(278, 317); + + Assert.assertTrue(sparse.equals(ref)); + + env.worldPush(); + // Clear a bit already cleared. No change expected. + sparse.clear(3); + Assert.assertTrue(sparse.equals(ref)); + + // Bit cleared in a new world. No longer equals. + sparse.clear(300); + Assert.assertFalse(sparse.equals(ref)); + + // Restore the event. + env.worldPop(); + Assert.assertTrue(sparse.equals(ref)); + + // Same cardinality, not the same bits. + sparse.clear(300); + sparse.set(1); + Assert.assertFalse(sparse.equals(ref)); + + Assert.assertEquals(sparse, sparse); + Assert.assertFalse(sparse.equals(null)); + } + + @Test + public void testHashCode() { + final IEnvironment env = new EnvironmentTrailing(); + // Different block sizes but same content. + final IStateBitSet sparse1 = new SparseBitSet(env, 128); + final IStateBitSet sparse2 = new SparseBitSet(env, 64); + Assert.assertEquals(sparse1.hashCode(), sparse2.hashCode()); + sparse1.set(1); + sparse1.set(64); + sparse1.set(137); + + sparse2.set(1); + sparse2.set(64); + sparse2.set(137); + Assert.assertEquals(sparse1.hashCode(), sparse2.hashCode()); + sparse2.clear(1); + Assert.assertNotEquals(sparse1.hashCode(), sparse2.hashCode()); + } +} \ No newline at end of file