From 54cd5c05959f45bc38b45047843c23afcb941205 Mon Sep 17 00:00:00 2001 From: Fabio Lima Date: Sun, 8 Nov 2020 07:46:26 -0300 Subject: [PATCH] [#4] Optimize the generation of ULID in string format Now the generation of ULID in string format is 2.5x faster than before. List of changes: Optimize UlidSpecCreator Optimize UlidConverter Optimize UlidValidator Create UlidStruct // class for internal use and test cases Add test cases Update README.md Update javadoc Coverage 94.3% --- README.md | 30 ++- .../f4b6a3/ulid/creator/UlidSpecCreator.java | 17 +- .../f4b6a3/ulid/util/UlidConverter.java | 47 +--- .../com/github/f4b6a3/ulid/util/UlidUtil.java | 11 +- .../f4b6a3/ulid/util/UlidValidator.java | 107 ++++++-- .../f4b6a3/ulid/util/internal/UlidStruct.java | 250 ++++++++++++++++++ .../com/github/f4b6a3/ulid/TestSuite.java | 2 + .../f4b6a3/ulid/util/UlidConverterTest.java | 83 ++++++ .../ulid/util/internal/UlidStructTest.java | 159 +++++++++++ 9 files changed, 611 insertions(+), 95 deletions(-) create mode 100644 src/main/java/com/github/f4b6a3/ulid/util/internal/UlidStruct.java create mode 100644 src/test/java/com/github/f4b6a3/ulid/util/internal/UlidStructTest.java diff --git a/README.md b/README.md index 301e9ba..4e0af6d 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Add these lines to your `pom.xml`. com.github.f4b6a3 ulid-creator - 2.1.0 + 2.2.0 ``` See more options in [maven.org](https://search.maven.org/artifact/com.github.f4b6a3/ulid-creator). @@ -146,30 +146,36 @@ Benchmark This section shows benchmarks comparing `UlidCreator` to `java.util.UUID`. +* **ulid-creator v2.1.0:** + ``` --------------------------------------------------------------------------- THROUGHPUT Mode Cnt Score Error Units --------------------------------------------------------------------------- -Throughput.Java_RandomBased thrpt 5 2234,199 ± 2,844 ops/ms -Throughput.UlidCreator_Ulid thrpt 5 19155,742 ± 22,195 ops/ms -Throughput.UlidCreator_UlidString thrpt 5 4946,479 ± 22,800 ops/ms +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 --------------------------------------------------------------------------- ``` +* **ulid-creator v2.2.0:** + ``` ----------------------------------------------------------------------- -AVERAGE TIME Mode Cnt Score Error Units ----------------------------------------------------------------------- -AverageTime.Java_RandomBased avgt 5 449,641 ± 0,994 ns/op -AverageTime.UlidCreator_Ulid avgt 5 52,199 ± 0,185 ns/op -AverageTime.UlidCreator_UlidString avgt 5 202,014 ± 2,111 ns/op ----------------------------------------------------------------------- +--------------------------------------------------------------------------- +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. 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 fd807c3..e17d64e 100644 --- a/src/main/java/com/github/f4b6a3/ulid/creator/UlidSpecCreator.java +++ b/src/main/java/com/github/f4b6a3/ulid/creator/UlidSpecCreator.java @@ -32,8 +32,8 @@ 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.UlidConverter; import com.github.f4b6a3.ulid.util.UlidUtil; +import com.github.f4b6a3.ulid.util.internal.UlidStruct; /** * Factory that creates lexicographically sortable GUIDs, based on the ULID @@ -121,16 +121,8 @@ public class UlidSpecCreator { * @return {@link UUID} a GUID value */ public synchronized UUID create() { - - final long timestamp = this.getTimestamp(); - - final long rnd1 = random1 & HALF_RANDOM_COMPONENT; - final long rnd2 = random2 & HALF_RANDOM_COMPONENT; - - final long msb = (timestamp << 16) | (rnd1 >>> 24); - final long lsb = (rnd1 << 40) | rnd2; - - return new UUID(msb, lsb); + final UlidStruct struct = new UlidStruct(this.getTimestamp(), random1, random2); + return struct.toUuid(); } /** @@ -144,7 +136,8 @@ public class UlidSpecCreator { * @return a ULID string */ public synchronized String createString() { - return UlidConverter.toString(create()); + final UlidStruct struct = new UlidStruct(this.getTimestamp(), random1, random2); + return struct.toString(); } /** diff --git a/src/main/java/com/github/f4b6a3/ulid/util/UlidConverter.java b/src/main/java/com/github/f4b6a3/ulid/util/UlidConverter.java index 3174f45..e685b31 100644 --- a/src/main/java/com/github/f4b6a3/ulid/util/UlidConverter.java +++ b/src/main/java/com/github/f4b6a3/ulid/util/UlidConverter.java @@ -27,8 +27,7 @@ package com.github.f4b6a3.ulid.util; import java.util.UUID; import com.github.f4b6a3.ulid.exception.InvalidUlidException; - -import static com.github.f4b6a3.ulid.util.UlidUtil.*; +import com.github.f4b6a3.ulid.util.internal.UlidStruct; public final class UlidConverter { @@ -43,25 +42,9 @@ public final class UlidConverter { * @param ulid a UUID * @return a ULID */ - public static String toString(UUID ulid) { - - final long msb = ulid.getMostSignificantBits(); - final long lsb = ulid.getLeastSignificantBits(); - - final long time = ((msb & 0xffffffffffff0000L) >>> 16); - final long random1 = ((msb & 0x000000000000ffffL) << 24) | ((lsb & 0xffffff0000000000L) >>> 40); - final long random2 = (lsb & 0x000000ffffffffffL); - - final char[] timeComponent = zerofill(toBase32Crockford(time), 10); - final char[] randomComponent1 = zerofill(toBase32Crockford(random1), 8); - final char[] randomComponent2 = zerofill(toBase32Crockford(random2), 8); - - char[] output = new char[ULID_CHAR_LENGTH]; - System.arraycopy(timeComponent, 0, output, 0, 10); - System.arraycopy(randomComponent1, 0, output, 10, 8); - System.arraycopy(randomComponent2, 0, output, 18, 8); - - return new String(output); + public static String toString(final UUID ulid) { + UlidStruct struct = new UlidStruct(ulid); + return struct.toString(); } /** @@ -77,26 +60,8 @@ public final class UlidConverter { * @throws InvalidUlidException if invalid */ public static UUID fromString(final String ulid) { - - UlidValidator.validate(ulid); - - final char[] input = ulid.toCharArray(); - final char[] timeComponent = new char[10]; - final char[] randomComponent1 = new char[8]; - final char[] randomComponent2 = new char[8]; - - System.arraycopy(input, 0, timeComponent, 0, 10); - System.arraycopy(input, 10, randomComponent1, 0, 8); - System.arraycopy(input, 18, randomComponent2, 0, 8); - - final long time = fromBase32Crockford(timeComponent); - final long random1 = fromBase32Crockford(randomComponent1); - final long random2 = fromBase32Crockford(randomComponent2); - - final long msb = ((time & 0x0000ffffffffffffL) << 16) | ((random1 & 0x000000ffff000000L) >>> 24); - final long lsb = ((random1 & 0x0000000000ffffffL) << 40) | (random2 & 0x000000ffffffffffL); - - return new UUID(msb, lsb); + UlidStruct struct = new UlidStruct(ulid); + return struct.toUuid(); } } 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 71a3e06..d61e76f 100644 --- a/src/main/java/com/github/f4b6a3/ulid/util/UlidUtil.java +++ b/src/main/java/com/github/f4b6a3/ulid/util/UlidUtil.java @@ -30,11 +30,14 @@ public final class UlidUtil { protected static final int BASE_32 = 32; - protected static final int ULID_CHAR_LENGTH = 26; + protected static final int ULID_LENGTH = 26; // Include 'O'->ZERO, 'I'->ONE and 'L'->ONE protected static final char[] ALPHABET_CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZOIL".toCharArray(); + protected static final char[] ALPHABET_JAVA = "0123456789abcdefghijklmnopqrstuv011".toCharArray(); + + private UlidUtil() { } @@ -55,7 +58,7 @@ public final class UlidUtil { public static String extractRandomnessComponent(String ulid) { UlidValidator.validate(ulid); - return ulid.substring(10, ULID_CHAR_LENGTH); + return ulid.substring(10, ULID_LENGTH); } protected static long extractUnixMilliseconds(String ulid) { @@ -164,6 +167,10 @@ public final class UlidUtil { 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++) { 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 49f913b..80186da 100644 --- a/src/main/java/com/github/f4b6a3/ulid/util/UlidValidator.java +++ b/src/main/java/com/github/f4b6a3/ulid/util/UlidValidator.java @@ -26,16 +26,59 @@ package com.github.f4b6a3.ulid.util; import com.github.f4b6a3.ulid.exception.InvalidUlidException; -import static com.github.f4b6a3.ulid.util.UlidUtil.*; +import static com.github.f4b6a3.ulid.util.internal.UlidStruct.BASE32_VALUES; public final class UlidValidator { // Date: 10889-08-02T05:31:50.655Z (epoch time: 281474976710655) protected static final long TIMESTAMP_MAX = (long) Math.pow(2, 48) - 1; + protected static final int ULID_LENGTH = 26; + private UlidValidator() { } + /** + * Checks if the string 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 + * @return boolean true if valid + */ + public static boolean isValid(String ulid) { + if (ulid == null) { + return false; + } + char[] chars = ulid.toCharArray(); + return isValidString(chars) && isValidTimestamp(chars); + } + + /** + * Checks if the ULID string is a valid. + * + * See {@link UlidValidator#isValid(String)}. + * + * @param ulid a ULID string + * @throws InvalidUlidException if invalid + */ + public static void validate(String ulid) { + if (!isValid(ulid)) { + throw new InvalidUlidException(String.format("Invalid ULID: %s.", ulid)); + } + } + /** * Checks if the string is a valid ULID. * @@ -50,40 +93,48 @@ public final class UlidValidator { * - 0123456789-ABCDEFGHIJ-KLMNOP (26 alphanumeric, case insensitive, including OIL, except U, with hyphens) * * - * @param ulid a ULID + * @param c a char array * @return boolean true if valid */ - public static boolean isValid(String ulid) { - - if (ulid == null) { - return false; + protected static boolean isValidString(final char[] c) { + int hyphen = 0; + for (int i = 0; i < c.length; i++) { + if (c[i] == '-') { + hyphen++; + continue; + } + if (c[i] == 'U' || c[i] == 'u') { + return false; + } + // ASCII codes: A-Z, 0-9, a-z + if (!((c[i] >= 0x41 && c[i] <= 0x5a) || (c[i] >= 0x30 && c[i] <= 0x39) || (c[i] >= 0x61 && c[i] <= 0x7a))) { + return false; + } } - - char[] chars = removeHyphens(ulid.toCharArray()); - if (chars.length != ULID_CHAR_LENGTH || !isCrockfordBase32(chars)) { - return false; - } - - // Extract time component - final char[] timestampComponent = new char[10]; - System.arraycopy(chars, 0, timestampComponent, 0, 10); - final long timestamp = fromBase32Crockford(timestampComponent); - - return timestamp >= 0 && timestamp <= TIMESTAMP_MAX; - + return (c.length - hyphen) == ULID_LENGTH; } /** - * Checks if the ULID string is a valid. + * Checks if the timestamp is between 0 and 2^48-1 * - * See {@link TsidValidator#isValid(String)}. - * - * @param ulid a ULID string - * @throws InvalidUlidException if invalid + * @param chars a char array + * @return false if invalid. */ - protected static void validate(String ulid) { - if (!isValid(ulid)) { - throw new InvalidUlidException(String.format("Invalid ULID: %s.", ulid)); - } + protected static boolean isValidTimestamp(char[] chars) { + + long time = 0; + + time |= BASE32_VALUES[chars[0x00]] << 45; + time |= BASE32_VALUES[chars[0x01]] << 40; + time |= BASE32_VALUES[chars[0x02]] << 35; + time |= BASE32_VALUES[chars[0x03]] << 30; + time |= BASE32_VALUES[chars[0x04]] << 25; + time |= BASE32_VALUES[chars[0x05]] << 20; + time |= BASE32_VALUES[chars[0x06]] << 15; + time |= BASE32_VALUES[chars[0x07]] << 10; + time |= BASE32_VALUES[chars[0x08]] << 5; + time |= BASE32_VALUES[chars[0x09]]; + + return time >= 0 && time <= TIMESTAMP_MAX; } } 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 new file mode 100644 index 0000000..321f0b8 --- /dev/null +++ b/src/main/java/com/github/f4b6a3/ulid/util/internal/UlidStruct.java @@ -0,0 +1,250 @@ +/* + * MIT License + * + * Copyright (c) 2020 Fabio Lima + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.github.f4b6a3.ulid.util.internal; + +import java.util.UUID; + +import com.github.f4b6a3.ulid.util.UlidValidator; + +/** + * This class represents the structure of a ULID. + * + * It is for internal use and test cases in this library. + */ +public final class UlidStruct { + + public final long time; + public final long random1; + public final long random2; + + protected static final long TIMESTAMP_COMPONENT = 0x0000ffffffffffffL; + protected static final long HALF_RANDOM_COMPONENT = 0x000000ffffffffffL; + + public static final char[] BASE32_CHARS = // + { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', // + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', // + 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z' }; + + public static final long[] BASE32_VALUES = new long[128]; + static { + + // Numbers + BASE32_VALUES['0'] = 0x00; + BASE32_VALUES['1'] = 0x01; + BASE32_VALUES['2'] = 0x02; + BASE32_VALUES['3'] = 0x03; + BASE32_VALUES['4'] = 0x04; + BASE32_VALUES['5'] = 0x05; + BASE32_VALUES['6'] = 0x06; + BASE32_VALUES['7'] = 0x07; + BASE32_VALUES['8'] = 0x08; + BASE32_VALUES['9'] = 0x09; + // Lower case + BASE32_VALUES['a'] = 0x0a; + BASE32_VALUES['b'] = 0x0b; + BASE32_VALUES['c'] = 0x0c; + BASE32_VALUES['d'] = 0x0d; + BASE32_VALUES['e'] = 0x0e; + BASE32_VALUES['f'] = 0x0f; + BASE32_VALUES['g'] = 0x10; + BASE32_VALUES['h'] = 0x11; + BASE32_VALUES['j'] = 0x12; + BASE32_VALUES['k'] = 0x13; + BASE32_VALUES['m'] = 0x14; + BASE32_VALUES['n'] = 0x15; + BASE32_VALUES['p'] = 0x16; + BASE32_VALUES['q'] = 0x17; + BASE32_VALUES['r'] = 0x18; + BASE32_VALUES['s'] = 0x19; + BASE32_VALUES['t'] = 0x1a; + BASE32_VALUES['v'] = 0x1b; + BASE32_VALUES['w'] = 0x1c; + BASE32_VALUES['x'] = 0x1d; + BASE32_VALUES['y'] = 0x1e; + BASE32_VALUES['z'] = 0x1f; + // Lower case OIL + BASE32_VALUES['o'] = 0x00; + BASE32_VALUES['i'] = 0x01; + BASE32_VALUES['l'] = 0x01; + // Upper case + BASE32_VALUES['A'] = 0x0a; + BASE32_VALUES['B'] = 0x0b; + BASE32_VALUES['C'] = 0x0c; + BASE32_VALUES['D'] = 0x0d; + BASE32_VALUES['E'] = 0x0e; + BASE32_VALUES['F'] = 0x0f; + BASE32_VALUES['G'] = 0x10; + BASE32_VALUES['H'] = 0x11; + BASE32_VALUES['J'] = 0x12; + BASE32_VALUES['K'] = 0x13; + BASE32_VALUES['M'] = 0x14; + BASE32_VALUES['N'] = 0x15; + BASE32_VALUES['P'] = 0x16; + BASE32_VALUES['Q'] = 0x17; + BASE32_VALUES['R'] = 0x18; + BASE32_VALUES['S'] = 0x19; + BASE32_VALUES['T'] = 0x1a; + BASE32_VALUES['V'] = 0x1b; + BASE32_VALUES['W'] = 0x1c; + BASE32_VALUES['X'] = 0x1d; + BASE32_VALUES['Y'] = 0x1e; + BASE32_VALUES['Z'] = 0x1f; + // Upper case OIL + BASE32_VALUES['O'] = 0x00; + BASE32_VALUES['I'] = 0x01; + BASE32_VALUES['L'] = 0x01; + + } + + public UlidStruct(long time, long random1, long random2) { + this.time = time & TIMESTAMP_COMPONENT; + this.random1 = random1 & HALF_RANDOM_COMPONENT; + this.random2 = random2 & HALF_RANDOM_COMPONENT; + } + + public UlidStruct(UUID uuid) { + final long msb = uuid.getMostSignificantBits(); + final long lsb = uuid.getLeastSignificantBits(); + + this.time = (msb >>> 16); + this.random1 = ((msb & 0x000000000000ffffL) << 24) | (lsb >>> 40); + this.random2 = (lsb & 0x000000ffffffffffL); + } + + public UlidStruct(String string) { + + UlidValidator.validate(string); + + 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; + tm |= BASE32_VALUES[chars[0x02]] << 35; + tm |= BASE32_VALUES[chars[0x03]] << 30; + tm |= BASE32_VALUES[chars[0x04]] << 25; + tm |= BASE32_VALUES[chars[0x05]] << 20; + tm |= BASE32_VALUES[chars[0x06]] << 15; + tm |= BASE32_VALUES[chars[0x07]] << 10; + tm |= BASE32_VALUES[chars[0x08]] << 5; + tm |= BASE32_VALUES[chars[0x09]]; + + r1 |= BASE32_VALUES[chars[0x0a]] << 35; + r1 |= BASE32_VALUES[chars[0x0b]] << 30; + r1 |= BASE32_VALUES[chars[0x0c]] << 25; + r1 |= BASE32_VALUES[chars[0x0d]] << 20; + r1 |= BASE32_VALUES[chars[0x0e]] << 15; + r1 |= BASE32_VALUES[chars[0x0f]] << 10; + r1 |= BASE32_VALUES[chars[0x10]] << 5; + r1 |= BASE32_VALUES[chars[0x11]]; + + r2 |= BASE32_VALUES[chars[0x12]] << 35; + r2 |= BASE32_VALUES[chars[0x13]] << 30; + r2 |= BASE32_VALUES[chars[0x14]] << 25; + r2 |= BASE32_VALUES[chars[0x15]] << 20; + r2 |= BASE32_VALUES[chars[0x16]] << 15; + r2 |= BASE32_VALUES[chars[0x17]] << 10; + r2 |= BASE32_VALUES[chars[0x18]] << 5; + r2 |= BASE32_VALUES[chars[0x19]]; + + this.time = tm; + this.random1 = r1; + this.random2 = r2; + } + + public String toString() { + + final char[] chars = new char[26]; + + chars[0x00] = BASE32_CHARS[(int) (time >>> 45 & 0b11111)]; + chars[0x01] = BASE32_CHARS[(int) (time >>> 40 & 0b11111)]; + chars[0x02] = BASE32_CHARS[(int) (time >>> 35 & 0b11111)]; + chars[0x03] = BASE32_CHARS[(int) (time >>> 30 & 0b11111)]; + chars[0x04] = BASE32_CHARS[(int) (time >>> 25 & 0b11111)]; + chars[0x05] = BASE32_CHARS[(int) (time >>> 20 & 0b11111)]; + chars[0x06] = BASE32_CHARS[(int) (time >>> 15 & 0b11111)]; + chars[0x07] = BASE32_CHARS[(int) (time >>> 10 & 0b11111)]; + chars[0x08] = BASE32_CHARS[(int) (time >>> 5 & 0b11111)]; + chars[0x09] = BASE32_CHARS[(int) (time & 0b11111)]; + + chars[0x0a] = BASE32_CHARS[(int) (random1 >>> 35 & 0b11111)]; + chars[0x0b] = BASE32_CHARS[(int) (random1 >>> 30 & 0b11111)]; + chars[0x0c] = BASE32_CHARS[(int) (random1 >>> 25 & 0b11111)]; + chars[0x0d] = BASE32_CHARS[(int) (random1 >>> 20 & 0b11111)]; + chars[0x0e] = BASE32_CHARS[(int) (random1 >>> 15 & 0b11111)]; + chars[0x0f] = BASE32_CHARS[(int) (random1 >>> 10 & 0b11111)]; + chars[0x10] = BASE32_CHARS[(int) (random1 >>> 5 & 0b11111)]; + chars[0x11] = BASE32_CHARS[(int) (random1 & 0b11111)]; + + chars[0x12] = BASE32_CHARS[(int) (random2 >>> 35 & 0b11111)]; + chars[0x13] = BASE32_CHARS[(int) (random2 >>> 30 & 0b11111)]; + chars[0x14] = BASE32_CHARS[(int) (random2 >>> 25 & 0b11111)]; + chars[0x15] = BASE32_CHARS[(int) (random2 >>> 20 & 0b11111)]; + chars[0x16] = BASE32_CHARS[(int) (random2 >>> 15 & 0b11111)]; + chars[0x17] = BASE32_CHARS[(int) (random2 >>> 10 & 0b11111)]; + chars[0x18] = BASE32_CHARS[(int) (random2 >>> 5 & 0b11111)]; + chars[0x19] = BASE32_CHARS[(int) (random2 & 0b11111)]; + + return new String(chars); + } + + public UUID toUuid() { + + final long msb = (time << 16) | (random1 >>> 24); + final long lsb = (random1 << 40) | random2; + + return new UUID(msb, lsb); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (int) (random1 ^ (random1 >>> 32)); + result = prime * result + (int) (random2 ^ (random2 >>> 32)); + result = prime * result + (int) (time ^ (time >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + UlidStruct other = (UlidStruct) obj; + if (random1 != other.random1) + return false; + if (random2 != other.random2) + return false; + if (time != other.time) + return false; + return true; + } +} diff --git a/src/test/java/com/github/f4b6a3/ulid/TestSuite.java b/src/test/java/com/github/f4b6a3/ulid/TestSuite.java index bc99110..4f5a0cf 100644 --- a/src/test/java/com/github/f4b6a3/ulid/TestSuite.java +++ b/src/test/java/com/github/f4b6a3/ulid/TestSuite.java @@ -8,6 +8,7 @@ import com.github.f4b6a3.ulid.ulid.UlidCreatorTest; import com.github.f4b6a3.ulid.util.UlidConverterTest; import com.github.f4b6a3.ulid.util.UlidUtilTest; import com.github.f4b6a3.ulid.util.UlidValidatorTest; +import com.github.f4b6a3.ulid.util.internal.UlidStructTest; @RunWith(Suite.class) @Suite.SuiteClasses({ @@ -16,6 +17,7 @@ import com.github.f4b6a3.ulid.util.UlidValidatorTest; UlidConverterTest.class, UlidUtilTest.class, UlidValidatorTest.class, + UlidStructTest.class, }) /** diff --git a/src/test/java/com/github/f4b6a3/ulid/util/UlidConverterTest.java b/src/test/java/com/github/f4b6a3/ulid/util/UlidConverterTest.java index 2bfda7e..2fea854 100644 --- a/src/test/java/com/github/f4b6a3/ulid/util/UlidConverterTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/util/UlidConverterTest.java @@ -1,6 +1,8 @@ package com.github.f4b6a3.ulid.util; import static org.junit.Assert.*; + +import java.util.Random; import java.util.UUID; import org.junit.Test; @@ -8,6 +10,7 @@ import org.junit.Test; import com.github.f4b6a3.ulid.UlidCreator; import com.github.f4b6a3.ulid.util.UlidConverter; import com.github.f4b6a3.ulid.util.UlidValidator; +import com.github.f4b6a3.ulid.util.internal.UlidStruct; public class UlidConverterTest { @@ -32,4 +35,84 @@ public class UlidConverterTest { } } + + @Test + public void testToString1() { + + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + + Random random = new Random(); + final long time = random.nextLong(); + final long random1 = random.nextLong(); + final long random2 = random.nextLong(); + UlidStruct struct0 = new UlidStruct(time, random1, random2); + + String string1 = struct0.toString(); + UlidStruct struct1 = new UlidStruct(string1); + + assertEquals(struct0.time, struct1.time); + assertEquals(struct0.random1, struct1.random1); + assertEquals(struct0.random2, struct1.random2); + } + } + + @Test + public void testToString2() { + + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + + UUID ulid0 = UlidCreator.getUlid(); + UlidStruct struct0 = new UlidStruct(ulid0); + + String string1 = UlidConverter.toString(ulid0); + UlidStruct struct1 = new UlidStruct(string1); + + assertEquals(struct0.time, struct1.time); + assertEquals(struct0.random1, struct1.random1); + assertEquals(struct0.random2, struct1.random2); + } + } + + @Test + public void testToString3() { + + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + + UUID ulid0 = UlidCreator.getUlid(); + UlidStruct struct0 = new UlidStruct(ulid0); + + String string1 = struct0.toString(); + UlidStruct struct1 = new UlidStruct(string1); + + assertEquals(struct0.time, struct1.time); + assertEquals(struct0.random1, struct1.random1); + assertEquals(struct0.random2, struct1.random2); + } + } + + @Test + public void testToString4() { + + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + + UUID ulid0 = UlidCreator.getUlid(); + UlidStruct struct0 = new UlidStruct(ulid0); + + String string1 = UlidConverter.toString(ulid0); + UlidStruct struct1 = new UlidStruct(string1); + + String string2 = struct0.toString(); + UlidStruct struct2 = new UlidStruct(string2); + + assertEquals(string1, string2); + + assertEquals(struct0.time, struct1.time); + assertEquals(struct0.random1, struct1.random1); + assertEquals(struct0.random2, struct1.random2); + + assertEquals(struct0.time, struct2.time); + assertEquals(struct0.random1, struct2.random1); + assertEquals(struct0.random2, struct2.random2); + } + } } 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 new file mode 100644 index 0000000..eb4d592 --- /dev/null +++ b/src/test/java/com/github/f4b6a3/ulid/util/internal/UlidStructTest.java @@ -0,0 +1,159 @@ +package com.github.f4b6a3.ulid.util.internal; + +import static org.junit.Assert.assertEquals; + +import java.util.Random; +import java.util.UUID; + +import org.junit.Test; + +public class UlidStructTest { + + private static final int DEFAULT_LOOP_MAX = 100_000; + + protected static final char[] ALPHABET_CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ".toCharArray(); + protected static final char[] ALPHABET_JAVA = "0123456789abcdefghijklmnopqrstuv".toCharArray(); // Long.parseUnsignedLong() + + @Test + public void testConstructorLongs() { + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + Random random = new Random(); + final long time = random.nextLong(); + final long random1 = random.nextLong(); + final long random2 = random.nextLong(); + UlidStruct struct0 = new UlidStruct(time, random1, random2); // <-- under test + + assertEquals(time & 0xffffffffffffL, struct0.time); + assertEquals(random1 & 0xffffffffffL, struct0.random1); + assertEquals(random2 & 0xffffffffffL, struct0.random2); + } + } + + @Test + public void testConstructorString() { + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + Random random = new Random(); + final long time = random.nextLong(); + final long random1 = random.nextLong(); + final long random2 = random.nextLong(); + UlidStruct struct0 = new UlidStruct(time, random1, random2); + + String string1 = toString(struct0); + UlidStruct struct1 = new UlidStruct(string1); // <-- under test + assertEquals(struct0, struct1); + } + } + + @Test + public void testConstructorUuid() { + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + Random random = new Random(); + final long msb = random.nextLong(); + final long lsb = random.nextLong(); + final UUID uuid0 = new UUID(msb, lsb); + UlidStruct struct0 = new UlidStruct(uuid0); // <-- under test + + UUID uuid1 = toUuid(struct0); + assertEquals(uuid0, uuid1); + } + } + + @Test + public void testToString() { + + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + Random random = new Random(); + final long time = random.nextLong(); + final long random1 = random.nextLong(); + final long random2 = random.nextLong(); + UlidStruct struct0 = new UlidStruct(time, random1, random2); + + String string1 = toString(struct0); + String string2 = struct0.toString(); // <-- under test + assertEquals(string1, string2); + } + } + + @Test + public void testToUuid() { + + for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { + Random random = new Random(); + final long time = random.nextLong(); + final long random1 = random.nextLong(); + final long random2 = random.nextLong(); + UlidStruct struct0 = new UlidStruct(time, random1, random2); + + UUID uuid1 = toUuid(struct0); + UUID uuid2 = struct0.toUuid(); // <-- under test + assertEquals(uuid1, uuid2); + } + } + + public UlidStruct fromString(String string) { + + long time = 0; + long random1 = 0; + long random2 = 0; + + String tm = string.substring(0, 10); + String r1 = string.substring(10, 18); + String r2 = string.substring(18, 26); + + tm = transliterate(tm, ALPHABET_CROCKFORD, ALPHABET_JAVA); + r1 = transliterate(r1, ALPHABET_CROCKFORD, ALPHABET_JAVA); + r2 = transliterate(r2, ALPHABET_CROCKFORD, ALPHABET_JAVA); + + time = Long.parseUnsignedLong(tm, 32); + random1 = Long.parseUnsignedLong(r1, 32); + random2 = Long.parseUnsignedLong(r2, 32); + + return new UlidStruct(time, random1, random2); + } + + public UUID toUuid(UlidStruct struct) { + + long time = struct.time & 0xffffffffffffL; + long random1 = struct.random1 & 0xffffffffffL; + long random2 = struct.random2 & 0xffffffffffL; + + final long msb = (time << 16) | (random1 >>> 24); + final long lsb = (random1 << 40) | random2; + + return new UUID(msb, lsb); + } + + public String toString(UlidStruct struct) { + + final String tzero = "0000000000"; + final String rzero = "00000000"; + + String time = Long.toUnsignedString(struct.time, 32); + String random1 = Long.toUnsignedString(struct.random1, 32); + String random2 = Long.toUnsignedString(struct.random2, 32); + + 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; + + time = transliterate(time, ALPHABET_JAVA, ALPHABET_CROCKFORD); + random1 = transliterate(random1, ALPHABET_JAVA, ALPHABET_CROCKFORD); + random2 = transliterate(random2, ALPHABET_JAVA, ALPHABET_CROCKFORD); + + return time + random1 + random2; + } + + private static String transliterate(String string, char[] alphabet1, char[] alphabet2) { + char[] output = string.toCharArray(); + 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 new String(output); + } + +}