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;