From c66ace6cb41ccb1d63c3b308f9554a334a2a3505 Mon Sep 17 00:00:00 2001 From: Mark Andreev Date: Sat, 20 Jun 2026 08:43:34 +0100 Subject: [PATCH] Add javadocs & Improve test coverage --- .github/workflows/publish.yml | 2 +- .gitignore | 3 +- build.gradle.kts | 19 +++ gradle.properties | 1 + gradle/libs.versions.toml | 3 + mapsmith-core/build.gradle.kts | 25 ++++ .../name/mrkandreev/mapsmith/LongLongMap.java | 44 +++++++ .../mrkandreev/mapsmith/LongObjectMap.java | 44 +++++++ .../mapsmith/openaddressing/LongHashing.java | 11 ++ .../openaddressing/LongLongMapFactory.java | 29 +++++ .../LongLongOpenAddressMap.java | 32 +++++ .../LongLongOpenAddressingStrategy.java | 13 ++ .../openaddressing/LongObjectMapFactory.java | 29 +++++ .../LongObjectOpenAddressMap.java | 32 +++++ .../LongObjectOpenAddressingStrategy.java | 11 ++ .../openaddressing/MapSpecialization.java | 14 +++ .../strategies/LinearProbingLongLongMap.java | 18 +++ .../LinearProbingLongObjectMap.java | 18 +++ .../strategies/RobinHoodLongLongMap.java | 18 +++ .../strategies/RobinHoodLongObjectMap.java | 18 +++ .../strategies/SwissTableLongLongMap.java | 18 +++ .../strategies/SwissTableLongObjectMap.java | 18 +++ .../mapsmith/range/LongBoundType.java | 3 + .../mapsmith/range/LongLongRangeMap.java | 69 ++++++++++ .../mapsmith/range/LongObjectRangeMap.java | 69 ++++++++++ .../mapsmith/range/TreeLongLongRangeMap.java | 9 +- .../range/TreeLongObjectRangeMap.java | 9 +- .../mapsmith/ranking/LongLongRankingMap.java | 11 ++ .../ranking/OrderStatisticLongLongMap.java | 7 ++ .../strategies/LongLongHashSupportTest.java | 44 +++++++ .../LongLongMapImplementationTest.java | 76 +++++++++++ .../strategies/LongLongMapPropertyTest.java | 109 ++++++++++++++++ .../LongObjectMapImplementationTest.java | 119 ++++++++++++++---- .../range/TreeLongLongRangeMapTest.java | 90 ++++++++++++- .../range/TreeLongObjectRangeMapTest.java | 107 ++++++++++++++-- .../OrderStatisticLongLongMapTest.java | 19 +++ 36 files changed, 1114 insertions(+), 47 deletions(-) create mode 100644 mapsmith-core/src/test/java/name/mrkandreev/mapsmith/openaddressing/strategies/LongLongHashSupportTest.java create mode 100644 mapsmith-core/src/test/java/name/mrkandreev/mapsmith/openaddressing/strategies/LongLongMapPropertyTest.java diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9273b77..0a0566d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -46,4 +46,4 @@ jobs: ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_IN_MEMORY_KEY }} ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_IN_MEMORY_KEY_ID }} ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_IN_MEMORY_KEY_PASSWORD }} - run: ./gradlew :mapsmith-core:check :mapsmith-core:publishAndReleaseToMavenCentral + run: ./gradlew :mapsmith-core:check :mapsmith-core:javadoc :mapsmith-core:publishAndReleaseToMavenCentral diff --git a/.gitignore b/.gitignore index 5c6c6eb..d19a23a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .idea/ .gradle +.jqwik-database build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ @@ -41,4 +42,4 @@ bin/ .vscode/ ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store diff --git a/build.gradle.kts b/build.gradle.kts index 04727b3..a7ede7a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,6 @@ +import net.ltgt.gradle.errorprone.errorprone +import org.gradle.external.javadoc.StandardJavadocDocletOptions + plugins { alias(libs.plugins.spotless) alias(libs.plugins.spotbugs) apply false @@ -51,6 +54,14 @@ subprojects { extensions.configure { java { + // google-java-format only recognizes block tags at the start of a Javadoc line, so + // normalize + // hand-written Javadoc before formatting it. + replaceRegex( + "separate Javadoc block tags", + "(?().configureEach { + options.compilerArgs.add("-Xlint:all") options.compilerArgs.add("-XDaddTypeAnnotationsToSymbol=true") + options.errorprone.disable("ThreadPriorityCheck") + } + + tasks.withType().configureEach { + (options as StandardJavadocDocletOptions).addBooleanOption("Werror", true) + (options as StandardJavadocDocletOptions).addBooleanOption("Xdoclint:all,-missing", true) } tasks.withType().configureEach { diff --git a/gradle.properties b/gradle.properties index 733cb59..da148fa 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,3 @@ org.gradle.jvmargs=-Xmx1g -Dfile.encoding=UTF-8 org.gradle.parallel=true +org.gradle.configuration-cache=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 78e9a3f..c091803 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,8 @@ errorprone = "2.50.0" errorprone-plugin = "5.1.0" jacoco = "0.8.15" jmh = "1.37" +# Keep this version fixed pending an explicit security review of newer jqwik releases. +jqwik = { strictly = "1.9.3" } junit = "6.0.0" ktfmt = "0.61" maven-publish = "0.36.0" @@ -16,6 +18,7 @@ assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" } errorprone-core = { module = "com.google.errorprone:error_prone_core", version.ref = "errorprone" } jmh-core = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" } jmh-generator-annprocess = { module = "org.openjdk.jmh:jmh-generator-annprocess", version.ref = "jmh" } +jqwik = { module = "net.jqwik:jqwik", version.ref = "jqwik" } junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" } junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" } junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } diff --git a/mapsmith-core/build.gradle.kts b/mapsmith-core/build.gradle.kts index 0ca308f..b1e7bb0 100644 --- a/mapsmith-core/build.gradle.kts +++ b/mapsmith-core/build.gradle.kts @@ -1,8 +1,33 @@ +import java.math.BigDecimal +import org.gradle.testing.jacoco.tasks.JacocoCoverageVerification + plugins { `java-library` alias(libs.plugins.maven.publish) } +tasks.named("jacocoTestCoverageVerification") { + dependsOn(tasks.named("test")) + executionData(layout.buildDirectory.file("jacoco/test.exec")) + + violationRules { + rule { + limit { + counter = "LINE" + value = "MISSEDCOUNT" + maximum = BigDecimal.ZERO + } + limit { + counter = "BRANCH" + value = "MISSEDCOUNT" + maximum = BigDecimal.ZERO + } + } + } +} + +tasks.named("check") { dependsOn(tasks.named("jacocoTestCoverageVerification")) } + mavenPublishing { coordinates(project.group.toString(), "mapsmith-core", project.version.toString()) diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/LongLongMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/LongLongMap.java index 71b63fc..943f77a 100644 --- a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/LongLongMap.java +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/LongLongMap.java @@ -1,21 +1,65 @@ package name.mrkandreev.mapsmith; +/** A map from primitive {@code long} keys to primitive {@code long} values. */ public interface LongLongMap { + /** + * Returns the number of entries. + * + * @return entry count + */ int size(); + /** + * Returns whether this map has no entries. + * + * @return whether empty + */ default boolean isEmpty() { return size() == 0; } + /** + * Returns whether {@code key} is present. + * + * @param key key to look up + * @return whether present + */ boolean containsKey(long key); + /** + * Returns the value for {@code key}. + * + * @param key key to look up + * @return mapped value + */ long get(long key); + /** + * Returns the value for {@code key}, or {@code defaultValue}. + * + * @param key key to look up + * @param defaultValue value returned when absent + * @return mapped or default value + */ long getOrDefault(long key, long defaultValue); + /** + * Associates {@code value} with {@code key}. + * + * @param key key to store + * @param value value to store + * @return previous value + */ long put(long key, long value); + /** + * Removes {@code key}. + * + * @param key key to remove + * @return previous value + */ long remove(long key); + /** Removes all entries. */ void clear(); } diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/LongObjectMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/LongObjectMap.java index bd2a98a..64d55b0 100644 --- a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/LongObjectMap.java +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/LongObjectMap.java @@ -1,21 +1,65 @@ package name.mrkandreev.mapsmith; +/** A map from primitive {@code long} keys to object values. */ public interface LongObjectMap { + /** + * Returns the number of entries. + * + * @return entry count + */ int size(); + /** + * Returns whether this map has no entries. + * + * @return whether empty + */ default boolean isEmpty() { return size() == 0; } + /** + * Returns whether {@code key} is present. + * + * @param key key to look up + * @return whether present + */ boolean containsKey(long key); + /** + * Returns the value for {@code key}. + * + * @param key key to look up + * @return mapped value + */ T get(long key); + /** + * Returns the value for {@code key}, or {@code defaultValue}. + * + * @param key key to look up + * @param defaultValue value returned when absent + * @return mapped or default value + */ T getOrDefault(long key, T defaultValue); + /** + * Associates {@code value} with {@code key}. + * + * @param key key to store + * @param value value to store + * @return previous value + */ T put(long key, T value); + /** + * Removes {@code key}. + * + * @param key key to remove + * @return previous value + */ T remove(long key); + /** Removes all entries. */ void clear(); } diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongHashing.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongHashing.java index bebdda4..525da04 100644 --- a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongHashing.java +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongHashing.java @@ -1,6 +1,8 @@ package name.mrkandreev.mapsmith.openaddressing; +/** Hash functions for primitive {@code long} values. */ public enum LongHashing { + /** MurmurHash3 finalizer. */ MURMUR3_FINALIZER { @Override public long hash(long value) { @@ -13,12 +15,14 @@ public long hash(long value) { return result; } }, + /** Fibonacci hashing. */ FIBONACCI { @Override public long hash(long value) { return value * 0x9e3779b97f4a7c15L; } }, + /** Xor-shift hashing. */ XOR_SHIFT { @Override public long hash(long value) { @@ -29,6 +33,7 @@ public long hash(long value) { return result; } }, + /** Identity hashing. */ IDENTITY { @Override public long hash(long value) { @@ -36,5 +41,11 @@ public long hash(long value) { } }; + /** + * Hashes {@code value}. + * + * @param value value to hash + * @return hash value + */ public abstract long hash(long value); } diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongLongMapFactory.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongLongMapFactory.java index a27b5b5..0f6a62d 100644 --- a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongLongMapFactory.java +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongLongMapFactory.java @@ -3,17 +3,38 @@ import java.util.Objects; import name.mrkandreev.mapsmith.LongLongMap; +/** Factory for primitive long-to-long maps. */ public final class LongLongMapFactory { private LongLongMapFactory() {} + /** + * Creates a map using the default hash function. + * + * @param specialization implementation to use + * @return new map + */ public static LongLongMap create(MapSpecialization specialization) { return create(specialization, LongHashing.MURMUR3_FINALIZER); } + /** + * Creates a map using the default hash function. + * + * @param specialization implementation to use + * @param expectedSize expected entry count + * @return new map + */ public static LongLongMap create(MapSpecialization specialization, int expectedSize) { return create(specialization, expectedSize, LongHashing.MURMUR3_FINALIZER); } + /** + * Creates a map. + * + * @param specialization implementation to use + * @param hashing hash function + * @return new map + */ public static LongLongMap create(MapSpecialization specialization, LongHashing hashing) { Objects.requireNonNull(specialization, "specialization must not be null"); Objects.requireNonNull(hashing, "hashing must not be null"); @@ -21,6 +42,14 @@ public static LongLongMap create(MapSpecialization specialization, LongHashing h return new LongLongOpenAddressMap(specialization.strategy(), hashing); } + /** + * Creates a map. + * + * @param specialization implementation to use + * @param expectedSize expected entry count + * @param hashing hash function + * @return new map + */ public static LongLongMap create( MapSpecialization specialization, int expectedSize, LongHashing hashing) { Objects.requireNonNull(specialization, "specialization must not be null"); diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongLongOpenAddressMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongLongOpenAddressMap.java index 0655a86..2d561df 100644 --- a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongLongOpenAddressMap.java +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongLongOpenAddressMap.java @@ -3,31 +3,63 @@ import java.util.Objects; import name.mrkandreev.mapsmith.LongLongMap; +/** A configurable primitive long-to-long open-addressing map. */ public final class LongLongOpenAddressMap implements LongLongMap { + /** Default expected entry count. */ public static final int DEFAULT_EXPECTED_SIZE = 16; private final LongLongMap delegate; + /** Creates a map with the default strategy and capacity. */ public LongLongOpenAddressMap() { this(LongLongOpenAddressingStrategy.LINEAR_PROBING); } + /** + * Creates a map with the default strategy. + * + * @param expectedSize expected entry count + */ public LongLongOpenAddressMap(int expectedSize) { this(LongLongOpenAddressingStrategy.LINEAR_PROBING, expectedSize); } + /** + * Creates a map. + * + * @param strategy strategy to use + */ public LongLongOpenAddressMap(LongLongOpenAddressingStrategy strategy) { this(strategy, DEFAULT_EXPECTED_SIZE); } + /** + * Creates a map. + * + * @param strategy strategy to use + * @param expectedSize expected entry count + */ public LongLongOpenAddressMap(LongLongOpenAddressingStrategy strategy, int expectedSize) { this(strategy, expectedSize, LongHashing.MURMUR3_FINALIZER); } + /** + * Creates a map. + * + * @param strategy strategy to use + * @param hashing hash function + */ public LongLongOpenAddressMap(LongLongOpenAddressingStrategy strategy, LongHashing hashing) { this(strategy, DEFAULT_EXPECTED_SIZE, hashing); } + /** + * Creates a map. + * + * @param strategy strategy to use + * @param expectedSize expected entry count + * @param hashing hash function + */ public LongLongOpenAddressMap( LongLongOpenAddressingStrategy strategy, int expectedSize, LongHashing hashing) { Objects.requireNonNull(strategy, "strategy must not be null"); diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongLongOpenAddressingStrategy.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongLongOpenAddressingStrategy.java index 5b0337a..36870d4 100644 --- a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongLongOpenAddressingStrategy.java +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongLongOpenAddressingStrategy.java @@ -5,11 +5,24 @@ import name.mrkandreev.mapsmith.openaddressing.strategies.RobinHoodLongLongMap; import name.mrkandreev.mapsmith.openaddressing.strategies.SwissTableLongLongMap; +/** Creates primitive long-to-long open-addressing maps. */ @FunctionalInterface public interface LongLongOpenAddressingStrategy { + /** Linear-probing strategy. */ LongLongOpenAddressingStrategy LINEAR_PROBING = LinearProbingLongLongMap::new; + + /** Robin Hood hashing strategy. */ LongLongOpenAddressingStrategy ROBIN_HOOD = RobinHoodLongLongMap::new; + + /** Swiss-table strategy. */ LongLongOpenAddressingStrategy SWISS_TABLE = SwissTableLongLongMap::new; + /** + * Creates a map. + * + * @param expectedSize expected entry count + * @param hashing hash function + * @return new map + */ LongLongMap create(int expectedSize, LongHashing hashing); } diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongObjectMapFactory.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongObjectMapFactory.java index 99150c7..b050697 100644 --- a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongObjectMapFactory.java +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongObjectMapFactory.java @@ -3,17 +3,38 @@ import java.util.Objects; import name.mrkandreev.mapsmith.LongObjectMap; +/** Factory for primitive long-to-object maps. */ public final class LongObjectMapFactory { private LongObjectMapFactory() {} + /** + * Creates a map using the default hash function. + * + * @param specialization implementation to use + * @return new map + */ public static LongObjectMap create(MapSpecialization specialization) { return create(specialization, LongHashing.MURMUR3_FINALIZER); } + /** + * Creates a map using the default hash function. + * + * @param specialization implementation to use + * @param expectedSize expected entry count + * @return new map + */ public static LongObjectMap create(MapSpecialization specialization, int expectedSize) { return create(specialization, expectedSize, LongHashing.MURMUR3_FINALIZER); } + /** + * Creates a map. + * + * @param specialization implementation to use + * @param hashing hash function + * @return new map + */ public static LongObjectMap create(MapSpecialization specialization, LongHashing hashing) { Objects.requireNonNull(specialization, "specialization must not be null"); Objects.requireNonNull(hashing, "hashing must not be null"); @@ -21,6 +42,14 @@ public static LongObjectMap create(MapSpecialization specialization, Long return new LongObjectOpenAddressMap<>(specialization.objectStrategy(), hashing); } + /** + * Creates a map. + * + * @param specialization implementation to use + * @param expectedSize expected entry count + * @param hashing hash function + * @return new map + */ public static LongObjectMap create( MapSpecialization specialization, int expectedSize, LongHashing hashing) { Objects.requireNonNull(specialization, "specialization must not be null"); diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongObjectOpenAddressMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongObjectOpenAddressMap.java index b008e02..94b8dfe 100644 --- a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongObjectOpenAddressMap.java +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongObjectOpenAddressMap.java @@ -3,32 +3,64 @@ import java.util.Objects; import name.mrkandreev.mapsmith.LongObjectMap; +/** A configurable primitive long-to-object open-addressing map. */ public final class LongObjectOpenAddressMap implements LongObjectMap { + /** Default expected entry count. */ public static final int DEFAULT_EXPECTED_SIZE = 16; private final LongObjectMap delegate; + /** Creates a map with the default strategy and capacity. */ public LongObjectOpenAddressMap() { this(defaultStrategy()); } + /** + * Creates a map with the default strategy. + * + * @param expectedSize expected entry count + */ public LongObjectOpenAddressMap(int expectedSize) { this(defaultStrategy(), expectedSize); } + /** + * Creates a map. + * + * @param strategy strategy to use + */ public LongObjectOpenAddressMap(LongObjectOpenAddressingStrategy strategy) { this(strategy, DEFAULT_EXPECTED_SIZE); } + /** + * Creates a map. + * + * @param strategy strategy to use + * @param expectedSize expected entry count + */ public LongObjectOpenAddressMap(LongObjectOpenAddressingStrategy strategy, int expectedSize) { this(strategy, expectedSize, LongHashing.MURMUR3_FINALIZER); } + /** + * Creates a map. + * + * @param strategy strategy to use + * @param hashing hash function + */ public LongObjectOpenAddressMap( LongObjectOpenAddressingStrategy strategy, LongHashing hashing) { this(strategy, DEFAULT_EXPECTED_SIZE, hashing); } + /** + * Creates a map. + * + * @param strategy strategy to use + * @param expectedSize expected entry count + * @param hashing hash function + */ public LongObjectOpenAddressMap( LongObjectOpenAddressingStrategy strategy, int expectedSize, LongHashing hashing) { Objects.requireNonNull(strategy, "strategy must not be null"); diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongObjectOpenAddressingStrategy.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongObjectOpenAddressingStrategy.java index 1bc985f..ce15390 100644 --- a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongObjectOpenAddressingStrategy.java +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/LongObjectOpenAddressingStrategy.java @@ -5,16 +5,27 @@ import name.mrkandreev.mapsmith.openaddressing.strategies.RobinHoodLongObjectMap; import name.mrkandreev.mapsmith.openaddressing.strategies.SwissTableLongObjectMap; +/** Creates primitive long-to-object open-addressing maps. */ @FunctionalInterface public interface LongObjectOpenAddressingStrategy { + /** Linear-probing strategy. */ @SuppressWarnings("rawtypes") LongObjectOpenAddressingStrategy LINEAR_PROBING = LinearProbingLongObjectMap::new; + /** Robin Hood hashing strategy. */ @SuppressWarnings("rawtypes") LongObjectOpenAddressingStrategy ROBIN_HOOD = RobinHoodLongObjectMap::new; + /** Swiss-table strategy. */ @SuppressWarnings("rawtypes") LongObjectOpenAddressingStrategy SWISS_TABLE = SwissTableLongObjectMap::new; + /** + * Creates a map. + * + * @param expectedSize expected entry count + * @param hashing hash function + * @return new map + */ LongObjectMap create(int expectedSize, LongHashing hashing); } diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/MapSpecialization.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/MapSpecialization.java index 5edd385..d3e318b 100644 --- a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/MapSpecialization.java +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/MapSpecialization.java @@ -1,6 +1,8 @@ package name.mrkandreev.mapsmith.openaddressing; +/** Supported open-addressing implementations. */ public enum MapSpecialization { + /** Linear probing. */ LINEAR_PROBING { @Override public LongLongOpenAddressingStrategy strategy() { @@ -12,6 +14,7 @@ public LongObjectOpenAddressingStrategy objectStrategy() { return castObjectStrategy(LongObjectOpenAddressingStrategy.LINEAR_PROBING); } }, + /** Robin Hood hashing. */ ROBIN_HOOD { @Override public LongLongOpenAddressingStrategy strategy() { @@ -23,6 +26,7 @@ public LongObjectOpenAddressingStrategy objectStrategy() { return castObjectStrategy(LongObjectOpenAddressingStrategy.ROBIN_HOOD); } }, + /** Swiss table hashing. */ SWISS_TABLE { @Override public LongLongOpenAddressingStrategy strategy() { @@ -35,8 +39,18 @@ public LongObjectOpenAddressingStrategy objectStrategy() { } }; + /** + * Returns the primitive map strategy. + * + * @return primitive map strategy + */ public abstract LongLongOpenAddressingStrategy strategy(); + /** + * Returns the object map strategy. + * + * @return object map strategy + */ public abstract LongObjectOpenAddressingStrategy objectStrategy(); @SuppressWarnings("unchecked") diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/LinearProbingLongLongMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/LinearProbingLongLongMap.java index 66b5300..792535e 100644 --- a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/LinearProbingLongLongMap.java +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/LinearProbingLongLongMap.java @@ -5,6 +5,7 @@ import name.mrkandreev.mapsmith.LongLongMap; import name.mrkandreev.mapsmith.openaddressing.LongHashing; +/** A primitive long-to-long map using linear probing. */ public final class LinearProbingLongLongMap implements LongLongMap { private static final byte EMPTY = 0; private static final byte OCCUPIED = 1; @@ -19,18 +20,35 @@ public final class LinearProbingLongLongMap implements LongLongMap { private int used; private final LongHashing hashing; + /** Creates a map with default capacity and hashing. */ public LinearProbingLongLongMap() { this(LongLongHashSupport.DEFAULT_EXPECTED_SIZE); } + /** + * Creates a map. + * + * @param expectedSize expected entry count + */ public LinearProbingLongLongMap(int expectedSize) { this(expectedSize, LongHashing.MURMUR3_FINALIZER); } + /** + * Creates a map. + * + * @param hashing hash function + */ public LinearProbingLongLongMap(LongHashing hashing) { this(LongLongHashSupport.DEFAULT_EXPECTED_SIZE, hashing); } + /** + * Creates a map. + * + * @param expectedSize expected entry count + * @param hashing hash function + */ public LinearProbingLongLongMap(int expectedSize, LongHashing hashing) { this.hashing = Objects.requireNonNull(hashing, "hashing must not be null"); allocate(LongLongHashSupport.capacityFor(expectedSize)); diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/LinearProbingLongObjectMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/LinearProbingLongObjectMap.java index 9d58652..36c20b0 100644 --- a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/LinearProbingLongObjectMap.java +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/LinearProbingLongObjectMap.java @@ -5,6 +5,7 @@ import name.mrkandreev.mapsmith.LongObjectMap; import name.mrkandreev.mapsmith.openaddressing.LongHashing; +/** A primitive long-to-object map using linear probing. */ public final class LinearProbingLongObjectMap implements LongObjectMap { private static final byte EMPTY = 0; private static final byte OCCUPIED = 1; @@ -19,18 +20,35 @@ public final class LinearProbingLongObjectMap implements LongObjectMap { private int used; private final LongHashing hashing; + /** Creates a map with default capacity and hashing. */ public LinearProbingLongObjectMap() { this(LongLongHashSupport.DEFAULT_EXPECTED_SIZE); } + /** + * Creates a map. + * + * @param expectedSize expected entry count + */ public LinearProbingLongObjectMap(int expectedSize) { this(expectedSize, LongHashing.MURMUR3_FINALIZER); } + /** + * Creates a map. + * + * @param hashing hash function + */ public LinearProbingLongObjectMap(LongHashing hashing) { this(LongLongHashSupport.DEFAULT_EXPECTED_SIZE, hashing); } + /** + * Creates a map. + * + * @param expectedSize expected entry count + * @param hashing hash function + */ public LinearProbingLongObjectMap(int expectedSize, LongHashing hashing) { this.hashing = Objects.requireNonNull(hashing, "hashing must not be null"); allocate(LongLongHashSupport.capacityFor(expectedSize)); diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/RobinHoodLongLongMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/RobinHoodLongLongMap.java index 7e311cf..9f7be14 100644 --- a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/RobinHoodLongLongMap.java +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/RobinHoodLongLongMap.java @@ -5,6 +5,7 @@ import name.mrkandreev.mapsmith.LongLongMap; import name.mrkandreev.mapsmith.openaddressing.LongHashing; +/** A primitive long-to-long map using Robin Hood hashing. */ public final class RobinHoodLongLongMap implements LongLongMap { private static final byte EMPTY = 0; private static final byte OCCUPIED = 1; @@ -17,18 +18,35 @@ public final class RobinHoodLongLongMap implements LongLongMap { private int entryCount; private final LongHashing hashing; + /** Creates a map with default capacity and hashing. */ public RobinHoodLongLongMap() { this(LongLongHashSupport.DEFAULT_EXPECTED_SIZE); } + /** + * Creates a map. + * + * @param expectedSize expected entry count + */ public RobinHoodLongLongMap(int expectedSize) { this(expectedSize, LongHashing.MURMUR3_FINALIZER); } + /** + * Creates a map. + * + * @param hashing hash function + */ public RobinHoodLongLongMap(LongHashing hashing) { this(LongLongHashSupport.DEFAULT_EXPECTED_SIZE, hashing); } + /** + * Creates a map. + * + * @param expectedSize expected entry count + * @param hashing hash function + */ public RobinHoodLongLongMap(int expectedSize, LongHashing hashing) { this.hashing = Objects.requireNonNull(hashing, "hashing must not be null"); allocate(LongLongHashSupport.capacityFor(expectedSize)); diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/RobinHoodLongObjectMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/RobinHoodLongObjectMap.java index c638161..eb2bbb8 100644 --- a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/RobinHoodLongObjectMap.java +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/RobinHoodLongObjectMap.java @@ -5,6 +5,7 @@ import name.mrkandreev.mapsmith.LongObjectMap; import name.mrkandreev.mapsmith.openaddressing.LongHashing; +/** A primitive long-to-object map using Robin Hood hashing. */ public final class RobinHoodLongObjectMap implements LongObjectMap { private static final byte EMPTY = 0; private static final byte OCCUPIED = 1; @@ -17,18 +18,35 @@ public final class RobinHoodLongObjectMap implements LongObjectMap { private int entryCount; private final LongHashing hashing; + /** Creates a map with default capacity and hashing. */ public RobinHoodLongObjectMap() { this(LongLongHashSupport.DEFAULT_EXPECTED_SIZE); } + /** + * Creates a map. + * + * @param expectedSize expected entry count + */ public RobinHoodLongObjectMap(int expectedSize) { this(expectedSize, LongHashing.MURMUR3_FINALIZER); } + /** + * Creates a map. + * + * @param hashing hash function + */ public RobinHoodLongObjectMap(LongHashing hashing) { this(LongLongHashSupport.DEFAULT_EXPECTED_SIZE, hashing); } + /** + * Creates a map. + * + * @param expectedSize expected entry count + * @param hashing hash function + */ public RobinHoodLongObjectMap(int expectedSize, LongHashing hashing) { this.hashing = Objects.requireNonNull(hashing, "hashing must not be null"); allocate(LongLongHashSupport.capacityFor(expectedSize)); diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/SwissTableLongLongMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/SwissTableLongLongMap.java index 0a9191a..b68388b 100644 --- a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/SwissTableLongLongMap.java +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/SwissTableLongLongMap.java @@ -5,6 +5,7 @@ import name.mrkandreev.mapsmith.LongLongMap; import name.mrkandreev.mapsmith.openaddressing.LongHashing; +/** A primitive long-to-long map using Swiss-table hashing. */ public final class SwissTableLongLongMap implements LongLongMap { private static final byte EMPTY = 0; private static final byte DELETED = 1; @@ -18,18 +19,35 @@ public final class SwissTableLongLongMap implements LongLongMap { private int used; private final LongHashing hashing; + /** Creates a map with default capacity and hashing. */ public SwissTableLongLongMap() { this(LongLongHashSupport.DEFAULT_EXPECTED_SIZE); } + /** + * Creates a map. + * + * @param expectedSize expected entry count + */ public SwissTableLongLongMap(int expectedSize) { this(expectedSize, LongHashing.MURMUR3_FINALIZER); } + /** + * Creates a map. + * + * @param hashing hash function + */ public SwissTableLongLongMap(LongHashing hashing) { this(LongLongHashSupport.DEFAULT_EXPECTED_SIZE, hashing); } + /** + * Creates a map. + * + * @param expectedSize expected entry count + * @param hashing hash function + */ public SwissTableLongLongMap(int expectedSize, LongHashing hashing) { this.hashing = Objects.requireNonNull(hashing, "hashing must not be null"); allocate(LongLongHashSupport.capacityFor(expectedSize)); diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/SwissTableLongObjectMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/SwissTableLongObjectMap.java index 98d9f21..2c4532f 100644 --- a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/SwissTableLongObjectMap.java +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/openaddressing/strategies/SwissTableLongObjectMap.java @@ -5,6 +5,7 @@ import name.mrkandreev.mapsmith.LongObjectMap; import name.mrkandreev.mapsmith.openaddressing.LongHashing; +/** A primitive long-to-object map using Swiss-table hashing. */ public final class SwissTableLongObjectMap implements LongObjectMap { private static final byte EMPTY = 0; private static final byte DELETED = 1; @@ -18,18 +19,35 @@ public final class SwissTableLongObjectMap implements LongObjectMap { private int used; private final LongHashing hashing; + /** Creates a map with default capacity and hashing. */ public SwissTableLongObjectMap() { this(LongLongHashSupport.DEFAULT_EXPECTED_SIZE); } + /** + * Creates a map. + * + * @param expectedSize expected entry count + */ public SwissTableLongObjectMap(int expectedSize) { this(expectedSize, LongHashing.MURMUR3_FINALIZER); } + /** + * Creates a map. + * + * @param hashing hash function + */ public SwissTableLongObjectMap(LongHashing hashing) { this(LongLongHashSupport.DEFAULT_EXPECTED_SIZE, hashing); } + /** + * Creates a map. + * + * @param expectedSize expected entry count + * @param hashing hash function + */ public SwissTableLongObjectMap(int expectedSize, LongHashing hashing) { this.hashing = Objects.requireNonNull(hashing, "hashing must not be null"); allocate(LongLongHashSupport.capacityFor(expectedSize)); diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/LongBoundType.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/LongBoundType.java index 9c8447f..f83d207 100644 --- a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/LongBoundType.java +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/LongBoundType.java @@ -1,6 +1,9 @@ package name.mrkandreev.mapsmith.range; +/** Whether a range bound includes its endpoint. */ public enum LongBoundType { + /** Excludes the endpoint. */ OPEN, + /** Includes the endpoint. */ CLOSED } diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/LongLongRangeMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/LongLongRangeMap.java index 2b13728..81a367e 100644 --- a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/LongLongRangeMap.java +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/LongLongRangeMap.java @@ -1,30 +1,99 @@ package name.mrkandreev.mapsmith.range; +/** A map from inclusive {@code long} ranges to primitive {@code long} values. */ public interface LongLongRangeMap { + /** Returns the number of ranges. */ int size(); + /** Returns whether this map has no ranges. */ default boolean isEmpty() { return size() == 0; } + /** + * Returns whether {@code key} is covered. + * + * @param key key to look up + * @return whether covered + */ boolean containsKey(long key); + /** + * Returns the value covering {@code key}. + * + * @param key key to look up + * @return mapped value + */ long get(long key); + /** + * Returns the value covering {@code key}, or {@code defaultValue}. + * + * @param key key to look up + * @param defaultValue value returned when absent + * @return mapped or default value + */ long getOrDefault(long key, long defaultValue); + /** + * Associates {@code value} with an inclusive range. + * + * @param fromInclusive lower bound + * @param toInclusive upper bound + * @param value value to store + */ void put(long fromInclusive, long toInclusive, long value); + /** + * Associates {@code value} with a bounded range. + * + * @param lower lower bound + * @param lowerType lower-bound type + * @param upper upper bound + * @param upperType upper-bound type + * @param value value to store + */ void put(long lower, LongBoundType lowerType, long upper, LongBoundType upperType, long value); + /** + * Associates {@code value} with an inclusive range and coalesces adjacent ranges. + * + * @param fromInclusive lower bound + * @param toInclusive upper bound + * @param value value to store + */ void putCoalescing(long fromInclusive, long toInclusive, long value); + /** + * Associates {@code value} with a bounded range and coalesces adjacent ranges. + * + * @param lower lower bound + * @param lowerType lower-bound type + * @param upper upper bound + * @param upperType upper-bound type + * @param value value to store + */ void putCoalescing( long lower, LongBoundType lowerType, long upper, LongBoundType upperType, long value); + /** + * Removes an inclusive range. + * + * @param fromInclusive lower bound + * @param toInclusive upper bound + */ void remove(long fromInclusive, long toInclusive); + /** + * Removes a bounded range. + * + * @param lower lower bound + * @param lowerType lower-bound type + * @param upper upper bound + * @param upperType upper-bound type + */ void remove(long lower, LongBoundType lowerType, long upper, LongBoundType upperType); + /** Removes all ranges. */ void clear(); } diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/LongObjectRangeMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/LongObjectRangeMap.java index cb8cbe6..6a40492 100644 --- a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/LongObjectRangeMap.java +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/LongObjectRangeMap.java @@ -1,30 +1,99 @@ package name.mrkandreev.mapsmith.range; +/** A map from inclusive {@code long} ranges to object values. */ public interface LongObjectRangeMap { + /** Returns the number of ranges. */ int size(); + /** Returns whether this map has no ranges. */ default boolean isEmpty() { return size() == 0; } + /** + * Returns whether {@code key} is covered. + * + * @param key key to look up + * @return whether covered + */ boolean containsKey(long key); + /** + * Returns the value covering {@code key}. + * + * @param key key to look up + * @return mapped value + */ T get(long key); + /** + * Returns the value covering {@code key}, or {@code defaultValue}. + * + * @param key key to look up + * @param defaultValue value returned when absent + * @return mapped or default value + */ T getOrDefault(long key, T defaultValue); + /** + * Associates {@code value} with an inclusive range. + * + * @param fromInclusive lower bound + * @param toInclusive upper bound + * @param value value to store + */ void put(long fromInclusive, long toInclusive, T value); + /** + * Associates {@code value} with a bounded range. + * + * @param lower lower bound + * @param lowerType lower-bound type + * @param upper upper bound + * @param upperType upper-bound type + * @param value value to store + */ void put(long lower, LongBoundType lowerType, long upper, LongBoundType upperType, T value); + /** + * Associates {@code value} with an inclusive range and coalesces adjacent ranges. + * + * @param fromInclusive lower bound + * @param toInclusive upper bound + * @param value value to store + */ void putCoalescing(long fromInclusive, long toInclusive, T value); + /** + * Associates {@code value} with a bounded range and coalesces adjacent ranges. + * + * @param lower lower bound + * @param lowerType lower-bound type + * @param upper upper bound + * @param upperType upper-bound type + * @param value value to store + */ void putCoalescing( long lower, LongBoundType lowerType, long upper, LongBoundType upperType, T value); + /** + * Removes an inclusive range. + * + * @param fromInclusive lower bound + * @param toInclusive upper bound + */ void remove(long fromInclusive, long toInclusive); + /** + * Removes a bounded range. + * + * @param lower lower bound + * @param lowerType lower-bound type + * @param upper upper bound + * @param upperType upper-bound type + */ void remove(long lower, LongBoundType lowerType, long upper, LongBoundType upperType); + /** Removes all ranges. */ void clear(); } diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/TreeLongLongRangeMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/TreeLongLongRangeMap.java index a589b44..b0d7671 100644 --- a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/TreeLongLongRangeMap.java +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/TreeLongLongRangeMap.java @@ -4,6 +4,7 @@ import java.util.Objects; import java.util.TreeMap; +/** A tree-backed primitive long-to-long range map. */ public final class TreeLongLongRangeMap implements LongLongRangeMap { private final TreeMap ranges = new TreeMap<>(); @@ -185,7 +186,7 @@ private Range mergePrevious(Range range) { } Range previous = previousEntry.getValue(); - if (previous.value != range.value || !touches(previous.to, range.from)) { + if (previous.value != range.value || areSeparated(previous.to, range.from)) { return range; } @@ -198,7 +199,7 @@ private Range mergeNext(Range range) { Map.Entry nextEntry = ranges.ceilingEntry(result.from); while (nextEntry != null) { Range next = nextEntry.getValue(); - if (next.value != result.value || !touches(result.to, next.from)) { + if (next.value != result.value || areSeparated(result.to, next.from)) { return result; } @@ -209,8 +210,8 @@ private Range mergeNext(Range range) { return result; } - private static boolean touches(long leftTo, long rightFrom) { - return leftTo == Long.MAX_VALUE || leftTo + 1L >= rightFrom; + private static boolean areSeparated(long leftTo, long rightFrom) { + return leftTo < rightFrom - 1L; } private static void validateTypedRange( diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/TreeLongObjectRangeMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/TreeLongObjectRangeMap.java index d066159..6838b5c 100644 --- a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/TreeLongObjectRangeMap.java +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/range/TreeLongObjectRangeMap.java @@ -4,6 +4,7 @@ import java.util.Objects; import java.util.TreeMap; +/** A tree-backed primitive long-to-object range map. */ public final class TreeLongObjectRangeMap implements LongObjectRangeMap { private final TreeMap> ranges = new TreeMap<>(); @@ -185,7 +186,7 @@ private Range mergePrevious(Range range) { } Range previous = previousEntry.getValue(); - if (!Objects.equals(previous.value, range.value) || !touches(previous.to, range.from)) { + if (!Objects.equals(previous.value, range.value) || areSeparated(previous.to, range.from)) { return range; } @@ -198,7 +199,7 @@ private Range mergeNext(Range range) { Map.Entry> nextEntry = ranges.ceilingEntry(result.from); while (nextEntry != null) { Range next = nextEntry.getValue(); - if (!Objects.equals(next.value, result.value) || !touches(result.to, next.from)) { + if (!Objects.equals(next.value, result.value) || areSeparated(result.to, next.from)) { return result; } @@ -209,8 +210,8 @@ private Range mergeNext(Range range) { return result; } - private static boolean touches(long leftTo, long rightFrom) { - return leftTo == Long.MAX_VALUE || leftTo + 1L >= rightFrom; + private static boolean areSeparated(long leftTo, long rightFrom) { + return leftTo < rightFrom - 1L; } private static void validateTypedRange( diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/ranking/LongLongRankingMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/ranking/LongLongRankingMap.java index aff81c6..2484ef2 100644 --- a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/ranking/LongLongRankingMap.java +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/ranking/LongLongRankingMap.java @@ -2,24 +2,35 @@ import name.mrkandreev.mapsmith.LongLongMap; +/** A long-to-long map that exposes the rank of each entry. */ public interface LongLongRankingMap extends LongLongMap { + /** Rank returned for absent keys. */ int MISSING_RANK = -1; /** * Returns a one-based rank for {@code key}. Higher values are ranked first. Keys with equal * values are ranked by key in ascending order. + * + * @param key key to rank + * @return one-based rank, or {@link #MISSING_RANK} when absent */ int rankOf(long key); /** * Returns how many entries are ranked before {@code key}, or {@link #MISSING_RANK} when the key * is not present. + * + * @param key key to inspect + * @return number of preceding entries, or {@link #MISSING_RANK} when absent */ int countBefore(long key); /** * Returns how many entries are ranked after {@code key}, or {@link #MISSING_RANK} when the key is * not present. + * + * @param key key to inspect + * @return number of following entries, or {@link #MISSING_RANK} when absent */ int countAfter(long key); } diff --git a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/ranking/OrderStatisticLongLongMap.java b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/ranking/OrderStatisticLongLongMap.java index b8d3a11..dcccbc1 100644 --- a/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/ranking/OrderStatisticLongLongMap.java +++ b/mapsmith-core/src/main/java/name/mrkandreev/mapsmith/ranking/OrderStatisticLongLongMap.java @@ -3,16 +3,23 @@ import name.mrkandreev.mapsmith.LongLongMap; import name.mrkandreev.mapsmith.openaddressing.LongLongOpenAddressMap; +/** A primitive map that ranks entries by value. */ public final class OrderStatisticLongLongMap implements LongLongRankingMap { private static final int MAX_BALANCE_DELTA = 1; private final LongLongMap scoreByKey; private Node root; + /** Creates a map with default capacity. */ public OrderStatisticLongLongMap() { this(LongLongOpenAddressMap.DEFAULT_EXPECTED_SIZE); } + /** + * Creates a map. + * + * @param expectedSize expected entry count + */ public OrderStatisticLongLongMap(int expectedSize) { scoreByKey = new LongLongOpenAddressMap(expectedSize); } diff --git a/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/openaddressing/strategies/LongLongHashSupportTest.java b/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/openaddressing/strategies/LongLongHashSupportTest.java new file mode 100644 index 0000000..6c80af4 --- /dev/null +++ b/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/openaddressing/strategies/LongLongHashSupportTest.java @@ -0,0 +1,44 @@ +package name.mrkandreev.mapsmith.openaddressing.strategies; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class LongLongHashSupportTest { + @Test + void returnsMinimumCapacityForZeroExpectedSize() { + assertThat(LongLongHashSupport.capacityFor(0)).isEqualTo(2); + } + + @Test + void returnsMinimumCapacityForOneExpectedEntry() { + assertThat(LongLongHashSupport.capacityFor(1)).isEqualTo(2); + } + + @Test + void rejectsNegativeExpectedSize() { + assertThatThrownBy(() -> LongLongHashSupport.capacityFor(-1)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("expectedSize must be non-negative"); + } + + @Test + void rejectsExpectedSizeAboveMaximumCapacity() { + assertThatThrownBy(() -> LongLongHashSupport.capacityFor(Integer.MAX_VALUE)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("expectedSize is too large"); + } + + @Test + void doublesCapacityBelowMaximum() { + assertThat(LongLongHashSupport.nextCapacity(2)).isEqualTo(4); + } + + @Test + void rejectsCapacityGrowthAtMaximum() { + assertThatThrownBy(() -> LongLongHashSupport.nextCapacity(1 << 30)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("maximum capacity reached"); + } +} diff --git a/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/openaddressing/strategies/LongLongMapImplementationTest.java b/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/openaddressing/strategies/LongLongMapImplementationTest.java index cc03542..654ca65 100644 --- a/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/openaddressing/strategies/LongLongMapImplementationTest.java +++ b/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/openaddressing/strategies/LongLongMapImplementationTest.java @@ -12,13 +12,17 @@ import name.mrkandreev.mapsmith.LongLongMap; import name.mrkandreev.mapsmith.openaddressing.LongHashing; import name.mrkandreev.mapsmith.openaddressing.LongLongOpenAddressMap; +import name.mrkandreev.mapsmith.openaddressing.LongLongOpenAddressingStrategy; import name.mrkandreev.mapsmith.ranking.OrderStatisticLongLongMap; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; class LongLongMapImplementationTest { private static final String MAP_IMPLEMENTATIONS = "mapImplementations"; + private static final String HASHING_METHOD_SOURCE = "hashings"; @ParameterizedTest @MethodSource(MAP_IMPLEMENTATIONS) @@ -165,6 +169,74 @@ void clearsValues(String name, IntFunction maps) { assertThat(map.containsKey(2L)).isFalse(); } + @ParameterizedTest + @MethodSource(MAP_IMPLEMENTATIONS) + void supportsZeroExpectedSize(String name, IntFunction maps) { + LongLongMap map = maps.apply(0); + + assertThat(map.put(1L, 1L)).as(name).isZero(); + } + + @Nested + class LinearProbingConstructors { + @Test + void createsMapWithDefaultExpectedSize() { + assertThat(new LinearProbingLongLongMap()).isNotNull(); + } + + @Test + void createsMapWithSpecifiedExpectedSize() { + assertThat(new LinearProbingLongLongMap(1)).isNotNull(); + } + + @ParameterizedTest + @MethodSource( + "name.mrkandreev.mapsmith.openaddressing.strategies.LongLongMapImplementationTest#hashings") + void createsMapWithSpecifiedHashing(LongHashing hashing) { + assertThat(new LinearProbingLongLongMap(hashing)).isNotNull(); + } + } + + @Nested + class RobinHoodConstructors { + @Test + void createsMapWithDefaultExpectedSize() { + assertThat(new RobinHoodLongLongMap()).isNotNull(); + } + + @Test + void createsMapWithSpecifiedExpectedSize() { + assertThat(new RobinHoodLongLongMap(1)).isNotNull(); + } + + @ParameterizedTest + @MethodSource( + "name.mrkandreev.mapsmith.openaddressing.strategies.LongLongMapImplementationTest#hashings") + void createsMapWithSpecifiedHashing(LongHashing hashing) { + assertThat(new RobinHoodLongLongMap(hashing)).isNotNull(); + } + } + + @ParameterizedTest + @MethodSource(HASHING_METHOD_SOURCE) + void supportsSwissTableConstructors(LongHashing hashing) { + assertThat(new SwissTableLongLongMap()).isNotNull(); + assertThat(new SwissTableLongLongMap(1)).isNotNull(); + assertThat(new SwissTableLongLongMap(hashing)).isNotNull(); + } + + @ParameterizedTest + @MethodSource(HASHING_METHOD_SOURCE) + void supportsOpenAddressMapConstructors(LongHashing hashing) { + assertThat(new LongLongOpenAddressMap()).isNotNull(); + assertThat(new LongLongOpenAddressMap(1)).isNotNull(); + assertThat(new LongLongOpenAddressMap(LongLongOpenAddressingStrategy.SWISS_TABLE)).isNotNull(); + assertThat(new LongLongOpenAddressMap(LongLongOpenAddressingStrategy.SWISS_TABLE, 1)) + .isNotNull(); + assertThat(new LongLongOpenAddressMap(LongLongOpenAddressingStrategy.SWISS_TABLE, hashing)) + .isNotNull(); + } + @ParameterizedTest @MethodSource(MAP_IMPLEMENTATIONS) void matchesHashMapForRandomOperations(String name, IntFunction maps) { @@ -240,4 +312,8 @@ private static Stream mapImplementations() { "order statistic " + hashing, (IntFunction) OrderStatisticLongLongMap::new))); } + + private static Stream hashings() { + return Arrays.stream(LongHashing.values()); + } } diff --git a/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/openaddressing/strategies/LongLongMapPropertyTest.java b/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/openaddressing/strategies/LongLongMapPropertyTest.java new file mode 100644 index 0000000..39ad9a4 --- /dev/null +++ b/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/openaddressing/strategies/LongLongMapPropertyTest.java @@ -0,0 +1,109 @@ +package name.mrkandreev.mapsmith.openaddressing.strategies; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.IntFunction; +import java.util.stream.Stream; +import name.mrkandreev.mapsmith.LongLongMap; +import name.mrkandreev.mapsmith.openaddressing.LongHashing; +import name.mrkandreev.mapsmith.openaddressing.LongLongOpenAddressMap; +import name.mrkandreev.mapsmith.ranking.OrderStatisticLongLongMap; +import net.jqwik.api.Arbitraries; +import net.jqwik.api.Arbitrary; +import net.jqwik.api.Combinators; +import net.jqwik.api.ForAll; +import net.jqwik.api.Property; +import net.jqwik.api.Provide; + +@SuppressWarnings("PMD.TestClassWithoutTestCases") // jqwik properties are not recognized by PMD. +class LongLongMapPropertyTest { + @Property + void mapsMatchHashMapForGeneratedOperationSequences( + @ForAll("operations") List operations) { + for (MapImplementation implementation : mapImplementations().toList()) { + LongLongMap actual = implementation.factory().apply(1); + Map expected = new HashMap<>(); + + for (Operation operation : operations) { + if (operation instanceof Put put) { + Long previous = expected.put(put.key(), put.value()); + assertThat(actual.put(put.key(), put.value())) + .as(implementation.name()) + .isEqualTo(previous == null ? 0L : previous); + } else if (operation instanceof Remove remove) { + Long previous = expected.remove(remove.key()); + assertThat(actual.remove(remove.key())) + .as(implementation.name()) + .isEqualTo(previous == null ? 0L : previous); + } else { + Get get = (Get) operation; + assertThat(actual.containsKey(get.key())) + .as(implementation.name()) + .isEqualTo(expected.containsKey(get.key())); + assertThat(actual.getOrDefault(get.key(), Long.MIN_VALUE)) + .as(implementation.name()) + .isEqualTo(expected.getOrDefault(get.key(), Long.MIN_VALUE)); + } + + assertThat(actual.size()).as(implementation.name()).isEqualTo(expected.size()); + } + } + } + + @Provide + Arbitrary> operations() { + Arbitrary keys = Arbitraries.longs().between(-100L, 100L); + Arbitrary values = Arbitraries.longs(); + Arbitrary puts = Combinators.combine(keys, values).as(Put::new); + Arbitrary removes = keys.map(Remove::new); + Arbitrary gets = keys.map(Get::new); + + return Arbitraries.oneOf(puts, removes, gets).list().ofMinSize(1).ofMaxSize(200); + } + + private static Stream mapImplementations() { + return Stream.of(LongHashing.values()) + .flatMap( + hashing -> + Stream.of( + new MapImplementation( + "linear probing " + hashing, + expectedSize -> new LinearProbingLongLongMap(expectedSize, hashing)), + new MapImplementation( + "robin hood " + hashing, + expectedSize -> new RobinHoodLongLongMap(expectedSize, hashing)), + new MapImplementation( + "swiss table " + hashing, + expectedSize -> new SwissTableLongLongMap(expectedSize, hashing)), + new MapImplementation( + "open address linear probing " + hashing, + expectedSize -> + new LongLongOpenAddressMap( + LinearProbingLongLongMap::new, expectedSize, hashing)), + new MapImplementation( + "open address robin hood " + hashing, + expectedSize -> + new LongLongOpenAddressMap( + RobinHoodLongLongMap::new, expectedSize, hashing)), + new MapImplementation( + "open address swiss table " + hashing, + expectedSize -> + new LongLongOpenAddressMap( + SwissTableLongLongMap::new, expectedSize, hashing)), + new MapImplementation( + "order statistic " + hashing, OrderStatisticLongLongMap::new))); + } + + private sealed interface Operation permits Put, Remove, Get {} + + private record Put(long key, long value) implements Operation {} + + private record Remove(long key) implements Operation {} + + private record Get(long key) implements Operation {} + + private record MapImplementation(String name, IntFunction factory) {} +} diff --git a/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/openaddressing/strategies/LongObjectMapImplementationTest.java b/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/openaddressing/strategies/LongObjectMapImplementationTest.java index 94f0f08..7cfce35 100644 --- a/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/openaddressing/strategies/LongObjectMapImplementationTest.java +++ b/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/openaddressing/strategies/LongObjectMapImplementationTest.java @@ -12,12 +12,16 @@ import name.mrkandreev.mapsmith.LongObjectMap; import name.mrkandreev.mapsmith.openaddressing.LongHashing; import name.mrkandreev.mapsmith.openaddressing.LongObjectOpenAddressMap; +import name.mrkandreev.mapsmith.openaddressing.LongObjectOpenAddressingStrategy; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; class LongObjectMapImplementationTest { private static final String MAP_IMPLEMENTATIONS = "mapImplementations"; + private static final String HASHING_METHOD_SOURCE = "hashings"; private static final String VALUE_PREFIX = "value-"; @ParameterizedTest @@ -144,6 +148,69 @@ void clearsValues(String name, IntFunction> maps) { assertThat(map.containsKey(2L)).isFalse(); } + @Nested + class LinearProbingConstructors { + @Test + void createsMapWithDefaultExpectedSize() { + assertThat(new LinearProbingLongObjectMap()).isNotNull(); + } + + @Test + void createsMapWithSpecifiedExpectedSize() { + assertThat(new LinearProbingLongObjectMap(1)).isNotNull(); + } + + @ParameterizedTest + @MethodSource( + "name.mrkandreev.mapsmith.openaddressing.strategies.LongObjectMapImplementationTest#hashings") + void createsMapWithSpecifiedHashing(LongHashing hashing) { + assertThat(new LinearProbingLongObjectMap(hashing)).isNotNull(); + } + } + + @Nested + class RobinHoodConstructors { + @Test + void createsMapWithDefaultExpectedSize() { + assertThat(new RobinHoodLongObjectMap()).isNotNull(); + } + + @Test + void createsMapWithSpecifiedExpectedSize() { + assertThat(new RobinHoodLongObjectMap(1)).isNotNull(); + } + + @ParameterizedTest + @MethodSource( + "name.mrkandreev.mapsmith.openaddressing.strategies.LongObjectMapImplementationTest#hashings") + void createsMapWithSpecifiedHashing(LongHashing hashing) { + assertThat(new RobinHoodLongObjectMap(hashing)).isNotNull(); + } + } + + @ParameterizedTest + @MethodSource(HASHING_METHOD_SOURCE) + void supportsSwissTableConstructors(LongHashing hashing) { + assertThat(new SwissTableLongObjectMap()).isNotNull(); + assertThat(new SwissTableLongObjectMap(1)).isNotNull(); + assertThat(new SwissTableLongObjectMap(hashing)).isNotNull(); + } + + @ParameterizedTest + @MethodSource(HASHING_METHOD_SOURCE) + void supportsOpenAddressMapConstructors(LongHashing hashing) { + @SuppressWarnings( + "unchecked") // Strategy constants are necessarily raw generic factory instances. + LongObjectOpenAddressingStrategy swissTable = + LongObjectOpenAddressingStrategy.SWISS_TABLE; + + assertThat(new LongObjectOpenAddressMap()).isNotNull(); + assertThat(new LongObjectOpenAddressMap(1)).isNotNull(); + assertThat(new LongObjectOpenAddressMap(swissTable)).isNotNull(); + assertThat(new LongObjectOpenAddressMap(swissTable, 1)).isNotNull(); + assertThat(new LongObjectOpenAddressMap(swissTable, hashing)).isNotNull(); + } + @ParameterizedTest @MethodSource(MAP_IMPLEMENTATIONS) void matchesHashMapForRandomOperations(String name, IntFunction> maps) { @@ -175,36 +242,38 @@ private static Stream mapImplementations() { .flatMap( hashing -> Stream.of( - Arguments.of( + mapImplementation( "linear probing " + hashing, - (IntFunction>) - expectedSize -> - new LinearProbingLongObjectMap<>(expectedSize, hashing)), - Arguments.of( + expectedSize -> new LinearProbingLongObjectMap<>(expectedSize, hashing)), + mapImplementation( "robin hood " + hashing, - (IntFunction>) - expectedSize -> new RobinHoodLongObjectMap<>(expectedSize, hashing)), - Arguments.of( + expectedSize -> new RobinHoodLongObjectMap<>(expectedSize, hashing)), + mapImplementation( "swiss table " + hashing, - (IntFunction>) - expectedSize -> new SwissTableLongObjectMap<>(expectedSize, hashing)), - Arguments.of( + expectedSize -> new SwissTableLongObjectMap<>(expectedSize, hashing)), + mapImplementation( "open address linear probing " + hashing, - (IntFunction>) - expectedSize -> - new LongObjectOpenAddressMap<>( - LinearProbingLongObjectMap::new, expectedSize, hashing)), - Arguments.of( + expectedSize -> + new LongObjectOpenAddressMap<>( + LinearProbingLongObjectMap::new, expectedSize, hashing)), + mapImplementation( "open address robin hood " + hashing, - (IntFunction>) - expectedSize -> - new LongObjectOpenAddressMap<>( - RobinHoodLongObjectMap::new, expectedSize, hashing)), - Arguments.of( + expectedSize -> + new LongObjectOpenAddressMap<>( + RobinHoodLongObjectMap::new, expectedSize, hashing)), + mapImplementation( "open address swiss table " + hashing, - (IntFunction>) - expectedSize -> - new LongObjectOpenAddressMap<>( - SwissTableLongObjectMap::new, expectedSize, hashing)))); + expectedSize -> + new LongObjectOpenAddressMap<>( + SwissTableLongObjectMap::new, expectedSize, hashing)))); + } + + private static Arguments mapImplementation( + String name, IntFunction> factory) { + return Arguments.of(name, factory); + } + + private static Stream hashings() { + return Arrays.stream(LongHashing.values()); } } diff --git a/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/range/TreeLongLongRangeMapTest.java b/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/range/TreeLongLongRangeMapTest.java index f2f4b4c..d3db040 100644 --- a/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/range/TreeLongLongRangeMapTest.java +++ b/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/range/TreeLongLongRangeMapTest.java @@ -143,7 +143,77 @@ void ignoresEmptyDiscreteRanges() { } @Test - void rejectsInvalidRangesAndNullBoundTypes() { + void doesNotCoalesceAdjacentRangesWithDifferentValues() { + LongLongRangeMap map = new TreeLongLongRangeMap(); + + map.putCoalescing(0L, 0L, 1L); + map.putCoalescing(2L, 2L, 2L); + map.putCoalescing(1L, 1L, 1L); + assertThat(map.size()).isEqualTo(2); + } + + @Test + void coalescesRangesAtMaximumLongValue() { + LongLongRangeMap map = new TreeLongLongRangeMap(); + + map.putCoalescing(Long.MAX_VALUE - 1L, Long.MAX_VALUE, 3L); + map.putCoalescing(Long.MAX_VALUE, Long.MAX_VALUE, 3L); + assertThat(map.get(Long.MAX_VALUE)).isEqualTo(3L); + } + + @Test + void ignoresEmptyTypedRanges() { + LongLongRangeMap map = new TreeLongLongRangeMap(); + + map.putCoalescing(Long.MAX_VALUE, LongBoundType.OPEN, Long.MAX_VALUE, LongBoundType.CLOSED, 4L); + map.putCoalescing(Long.MIN_VALUE, LongBoundType.CLOSED, Long.MIN_VALUE, LongBoundType.OPEN, 4L); + map.remove(Long.MAX_VALUE, LongBoundType.OPEN, Long.MAX_VALUE, LongBoundType.CLOSED); + map.remove(Long.MIN_VALUE, LongBoundType.CLOSED, Long.MIN_VALUE, LongBoundType.OPEN); + assertThat(map.isEmpty()).isTrue(); + } + + @Test + void clearsRanges() { + LongLongRangeMap map = new TreeLongLongRangeMap(); + + map.put(0L, 1L, 1L); + map.clear(); + assertThat(map.isEmpty()).isTrue(); + } + + @Test + void coalescesForwardAndRemovesTypedRanges() { + LongLongRangeMap map = new TreeLongLongRangeMap(); + + map.putCoalescing(2L, 2L, 1L); + map.putCoalescing(0L, 1L, 1L); + assertThat(map.size()).isEqualTo(1); + map.remove(0L, LongBoundType.OPEN, 2L, LongBoundType.OPEN); + assertThat(map.get(0L)).isEqualTo(1L); + assertThat(map.get(1L)).isZero(); + assertThat(map.get(2L)).isEqualTo(1L); + assertThat(map.isEmpty()).isFalse(); + } + + @Test + void handlesNonTouchingEqualRangesAndEmptyTypedRemoval() { + LongLongRangeMap map = new TreeLongLongRangeMap(); + + map.putCoalescing(0L, 0L, 1L); + map.putCoalescing(2L, 2L, 1L); + assertThat(map.size()).isEqualTo(2); + map.clear(); + map.putCoalescing(2L, 2L, 1L); + map.putCoalescing(0L, 0L, 1L); + assertThat(map.size()).isEqualTo(2); + map.remove(Long.MIN_VALUE, LongBoundType.CLOSED, Long.MIN_VALUE, LongBoundType.OPEN); + map.remove(0L, LongBoundType.CLOSED, 2L, LongBoundType.CLOSED); + map.remove(0L, LongBoundType.CLOSED, 2L, LongBoundType.OPEN); + map.remove(1L, LongBoundType.CLOSED, 1L, LongBoundType.OPEN); + } + + @Test + void rejectsInvalidClosedRanges() { LongLongRangeMap map = new TreeLongLongRangeMap(); assertThatThrownBy(() -> map.put(2L, 1L, 10L)) @@ -152,21 +222,33 @@ void rejectsInvalidRangesAndNullBoundTypes() { assertThatThrownBy(() -> map.remove(2L, 1L)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("fromInclusive must be <= toInclusive"); + } + + @Test + void rejectsNullBoundTypes() { + LongLongRangeMap map = new TreeLongLongRangeMap(); + assertThatThrownBy(() -> map.put(1L, null, 2L, LongBoundType.CLOSED, 10L)) .isInstanceOf(NullPointerException.class) .hasMessage("lowerType must not be null"); assertThatThrownBy(() -> map.putCoalescing(1L, LongBoundType.CLOSED, 2L, null, 10L)) .isInstanceOf(NullPointerException.class) .hasMessage("upperType must not be null"); + assertThatThrownBy(() -> map.remove(1L, null, 2L, LongBoundType.CLOSED)) + .isInstanceOf(NullPointerException.class) + .hasMessage("lowerType must not be null"); + } + + @Test + void rejectsInvalidTypedRanges() { + LongLongRangeMap map = new TreeLongLongRangeMap(); + assertThatThrownBy(() -> map.remove(1L, LongBoundType.OPEN, 1L, LongBoundType.OPEN)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("open range endpoints must be different"); assertThatThrownBy(() -> map.remove(2L, LongBoundType.CLOSED, 1L, LongBoundType.CLOSED)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("lower must be <= upper"); - assertThatThrownBy(() -> map.remove(1L, null, 2L, LongBoundType.CLOSED)) - .isInstanceOf(NullPointerException.class) - .hasMessage("lowerType must not be null"); } @Test diff --git a/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/range/TreeLongObjectRangeMapTest.java b/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/range/TreeLongObjectRangeMapTest.java index 06668d2..628b503 100644 --- a/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/range/TreeLongObjectRangeMapTest.java +++ b/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/range/TreeLongObjectRangeMapTest.java @@ -9,6 +9,9 @@ import org.junit.jupiter.api.Test; class TreeLongObjectRangeMapTest { + private static final String LEFT_VALUE = "left"; + private static final String MIDDLE_VALUE = "middle"; + private static final String RIGHT_VALUE = "right"; private static final String SAME_VALUE = "same"; private static final String STORED_VALUE = "value"; @@ -40,16 +43,16 @@ void storesAndFindsRanges() { @Test void rangePutOverwritesAndSplitsOverlaps() { LongObjectRangeMap map = new TreeLongObjectRangeMap<>(); - map.put(0L, 9L, "left"); - map.put(20L, 29L, "right"); + map.put(0L, 9L, LEFT_VALUE); + map.put(20L, 29L, RIGHT_VALUE); - map.put(5L, 24L, "middle"); + map.put(5L, 24L, MIDDLE_VALUE); assertThat(map.size()).isEqualTo(3); - assertThat(map.get(4L)).isEqualTo("left"); - assertThat(map.get(5L)).isEqualTo("middle"); - assertThat(map.get(24L)).isEqualTo("middle"); - assertThat(map.get(25L)).isEqualTo("right"); + assertThat(map.get(4L)).isEqualTo(LEFT_VALUE); + assertThat(map.get(5L)).isEqualTo(MIDDLE_VALUE); + assertThat(map.get(24L)).isEqualTo(MIDDLE_VALUE); + assertThat(map.get(25L)).isEqualTo(RIGHT_VALUE); } @Test @@ -110,7 +113,7 @@ void supportsOpenClosedAndUnboundedRanges() { } @Test - void rejectsInvalidRangesAndNullBoundTypes() { + void rejectsInvalidClosedRanges() { LongObjectRangeMap map = new TreeLongObjectRangeMap<>(); assertThatThrownBy(() -> map.put(2L, 1L, STORED_VALUE)) @@ -119,15 +122,103 @@ void rejectsInvalidRangesAndNullBoundTypes() { assertThatThrownBy(() -> map.remove(2L, 1L)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("fromInclusive must be <= toInclusive"); + } + + @Test + void rejectsNullBoundTypes() { + LongObjectRangeMap map = new TreeLongObjectRangeMap<>(); + assertThatThrownBy(() -> map.put(1L, null, 2L, LongBoundType.CLOSED, STORED_VALUE)) .isInstanceOf(NullPointerException.class) .hasMessage("lowerType must not be null"); assertThatThrownBy(() -> map.putCoalescing(1L, LongBoundType.CLOSED, 2L, null, STORED_VALUE)) .isInstanceOf(NullPointerException.class) .hasMessage("upperType must not be null"); + } + + @Test + void rejectsInvalidTypedRanges() { + LongObjectRangeMap map = new TreeLongObjectRangeMap<>(); + assertThatThrownBy(() -> map.remove(1L, LongBoundType.OPEN, 1L, LongBoundType.OPEN)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("open range endpoints must be different"); + assertThatThrownBy(() -> map.remove(2L, LongBoundType.CLOSED, 1L, LongBoundType.CLOSED)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("lower must be <= upper"); + } + + @Test + void doesNotCoalesceAdjacentRangesWithDifferentValues() { + LongObjectRangeMap map = new TreeLongObjectRangeMap<>(); + + map.putCoalescing(0L, 0L, "left"); + map.putCoalescing(2L, 2L, "right"); + map.putCoalescing(1L, 1L, "left"); + assertThat(map.size()).isEqualTo(2); + } + + @Test + void coalescesRangesAtMaximumLongValue() { + LongObjectRangeMap map = new TreeLongObjectRangeMap<>(); + + map.putCoalescing(Long.MAX_VALUE - 1L, Long.MAX_VALUE, "edge"); + map.putCoalescing(Long.MAX_VALUE, Long.MAX_VALUE, "edge"); + assertThat(map.get(Long.MAX_VALUE)).isEqualTo("edge"); + } + + @Test + void ignoresEmptyTypedRanges() { + LongObjectRangeMap map = new TreeLongObjectRangeMap<>(); + + map.putCoalescing( + Long.MAX_VALUE, LongBoundType.OPEN, Long.MAX_VALUE, LongBoundType.CLOSED, "none"); + map.putCoalescing( + Long.MIN_VALUE, LongBoundType.CLOSED, Long.MIN_VALUE, LongBoundType.OPEN, "none"); + map.remove(Long.MAX_VALUE, LongBoundType.OPEN, Long.MAX_VALUE, LongBoundType.CLOSED); + map.remove(Long.MIN_VALUE, LongBoundType.CLOSED, Long.MIN_VALUE, LongBoundType.OPEN); + assertThat(map.isEmpty()).isTrue(); + } + + @Test + void clearsRanges() { + LongObjectRangeMap map = new TreeLongObjectRangeMap<>(); + + map.put(0L, 1L, STORED_VALUE); + map.clear(); + assertThat(map.isEmpty()).isTrue(); + } + + @Test + void coalescesForwardAndRemovesTypedRanges() { + LongObjectRangeMap map = new TreeLongObjectRangeMap<>(); + + map.putCoalescing(2L, 2L, STORED_VALUE); + map.putCoalescing(0L, 1L, STORED_VALUE); + assertThat(map.size()).isEqualTo(1); + map.remove(0L, LongBoundType.OPEN, 2L, LongBoundType.OPEN); + assertThat(map.get(0L)).isEqualTo(STORED_VALUE); + assertThat(map.get(1L)).isNull(); + assertThat(map.get(2L)).isEqualTo(STORED_VALUE); + assertThat(map.isEmpty()).isFalse(); + } + + @Test + void handlesNonTouchingEqualRangesAndEmptyTypedRemoval() { + LongObjectRangeMap map = new TreeLongObjectRangeMap<>(); + + map.putCoalescing(0L, 0L, STORED_VALUE); + map.putCoalescing(2L, 2L, STORED_VALUE); + assertThat(map.size()).isEqualTo(2); + map.clear(); + map.putCoalescing(2L, 2L, STORED_VALUE); + map.putCoalescing(0L, 0L, STORED_VALUE); + assertThat(map.size()).isEqualTo(2); + map.put(1L, LongBoundType.CLOSED, 1L, LongBoundType.OPEN, STORED_VALUE); + map.remove(Long.MIN_VALUE, LongBoundType.CLOSED, Long.MIN_VALUE, LongBoundType.OPEN); + map.remove(0L, LongBoundType.CLOSED, 2L, LongBoundType.CLOSED); + map.remove(0L, LongBoundType.CLOSED, 2L, LongBoundType.OPEN); + map.remove(1L, LongBoundType.CLOSED, 1L, LongBoundType.OPEN); } @Test diff --git a/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/ranking/OrderStatisticLongLongMapTest.java b/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/ranking/OrderStatisticLongLongMapTest.java index 8b585c1..5a025ac 100644 --- a/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/ranking/OrderStatisticLongLongMapTest.java +++ b/mapsmith-core/src/test/java/name/mrkandreev/mapsmith/ranking/OrderStatisticLongLongMapTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; @@ -112,6 +113,24 @@ void matchesExpectedRanksForRandomOperations() { } } + @Test + @SuppressWarnings("PMD.AvoidAccessibilityAlteration") + void handlesDefensiveTreeCases() throws ReflectiveOperationException { + Class nodeType = Class.forName(OrderStatisticLongLongMap.class.getName() + "$Node"); + Method insert = + OrderStatisticLongLongMap.class.getDeclaredMethod( + "insert", nodeType, long.class, long.class); + Method delete = + OrderStatisticLongLongMap.class.getDeclaredMethod( + "delete", nodeType, long.class, long.class); + insert.setAccessible(true); + delete.setAccessible(true); + + Object node = insert.invoke(null, null, 1L, 10L); + assertThat(insert.invoke(null, node, 1L, 10L)).isSameAs(node); + assertThat(delete.invoke(null, null, 1L, 20L)).isNull(); + } + private static void assertRanks(LongLongRankingMap map, Map expected) { List> ordered = new ArrayList<>(expected.entrySet()); ordered.sort(