Skip to content

Commit

Permalink
Add a Seed utility class for working with seed bits
Browse files Browse the repository at this point in the history
  • Loading branch information
aherbert committed Jul 27, 2023
1 parent 6ec7fb3 commit 2a33303
Show file tree
Hide file tree
Showing 2 changed files with 260 additions and 0 deletions.
150 changes: 150 additions & 0 deletions gdsc-core/src/main/java/uk/ac/sussex/gdsc/core/utils/Seed.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*-
* #%L
* Genome Damage and Stability Centre Core Package
*
* Contains core utilities for image analysis and is used by:
*
* GDSC ImageJ Plugins - Microscopy image analysis
*
* GDSC SMLM ImageJ Plugins - Single molecule localisation microscopy (SMLM)
* %%
* Copyright (C) 2011 - 2023 Alex Herbert
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/gpl-3.0.html>.
* #L%
*/

package uk.ac.sussex.gdsc.core.utils;

import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Objects;

/**
* Provides a seed of bits. Methods are provided to convert between data representations.
*/
public final class Seed {

/** The bytes. */
private final byte[] bytes;
/** The hash code. */
private int hash;

/**
* Create an instance.
*
* @param bytes the bytes
*/
private Seed(byte[] bytes) {
this.bytes = bytes;
}

/**
* Create a seed from the bytes. No reference to the input array is stored.
*
* @param bytes the bytes (must not be null)
* @return the seed
*/
public static Seed from(byte[] bytes) {
return new Seed(Objects.requireNonNull(bytes, "The bytes must not be null").clone());
}

/**
* Create a seed from the hex-encoded characters. No reference to the input is stored.
*
* @param cs the characters
* @return the seed
*/
public static Seed from(CharSequence cs) {
return new Seed(Hex.decode(Objects.requireNonNull(cs, "The characters must not be null")));
}

/**
* Create a seed from the value. All bits in the input are used including leading zeros.
*
* @param value the value
* @return the seed
*/
public static Seed from(long value) {
return new Seed(ByteBuffer.allocate(8).putLong(value).array());
}

/**
* Converts to a byte representation.
*
* @return the bytes
*/
public byte[] toBytes() {
return bytes.clone();
}

/**
* Converts to a long representation. The value is created using all of the information in the
* seed.
*
* @return the long
*/
public long toLong() {
long result = 0;
// Process blocks of 8, then the remaining part
final int length = bytes.length;
final int limit = length & 0x7ffffff8;
if (length >= 8) {
final ByteBuffer bb = ByteBuffer.wrap(bytes);
for (int i = 0; i < limit; i += 8) {
result ^= bb.getLong();
}
}
// Consume remaining bytes
if (limit < length) {
final ByteBuffer bb = ByteBuffer.allocate(8);
bb.put(bytes, limit, length - limit);
bb.rewind();
result ^= bb.getLong();
}
return result;
}

@Override
public int hashCode() {
int result = hash;
if (result == 0) {
hash = result = Arrays.hashCode(bytes);
}
return result;
}

@Override
public boolean equals(Object obj) {
// self check
if (this == obj) {
return true;
}
// null check and type check (this class is final so no sub-classes are possible)
if (!(obj instanceof Seed)) {
return false;
}
// field comparison
return Arrays.equals(bytes, ((Seed) obj).bytes);
}

/**
* Converts to a hex-encoded String of the byte representation.
*/
@Override
public String toString() {
return new String(Hex.encode(bytes));
}
}
110 changes: 110 additions & 0 deletions gdsc-core/src/test/java/uk/ac/sussex/gdsc/core/utils/SeedTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*-
* #%L
* Genome Damage and Stability Centre Core Package
*
* Contains core utilities for image analysis and is used by:
*
* GDSC ImageJ Plugins - Microscopy image analysis
*
* GDSC SMLM ImageJ Plugins - Single molecule localisation microscopy (SMLM)
* %%
* Copyright (C) 2011 - 2023 Alex Herbert
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/gpl-3.0.html>.
* #L%
*/

package uk.ac.sussex.gdsc.core.utils;

import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.stream.Stream;
import org.apache.commons.rng.UniformRandomProvider;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import uk.ac.sussex.gdsc.test.junit5.SeededTest;
import uk.ac.sussex.gdsc.test.rng.RngFactory;
import uk.ac.sussex.gdsc.test.utils.RandomSeed;

@SuppressWarnings({"javadoc"})
class SeedTest {
@Test
void nullArgumentThrows() {
Assertions.assertThrows(NullPointerException.class, () -> Seed.from((byte[]) null));
Assertions.assertThrows(NullPointerException.class, () -> Seed.from((String) null));
}

@ParameterizedTest
@ValueSource(longs = {0, 1, -1, 1253717123, 192384179239871L})
void testLong(long value) {
final Seed seed = Seed.from(value);
Assertions.assertEquals(value, seed.toLong());
Assertions.assertEquals(value, Seed.from(seed.toString()).toLong());
Assertions.assertEquals(value, Seed.from(seed.toBytes()).toLong());
}

@ParameterizedTest
@MethodSource
void testBytes(byte[] value) {
final Seed seed = Seed.from(value);
Assertions.assertArrayEquals(value, seed.toBytes());
Assertions.assertArrayEquals(value, Seed.from(seed.toString()).toBytes());
if (value.length == 8) {
Assertions.assertArrayEquals(value, Seed.from(seed.toLong()).toBytes());
}
}

static Stream<byte[]> testBytes() {
final UniformRandomProvider rng = RngFactory.createWithFixedSeed();
final Stream.Builder<byte[]> builder = Stream.builder();
for (int i = 0; i < 16; i++) {
final byte[] bytes = new byte[i];
rng.nextBytes(bytes);
builder.add(bytes);
}
return builder.build();
}

@Test
void testBytesToLong() {
final byte[] b0 = {};
final byte[] b1 = {0};
final byte[] b2 = {1};
Assertions.assertEquals(0, Seed.from(b0).toLong());
Assertions.assertEquals(0, Seed.from(b1).toLong());
Assertions.assertEquals(ByteBuffer.wrap(Arrays.copyOf(b2, 8)).getLong(),
Seed.from(b2).toLong());
final byte[] b3 = {1, 2, 3, 4, 5, 6, 7, 8};
final byte[] b4 = {1, 2, 3, 4, 5, 6, 7, 8, 9};
Assertions.assertNotEquals(Seed.from(b3).toLong(), Seed.from(b4).toLong());
}

@SeededTest
void testEqualsAndHashCode(RandomSeed rs) {
final byte[] bytes = rs.get();
final int hash = Arrays.hashCode(bytes);
final Seed seed = Seed.from(bytes);
Assertions.assertEquals(hash, seed.hashCode());
Assertions.assertEquals(hash, seed.hashCode(), "hash code is cached");
Assertions.assertTrue(seed.equals(seed));
Assertions.assertTrue(seed.equals(Seed.from(bytes)));
Assertions.assertFalse(seed.equals("hello"));
bytes[0]++;
Assertions.assertFalse(seed.equals(Seed.from(bytes)));
}
}

0 comments on commit 2a33303

Please sign in to comment.