diff --git a/CHANGELOG.md b/CHANGELOG.md
index a1ee02c..0f1e157 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
+Add Hash ULID generator methods. #25
Add a MIN and MAX constants and methods. #26
## [5.2.0] - 2023-??-??
diff --git a/README.md b/README.md
index e45b87e..907e7ca 100644
--- a/README.md
+++ b/README.md
@@ -43,7 +43,7 @@ Add these lines to your `pom.xml`.
* Both types of ULID can be easily created by this generator, i.e. monotonic * and non-monotonic. + *
+ * In addition, a "non-standard" hash-based ULID can also be generated, in which + * the random component is replaced with the first 10 bytes of an SHA-256 hash. */ public final class UlidCreator { @@ -37,6 +45,8 @@ public final class UlidCreator { /** * Returns a ULID. + *
+ * The random component is reset for each new ULID generated. * * @return a ULID */ @@ -45,7 +55,9 @@ public final class UlidCreator { } /** - * Returns a ULID with a given time. + * Returns a ULID. + *
+ * The random component is reset for each new ULID generated. * * @param time a number of milliseconds since 1970-01-01 (Unix epoch). * @return a ULID @@ -56,6 +68,9 @@ public final class UlidCreator { /** * Returns a Monotonic ULID. + *
+ * The random component is incremented for each new ULID generated in the same + * millisecond. * * @return a ULID */ @@ -64,7 +79,10 @@ public final class UlidCreator { } /** - * Returns a Monotonic ULID with a given time. + * Returns a Monotonic ULID. + *
+ * The random component is incremented for each new ULID generated in the same + * millisecond. * * @param time a number of milliseconds since 1970-01-01 (Unix epoch). * @return a ULID @@ -73,6 +91,68 @@ public final class UlidCreator { return MonotonicFactoryHolder.INSTANCE.create(time); } + /** + * Returns a Hash ULID. + *
+ * The random component is replaced with the first 10 bytes of an SHA-256 hash. + *
+ * It always returns the same ULID for a specific pair of {@code time} and + * {@code string}. + *
+ * Usage example: + * + *
{@code
+ * long time = file.getCreatedAt();
+ * String name = file.getFileName();
+ * Ulid ulid = HashUlid.generate(time, name);
+ * }
+ *
+ * @param time a number of milliseconds since 1970-01-01 (Unix epoch).
+ * @param string a string to be hashed using SHA-256 algorithm.
+ * @return a ULID
+ * @since 5.2.0
+ */
+ public static Ulid getHashUlid(final long time, String string) {
+ byte[] bytes = string.getBytes(StandardCharsets.UTF_8);
+ return getHashUlid(time, bytes);
+ }
+
+ /**
+ * Returns a Hash ULID.
+ * + * The random component is replaced with the first 10 bytes of an SHA-256 hash. + *
+ * It always returns the same ULID for a specific pair of {@code time} and + * {@code bytes}. + *
+ * Usage example: + * + *
{@code
+ * long time = file.getCreatedAt();
+ * byte[] bytes = file.getFileBinary();
+ * Ulid ulid = HashUlid.generate(time, bytes);
+ * }
+ *
+ * @param time a number of milliseconds since 1970-01-01 (Unix epoch).
+ * @param bytes a byte array to be hashed using SHA-256 algorithm.
+ * @return a ULID
+ * @since 5.2.0
+ */
+ public static Ulid getHashUlid(final long time, byte[] bytes) {
+ // Calculate the hash and take the first 10 bytes
+ byte[] hash = hasher("SHA-256").digest(bytes);
+ byte[] rand = Arrays.copyOf(hash, 10);
+ return new Ulid(time, rand);
+ }
+
+ private static MessageDigest hasher(final String algorithm) {
+ try {
+ return MessageDigest.getInstance(algorithm);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(String.format("%s not supported", algorithm));
+ }
+ }
+
private static class UlidFactoryHolder {
static final UlidFactory INSTANCE = UlidFactory.newInstance();
}
diff --git a/src/test/java/com/github/f4b6a3/ulid/UlidTest.java b/src/test/java/com/github/f4b6a3/ulid/UlidTest.java
index 0337bf3..f1d66e0 100644
--- a/src/test/java/com/github/f4b6a3/ulid/UlidTest.java
+++ b/src/test/java/com/github/f4b6a3/ulid/UlidTest.java
@@ -629,6 +629,32 @@ public class UlidTest extends UlidFactoryTest {
checkCreationTime(list, startTime, endTime);
}
+ @Test
+ public void testGetHashUlid() throws NoSuchAlgorithmException {
+
+ Ulid prev = Ulid.MIN;
+ for (int i = 0; i < DEFAULT_LOOP_MAX; i++) {
+
+ long time = (new Random()).nextLong() >>> 16;
+ String string = UUID.randomUUID().toString();
+ Ulid ulid = UlidCreator.getHashUlid(time, string);
+
+ assertNotNull(ulid);
+ assertNotEquals(prev, ulid);
+ assertNotEquals(Ulid.MIN, ulid);
+ assertNotEquals(Ulid.MAX, ulid);
+ assertEquals(time, ulid.getTime());
+ assertEquals(Arrays.toString(ulid.getRandom()), Arrays.toString(ulid.getRandom()));
+
+ MessageDigest md = MessageDigest.getInstance("SHA-256");
+ byte[] utf8 = string.getBytes(StandardCharsets.UTF_8);
+ byte[] hash = Arrays.copyOf(md.digest(utf8), 10);
+ assertEquals(Arrays.toString(ulid.getRandom()), Arrays.toString(hash));
+
+ prev = ulid;
+ }
+ }
+
public static Ulid fromString(String string) {
long time = 0;