From e52b2cdefdb33d3008aa9421a7eb9c7c3e9665bc Mon Sep 17 00:00:00 2001 From: Fabio Lima Date: Mon, 16 Nov 2020 01:39:52 -0300 Subject: [PATCH] Add test cases, and remove unused code List of changes: Change UlidUtil // remove unused code Change UlidUtilTest // add tests and remove unused code Change UlidStructTest // add tests Change UlidValidator // add methods for char[] args Optimaze UlidSpecCreator // small optimization Optimaze UlidStruct // small optimization Update README.md --- README.md | 41 +- .../f4b6a3/ulid/creator/UlidSpecCreator.java | 15 +- .../com/github/f4b6a3/ulid/util/UlidUtil.java | 198 +--------- .../f4b6a3/ulid/util/UlidValidator.java | 41 +- .../f4b6a3/ulid/util/internal/UlidStruct.java | 20 +- .../github/f4b6a3/ulid/util/UlidUtilTest.java | 354 ++++++------------ .../ulid/util/internal/UlidStructTest.java | 110 +++++- 7 files changed, 281 insertions(+), 498 deletions(-) diff --git a/README.md b/README.md index 37c17ab..3e2a222 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Add these lines to your `pom.xml`. com.github.f4b6a3 ulid-creator - 2.3.2 + 2.3.3 ``` See more options in [maven.org](https://search.maven.org/artifact/com.github.f4b6a3/ulid-creator). @@ -158,37 +158,20 @@ Benchmark This section shows benchmarks comparing `UlidCreator` to `java.util.UUID`. -* **ulid-creator v2.1.0:** - ``` ---------------------------------------------------------------------------- -THROUGHPUT Mode Cnt Score Error Units ---------------------------------------------------------------------------- -Throughput.JDK_RandomBased thrpt 5 2196,215 ± 13,668 ops/ms -Throughput.UlidCreator_Ulid thrpt 5 19224,340 ± 106,231 ops/ms -Throughput.UlidCreator_UlidString thrpt 5 5006,424 ± 26,946 ops/ms ---------------------------------------------------------------------------- -Total time: 00:04:01 ---------------------------------------------------------------------------- +================================================================================ +THROUGHPUT (operations/millis) Mode Cnt Score Error Units +================================================================================ +Throughput.JDK_RandomBased thrpt 5 2050,995 ± 21,636 ops/ms +-------------------------------------------------------------------------------- +Throughput.UlidCreator_Ulid thrpt 5 18524,721 ± 563,781 ops/ms +Throughput.UlidCreator_UlidString thrpt 5 12223,501 ± 89,836 ops/ms +================================================================================ +Total time: 00:04:00 +================================================================================ ``` -* **ulid-creator v2.2.0:** - -``` ---------------------------------------------------------------------------- -THROUGHPUT Mode Cnt Score Error Units ---------------------------------------------------------------------------- -Throughput.JDK_RandomBased thrpt 5 2191,690 ± 8,947 ops/ms -Throughput.UlidCreator_Ulid thrpt 5 19236,123 ± 156,123 ops/ms -Throughput.UlidCreator_UlidString thrpt 5 12893,016 ± 179,618 ops/ms <- 2.5x faster ---------------------------------------------------------------------------- -Total time: 00:04:01 ---------------------------------------------------------------------------- -``` - -The ULID string generation is 2.5x faster in version 2.2.0 than before. - -System: CPU i5-3330, 8G RAM, Ubuntu 20.04. +System: JVM 8, Ubuntu 20.04, CPU i5-3330, 8G RAM. See: [uuid-creator-benchmark](https://github.com/fabiolimace/uuid-creator-benchmark) diff --git a/src/main/java/com/github/f4b6a3/ulid/creator/UlidSpecCreator.java b/src/main/java/com/github/f4b6a3/ulid/creator/UlidSpecCreator.java index 6c086be..73034ee 100644 --- a/src/main/java/com/github/f4b6a3/ulid/creator/UlidSpecCreator.java +++ b/src/main/java/com/github/f4b6a3/ulid/creator/UlidSpecCreator.java @@ -32,7 +32,6 @@ import com.github.f4b6a3.ulid.strategy.random.DefaultRandomStrategy; import com.github.f4b6a3.ulid.strategy.random.OtherRandomStrategy; import com.github.f4b6a3.ulid.strategy.TimestampStrategy; import com.github.f4b6a3.ulid.strategy.timestamp.DefaultTimestampStrategy; -import com.github.f4b6a3.ulid.util.UlidUtil; import com.github.f4b6a3.ulid.util.internal.UlidStruct; /** @@ -193,9 +192,19 @@ public class UlidSpecCreator { // Get random values final byte[] bytes = new byte[10]; this.randomStrategy.nextBytes(bytes); - this.random1 = UlidUtil.toNumber(bytes, 0, 5); - this.random2 = UlidUtil.toNumber(bytes, 5, 10); + + this.random1 = (long) (bytes[0x0] & 0xff) << 32; + this.random1 |= (long) (bytes[0x1] & 0xff) << 24; + this.random1 |= (long) (bytes[0x2] & 0xff) << 16; + this.random1 |= (long) (bytes[0x3] & 0xff) << 8; + this.random1 |= (long) (bytes[0x4] & 0xff); + this.random2 = (long) (bytes[0x5] & 0xff) << 32; + this.random2 |= (long) (bytes[0x6] & 0xff) << 24; + this.random2 |= (long) (bytes[0x7] & 0xff) << 16; + this.random2 |= (long) (bytes[0x8] & 0xff) << 8; + this.random2 |= (long) (bytes[0x9] & 0xff); + // Save the random values this.randomMax1 = this.random1 | INCREMENT_MAX; this.randomMax2 = this.random2 | INCREMENT_MAX; diff --git a/src/main/java/com/github/f4b6a3/ulid/util/UlidUtil.java b/src/main/java/com/github/f4b6a3/ulid/util/UlidUtil.java index 3ee09b6..58331c1 100644 --- a/src/main/java/com/github/f4b6a3/ulid/util/UlidUtil.java +++ b/src/main/java/com/github/f4b6a3/ulid/util/UlidUtil.java @@ -27,6 +27,8 @@ package com.github.f4b6a3.ulid.util; import java.time.Instant; import java.util.UUID; +import com.github.f4b6a3.ulid.util.internal.UlidStruct; + public final class UlidUtil { protected static final int BASE_32 = 32; @@ -61,12 +63,12 @@ public final class UlidUtil { return Instant.ofEpochMilli(milliseconds); } - private static long extractTimestamp(UUID ulid) { + protected static long extractTimestamp(UUID ulid) { return (ulid.getMostSignificantBits() >>> 16); } - private static long extractTimestamp(String ulid) { - return fromBase32Crockford(extractTimestampComponent(ulid).toCharArray()); + protected static long extractTimestamp(String ulid) { + return UlidStruct.of(ulid).time; } public static String extractTimestampComponent(String ulid) { @@ -78,194 +80,4 @@ public final class UlidUtil { UlidValidator.validate(ulid); return ulid.substring(10, ULID_LENGTH); } - - /** - * Get a number from a given array of bytes. - * - * @param bytes a byte array - * @return a long - */ - public static long toNumber(final byte[] bytes) { - return toNumber(bytes, 0, bytes.length); - } - - public static long toNumber(final byte[] bytes, final int start, final int end) { - long result = 0; - for (int i = start; i < end; i++) { - result = (result << 8) | (bytes[i] & 0xff); - } - return result; - } - - /** - * Get an array of bytes from a given number. - * - * @param number a long value - * @return a byte array - */ - protected static byte[] toBytes(final long number) { - return new byte[] { (byte) (number >>> 56), (byte) (number >>> 48), (byte) (number >>> 40), - (byte) (number >>> 32), (byte) (number >>> 24), (byte) (number >>> 16), (byte) (number >>> 8), - (byte) (number) }; - } - - protected static char[] removeHyphens(final char[] input) { - - int count = 0; - char[] buffer = new char[input.length]; - - for (int i = 0; i < input.length; i++) { - if ((input[i] != '-')) { - buffer[count++] = input[i]; - } - } - - char[] output = new char[count]; - System.arraycopy(buffer, 0, output, 0, count); - - return output; - } - - public static char[] toBase32Crockford(long number) { - return encode(number, UlidUtil.ALPHABET_CROCKFORD); - } - - public static long fromBase32Crockford(char[] chars) { - return decode(chars, UlidUtil.ALPHABET_CROCKFORD); - } - - protected static boolean isCrockfordBase32(final char[] chars) { - char[] input = toUpperCase(chars); - for (int i = 0; i < input.length; i++) { - if (!isCrockfordBase32(input[i])) { - return false; - } - } - return true; - } - - protected static boolean isCrockfordBase32(char c) { - for (int j = 0; j < ALPHABET_CROCKFORD.length; j++) { - if (c == ALPHABET_CROCKFORD[j]) { - return true; - } - } - return false; - } - - protected static char[] zerofill(char[] chars, int length) { - return lpad(chars, length, '0'); - } - - protected static char[] lpad(char[] chars, int length, char fill) { - - int delta = 0; - int limit = 0; - - if (length > chars.length) { - delta = length - chars.length; - limit = length; - } else { - delta = 0; - limit = chars.length; - } - - char[] output = new char[chars.length + delta]; - for (int i = 0; i < limit; i++) { - if (i < delta) { - output[i] = fill; - } else { - output[i] = chars[i - delta]; - } - } - return output; - } - - protected static String transliterate(String string, char[] alphabet1, char[] alphabet2) { - return new String(transliterate(string.toCharArray(), alphabet1, alphabet2)); - } - - protected static char[] transliterate(char[] chars, char[] alphabet1, char[] alphabet2) { - char[] output = chars.clone(); - for (int i = 0; i < output.length; i++) { - for (int j = 0; j < alphabet1.length; j++) { - if (output[i] == alphabet1[j]) { - output[i] = alphabet2[j]; - break; - } - } - } - return output; - } - - protected static char[] toUpperCase(final char[] chars) { - char[] output = new char[chars.length]; - for (int i = 0; i < output.length; i++) { - if (chars[i] >= 0x61 && chars[i] <= 0x7a) { - output[i] = (char) ((int) chars[i] & 0xffffffdf); - } else { - output[i] = chars[i]; - } - } - return output; - } - - /** - * Encode a long number to base 32 char array. - * - * @param number a long number - * @param alphabet an alphabet - * @return a base32 encoded char array - */ - protected static char[] encode(long number, char[] alphabet) { - - final int CHARS_MAX = 13; // 13 * 5 = 65 - - if (number < 0) { - throw new IllegalArgumentException(String.format("Number '%d' is not a positive integer.", number)); - } - - long n = number; - char[] buffer = new char[CHARS_MAX]; - char[] output; - - int count = CHARS_MAX; - while (n > 0) { - buffer[--count] = alphabet[(int) (n % BASE_32)]; - n = n / BASE_32; - } - - output = new char[buffer.length - count]; - System.arraycopy(buffer, count, output, 0, output.length); - - return output; - } - - /** - * Decode a base 32 char array to a long number. - * - * @param chars a base 32 encoded char array - * @param alphabet an alphabet - * @return a long number - */ - protected static long decode(char[] chars, char[] alphabet) { - - long n = 0; - - for (int i = 0; i < chars.length; i++) { - int d = chr(chars[i], alphabet); - n = BASE_32 * n + d; - } - - return n; - } - - private static int chr(char c, char[] alphabet) { - for (int i = 0; i < alphabet.length; i++) { - if (alphabet[i] == c) { - return (byte) i; - } - } - return (byte) '0'; - } } diff --git a/src/main/java/com/github/f4b6a3/ulid/util/UlidValidator.java b/src/main/java/com/github/f4b6a3/ulid/util/UlidValidator.java index 04b32e5..d739aa0 100644 --- a/src/main/java/com/github/f4b6a3/ulid/util/UlidValidator.java +++ b/src/main/java/com/github/f4b6a3/ulid/util/UlidValidator.java @@ -59,7 +59,30 @@ public final class UlidValidator { } /** - * Checks if the ULID string is a valid. + * Checks if the char array is a valid ULID. + * + * A valid ULID string is a sequence of 26 characters from Crockford's base 32 + * alphabet. + * + * It also checks if the timestamp is between 0 and 2^48-1. + * + *
+	 * Examples of valid ULID strings:
+	 * - 0123456789ABCDEFGHJKMNPKRS (26 alphanumeric, case insensitive, except U)
+	 * - 0123456789ABCDEFGHIJKLMNOP (26 alphanumeric, case insensitive, including OIL, except U)
+	 * - 0123456789-ABCDEFGHJK-MNPKRS (26 alphanumeric, case insensitive, except U, with hyphens)
+	 * - 0123456789-ABCDEFGHIJ-KLMNOP (26 alphanumeric, case insensitive, including OIL, except U, with hyphens)
+	 * 
+ * + * @param ulid a ULID char array + * @return boolean true if valid + */ + public static boolean isValid(char[] ulid) { + return (ulid != null && ulid.length != 0 && isValidString(ulid)); + } + + /** + * Checks if the ULID string is valid. * * See {@link UlidValidator#isValid(String)}. * @@ -68,7 +91,21 @@ public final class UlidValidator { */ public static void validate(String ulid) { if (ulid == null || ulid.length() == 0 || !isValidString(ulid.toCharArray())) { - throw new InvalidUlidException(String.format("Invalid ULID: %s.", ulid)); + throw new InvalidUlidException("Invalid ULID: \"" + ulid + "\""); + } + } + + /** + * Checks if the ULID char array is valid. + * + * See {@link UlidValidator#isValid(String)}. + * + * @param ulid a ULID char array + * @throws InvalidUlidException if invalid + */ + public static void validate(char[] ulid) { + if (ulid == null || ulid.length == 0 || !isValidString(ulid)) { + throw new InvalidUlidException("Invalid ULID: \"" + (ulid == null ? null : new String(ulid)) + "\""); } } diff --git a/src/main/java/com/github/f4b6a3/ulid/util/internal/UlidStruct.java b/src/main/java/com/github/f4b6a3/ulid/util/internal/UlidStruct.java index 7f0ae43..92b8d49 100644 --- a/src/main/java/com/github/f4b6a3/ulid/util/internal/UlidStruct.java +++ b/src/main/java/com/github/f4b6a3/ulid/util/internal/UlidStruct.java @@ -130,23 +130,23 @@ public final class UlidStruct { this.random2 = random2 & HALF_RANDOM_COMPONENT; } - private UlidStruct(UUID uuid) { - final long msb = uuid.getMostSignificantBits(); - final long lsb = uuid.getLeastSignificantBits(); + private UlidStruct(UUID ulid) { + final long msb = ulid.getMostSignificantBits(); + final long lsb = ulid.getLeastSignificantBits(); this.time = (msb >>> 16); this.random1 = ((msb & 0x000000000000ffffL) << 24) | (lsb >>> 40); this.random2 = (lsb & 0x000000ffffffffffL); } - private UlidStruct(String string) { + private UlidStruct(String ulid) { - UlidValidator.validate(string); + final char[] chars = ulid == null ? new char[0] : ulid.toCharArray(); + UlidValidator.validate(chars); long tm = 0; long r1 = 0; long r2 = 0; - final char[] chars = string.toCharArray(); tm |= BASE32_VALUES[chars[0x00]] << 45; tm |= BASE32_VALUES[chars[0x01]] << 40; @@ -232,8 +232,8 @@ public final class UlidStruct { public String toString4() { // apply RFC-4122 version 4 and variant 2 - final long newrandom1 = ((this.random1 & 0x0fff3fffffL) | 0x4000000000L) | 0x0000800000L; - return UlidStruct.of(this.time, newrandom1, this.random2).toString(); + final long random1v4 = ((this.random1 & 0x0fff3fffffL) | 0x4000000000L) | 0x0000800000L; + return UlidStruct.of(this.time, random1v4, this.random2).toString(); } public UUID toUuid() { @@ -246,8 +246,8 @@ public final class UlidStruct { public UUID toUuid4() { // apply RFC-4122 version 4 and variant 2 - final long newrandom1 = ((this.random1 & 0x0fff3fffffL) | 0x4000000000L) | 0x0000800000L; - return UlidStruct.of(this.time, newrandom1, this.random2).toUuid(); + final long random1v4 = ((this.random1 & 0x0fff3fffffL) | 0x4000000000L) | 0x0000800000L; + return UlidStruct.of(this.time, random1v4, this.random2).toUuid(); } @Override diff --git a/src/test/java/com/github/f4b6a3/ulid/util/UlidUtilTest.java b/src/test/java/com/github/f4b6a3/ulid/util/UlidUtilTest.java index e017393..d9eafc4 100644 --- a/src/test/java/com/github/f4b6a3/ulid/util/UlidUtilTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/util/UlidUtilTest.java @@ -2,293 +2,155 @@ package com.github.f4b6a3.ulid.util; import static org.junit.Assert.*; import java.time.Instant; -import java.util.UUID; +import java.util.Random; import org.junit.Test; import com.github.f4b6a3.ulid.exception.InvalidUlidException; +import com.github.f4b6a3.ulid.util.internal.UlidStructTest; + import static com.github.f4b6a3.ulid.util.UlidUtil.*; public class UlidUtilTest { - private static final String EXAMPLE_TIMESTAMP = "0123456789"; - private static final String EXAMPLE_RANDOMNESS = "ABCDEFGHJKMNPQRS"; - private static final String EXAMPLE_ULID = "0123456789ABCDEFGHJKMNPQRS"; - // Date: 10889-08-02T05:31:50.655Z: 281474976710655 (2^48-1) private static final long TIMESTAMP_MAX = 0xffffffffffffL; + private static final long HALF_RANDOM_MAX = 0xffffffffffL; - private static final String[] EXAMPLE_DATES = { "1970-01-01T00:00:00.000Z", "1985-10-26T01:16:00.123Z", - "2001-09-09T01:46:40.456Z", "2020-01-15T14:30:33.789Z", "2038-01-19T03:14:07.321Z" }; + private static final int DEFAULT_LOOP_MAX = 100_000; - private static final int[] NUMBERS = { 102685630, 725393777, 573697669, 614668535, 790665079, 728958755, 966150230, - 410015018, 605266173, 946077566, 214051168, 775737014, 723003700, 391609366, 147844737, 514081413, - 488279622, 550860813, 611087782, 223492126, 706308515, 158990768, 549042286, 26926303, 775714134, 602886016, - 27282100, 675097356, 641101167, 515280699, 454184468, 371424784, 633917378, 887459583, 792903202, 168552040, - 824806922, 696445335, 653338746, 357696553, 353677217, 972662902, 400738139, 537701151, 202077579, - 110209145, 356152341, 168702810, 684185451, 419840003, 480132486, 308833881, 997154252, 918202260, - 103304091, 328467776, 648729690, 733655121, 645189051, 342500864, 560919543, 509761384, 626871960, - 429248550, 319025067, 507317265, 348303729, 256009160, 660250872, 85224414, 414490625, 355994979, 318005886, - 326093128, 492813589, 569014099, 503350412, 168303553, 801566586, 800368918, 742601973, 395588591, - 257341245, 722366808, 501878988, 200718306, 184948029, 149469829, 992401543, 240364551, 976817281, - 161998068, 515579566, 275182272, 376045488, 899163436, 941443452, 974372015, 934795357, 958806784 }; - - private static final String[] NUMBERS_BASE_32_CROCKFORD = { "31XPXY", "NKS8BH", "H33VM5", "JA667Q", "QJ15VQ", - "NQ61S3", "WSCJ2P", "C70N9A", "J1787X", "W67ZVY", "6C4AB0", "Q3SKNP", "NHGA9M", "BNEZ0P", "4CZVM1", - "FA8GM5", "EHN3J6", "GDAY0D", "J6RXD6", "6N4E0Y", "N1JTD3", "4QM0DG", "GBKE3E", "SNQ6Z", "Q3RXAP", "HYYKW0", - "T0JNM", "M3TARC", "K3CVBF", "FBD3SV", "DH4KGM", "B26ZGG", "JWHKY2", "TEB3QZ", "QM5FH2", "50QSK8", "RJK3GA", - "MR5TCQ", "KF2A3T", "AN4119", "AH9BX1", "WZKA3P", "BY5HTV", "G0SARZ", "60PXCB", "393A3S", "AKMX0N", - "50WCTT", "MCFNVB", "CGCG03", "E9WFC6", "96GVJS", "XPYQEC", "VBN9WM", "32GJWV", "9S81A0", "KANN2T", - "NVNC2H", "K79KDV", "A6M9G0", "GPXWZQ", "F64NV8", "JNTKMR", "CSBM16", "9G7VXB", "F3T30H", "AC5CBH", - "7M4RY8", "KNN87R", "2H8TYY", "CB9801", "AKG3B3", "9F8RKY", "9PZJA8", "ENZF8N", "GYMXTK", "F0114C", - "50G6Y1", "QWDVVT", "QV9A8P", "P46D7N", "BS8CZF", "7NDDSX", "NGWWAR", "EYM46C", "5ZDDZ2", "5GC59X", - "4EHEM5", "XJDP47", "757B07", "X3J341", "4TFS7M", "FBP7NE", "86DWP0", "B6KZXG", "TSG99C", "W1TJBW", - "X17F5F", "VVFP2X", "WJCER0" }; + private static final Random RANDOM = new Random(); @Test public void testExtractTimestamp1() { + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { - String ulid = "0000000000" + EXAMPLE_RANDOMNESS; - long milliseconds = extractUnixMilliseconds(ulid); - assertEquals(0, milliseconds); + long time = RANDOM.nextLong() & TIMESTAMP_MAX; + long random1 = RANDOM.nextLong() & HALF_RANDOM_MAX; + long random2 = RANDOM.nextLong() & HALF_RANDOM_MAX; - ulid = "7ZZZZZZZZZ" + EXAMPLE_RANDOMNESS; - milliseconds = extractUnixMilliseconds(ulid); - assertEquals(TIMESTAMP_MAX, milliseconds); - - try { - // Test the first extra bit added by the base32 encoding - ulid = "G0000000000000000000000000"; - extractUnixMilliseconds(ulid); - fail("Should throw an InvalidUlidException"); - } catch (InvalidUlidException e) { - // success + String timeComponent = UlidStructTest.toTimeComponent(time); + String randomComponent = UlidStructTest.toRandomComponent(random1, random2); + + String ulid = timeComponent + randomComponent; + long result = extractTimestamp(ulid); + assertEquals(time, result); } - - try { - // Test the second extra bit added by the base32 encoding - ulid = "80000000000000000000000000"; - extractUnixMilliseconds(ulid); - fail("Should throw an InvalidUlidException"); - } catch (InvalidUlidException e) { - // success - } - } - - @Test - public void testExtractTimestamp2() { - - String string = "0000000000" + EXAMPLE_RANDOMNESS; - UUID ulid = UlidConverter.fromString(string); - long milliseconds = extractUnixMilliseconds(ulid); - assertEquals(0, milliseconds); - - string = "7ZZZZZZZZZ" + EXAMPLE_RANDOMNESS; - ulid = UlidConverter.fromString(string); - milliseconds = extractUnixMilliseconds(ulid); - assertEquals(TIMESTAMP_MAX, milliseconds); } @Test - public void testExtractTimestampList() { + public void testExtractTimestamp2() { + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + + String ulid; + String timeComponent; + String randomComponent; + + long time; + long random1; + long random2; + + time = RANDOM.nextLong() & TIMESTAMP_MAX; + random1 = RANDOM.nextLong() & HALF_RANDOM_MAX; + random2 = RANDOM.nextLong() & HALF_RANDOM_MAX; + + timeComponent = UlidStructTest.toTimeComponent(time); + randomComponent = UlidStructTest.toRandomComponent(random1, random2); - String randomnessComponent = EXAMPLE_RANDOMNESS; + timeComponent = "7ZZZZZZZZZ"; + ulid = timeComponent + randomComponent; + time = extractTimestamp(ulid); + assertEquals(TIMESTAMP_MAX, time); + + timeComponent = "0000000000"; + ulid = timeComponent + randomComponent; + time = extractTimestamp(ulid); + assertEquals(0, time); - for (String i : EXAMPLE_DATES) { - long milliseconds = Instant.parse(i).toEpochMilli(); + try { + // Test the first extra bit added by the base32 encoding + char[] chars = timeComponent.toCharArray(); + chars[0] = 'G'; // GZZZZZZZZZ + timeComponent = new String(chars); + ulid = timeComponent + randomComponent; + extractTimestamp(ulid); + fail("Should throw an InvalidUlidException"); + } catch (InvalidUlidException e) { + // success + } - String timestampComponent = new String(UlidUtil.zerofill(toBase32Crockford(milliseconds), 10)); - String ulid = timestampComponent + randomnessComponent; + try { + // Test the second extra bit added by the base32 encoding + char[] chars = timeComponent.toCharArray(); + chars[0] = '8'; // 8ZZZZZZZZZ + timeComponent = new String(chars); + ulid = timeComponent + randomComponent; + extractTimestamp(ulid); + fail("Should throw an InvalidUlidException"); + } catch (InvalidUlidException e) { + // success + } + } + } + + @Test + public void testExtractUnixMilliseconds() { + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + long time = RANDOM.nextLong() & TIMESTAMP_MAX; + long random1 = RANDOM.nextLong() & HALF_RANDOM_MAX; + long random2 = RANDOM.nextLong() & HALF_RANDOM_MAX; + String ulid = UlidStructTest.toString(time, random1, random2); long result = extractUnixMilliseconds(ulid); - - assertEquals(milliseconds, result); + assertEquals(time, result); } } @Test public void testExtractInstant() { - - String randomnessComponent = EXAMPLE_RANDOMNESS; - - for (String i : EXAMPLE_DATES) { - - Instant instant = Instant.parse(i); - long milliseconds = Instant.parse(i).toEpochMilli(); - - byte[] bytes = new byte[6]; - System.arraycopy(toBytes(milliseconds), 2, bytes, 0, 6); - - String timestampComponent = new String(UlidUtil.zerofill(toBase32Crockford(milliseconds), 10)); - String ulid = timestampComponent + randomnessComponent; + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + long time = RANDOM.nextLong() & TIMESTAMP_MAX; + Instant instant = Instant.ofEpochMilli(time); + long random1 = RANDOM.nextLong() & HALF_RANDOM_MAX; + long random2 = RANDOM.nextLong() & HALF_RANDOM_MAX; + String ulid = UlidStructTest.toString(time, random1, random2); Instant result = extractInstant(ulid); - assertEquals(instant, result); } } @Test public void testExtractTimestampComponent() { - String ulid = EXAMPLE_ULID; - String expected = EXAMPLE_TIMESTAMP; - String result = extractTimestampComponent(ulid); - assertEquals(expected, result); - } + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + long time = RANDOM.nextLong() & TIMESTAMP_MAX; + long random1 = RANDOM.nextLong() & HALF_RANDOM_MAX; + long random2 = RANDOM.nextLong() & HALF_RANDOM_MAX; + String ulid = UlidStructTest.toString(time, random1, random2); + + char[] chars = ulid.toCharArray(); + char[] timeComponent = new char[10]; + System.arraycopy(chars, 0, timeComponent, 0, 10); + String expected = new String(timeComponent); - @Test - public void testExtractRandomnessComponent() { - String ulid = EXAMPLE_ULID; - String expected = EXAMPLE_RANDOMNESS; - String result = extractRandomnessComponent(ulid); - assertEquals(expected, result); - } - - @Test - public void testToUpperCase() { - String string = "Aq7zmxKxPc61QKiGRu8Y3PdYMer64lrRxfb9A5JAJuDeEhXSrbsxsaUoHrFzmEJUYBKJPgV+1rAd"; - char[] chars1 = string.toCharArray(); - char[] chars2 = UlidUtil.toUpperCase(chars1); - assertEquals(new String(string).toUpperCase(), new String(chars2)); - - string = "kL9zTzZfzlwKYCEmWKPxFYxINf6JZCSmSqykyG5ONWZcFkJG2WGc7gq71YCEzt2hYcsTvfQqEmn0"; - chars1 = string.toCharArray(); - chars2 = UlidUtil.toUpperCase(chars1); - assertEquals(new String(string).toUpperCase(), new String(chars2)); - - string = "XXEOUV3jJb3f+wpRPDVke9NgWwEgdkzChnKnpZZWS/mCSqTi757GmmqYdzuDGOa5ftqHI3/zqKrS"; - chars1 = string.toCharArray(); - chars2 = UlidUtil.toUpperCase(chars1); - assertEquals(new String(string).toUpperCase(), new String(chars2)); - - string = "t9LVRQZbCxTQgaxlajNE/VYpLpKiHtKt7jHrtxSDIJ2hrHaJI2UPF1zA7I35m9cKz01lHYD1IXlM"; - chars1 = string.toCharArray(); - chars2 = UlidUtil.toUpperCase(chars1); - assertEquals(new String(string).toUpperCase(), new String(chars2)); - - string = "jyS52J42LLT6GY+Zywo1R4tQv4bTfAqpFB6aiKEuA3yDxFkuXzuKe8PaGlUTaXD5WgRFMnO9nRLU"; - chars1 = string.toCharArray(); - chars2 = UlidUtil.toUpperCase(chars1); - assertEquals(new String(string).toUpperCase(), new String(chars2)); - } - - @Test - public void testZerofill() { - assertEquals("001", new String(UlidUtil.zerofill("1".toCharArray(), 3))); - assertEquals("000123", new String(UlidUtil.zerofill("123".toCharArray(), 6))); - assertEquals("0000000000", new String(UlidUtil.zerofill("".toCharArray(), 10))); - assertEquals("9876543210", new String(UlidUtil.zerofill("9876543210".toCharArray(), 10))); - assertEquals("0000000000123456", new String(UlidUtil.zerofill("123456".toCharArray(), 16))); - } - - @Test - public void testLpad() { - - String string = ""; - char[] chars1 = string.toCharArray(); - char[] chars2 = UlidUtil.lpad(chars1, 8, 'x'); - assertEquals("xxxxxxxx", new String(chars2)); - - string = ""; - chars1 = string.toCharArray(); - chars2 = UlidUtil.lpad(chars1, 12, 'W'); - assertEquals("WWWWWWWWWWWW", new String(chars2)); - - string = "TCgpYATMlK9BmSzX"; - chars1 = string.toCharArray(); - chars2 = UlidUtil.lpad(chars1, 13, '0'); - assertEquals(string, new String(chars2)); - - string = "2kgy3m9U646L6TJ5"; - chars1 = string.toCharArray(); - chars2 = UlidUtil.lpad(chars1, 16, '0'); - assertEquals(string, new String(chars2)); - - string = "2kgy3m9U646L6TJ5"; - chars1 = string.toCharArray(); - chars2 = UlidUtil.lpad(chars1, 17, '0'); - assertEquals("0" + string, new String(chars2)); - - string = "LH6hfYcGJu06xSNF"; - chars1 = string.toCharArray(); - chars2 = UlidUtil.lpad(chars1, 25, '0'); - assertEquals("000000000" + string, new String(chars2)); - - string = "t9LVRQZbCxTQgaxlajNE/VYpLpKiHtKt7jHrtxSDIJ2hrHaJI2UPF1zA7I35m9cKz01lHYD1IXlM"; - chars1 = string.toCharArray(); - chars2 = UlidUtil.lpad(chars1, 80, '0'); - assertEquals("0000" + string, new String(chars2)); - } - - @Test - public void testRemoveHyphens() { - String string = "-ZGQ8yCsza-RFxlYyA-FaXa4wd-k4Owa-/ITDvqWOl4-Do3/NwW--Lawx6GcO-LmSRDsd3af3Zt-VMNnvLgIw9-"; - char[] chars1 = string.toCharArray(); - char[] chars2 = UlidUtil.removeHyphens(chars1); - assertEquals(string.replace("-", ""), new String(chars2)); - - string = "qi3q-EMvc1-Kk7XzYMj----SUnwf-lp0K7-Ucj-W-cDplP-2dG-3x+5y-r9JBc-ZT-0e--cRHoMbU/lBzZsJ6rcJ5zT/J"; - chars1 = string.toCharArray(); - chars2 = UlidUtil.removeHyphens(chars1); - assertEquals(string.replace("-", ""), new String(chars2)); - - string = "RXMD0DJV---Zf3Jqcv39uGjzBuiLkLNL-IvPnTyfMteEet-I7u-Z8oyE+BIUBf/OPi30iICP1TnQpMve4j"; - chars1 = string.toCharArray(); - chars2 = UlidUtil.removeHyphens(chars1); - assertEquals(string.replace("-", ""), new String(chars2)); - - string = "---dFH-b-ylQPA60-kuRxZ9-6q5MLd1-qLTKdma-rF2yEABt-t6mJg0U-ibIYcVnt-Guqdn-z-G-43Ob-W/-Gxah1+53a-"; - chars1 = string.toCharArray(); - chars2 = UlidUtil.removeHyphens(chars1); - assertEquals(string.replace("-", ""), new String(chars2)); - - string = "-------------------------------"; - chars1 = string.toCharArray(); - chars2 = UlidUtil.removeHyphens(chars1); - assertEquals(string.replace("-", ""), new String(chars2)); - } - - @Test - public void testTransliterate() { - - char[] alphabetCrockford = "0123456789ABCDEFGHJKMNPQRSTVWXYZ".toCharArray(); - char[] alphabetDefault = "0123456789abcdefghijklmnopqrstuv".toCharArray(); - - char[] chars2 = UlidUtil.transliterate(alphabetCrockford, alphabetCrockford, alphabetDefault); - assertEquals(new String(alphabetDefault), new String(chars2)); - - chars2 = UlidUtil.transliterate(alphabetDefault, alphabetDefault, alphabetCrockford); - assertEquals(new String(alphabetCrockford), new String(chars2)); - } - - @Test - public void testIsBase32Crockford() { - assertTrue(UlidUtil.isCrockfordBase32("16JD".toCharArray())); - assertTrue(UlidUtil.isCrockfordBase32("01BX5ZZKBKACTAV9WEVGEMMVRY".toCharArray())); - assertTrue(UlidUtil.isCrockfordBase32(UlidUtil.ALPHABET_CROCKFORD)); - assertFalse(UlidUtil.isCrockfordBase32("U6JD".toCharArray())); - assertFalse(UlidUtil.isCrockfordBase32("*1BX5ZZKBKACTAV9WEVGEMMVRY".toCharArray())); - assertFalse(UlidUtil.isCrockfordBase32("u".toCharArray())); - assertFalse(UlidUtil.isCrockfordBase32("U".toCharArray())); - } - - @Test - public void testToBase32Crockford() { - assertEquals("7ZZZZZZZZZ", new String(UlidUtil.toBase32Crockford(281474976710655L))); - // Encode from long to base 32 - for (int i = 0; i < NUMBERS.length; i++) { - String result = new String(UlidUtil.toBase32Crockford(NUMBERS[i])); - assertEquals(NUMBERS_BASE_32_CROCKFORD[i].length(), result.length()); - assertEquals(NUMBERS_BASE_32_CROCKFORD[i], result); + String result = extractTimestampComponent(ulid); + assertEquals(expected, result); } } - + @Test - public void testFromBase32Crockford() { - assertEquals(281474976710655L, UlidUtil.fromBase32Crockford("7ZZZZZZZZZ".toCharArray())); - // Decode from base 32 to long - long number = 0; - for (int i = 0; i < NUMBERS.length; i++) { - number = UlidUtil.fromBase32Crockford((NUMBERS_BASE_32_CROCKFORD[i]).toCharArray()); - assertEquals(NUMBERS[i], number); + public void testExtractRandomnessComponent() { + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + long time = RANDOM.nextLong() & TIMESTAMP_MAX; + long random1 = RANDOM.nextLong() & HALF_RANDOM_MAX; + long random2 = RANDOM.nextLong() & HALF_RANDOM_MAX; + String ulid = UlidStructTest.toString(time, random1, random2); + + char[] chars = ulid.toCharArray(); + char[] randomComponent = new char[16]; + System.arraycopy(chars, 10, randomComponent, 0, 16); + String expected = new String(randomComponent); + + String result = extractRandomnessComponent(ulid); + assertEquals(expected, result); } } } diff --git a/src/test/java/com/github/f4b6a3/ulid/util/internal/UlidStructTest.java b/src/test/java/com/github/f4b6a3/ulid/util/internal/UlidStructTest.java index 35b8737..f303013 100644 --- a/src/test/java/com/github/f4b6a3/ulid/util/internal/UlidStructTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/util/internal/UlidStructTest.java @@ -14,6 +14,51 @@ public class UlidStructTest { protected static final char[] ALPHABET_CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ".toCharArray(); protected static final char[] ALPHABET_JAVA = "0123456789abcdefghijklmnopqrstuv".toCharArray(); // Long.parseUnsignedLong() + @Test + public void testOfAndToString() { + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + UUID uuid0 = UUID.randomUUID(); + String string0 = toString(uuid0); + String string1 = UlidStruct.of(string0).toString(); + assertEquals(string0, string1); + } + + // Test RFC-4122 UUID version 4 + final long versionMask = 0xffffffffffff0fffL; + final long variantMask = 0x3fffffffffffffffL; + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + UUID uuid0 = UUID.randomUUID(); + String string0 = toString(uuid0); + String string1 = UlidStruct.of(string0).toString4(); // UUID v4 in base32 + UUID uuid1 = toUuid(fromString(string1)); + assertEquals(uuid0.getMostSignificantBits() & versionMask, uuid1.getMostSignificantBits() & versionMask); + assertEquals(uuid0.getLeastSignificantBits() & variantMask, uuid1.getLeastSignificantBits() & variantMask); + assertEquals(4, uuid1.version()); + assertEquals(2, uuid1.variant()); + } + } + + @Test + public void testOfAndToUuid() { + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + UUID uuid0 = UUID.randomUUID(); + UUID uuid1 = UlidStruct.of(uuid0).toUuid(); + assertEquals(uuid0, uuid1); + } + + // Test RFC-4122 UUID version 4 + final long versionMask = 0xffffffffffff0fffL; + final long variantMask = 0x3fffffffffffffffL; + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + UUID uuid0 = UUID.randomUUID(); + UUID uuid1 = UlidStruct.of(uuid0).toUuid4(); // UUID v4 + assertEquals(uuid0.getMostSignificantBits() & versionMask, uuid1.getMostSignificantBits() & versionMask); + assertEquals(uuid0.getLeastSignificantBits() & variantMask, uuid1.getLeastSignificantBits() & variantMask); + assertEquals(4, uuid1.version()); + assertEquals(2, uuid1.variant()); + } + } + @Test public void testConstructorLongs() { for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { @@ -90,7 +135,7 @@ public class UlidStructTest { } } - public UlidStruct fromString(String string) { + public static UlidStruct fromString(String string) { long time = 0; long random1 = 0; @@ -111,7 +156,7 @@ public class UlidStructTest { return UlidStruct.of(time, random1, random2); } - public UUID toUuid(UlidStruct struct) { + public static UUID toUuid(UlidStruct struct) { long time = struct.time & 0xffffffffffffL; long random1 = struct.random1 & 0xffffffffffL; @@ -123,24 +168,60 @@ public class UlidStructTest { return new UUID(msb, lsb); } - public String toString(UlidStruct struct) { + public static UUID toUuid(final long time, final long random1, final long random2) { + long tm = time & 0xffffffffffffL; + long r1 = random1 & 0xffffffffffL; + long r2 = random2 & 0xffffffffffL; + + final long msb = (tm << 16) | (r1 >>> 24); + final long lsb = (r1 << 40) | r2; + + return new UUID(msb, lsb); + } + + public static String toString(UlidStruct struct) { + return toString(struct.time, struct.random1, struct.random2); + } + + public static String toString(UUID uuid) { + final long msb = uuid.getMostSignificantBits(); + final long lsb = uuid.getLeastSignificantBits(); + + final long time = (msb >>> 16); + final long random1 = ((msb & 0xffffL) << 24) | (lsb >>> 40); + final long random2 = (lsb & 0xffffffffffL); + + return toString(time, random1, random2); + } + + public static String toString(final long time, final long random1, final long random2) { + String timeComponent = toTimeComponent(time); + String randomComponent = toRandomComponent(random1, random2); + return timeComponent + randomComponent; + } + + public static String toTimeComponent(final long time) { final String tzero = "0000000000"; - final String rzero = "00000000"; + String tm = Long.toUnsignedString(time, 32); + tm = tzero.substring(0, tzero.length() - tm.length()) + tm; + return transliterate(tm, ALPHABET_JAVA, ALPHABET_CROCKFORD); + } + + public static String toRandomComponent(final long random1, final long random2) { - String time = Long.toUnsignedString(struct.time, 32); - String random1 = Long.toUnsignedString(struct.random1, 32); - String random2 = Long.toUnsignedString(struct.random2, 32); + final String zeros = "00000000"; - time = tzero.substring(0, tzero.length() - time.length()) + time; - random1 = rzero.substring(0, rzero.length() - random1.length()) + random1; - random2 = rzero.substring(0, rzero.length() - random2.length()) + random2; + String r1 = Long.toUnsignedString(random1, 32); + String r2 = Long.toUnsignedString(random2, 32); - time = transliterate(time, ALPHABET_JAVA, ALPHABET_CROCKFORD); - random1 = transliterate(random1, ALPHABET_JAVA, ALPHABET_CROCKFORD); - random2 = transliterate(random2, ALPHABET_JAVA, ALPHABET_CROCKFORD); + r1 = zeros.substring(0, zeros.length() - r1.length()) + r1; + r2 = zeros.substring(0, zeros.length() - r2.length()) + r2; - return time + random1 + random2; + r1 = transliterate(r1, ALPHABET_JAVA, ALPHABET_CROCKFORD); + r2 = transliterate(r2, ALPHABET_JAVA, ALPHABET_CROCKFORD); + + return r1 + r2; } private static String transliterate(String string, char[] alphabet1, char[] alphabet2) { @@ -155,5 +236,4 @@ public class UlidStructTest { } return new String(output); } - }