diff --git a/.github/release-settings.xml b/.github/release-settings.xml new file mode 100644 index 0000000..be56a53 --- /dev/null +++ b/.github/release-settings.xml @@ -0,0 +1,20 @@ + + + + eu.maveniverse.maven.plugins + + + + + sonatype-central-portal + ${env.MAVEN_USER} + ${env.MAVEN_PASSWORD} + + sonatype-cp + njord:template:release-sca + + + + + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d6ad167 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,90 @@ +name: Release + +on: + workflow_dispatch: + inputs: + releaseVersion: + description: "Release version, e.g. 0.3.6 (optional — auto-detected from the current POM)" + required: false + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write # to automatically create tags + + steps: + - name: Validate release version + if: ${{ github.event.inputs.releaseVersion != '' }} + run: | + RELEASE=${{ github.event.inputs.releaseVersion }} + if [[ ! $RELEASE =~ ^[0-9]+\.[0-9]+\.[0-9]+(-SNAPSHOT)?$ ]]; then + echo "Error: releaseVersion '$RELEASE' is not in the correct format x.y.z or x.y.z-SNAPSHOT" + exit 1 + fi + + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: master + + - name: Set up Java + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'adopt' + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: MAVEN_GPG_PASSPHRASE + + - name: Configure git + run: | + git config user.email "actions@github.com" + git config user.name "GitHub Actions" + + - name: Prepare Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: | + MVN_ARGS="" + if [ -n "${{ github.event.inputs.releaseVersion }}" ]; then + MVN_ARGS="$MVN_ARGS -DreleaseVersion=${{ github.event.inputs.releaseVersion }}" + fi + mvn -B release:prepare $MVN_ARGS + + - name: Check release.properties + run: | + if [ ! -f release.properties ]; then + echo "release.properties not found" + exit 1 + fi + echo "Contents of release.properties:" + cat release.properties + + - name: Determine release version + id: version + run: | + export TAG=$(grep 'scm.tag=' release.properties | cut -d'=' -f2) + export VERSION=${TAG#JavaFastPFOR-} + + echo "released_tag=${TAG}" >> $GITHUB_OUTPUT + echo "released_version=${VERSION}" >> $GITHUB_OUTPUT + + echo "Releasing tag: ${TAG}" + echo "Releasing version: ${VERSION}" + + - name: Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + MAVEN_GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + MAVEN_USER: ${{ secrets.MAVEN_USER }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} + run: | + mvn -B release:perform -Darguments="-DskipTests -DaltDeploymentRepository=id::default::njord: -Dnjord.autoPublish=true -Dnjord.publishingType=automatic" -s .github/release-settings.xml + + - name: Create GitHub Release + run: gh release create "${{ steps.version.outputs.released_tag }}" --generate-notes --title "Version ${{ steps.version.outputs.released_version }}" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 88b3320..5a78c84 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ tags target/ tmp/ /bin +.idea +*.iml diff --git a/README.md b/README.md index 2ef8fc5..0246789 100644 --- a/README.md +++ b/README.md @@ -104,53 +104,62 @@ non-vectorized implementation. For an example usage, see examples/vector/Example.java. The feature requires JDK 19+ and is currently for advanced users. -JavaFastPFOR as a dependency (JitPack) +JavaFastPFOR as a dependency ------------------------ +JavaFastPFOR is available both on Maven Central and JitPack, so you can easily +include it in your project using either source. + We have a demo project using JavaFastPFOR as a dependency (both Maven and Gradle). See... https://github.com/fast-pack/JavaFastPFORDemo -1. **Maven** +### Maven Central + +You can add JavaFastPFOR directly from Maven Central — no extra repository configuration needed: -Using this code in your own project is easy with maven, just add -the following code in your pom.xml file: +**Maven** ```xml - - com.github.fast-pack - JavaFastPFor - JavaFastPFOR-0.3.2 - + + me.lemire.integercompression + JavaFastPFOR + 0.3.8 + ``` -as well as jitpack as a repository... +**Gradle (Groovy)** -```xml - - - jitpack.io - https://jitpack.io - - +```groovy +dependencies { + implementation 'me.lemire.integercompression:JavaFastPFOR:0.3.8' +} ``` -Naturally, you should replace "version" by the version -you desire. - +### JitPack -2. **Gradle (groovy)** +If you prefer or need to use JitPack, you can include the dependency like this: +**Maven** -Then all you need is to edit your `build.gradle` file like so: +```xml + + + jitpack.io + https://jitpack.io + + + + + com.github.fast-pack + JavaFastPFOR + JavaFastPFOR-0.3.8 + +``` +**Gradle (groovy)** ```groovy -plugins { - id 'java' -} - - repositories { mavenCentral() maven { @@ -159,11 +168,10 @@ repositories { } dependencies { - implementation 'com.github.fast-pack:JavaFastPFor:JavaFastPFOR-0.3.2' + implementation 'com.github.fast-pack:JavaFastPFOR:JavaFastPFOR-0.3.8' } ``` - Naturally, you should replace "version" by the version you desire. @@ -244,6 +252,21 @@ API Documentation http://www.javadoc.io/doc/me.lemire.integercompression/JavaFastPFOR/ + +Citing this work +----------------- + +If you use JavaFastPFOR in your work, please consider citing the project. A recommended BibTeX entry is: + +```bibtex +@misc{lemire2025_javafastpfor, + author = {Daniel Lemire}, + title = {{JavaFastPFOR: A simple integer compression library in Java}}, + year = {2025}, + howpublished = {\url{https://github.com/fast-pack/JavaFastPFOR}}, +} +``` + Want to read more? ------------------ diff --git a/pom.xml b/pom.xml index 928a3d2..33db8e6 100644 --- a/pom.xml +++ b/pom.xml @@ -2,13 +2,14 @@ 4.0.0 me.lemire.integercompression JavaFastPFOR - 0.3.3-SNAPSHOT + 0.3.11-SNAPSHOT jar 21 21 21 UTF-8 + 0.8.5 @@ -19,11 +20,25 @@ - scm:git:git@github.com:fast-pack/JavaFastPFOR.git - scm:git:git@github.com:fast-pack/JavaFastPFOR.git - scm:git:git@github.com:fast-pack/JavaFastPFOR.git + scm:git:https://github.com/fast-pack/JavaFastPFOR.git + scm:git:https://github.com/fast-pack/JavaFastPFOR.git + scm:git:https://github.com/fast-pack/JavaFastPFOR.git HEAD + + + + sonatype-central-portal + Sonatype Central Portal + https://central.sonatype.com/repository/maven-snapshots/ + + + sonatype-central-portal + Sonatype Central Portal + https://repo.maven.apache.org/maven2/ + + + lemire @@ -60,6 +75,13 @@ https://github.com/fast-pack/JavaFastPFOR/issues + + + eu.maveniverse.maven.njord + extension3 + ${njord.version} + + org.apache.maven.plugins @@ -170,14 +192,33 @@ maven-release-plugin 3.0.1 - install + deploy true + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.8 + + + sign-artifacts + verify + + sign + + + + + + eu.maveniverse.maven.plugins + njord + ${njord.version} + maven-clean-plugin 2.5 diff --git a/src/main/java/me/lemire/integercompression/SkippableComposition.java b/src/main/java/me/lemire/integercompression/SkippableComposition.java index 7dd4736..fc3c18e 100644 --- a/src/main/java/me/lemire/integercompression/SkippableComposition.java +++ b/src/main/java/me/lemire/integercompression/SkippableComposition.java @@ -52,12 +52,14 @@ public void headlessCompress(int[] in, IntWrapper inpos, int inlength, int[] out public void headlessUncompress(int[] in, IntWrapper inpos, int inlength, int[] out, IntWrapper outpos, int num) { int init = inpos.get(); + int outposInit = outpos.get(); + F1.headlessUncompress(in, inpos, inlength, out, outpos, num); if (inpos.get() == init) { inpos.increment(); } inlength -= inpos.get() - init; - num -= outpos.get(); + num -= outpos.get() - outposInit; F2.headlessUncompress(in, inpos, inlength, out, outpos, num); } diff --git a/src/main/java/me/lemire/integercompression/differential/SkippableIntegratedComposition.java b/src/main/java/me/lemire/integercompression/differential/SkippableIntegratedComposition.java index a1379ad..4786ec5 100644 --- a/src/main/java/me/lemire/integercompression/differential/SkippableIntegratedComposition.java +++ b/src/main/java/me/lemire/integercompression/differential/SkippableIntegratedComposition.java @@ -66,13 +66,15 @@ public void headlessUncompress(int[] in, IntWrapper inpos, int inlength, if (inlength == 0) return; int init = inpos.get(); + int outposInit = outpos.get(); + F1.headlessUncompress(in, inpos, inlength, out, outpos,num,initvalue); if (inpos.get() == init) { inpos.increment(); } inlength -= inpos.get() - init; - num -= outpos.get(); + num -= outpos.get() - outposInit; F2.headlessUncompress(in, inpos, inlength, out, outpos,num,initvalue); } diff --git a/src/main/java/me/lemire/integercompression/synth/UniformDataGenerator.java b/src/main/java/me/lemire/integercompression/synth/UniformDataGenerator.java index bbd386a..a50497c 100644 --- a/src/main/java/me/lemire/integercompression/synth/UniformDataGenerator.java +++ b/src/main/java/me/lemire/integercompression/synth/UniformDataGenerator.java @@ -42,7 +42,7 @@ int[] generateUniformHash(int N, int Max) { int[] ans = new int[N]; HashSet s = new HashSet(); while (s.size() < N) - s.add(new Integer(this.rand.nextInt(Max))); + s.add(this.rand.nextInt(Max)); Iterator i = s.iterator(); for (int k = 0; k < N; ++k) ans[k] = i.next().intValue(); diff --git a/src/main/java/me/lemire/longcompression/SkippableLongComposition.java b/src/main/java/me/lemire/longcompression/SkippableLongComposition.java index 0f9800e..eb03b72 100644 --- a/src/main/java/me/lemire/longcompression/SkippableLongComposition.java +++ b/src/main/java/me/lemire/longcompression/SkippableLongComposition.java @@ -53,12 +53,14 @@ public void headlessCompress(long[] in, IntWrapper inpos, int inlength, long[] o public void headlessUncompress(long[] in, IntWrapper inpos, int inlength, long[] out, IntWrapper outpos, int num) { int init = inpos.get(); + int outposInit = outpos.get(); + F1.headlessUncompress(in, inpos, inlength, out, outpos, num); if (inpos.get() == init) { inpos.increment(); } inlength -= inpos.get() - init; - num -= outpos.get(); + num -= outpos.get() - outposInit; F2.headlessUncompress(in, inpos, inlength, out, outpos, num); } diff --git a/src/main/java/me/lemire/longcompression/differential/LongDelta.java b/src/main/java/me/lemire/longcompression/differential/LongDelta.java index 184e53c..8399f94 100644 --- a/src/main/java/me/lemire/longcompression/differential/LongDelta.java +++ b/src/main/java/me/lemire/longcompression/differential/LongDelta.java @@ -107,7 +107,7 @@ public static void fastinverseDelta(long[] data) { } } - for (; i != data.length; ++i) { + for (; i < data.length; ++i) { data[i] += data[i - 1]; } } diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index d254c12..f134601 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -6,5 +6,7 @@ // requires jdk.incubator.vector; exports me.lemire.integercompression; exports me.lemire.longcompression; + exports me.lemire.longcompression.differential; + exports me.lemire.integercompression.differential; // exports me.lemire.integercompression.vector; } diff --git a/src/test/java/me/lemire/integercompression/SkippableBasicTest.java b/src/test/java/me/lemire/integercompression/SkippableBasicTest.java index 57a07e3..881dada 100644 --- a/src/test/java/me/lemire/integercompression/SkippableBasicTest.java +++ b/src/test/java/me/lemire/integercompression/SkippableBasicTest.java @@ -15,6 +15,7 @@ import me.lemire.integercompression.differential.SkippableIntegratedIntegerCODEC; import org.junit.Test; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertTrue; /** @@ -236,4 +237,52 @@ private static void testMaxHeadlessCompressedLength(SkippableIntegerCODEC codec, } } } + + @Test + public void testUncompressOutputOffset_SkippableComposition() { + for (int offset : new int[] {0, 1, 6}) { + SkippableComposition codec = new SkippableComposition(new BinaryPacking(), new VariableByte()); + + int[] input = { 2, 3, 4, 5 }; + int[] compressed = new int[codec.maxHeadlessCompressedLength(new IntWrapper(0), input.length)]; + int[] uncompressed = new int[offset + input.length]; + + IntWrapper inputOffset = new IntWrapper(0); + IntWrapper compressedOffset = new IntWrapper(0); + + codec.headlessCompress(input, inputOffset, input.length, compressed, compressedOffset); + + int compressedLength = compressedOffset.get(); + IntWrapper uncompressedOffset = new IntWrapper(offset); + compressedOffset = new IntWrapper(0); + codec.headlessUncompress(compressed, compressedOffset, compressedLength, uncompressed, uncompressedOffset, input.length); + + assertArrayEquals(input, Arrays.copyOfRange(uncompressed, offset, offset + input.length)); + } + } + + @Test + public void testUncompressOutputOffset_SkippableIntegratedComposition() { + for (int offset : new int[] {0, 1, 6}) { + SkippableIntegratedComposition codec = new SkippableIntegratedComposition(new IntegratedBinaryPacking(), new IntegratedVariableByte()); + + int[] input = { 2, 3, 4, 5 }; + int[] compressed = new int[codec.maxHeadlessCompressedLength(new IntWrapper(0), input.length)]; + int[] uncompressed = new int[offset + input.length]; + + IntWrapper inputOffset = new IntWrapper(0); + IntWrapper compressedOffset = new IntWrapper(0); + IntWrapper initValue = new IntWrapper(0); + + codec.headlessCompress(input, inputOffset, input.length, compressed, compressedOffset, initValue); + + int compressedLength = compressedOffset.get(); + IntWrapper uncompressedOffset = new IntWrapper(offset); + compressedOffset = new IntWrapper(0); + initValue = new IntWrapper(0); + codec.headlessUncompress(compressed, compressedOffset, compressedLength, uncompressed, uncompressedOffset, input.length, initValue); + + assertArrayEquals(input, Arrays.copyOfRange(uncompressed, offset, offset + input.length)); + } + } } diff --git a/src/test/java/me/lemire/longcompression/LongDeltaTest.java b/src/test/java/me/lemire/longcompression/LongDeltaTest.java new file mode 100644 index 0000000..bfa1e6f --- /dev/null +++ b/src/test/java/me/lemire/longcompression/LongDeltaTest.java @@ -0,0 +1,23 @@ +package me.lemire.longcompression; + +import me.lemire.longcompression.differential.LongDelta; +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNotNull; + +public class LongDeltaTest { + @Test + public void testEmptyArrayFastInverseDelta() { + LongCompressor compressor = new LongCompressor(); + long[] input = new long[0]; + + LongDelta.delta(input); + long[] compressed = compressor.compress(input); + long[] result = compressor.uncompress(compressed); + LongDelta.fastinverseDelta(result); + + assertNotNull(result); + assertArrayEquals(input, result); + } +} diff --git a/src/test/java/me/lemire/longcompression/SkippableLongBasicTest.java b/src/test/java/me/lemire/longcompression/SkippableLongBasicTest.java index 4309e9d..c4b7e01 100644 --- a/src/test/java/me/lemire/longcompression/SkippableLongBasicTest.java +++ b/src/test/java/me/lemire/longcompression/SkippableLongBasicTest.java @@ -15,6 +15,7 @@ import me.lemire.integercompression.TestUtils; import me.lemire.integercompression.VariableByte; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertTrue; /** @@ -167,4 +168,27 @@ private static void testMaxHeadlessCompressedLength(SkippableLongCODEC codec, in assertTrue(maxOutputLength <= outPos.get() + 1); // +1 because SkippableLongComposition always adds one extra integer for the potential header } } + + @Test + public void testUncompressOutputOffset_SkippableLongComposition() { + for (int offset : new int[] {0, 1, 6}) { + SkippableLongComposition codec = new SkippableLongComposition(new LongBinaryPacking(), new LongVariableByte()); + + long[] input = { 2, 3, 4, 5 }; + long[] compressed = new long[codec.maxHeadlessCompressedLength(new IntWrapper(0), input.length)]; + long[] uncompressed = new long[offset + input.length]; + + IntWrapper inputOffset = new IntWrapper(0); + IntWrapper compressedOffset = new IntWrapper(0); + + codec.headlessCompress(input, inputOffset, input.length, compressed, compressedOffset); + + int compressedLength = compressedOffset.get(); + IntWrapper uncompressedOffset = new IntWrapper(offset); + compressedOffset = new IntWrapper(0); + codec.headlessUncompress(compressed, compressedOffset, compressedLength, uncompressed, uncompressedOffset, input.length); + + assertArrayEquals(input, Arrays.copyOfRange(uncompressed, offset, offset + input.length)); + } + } }