Version 2.0.0

Rename UlidBasedGuidCreator to UlidSpecCreator

Add method UlidCreator.fromString()
Add RandomStrategy for UlidSpecCreator
Add DefaultRandomStrategy using thread local SecureRandom
Add OtherRandomStrategy for any instance of Random
Add tests cases

Optimize UlidConverter.fromString()
Optimize UlidConverter.toString()
Optimize UlidValidator.isValid()
Optimize UlidSpecCreator

Update README.md
Update tests cases
This commit is contained in:
Fabio Lima 2020-07-04 12:25:37 -03:00
parent a79edd90e5
commit 327aa7bc6b
25 changed files with 1027 additions and 528 deletions

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2020 f4b6a3 Copyright (c) 2020 Fabio Lima
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

140
README.md
View File

@ -12,7 +12,7 @@ Create a ULID as GUID:
UUID ulid = UlidCreator.getUlid(); UUID ulid = UlidCreator.getUlid();
``` ```
Create a ULID string: Create a ULID as string:
```java ```java
String ulid = UlidCreator.getUlidString(); String ulid = UlidCreator.getUlidString();
@ -28,7 +28,7 @@ Add these lines to your `pom.xml`.
<dependency> <dependency>
<groupId>com.github.f4b6a3</groupId> <groupId>com.github.f4b6a3</groupId>
<artifactId>ulid-creator</artifactId> <artifactId>ulid-creator</artifactId>
<version>1.1.1</version> <version>2.0.0</version>
</dependency> </dependency>
``` ```
See more options in [maven.org](https://search.maven.org/artifact/com.github.f4b6a3/ulid-creator). See more options in [maven.org](https://search.maven.org/artifact/com.github.f4b6a3/ulid-creator).
@ -36,7 +36,45 @@ See more options in [maven.org](https://search.maven.org/artifact/com.github.f4b
Implementation Implementation
------------------------------------------------------ ------------------------------------------------------
### ULID string ### ULID as GUID
The GUIDs in this library are based on the [ULID specification](https://github.com/ulid/spec). The first 48 bits represent the count of milliseconds since Unix Epoch, 1 January 1970. The remaining 60 bits are generated by a secure random number generator.
Every time the timestamp changes the random part is reset to a new random value. If the current timestamp is equal to the previous one, the random bits are incremented by 1.
The default random number generator is a thread safe `java.security.SecureRandom`, but it's possible to use any RNG that extends `java.util.Random`.
```java
// GUID based on ULID spec
UUID ulid = UlidCreator.getUlid();
```
Examples of GUIDs based on ULID spec:
```text
01706d6c-6aac-80bd-7ff5-f660c2dd58ea
01706d6c-6aac-80bd-7ff5-f660c2dd58eb
01706d6c-6aac-80bd-7ff5-f660c2dd58ec
01706d6c-6aac-80bd-7ff5-f660c2dd58ed
01706d6c-6aac-80bd-7ff5-f660c2dd58ee
01706d6c-6aac-80bd-7ff5-f660c2dd58ef
01706d6c-6aac-80bd-7ff5-f660c2dd58f0
01706d6c-6aac-80bd-7ff5-f660c2dd58f1
01706d6c-6aad-c795-370c-98d0be881bb8 < millisecond changed
01706d6c-6aad-c795-370c-98d0be881bb9
01706d6c-6aad-c795-370c-98d0be881bba
01706d6c-6aad-c795-370c-98d0be881bbb
01706d6c-6aad-c795-370c-98d0be881bbc
01706d6c-6aad-c795-370c-98d0be881bbd
01706d6c-6aad-c795-370c-98d0be881bbe
01706d6c-6aad-c795-370c-98d0be881bbf
^ look ^ look
|------------|---------------------|
millisecs randomness
```
### ULID as string
The ULID is a 26 char sequence. See the [ULID specification](https://github.com/ulid/spec) for more information. The ULID is a 26 char sequence. See the [ULID specification](https://github.com/ulid/spec) for more information.
@ -72,66 +110,68 @@ Examples of ULIDs:
milli randomness milli randomness
``` ```
### Ulid-based GUID #### How use the `UlidSpecCreator` directly
The GUIDs in this library are based on the [ULID specification](https://github.com/ulid/spec). The first 48 bits represent the count of milliseconds since Unix Epoch, 1 January 1970. The remaining 60 bits are generated by a secure random number generator. These are some examples of using the `UlidSpecCreator` to create ULIDs strings:
Every time the timestamp changes the random part is reset to a new random value. If the current timestamp is equal to the previous one, the random bits are incremented by 1.
The default random number generator is `java.security.SecureRandom`, but it's possible to use any RNG that extends `java.util.Random`.
```java
// GUID based on ULID spec
UUID ulid = UlidCreator.getUlid();
```
Examples of GUIDs based on ULID spec:
```text
01706d6c-6aac-80bd-7ff5-f660c2dd58ea
01706d6c-6aac-80bd-7ff5-f660c2dd58eb
01706d6c-6aac-80bd-7ff5-f660c2dd58ec
01706d6c-6aac-80bd-7ff5-f660c2dd58ed
01706d6c-6aac-80bd-7ff5-f660c2dd58ee
01706d6c-6aac-80bd-7ff5-f660c2dd58ef
01706d6c-6aac-80bd-7ff5-f660c2dd58f0
01706d6c-6aac-80bd-7ff5-f660c2dd58f1
01706d6c-6aad-c795-370c-98d0be881bb8 < millisecond changed
01706d6c-6aad-c795-370c-98d0be881bb9
01706d6c-6aad-c795-370c-98d0be881bba
01706d6c-6aad-c795-370c-98d0be881bbb
01706d6c-6aad-c795-370c-98d0be881bbc
01706d6c-6aad-c795-370c-98d0be881bbd
01706d6c-6aad-c795-370c-98d0be881bbe
01706d6c-6aad-c795-370c-98d0be881bbf
^ look ^ look
|------------|---------------------|
millisecs randomness
```
#### How use the `UlidBasedGuidCreator` directly
These are some examples of using the `UlidBasedGuidCreator` to create ULIDs strings:
```java ```java
// with your custom timestamp strategy // with your custom timestamp strategy
TimestampStrategy customStrategy = new CustomTimestampStrategy(); TimestampStrategy customStrategy = new CustomTimestampStrategy();
String ulid = UlidCreator.getUlidBasedGuidCreator() String ulid = UlidCreator.getUlidSpecCreator()
.withTimestampStrategy(customStrategy) .withTimestampStrategy(customStrategy)
.createString(); .createString();
// with your custom random strategy that wraps any random generator
RandomStrategy customStrategy = new CustomRandomStrategy();
String ulid = UlidCreator.getUlidSpecCreator()
.withRandomStrategy(customStrategy)
.createString();
// with `java.util.Random` number generator // with `java.util.Random` number generator
Random random = new Random(); Random random = new Random();
String ulid = UlidCreator.getUlidBasedGuidCreator() String ulid = UlidCreator.getUlidSpecCreator()
.withRandomGenerator(random) .withRandomGenerator(random)
.createString(); .createString();
// with fast random generator (the same as above)
String ulid = UlidCreator.getUlidBasedGuidCreator()
.withFastRandomGenerator()
.createString();
``` ```
Benchmark
------------------------------------------------------
This section shows benchmarks comparing `UlidCreator` to `java.util.UUID`.
```
---------------------------------------------------------------------------
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
---------------------------------------------------------------------------
Total time: 00:06:41
---------------------------------------------------------------------------
```
```
----------------------------------------------------------------------
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
----------------------------------------------------------------------
Total time: 00:06:41
----------------------------------------------------------------------
```
System: CPU i5-3330, 8G RAM, Ubuntu 20.04.
See: [uuid-creator-benchmark](https://github.com/fabiolimace/uuid-creator-benchmark)
Links for generators
-------------------------------------------
* [UUID Creator](https://github.com/f4b6a3/uuid-creator)
* [ULID Creator](https://github.com/f4b6a3/ulid-creator)
* [TSID Creator](https://github.com/f4b6a3/tsid-creator)

19
pom.xml
View File

@ -3,11 +3,11 @@
<groupId>com.github.f4b6a3</groupId> <groupId>com.github.f4b6a3</groupId>
<artifactId>ulid-creator</artifactId> <artifactId>ulid-creator</artifactId>
<version>1.1.2-SNAPSHOT</version> <version>2.0.0-SNAPSHOT</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>ulid-creator</name> <name>ulid-creator</name>
<url>http://github.com/f4b6a3</url> <url>http://github.com/f4b6a3/ulid-creator</url>
<description>A Java library for generating and handling ULIDs.</description> <description>A Java library for generating and handling ULIDs.</description>
@ -26,17 +26,12 @@
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jdk.version>11</jdk.version> <jdk.version>8</jdk.version>
<maven.compiler.source>${jdk.version}</maven.compiler.source> <maven.compiler.source>${jdk.version}</maven.compiler.source>
<maven.compiler.target>${jdk.version}</maven.compiler.target> <maven.compiler.target>${jdk.version}</maven.compiler.target>
</properties> </properties>
<dependencies> <dependencies>
<dependency>
<groupId>com.github.f4b6a3</groupId>
<artifactId>util</artifactId>
<version>1.0.0</version>
</dependency>
<dependency> <dependency>
<groupId>junit</groupId> <groupId>junit</groupId>
<artifactId>junit</artifactId> <artifactId>junit</artifactId>
@ -46,10 +41,10 @@
</dependencies> </dependencies>
<scm> <scm>
<connection>scm:git:git://github.com/dexecutor/dependent-tasks-executor.git</connection> <url>https://github.com/f4b6a3/ulid-creator</url>
<developerConnection>scm:git:git@github.com:dexecutor/dexecutor.git</developerConnection> <connection>scm:git:ssh://git@github.com/f4b6a3/ulid-creator.git</connection>
<url>https://github.com/dexecutor/dependent-tasks-executor</url> <developerConnection>scm:git:ssh://git@github.com/f4b6a3/ulid-creator.git</developerConnection>
<tag>ulid-creator-1.0.0</tag> <tag>HEAD</tag>
</scm> </scm>
<distributionManagement> <distributionManagement>

View File

@ -26,19 +26,29 @@ package com.github.f4b6a3.ulid;
import java.util.UUID; import java.util.UUID;
import com.github.f4b6a3.util.random.Xorshift128PlusRandom; import com.github.f4b6a3.ulid.creator.UlidSpecCreator;
import com.github.f4b6a3.ulid.creator.UlidBasedGuidCreator; import com.github.f4b6a3.ulid.util.UlidConverter;
/** /**
* A factory for Universally Unique Lexicographically Sortable Identifiers. * A factory for Universally Unique Lexicographically Sortable Identifiers.
* *
* See the ULID spec: https://github.com/ulid/spec * See the ULID spec: https://github.com/ulid/spec
*/ */
public class UlidCreator { public final class UlidCreator {
private UlidCreator() { private UlidCreator() {
} }
/**
* Returns a ULID as GUID from a string.
*
* @param ulid a ULID string
* @return a UUID
*/
public static UUID fromString(String ulid) {
return UlidConverter.fromString(ulid);
}
/** /**
* Returns a ULID as GUID. * Returns a ULID as GUID.
* *
@ -48,7 +58,7 @@ public class UlidCreator {
* @return a UUID * @return a UUID
*/ */
public static UUID getUlid() { public static UUID getUlid() {
return UlidBasedGuidCreatorHolder.INSTANCE.create(); return UlidSpecCreatorHolder.INSTANCE.create();
} }
/** /**
@ -62,49 +72,19 @@ public class UlidCreator {
* @return a ULID * @return a ULID
*/ */
public static String getUlidString() { public static String getUlidString() {
return UlidBasedGuidCreatorHolder.INSTANCE.createString(); return UlidSpecCreatorHolder.INSTANCE.createString();
}
/**
* Returns a ULID as GUID.
*
* The random component is generated by a fast random number generator:
* {@link Xorshift128PlusRandom}.
*
* @return a UUID
*/
public static UUID getFastUlid() {
return FastUlidBasedGuidCreatorHolder.INSTANCE.create();
}
/**
* Returns a fast ULID string.
*
* The returning string is encoded to Crockford's base32.
*
* The random component is generated by a fast random number generator:
* {@link Xorshift128PlusRandom}.
*
* @return a ULID
*/
public static String getFastUlidString() {
return FastUlidBasedGuidCreatorHolder.INSTANCE.createString();
} }
/** /**
* Return a GUID creator for direct use. * Return a GUID creator for direct use.
* *
* @return a {@link UlidBasedGuidCreator} * @return a {@link UlidSpecCreator}
*/ */
public static UlidBasedGuidCreator getUlidBasedCreator() { public static UlidSpecCreator getUlidSpecCreator() {
return new UlidBasedGuidCreator(); return new UlidSpecCreator();
} }
private static class UlidBasedGuidCreatorHolder { private static class UlidSpecCreatorHolder {
static final UlidBasedGuidCreator INSTANCE = getUlidBasedCreator(); static final UlidSpecCreator INSTANCE = getUlidSpecCreator();
}
private static class FastUlidBasedGuidCreatorHolder {
static final UlidBasedGuidCreator INSTANCE = getUlidBasedCreator().withFastRandomGenerator();
} }
} }

View File

@ -27,13 +27,14 @@ package com.github.f4b6a3.ulid.creator;
import java.util.Random; import java.util.Random;
import java.util.UUID; import java.util.UUID;
import com.github.f4b6a3.ulid.util.UlidConverter; import com.github.f4b6a3.ulid.strategy.RandomStrategy;
import com.github.f4b6a3.util.random.Xorshift128PlusRandom; import com.github.f4b6a3.ulid.strategy.random.DefaultRandomStrategy;
import com.github.f4b6a3.util.FingerprintUtil; import com.github.f4b6a3.ulid.strategy.random.OtherRandomStrategy;
import com.github.f4b6a3.util.RandomUtil;
import com.github.f4b6a3.ulid.exception.UlidCreatorException; import com.github.f4b6a3.ulid.exception.UlidCreatorException;
import com.github.f4b6a3.ulid.strategy.TimestampStrategy; import com.github.f4b6a3.ulid.strategy.TimestampStrategy;
import com.github.f4b6a3.ulid.strategy.timestamp.DefaultTimestampStrategy; import com.github.f4b6a3.ulid.strategy.timestamp.DefaultTimestampStrategy;
import com.github.f4b6a3.ulid.util.UlidConverter;
import com.github.f4b6a3.ulid.util.UlidUtil;
/** /**
* Factory that creates lexicographically sortable GUIDs, based on the ULID * Factory that creates lexicographically sortable GUIDs, based on the ULID
@ -41,28 +42,28 @@ import com.github.f4b6a3.ulid.strategy.timestamp.DefaultTimestampStrategy;
* *
* ULID specification: https://github.com/ulid/spec * ULID specification: https://github.com/ulid/spec
*/ */
public class UlidBasedGuidCreator { public class UlidSpecCreator {
protected long randomMsb = 0; protected long random1 = 0;
protected long randomLsb = 0; protected long random2 = 0;
protected long randomLsbMax; protected long randomMax2;
protected long randomMsbMax; protected long randomMax1;
protected static final long HALF_RANDOM_COMPONENT = 0x000000ffffffffffL; protected static final long HALF_RANDOM_COMPONENT = 0x000000ffffffffffL;
protected static final long INCREMENT_MAX = 0x0000010000000000L; protected static final long INCREMENT_MAX = 0x0000010000000000L;
protected long previousTimestamp; protected long previousTimestamp;
protected Random random; protected static final String OVERRUN_MESSAGE = "The system overran the generator by requesting too many ULIDs.";
protected static final String OVERRUN_MESSAGE = "The system overran the generator by requesting too many GUIDs.";
protected TimestampStrategy timestampStrategy; protected TimestampStrategy timestampStrategy;
protected RandomStrategy randomStrategy;
public UlidBasedGuidCreator() { public UlidSpecCreator() {
this.reset();
this.timestampStrategy = new DefaultTimestampStrategy(); this.timestampStrategy = new DefaultTimestampStrategy();
this.randomStrategy = new DefaultRandomStrategy();
this.reset();
} }
/** /**
@ -128,11 +129,11 @@ public class UlidBasedGuidCreator {
final long timestamp = this.getTimestamp(); final long timestamp = this.getTimestamp();
final long randomHi = truncate(randomMsb); final long rnd1 = random1 & HALF_RANDOM_COMPONENT;
final long randomLo = truncate(randomLsb); final long rnd2 = random2 & HALF_RANDOM_COMPONENT;
final long msb = (timestamp << 16) | (randomHi >>> 24); final long msb = (timestamp << 16) | (rnd1 >>> 24);
final long lsb = (randomHi << 40) | randomLo; final long lsb = (rnd1 << 40) | rnd2;
return new UUID(msb, lsb); return new UUID(msb, lsb);
} }
@ -142,7 +143,10 @@ public class UlidBasedGuidCreator {
* *
* The returning string is encoded to Crockford's base32. * The returning string is encoded to Crockford's base32.
* *
* @return a ULID string * The random component is generated by a secure random number generator:
* {@link java.security.SecureRandom}.
*
* @return a ULID
*/ */
public synchronized String createString() { public synchronized String createString() {
return UlidConverter.toString(create()); return UlidConverter.toString(create());
@ -173,17 +177,13 @@ public class UlidBasedGuidCreator {
protected synchronized void reset() { protected synchronized void reset() {
// Get random values // Get random values
if (random == null) { final byte[] bytes = new byte[10];
this.randomMsb = truncate(RandomUtil.get().nextLong()); this.randomStrategy.nextBytes(bytes);
this.randomLsb = truncate(RandomUtil.get().nextLong()); this.random1 = UlidUtil.toNumber(bytes, 0, 5);
} else { this.random2 = UlidUtil.toNumber(bytes, 5, 10);
this.randomMsb = truncate(random.nextLong());
this.randomLsb = truncate(random.nextLong());
}
// Save the random values // Save the random values
this.randomMsbMax = this.randomMsb | INCREMENT_MAX; this.randomMax1 = this.random1 | INCREMENT_MAX;
this.randomLsbMax = this.randomLsb | INCREMENT_MAX; this.randomMax2 = this.random2 | INCREMENT_MAX;
} }
/** /**
@ -196,7 +196,7 @@ public class UlidBasedGuidCreator {
*/ */
protected synchronized void increment() { protected synchronized void increment() {
if ((++this.randomLsb == this.randomLsbMax) && (++this.randomMsb == this.randomMsbMax)) { if ((++this.random2 > this.randomMax2) && (++this.random1 > this.randomMax1)) {
this.reset(); this.reset();
throw new UlidCreatorException(OVERRUN_MESSAGE); throw new UlidCreatorException(OVERRUN_MESSAGE);
} }
@ -206,73 +206,58 @@ public class UlidBasedGuidCreator {
* Used for changing the timestamp strategy. * Used for changing the timestamp strategy.
* *
* @param timestampStrategy a timestamp strategy * @param timestampStrategy a timestamp strategy
* @return {@link UlidBasedGuidCreator} * @return {@link UlidSpecCreator}
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public synchronized <T extends UlidBasedGuidCreator> T withTimestampStrategy(TimestampStrategy timestampStrategy) { public synchronized <T extends UlidSpecCreator> T withTimestampStrategy(TimestampStrategy timestampStrategy) {
this.timestampStrategy = timestampStrategy; this.timestampStrategy = timestampStrategy;
return (T) this; return (T) this;
} }
/** /**
* Replace the default random generator, in a fluent way, to another that * Replaces the default random strategy with another.
* extends {@link Random}.
* *
* The default random generator is {@link java.security.SecureRandom}. * The default random strategy uses {@link java.security.SecureRandom}.
*
* For other faster pseudo-random generators, see {@link XorshiftRandom} and its
* variations.
* *
* See {@link Random}. * See {@link Random}.
* *
* @param random a random generator * @param random a random generator
* @return {@link UlidBasedGuidCreator} * @param <T> the type parameter
* @return {@link AbstractRandomBasedUuidCreator}
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public synchronized <T extends UlidBasedGuidCreator> T withRandomGenerator(Random random) { public synchronized <T extends UlidSpecCreator> T withRandomStrategy(RandomStrategy randomStrategy) {
this.random = random; this.randomStrategy = randomStrategy;
return (T) this; return (T) this;
} }
/** /**
* Replaces the default random generator with a faster one. * Replaces the default random strategy with another that uses the input
* {@link Random} instance.
* *
* The host fingerprint is used to generate a seed for the random number * It replaces the internal {@link DefaultRandomStrategy} with
* generator. * {@link OtherRandomStrategy}.
* *
* See {@link Xorshift128PlusRandom} and * @param random a random generator
* {@link FingerprintUtil#getFingerprint()} * @return {@link UlidSpecCreator}
*
* @return {@link UlidBasedGuidCreator}
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public synchronized <T extends UlidBasedGuidCreator> T withFastRandomGenerator() { public synchronized <T extends UlidSpecCreator> T withRandomGenerator(Random random) {
final int salt = (int) FingerprintUtil.getFingerprint(); this.randomStrategy = new OtherRandomStrategy(random);
this.random = new Xorshift128PlusRandom(salt);
return (T) this; return (T) this;
} }
/**
* Truncate long to half random component.
*
* @param value a value to be truncated.
* @return truncated value
*/
protected synchronized long truncate(final long value) {
return (value & HALF_RANDOM_COMPONENT);
}
/** /**
* For unit tests * For unit tests
*/ */
protected long extractRandomLsb(UUID uuid) { protected long extractRandom1(UUID uuid) {
return ((uuid.getMostSignificantBits() & 0x000000000000ffff) << 24) | (uuid.getLeastSignificantBits() >>> 40);
}
/**
* For unit tests
*/
protected long extractRandom2(UUID uuid) {
return uuid.getLeastSignificantBits() & HALF_RANDOM_COMPONENT; return uuid.getLeastSignificantBits() & HALF_RANDOM_COMPONENT;
} }
/**
* For unit tests
*/
protected long extractRandomMsb(UUID uuid) {
return ((uuid.getMostSignificantBits() & 0xffff) << 24) | (uuid.getLeastSignificantBits() >>> 40);
}
} }

View File

@ -24,7 +24,7 @@
package com.github.f4b6a3.ulid.exception; package com.github.f4b6a3.ulid.exception;
public class InvalidUlidException extends RuntimeException { public final class InvalidUlidException extends RuntimeException {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;

View File

@ -24,7 +24,7 @@
package com.github.f4b6a3.ulid.exception; package com.github.f4b6a3.ulid.exception;
public class UlidCreatorException extends RuntimeException { public final class UlidCreatorException extends RuntimeException {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;

View File

@ -0,0 +1,29 @@
/*
* MIT License
*
* Copyright (c) 2018-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.strategy;
public interface RandomStrategy {
void nextBytes(byte[] bytes);
}

View File

@ -0,0 +1,43 @@
/*
* MIT License
*
* Copyright (c) 2018-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.strategy.random;
import java.security.SecureRandom;
import java.util.Random;
import com.github.f4b6a3.ulid.strategy.RandomStrategy;
/**
* It uses a thread local instance of {@link java.security.SecureRandom}.
*/
public final class DefaultRandomStrategy implements RandomStrategy {
protected static final ThreadLocal<Random> THREAD_LOCAL_RANDOM = ThreadLocal.withInitial(SecureRandom::new);
@Override
public void nextBytes(byte[] bytes) {
THREAD_LOCAL_RANDOM.get().nextBytes(bytes);
}
}

View File

@ -0,0 +1,46 @@
/*
* MIT License
*
* Copyright (c) 2018-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.strategy.random;
import java.util.Random;
import com.github.f4b6a3.ulid.strategy.RandomStrategy;
/**
* It uses an instance of {@link java.util.Random} injected by constructor.
*/
public final class OtherRandomStrategy implements RandomStrategy {
private final Random random;
public OtherRandomStrategy(Random random) {
this.random = random;
}
@Override
public void nextBytes(byte[] bytes) {
this.random.nextBytes(bytes);
}
}

View File

@ -26,10 +26,10 @@ package com.github.f4b6a3.ulid.strategy.timestamp;
import com.github.f4b6a3.ulid.strategy.TimestampStrategy; import com.github.f4b6a3.ulid.strategy.TimestampStrategy;
public class DefaultTimestampStrategy implements TimestampStrategy { public final class DefaultTimestampStrategy implements TimestampStrategy {
/** /**
* Returns the count of milliseconds since 01-01-1970. * Returns the count of milliseconds since 1970-01-01 (Unix epoch).
*/ */
@Override @Override
public long getTimestamp() { public long getTimestamp() {

View File

@ -26,9 +26,9 @@ package com.github.f4b6a3.ulid.strategy.timestamp;
import com.github.f4b6a3.ulid.strategy.TimestampStrategy; import com.github.f4b6a3.ulid.strategy.TimestampStrategy;
public class FixedTimestampStretegy implements TimestampStrategy { public final class FixedTimestampStretegy implements TimestampStrategy {
protected long timestamp = 0; private long timestamp = 0;
public FixedTimestampStretegy(long timestamp) { public FixedTimestampStretegy(long timestamp) {
this.timestamp = timestamp; this.timestamp = timestamp;

View File

@ -26,10 +26,11 @@ package com.github.f4b6a3.ulid.util;
import java.util.UUID; import java.util.UUID;
import com.github.f4b6a3.util.Base32Util; import com.github.f4b6a3.ulid.exception.InvalidUlidException;
import com.github.f4b6a3.util.ByteUtil;
public class UlidConverter { import static com.github.f4b6a3.ulid.util.UlidUtil.*;
public final class UlidConverter {
private UlidConverter() { private UlidConverter() {
} }
@ -39,8 +40,6 @@ public class UlidConverter {
* *
* The returning string is encoded to Crockford's base32. * The returning string is encoded to Crockford's base32.
* *
* The timestamp and random components are encoded separated.
*
* @param uuid a UUID * @param uuid a UUID
* @return a ULID * @return a ULID
*/ */
@ -49,19 +48,20 @@ public class UlidConverter {
final long msb = uuid.getMostSignificantBits(); final long msb = uuid.getMostSignificantBits();
final long lsb = uuid.getLeastSignificantBits(); final long lsb = uuid.getLeastSignificantBits();
// Extract timestamp component final long time = ((msb & 0xffffffffffff0000L) >>> 16);
final long timeNumber = (msb >>> 16); final long random1 = ((msb & 0x000000000000ffffL) << 24) | ((lsb & 0xffffff0000000000L) >>> 40);
String timestampComponent = leftPad(Base32Util.toBase32Crockford(timeNumber)); final long random2 = (lsb & 0x000000ffffffffffL);
// Extract randomness component final char[] timeComponent = zerofill(toBase32Crockford(time), 10);
byte[] randBytes = new byte[10]; final char[] randomComponent1 = zerofill(toBase32Crockford(random1), 8);
randBytes[0] = (byte) (msb >>> 8); final char[] randomComponent2 = zerofill(toBase32Crockford(random2), 8);
randBytes[1] = (byte) (msb);
byte[] lsbBytes = ByteUtil.toBytes(lsb);
System.arraycopy(lsbBytes, 0, randBytes, 2, 8);
String randomnessComponent = Base32Util.toBase32Crockford(randBytes);
return timestampComponent + randomnessComponent; 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);
} }
/** /**
@ -70,34 +70,33 @@ public class UlidConverter {
* The input string must be encoded to Crockford's base32, following the ULID * The input string must be encoded to Crockford's base32, following the ULID
* specification. * specification.
* *
* The timestamp and random components are decoded separated.
*
* An exception is thrown if the ULID string is invalid. * An exception is thrown if the ULID string is invalid.
* *
* @param ulid a ULID * @param ulid a ULID
* @return a UUID if valid * @return a UUID if valid
* @throws InvalidUlidException if invalid
*/ */
public static UUID fromString(final String ulid) { public static UUID fromString(final String ulid) {
UlidValidator.validate(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);
// Extract timestamp component final long time = fromBase32Crockford(timeComponent);
final String timestampComponent = ulid.substring(0, 10); final long random1 = fromBase32Crockford(randomComponent1);
final long timeNumber = Base32Util.fromBase32CrockfordAsLong(timestampComponent); final long random2 = fromBase32Crockford(randomComponent2);
// Extract randomness component final long msb = ((time & 0x0000ffffffffffffL) << 16) | ((random1 & 0x000000ffff000000L) >>> 24);
final String randomnessComponent = ulid.substring(10, 26); final long lsb = ((random1 & 0x0000000000ffffffL) << 40) | (random2 & 0x000000ffffffffffL);
byte[] randBytes = Base32Util.fromBase32Crockford(randomnessComponent);
byte[] lsbBytes = new byte[8];
System.arraycopy(randBytes, 2, lsbBytes, 0, 8);
final long msb = (timeNumber << 16) | ((randBytes[0] << 8) & 0x0000ff00L) | ((randBytes[1]) & 0x000000ffL);
final long lsb = ByteUtil.toNumber(lsbBytes);
return new UUID(msb, lsb); return new UUID(msb, lsb);
} }
private static String leftPad(String unpadded) {
return "0000000000".substring(unpadded.length()) + unpadded;
}
} }

View File

@ -26,9 +26,14 @@ package com.github.f4b6a3.ulid.util;
import java.time.Instant; import java.time.Instant;
import com.github.f4b6a3.util.Base32Util; public final class UlidUtil {
public class UlidUtil { protected static final int BASE_32 = 32;
protected static final int ULID_CHAR_LENGTH = 26;
// Include 'O'->ZERO, 'I'->ONE and 'L'->ONE
protected static final char[] ALPHABET_CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZOIL".toCharArray();
private UlidUtil() { private UlidUtil() {
} }
@ -50,11 +55,196 @@ public class UlidUtil {
public static String extractRandomnessComponent(String ulid) { public static String extractRandomnessComponent(String ulid) {
UlidValidator.validate(ulid); UlidValidator.validate(ulid);
return ulid.substring(10, 26); return ulid.substring(10, ULID_CHAR_LENGTH);
} }
protected static long extractUnixMilliseconds(String ulid) { protected static long extractUnixMilliseconds(String ulid) {
String milliseconds = ulid.substring(0, 10); return fromBase32Crockford(extractTimestampComponent(ulid).toCharArray());
return Base32Util.fromBase32CrockfordAsLong(milliseconds); }
/**
* 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 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';
} }
} }

View File

@ -26,11 +26,11 @@ package com.github.f4b6a3.ulid.util;
import com.github.f4b6a3.ulid.exception.InvalidUlidException; import com.github.f4b6a3.ulid.exception.InvalidUlidException;
public class UlidValidator { import static com.github.f4b6a3.ulid.util.UlidUtil.*;
protected static final String ULID_PATTERN = "^[0-9a-tv-zA-TV-Z]{26}$"; public final class UlidValidator {
// Date: 10889-08-02T05:31:50.655Z // 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 long TIMESTAMP_MAX = (long) Math.pow(2, 48) - 1;
private UlidValidator() { private UlidValidator() {
@ -42,14 +42,12 @@ public class UlidValidator {
* A valid ULID string is a sequence of 26 characters from Crockford's base 32 * A valid ULID string is a sequence of 26 characters from Crockford's base 32
* alphabet. * alphabet.
* *
* Dashes are ignored by this validator.
*
* <pre> * <pre>
* Examples of valid ULID strings: * Examples of valid ULID strings:
* - 0123456789ABCDEFGHJKMNPKRS (26 alphanumeric, case insensitive, except iI, lL, oO and uU) * - 0123456789ABCDEFGHJKMNPKRS (26 alphanumeric, case insensitive, except U)
* - 0123456789ABCDEFGHIJKLMNOP (26 alphanumeric, case insensitive, except uU) * - 0123456789ABCDEFGHIJKLMNOP (26 alphanumeric, case insensitive, including OIL, except U)
* - 0123456789-ABCDEFGHJK-MNPKRS (26 alphanumeric, case insensitive, except iI, lL, oO and uU) * - 0123456789-ABCDEFGHJK-MNPKRS (26 alphanumeric, case insensitive, except U, with hyphens)
* - 0123456789-ABCDEFGHIJ-KLMNOP (26 alphanumeric, case insensitive, except uU, with dashes) * - 0123456789-ABCDEFGHIJ-KLMNOP (26 alphanumeric, case insensitive, including OIL, except U, with hyphens)
* </pre> * </pre>
* *
* @param ulid a ULID * @param ulid a ULID
@ -57,16 +55,20 @@ public class UlidValidator {
*/ */
public static boolean isValid(String ulid) { public static boolean isValid(String ulid) {
if (ulid == null || ulid.isEmpty()) { if (ulid == null) {
return false; return false;
} }
String u = ulid.replaceAll("-", ""); char[] chars = removeHyphens(ulid.toCharArray());
if (!u.matches(ULID_PATTERN)) { if (chars.length != ULID_CHAR_LENGTH || !isCrockfordBase32(chars)) {
return false; return false;
} }
long timestamp = UlidUtil.extractUnixMilliseconds(ulid); // 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 timestamp >= 0 && timestamp <= TIMESTAMP_MAX;
} }

View File

@ -3,7 +3,7 @@ package com.github.f4b6a3.ulid;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.Suite; import org.junit.runners.Suite;
import com.github.f4b6a3.ulid.creator.UlidBasedGuidCreatorTest; import com.github.f4b6a3.ulid.creator.UlidSpecCreatorTest;
import com.github.f4b6a3.ulid.ulid.UlidCreatorTest; import com.github.f4b6a3.ulid.ulid.UlidCreatorTest;
import com.github.f4b6a3.ulid.util.UlidConverterTest; import com.github.f4b6a3.ulid.util.UlidConverterTest;
import com.github.f4b6a3.ulid.util.UlidUtilTest; import com.github.f4b6a3.ulid.util.UlidUtilTest;
@ -12,7 +12,7 @@ import com.github.f4b6a3.ulid.util.UlidValidatorTest;
@RunWith(Suite.class) @RunWith(Suite.class)
@Suite.SuiteClasses({ @Suite.SuiteClasses({
UlidCreatorTest.class, UlidCreatorTest.class,
UlidBasedGuidCreatorTest.class, UlidSpecCreatorTest.class,
UlidConverterTest.class, UlidConverterTest.class,
UlidUtilTest.class, UlidUtilTest.class,
UlidValidatorTest.class, UlidValidatorTest.class,

View File

@ -4,7 +4,7 @@ import java.util.HashSet;
import java.util.UUID; import java.util.UUID;
import com.github.f4b6a3.ulid.UlidCreator; import com.github.f4b6a3.ulid.UlidCreator;
import com.github.f4b6a3.ulid.creator.UlidBasedGuidCreator; import com.github.f4b6a3.ulid.creator.UlidSpecCreator;
import com.github.f4b6a3.ulid.exception.UlidCreatorException; import com.github.f4b6a3.ulid.exception.UlidCreatorException;
import com.github.f4b6a3.ulid.strategy.timestamp.FixedTimestampStretegy; import com.github.f4b6a3.ulid.strategy.timestamp.FixedTimestampStretegy;
@ -27,7 +27,7 @@ public class UniquenessTest {
private boolean verbose; // Show progress or not private boolean verbose; // Show progress or not
// GUID creator based on ULID spec // GUID creator based on ULID spec
private UlidBasedGuidCreator creator; private UlidSpecCreator creator;
/** /**
* Initialize the test. * Initialize the test.
@ -36,7 +36,7 @@ public class UniquenessTest {
* @param requestCount * @param requestCount
* @param creator * @param creator
*/ */
public UniquenessTest(int threadCount, int requestCount, UlidBasedGuidCreator creator, boolean progress) { public UniquenessTest(int threadCount, int requestCount, UlidSpecCreator creator, boolean progress) {
this.threadCount = threadCount; this.threadCount = threadCount;
this.requestCount = requestCount; this.requestCount = requestCount;
this.creator = creator; this.creator = creator;
@ -125,7 +125,7 @@ public class UniquenessTest {
} }
public static void execute(boolean verbose, int threadCount, int requestCount) { public static void execute(boolean verbose, int threadCount, int requestCount) {
UlidBasedGuidCreator creator = UlidCreator.getUlidBasedCreator() UlidSpecCreator creator = UlidCreator.getUlidSpecCreator()
.withTimestampStrategy(new FixedTimestampStretegy(System.currentTimeMillis())); .withTimestampStrategy(new FixedTimestampStretegy(System.currentTimeMillis()));
UniquenessTest test = new UniquenessTest(threadCount, requestCount, creator, verbose); UniquenessTest test = new UniquenessTest(threadCount, requestCount, creator, verbose);

View File

@ -1,38 +0,0 @@
package com.github.f4b6a3.ulid.creator;
import com.github.f4b6a3.ulid.creator.UlidBasedGuidCreator;
class UlidBasedGuidCreatorMock extends UlidBasedGuidCreator {
public UlidBasedGuidCreatorMock(long previousTimestamp) {
super();
this.previousTimestamp = previousTimestamp;
}
public UlidBasedGuidCreatorMock(long randomMsb, long randomLsb, long randomMsbMax, long randomLsbMax, long previousTimestamp) {
this.randomMsb = randomMsb;
this.randomLsb = randomLsb;
this.randomMsbMax = randomMsbMax;
this.randomLsbMax = randomLsbMax;
this.previousTimestamp = previousTimestamp;
}
public long getRandomMsb() {
return this.randomMsb;
}
public long getRandomLsb() {
return this.randomLsb;
}
public long getRandomHiMax() {
return this.randomMsb;
}
public long getRandomLoMax() {
return this.randomLsb;
}
}

View File

@ -1,168 +0,0 @@
package com.github.f4b6a3.ulid.creator;
import java.util.Random;
import java.util.UUID;
import org.junit.Test;
import com.github.f4b6a3.util.random.Xorshift128PlusRandom;
import com.github.f4b6a3.ulid.exception.UlidCreatorException;
import com.github.f4b6a3.ulid.strategy.timestamp.FixedTimestampStretegy;
import static org.junit.Assert.*;
public class UlidBasedGuidCreatorTest {
private static final long DEFAULT_LOOP_MAX = 1_000_000;
private static final long TIMESTAMP = System.currentTimeMillis();
private static final Random RANDOM = new Xorshift128PlusRandom();
@Test
public void testRandomMostSignificantBits() {
UlidBasedGuidCreatorMock creator = new UlidBasedGuidCreatorMock(TIMESTAMP);
creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP));
UUID uuid = creator.create();
long firstMsb = creator.extractRandomMsb(uuid);
for (int i = 0; i < DEFAULT_LOOP_MAX; i++) {
uuid = creator.create();
}
long lastMsb = creator.extractRandomMsb(uuid);
long expectedMsb = firstMsb;
assertEquals(String.format("The last MSB should be iqual to the first %s.", expectedMsb), expectedMsb, lastMsb);
creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP + 1));
uuid = creator.create();
lastMsb = uuid.getMostSignificantBits();
assertNotEquals("The last MSB should be random after timestamp changed.", firstMsb, lastMsb);
}
@Test
public void testRandomLeastSignificantBits() {
UlidBasedGuidCreatorMock creator = new UlidBasedGuidCreatorMock(TIMESTAMP);
creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP));
UUID uuid = creator.create();
long firstLsb = creator.extractRandomLsb(uuid);
for (int i = 0; i < DEFAULT_LOOP_MAX; i++) {
uuid = creator.create();
}
long lastLsb = creator.extractRandomLsb(uuid);
long expected = firstLsb + DEFAULT_LOOP_MAX;
assertEquals(String.format("The last LSB should be iqual to %s.", expected), expected, lastLsb);
long notExpected = firstLsb + DEFAULT_LOOP_MAX + 1;
creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP + 1));
uuid = creator.create();
lastLsb = uuid.getLeastSignificantBits();
assertNotEquals("The last LSB should be random after timestamp changed.", notExpected, lastLsb);
}
@Test
public void testIncrementOfRandomLeastSignificantBits() {
UlidBasedGuidCreatorMock creator = new UlidBasedGuidCreatorMock(TIMESTAMP);
creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP));
long lsb = creator.getRandomLsb();
UUID uuid = new UUID(0, 0);
for (int i = 0; i < DEFAULT_LOOP_MAX; i++) {
uuid = creator.create();
}
long expectedLsb = lsb + DEFAULT_LOOP_MAX;
long randomLsb = creator.getRandomLsb();
assertEquals("Wrong LSB after loop.", expectedLsb, randomLsb);
randomLsb = creator.extractRandomLsb(uuid);
assertEquals("Wrong LSB after loop.", expectedLsb, randomLsb);
}
@Test
public void testIncrementOfRandomMostSignificantBits() {
UlidBasedGuidCreatorMock creator = new UlidBasedGuidCreatorMock(TIMESTAMP);
creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP));
long msb = creator.getRandomMsb();
UUID uuid = new UUID(0, 0);
for (int i = 0; i < DEFAULT_LOOP_MAX; i++) {
uuid = creator.create();
}
long expectedMsb = msb;
long randomMsb = creator.getRandomMsb();
assertEquals("Wrong MSB after loop.", expectedMsb, randomMsb);
randomMsb = creator.extractRandomMsb(uuid);
assertEquals("Wrong MSB after loop.", expectedMsb, randomMsb);
}
@Test
public void testShouldThrowOverflowException1() {
long msbMax = 0x000001ffffffffffL;
long lsbMax = 0x000001ffffffffffL;
long msb = msbMax - 1;
long lsb = lsbMax - DEFAULT_LOOP_MAX;
UlidBasedGuidCreatorMock creator = new UlidBasedGuidCreatorMock(msb, lsb, msbMax, lsbMax, TIMESTAMP);
creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP));
for (int i = 0; i < DEFAULT_LOOP_MAX - 1; i++) {
creator.create();
}
try {
creator.create();
fail("It should throw an overflow exception.");
} catch (UlidCreatorException e) {
// success
}
}
@Test
public void testShouldThrowOverflowException2() {
long msbMax = (RANDOM.nextLong() & UlidBasedGuidCreatorMock.HALF_RANDOM_COMPONENT)
| UlidBasedGuidCreatorMock.INCREMENT_MAX;
long lsbMax = (RANDOM.nextLong() & UlidBasedGuidCreatorMock.HALF_RANDOM_COMPONENT)
| UlidBasedGuidCreatorMock.INCREMENT_MAX;
long msb = msbMax - 1;
long lsb = lsbMax - DEFAULT_LOOP_MAX;
UlidBasedGuidCreatorMock creator = new UlidBasedGuidCreatorMock(msb, lsb, msbMax, lsbMax, TIMESTAMP);
creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP));
UUID uuid = new UUID(0, 0);
for (int i = 0; i < DEFAULT_LOOP_MAX - 1; i++) {
uuid = creator.create();
}
long expectedLsb = (lsbMax - 1) & UlidBasedGuidCreatorMock.HALF_RANDOM_COMPONENT;
long randomLsb = creator.extractRandomLsb(uuid);
assertEquals("Incorrect LSB after loop.", expectedLsb, randomLsb);
long expectedMsb = (msbMax - 1) & UlidBasedGuidCreatorMock.HALF_RANDOM_COMPONENT;
long randomMsb = creator.extractRandomMsb(uuid);
assertEquals("Incorrect MSB after loop.", expectedMsb, randomMsb);
try {
creator.create();
fail("It should throw an overflow exception.");
} catch (UlidCreatorException e) {
// success
}
}
}

View File

@ -0,0 +1,38 @@
package com.github.f4b6a3.ulid.creator;
import com.github.f4b6a3.ulid.creator.UlidSpecCreator;
class UlidSpecCreatorMock extends UlidSpecCreator {
public UlidSpecCreatorMock(long previousTimestamp) {
super();
this.previousTimestamp = previousTimestamp;
}
public UlidSpecCreatorMock(long random1, long random2, long randomMax1, long randomMax2, long previousTimestamp) {
this.random1 = random1;
this.random2 = random2;
this.randomMax1 = randomMax1;
this.randomMax2 = randomMax2;
this.previousTimestamp = previousTimestamp;
}
public long getRandom1() {
return this.random1;
}
public long getRandom2() {
return this.random2;
}
public long getRandomMax1() {
return this.random1;
}
public long getRandomMax2() {
return this.random2;
}
}

View File

@ -0,0 +1,255 @@
package com.github.f4b6a3.ulid.creator;
import java.math.BigInteger;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
import org.junit.Test;
import com.github.f4b6a3.ulid.UlidCreator;
import com.github.f4b6a3.ulid.exception.UlidCreatorException;
import com.github.f4b6a3.ulid.strategy.timestamp.FixedTimestampStretegy;
import static org.junit.Assert.*;
public class UlidSpecCreatorTest {
private static final int DEFAULT_LOOP_MAX = 1_000_000;
private static final long TIMESTAMP = System.currentTimeMillis();
private static final Random RANDOM = new Random();
protected static final String DUPLICATE_UUID_MSG = "A duplicate ULID was created";
protected static final int THREAD_TOTAL = availableProcessors();
private static int availableProcessors() {
int processors = Runtime.getRuntime().availableProcessors();
if (processors < 4) {
processors = 4;
}
return processors;
}
@Test
public void testRandomMostSignificantBits() {
UlidSpecCreatorMock creator = new UlidSpecCreatorMock(TIMESTAMP);
creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP));
UUID uuid = creator.create();
long firstRand1 = creator.extractRandom1(uuid);
for (int i = 0; i < DEFAULT_LOOP_MAX; i++) {
uuid = creator.create();
}
long lastRand1 = creator.extractRandom1(uuid);
long expected1 = firstRand1;
assertEquals(String.format("The last high random should be iqual to the first %s.", expected1), expected1,
lastRand1);
creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP + 1));
uuid = creator.create();
lastRand1 = uuid.getMostSignificantBits();
assertNotEquals("The last high random should be random after timestamp changed.", firstRand1, lastRand1);
}
@Test
public void testRandomLeastSignificantBits() {
UlidSpecCreatorMock creator = new UlidSpecCreatorMock(TIMESTAMP);
creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP));
UUID uuid = creator.create();
long firstRnd2 = creator.extractRandom2(uuid);
for (int i = 0; i < DEFAULT_LOOP_MAX; i++) {
uuid = creator.create();
}
long lastRand2 = creator.extractRandom2(uuid);
long expected = firstRnd2 + DEFAULT_LOOP_MAX;
assertEquals(String.format("The last low random should be iqual to %s.", expected), expected, lastRand2);
long notExpected = firstRnd2 + DEFAULT_LOOP_MAX + 1;
creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP + 1));
uuid = creator.create();
lastRand2 = uuid.getLeastSignificantBits();
assertNotEquals("The last low random should be random after timestamp changed.", notExpected, lastRand2);
}
@Test
public void testIncrementOfRandomLeastSignificantBits() {
UlidSpecCreatorMock creator = new UlidSpecCreatorMock(TIMESTAMP);
creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP));
long random2 = creator.getRandom2();
UUID uuid = new UUID(0, 0);
for (int i = 0; i < DEFAULT_LOOP_MAX; i++) {
uuid = creator.create();
}
long expected2 = random2 + DEFAULT_LOOP_MAX;
long rand2 = creator.getRandom2();
assertEquals("Wrong low random after loop.", expected2, rand2);
rand2 = creator.extractRandom2(uuid);
assertEquals("Wrong low random after loop.", expected2, rand2);
}
@Test
public void testIncrementOfRandomMostSignificantBits() {
UlidSpecCreatorMock creator = new UlidSpecCreatorMock(TIMESTAMP);
creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP));
long random1 = creator.getRandom1();
UUID uuid = new UUID(0, 0);
for (int i = 0; i < DEFAULT_LOOP_MAX; i++) {
uuid = creator.create();
}
long expected1 = random1;
long rand1 = creator.getRandom1();
assertEquals("Wrong high random after loop.", expected1, rand1);
rand1 = creator.extractRandom1(uuid);
assertEquals("Wrong high random after loop.", expected1, rand1);
}
@Test
public void testShouldThrowOverflowException1() {
long random1 = 0x000000ffffffffffL;
long random2 = 0x000000ffffffffffL;
long max1 = random1 | UlidSpecCreatorMock.INCREMENT_MAX;
long max2 = random2 | UlidSpecCreatorMock.INCREMENT_MAX;
random1 = max1;
random2 = max2 - DEFAULT_LOOP_MAX;
UlidSpecCreatorMock creator = new UlidSpecCreatorMock(random1, random2, max1, max2, TIMESTAMP);
creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP));
UUID uuid = new UUID(0, 0);
for (int i = 0; i < DEFAULT_LOOP_MAX; i++) {
uuid = creator.create();
}
long hi1 = random1 & UlidSpecCreatorMock.HALF_RANDOM_COMPONENT;
long lo1 = random2 & UlidSpecCreatorMock.HALF_RANDOM_COMPONENT;
String concat1 = (Long.toHexString(hi1) + Long.toHexString(lo1));
BigInteger bigint1 = new BigInteger(concat1, 16);
long hi2 = creator.extractRandom1(uuid);
long lo2 = creator.extractRandom2(uuid);
String concat2 = (Long.toHexString(hi2) + Long.toHexString(lo2));
BigInteger bigint2 = new BigInteger(concat2, 16);
assertEquals(bigint1.add(BigInteger.valueOf(DEFAULT_LOOP_MAX)), bigint2);
try {
uuid = creator.create();
fail("It should throw an overflow exception.");
} catch (UlidCreatorException e) {
// success
}
}
@Test
public void testShouldThrowOverflowException2() {
long random1 = (RANDOM.nextLong() & UlidSpecCreatorMock.HALF_RANDOM_COMPONENT);
long random2 = (RANDOM.nextLong() & UlidSpecCreatorMock.HALF_RANDOM_COMPONENT);
long max1 = random1 | UlidSpecCreatorMock.INCREMENT_MAX;
long max2 = random2 | UlidSpecCreatorMock.INCREMENT_MAX;
random1 = max1;
random2 = max2 - DEFAULT_LOOP_MAX;
UlidSpecCreatorMock creator = new UlidSpecCreatorMock(random1, random2, max1, max2, TIMESTAMP);
creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP));
UUID uuid = new UUID(0, 0);
for (int i = 0; i < DEFAULT_LOOP_MAX; i++) {
uuid = creator.create();
}
long rand1 = creator.extractRandom1(uuid);
long expected1 = (max1 & UlidSpecCreatorMock.HALF_RANDOM_COMPONENT);
assertEquals("Incorrect high random after loop.", expected1, rand1);
long rand2 = creator.extractRandom2(uuid);
long expected2 = (max2 & UlidSpecCreatorMock.HALF_RANDOM_COMPONENT);
assertEquals("Incorrect low random after loop.", expected2, rand2);
long hi1 = random1 & UlidSpecCreatorMock.HALF_RANDOM_COMPONENT;
long lo1 = random2 & UlidSpecCreatorMock.HALF_RANDOM_COMPONENT;
String concat1 = (Long.toHexString(hi1) + Long.toHexString(lo1));
BigInteger bigint1 = new BigInteger(concat1, 16);
long hi2 = creator.extractRandom1(uuid);
long lo2 = creator.extractRandom2(uuid);
String concat2 = (Long.toHexString(hi2) + Long.toHexString(lo2));
BigInteger bigint2 = new BigInteger(concat2, 16);
assertEquals(bigint1.add(BigInteger.valueOf(DEFAULT_LOOP_MAX)), bigint2);
try {
creator.create();
fail("It should throw an overflow exception.");
} catch (UlidCreatorException e) {
// success
}
}
@Test
public void testGetUlidParallelGeneratorsShouldCreateUniqueUlids() throws InterruptedException {
Thread[] threads = new Thread[THREAD_TOTAL];
TestThread.clearHashSet();
// Instantiate and start many threads
for (int i = 0; i < THREAD_TOTAL; i++) {
threads[i] = new TestThread(UlidCreator.getUlidSpecCreator(), DEFAULT_LOOP_MAX);
threads[i].start();
}
// Wait all the threads to finish
for (Thread thread : threads) {
thread.join();
}
// Check if the quantity of unique UUIDs is correct
assertEquals(DUPLICATE_UUID_MSG, TestThread.hashSet.size(), (DEFAULT_LOOP_MAX * THREAD_TOTAL));
}
public static class TestThread extends Thread {
public static Set<UUID> hashSet = new HashSet<>();
private UlidSpecCreator creator;
private int loopLimit;
public TestThread(UlidSpecCreator creator, int loopLimit) {
this.creator = creator;
this.loopLimit = loopLimit;
}
public static void clearHashSet() {
hashSet = new HashSet<>();
}
@Override
public void run() {
for (int i = 0; i < loopLimit; i++) {
synchronized (hashSet) {
hashSet.add(creator.create());
}
}
}
}
}

View File

@ -1,37 +1,20 @@
package com.github.f4b6a3.ulid.ulid; package com.github.f4b6a3.ulid.ulid;
import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
import com.github.f4b6a3.ulid.UlidCreator; import com.github.f4b6a3.ulid.UlidCreator;
import com.github.f4b6a3.ulid.creator.UlidBasedGuidCreator;
import com.github.f4b6a3.ulid.util.UlidUtil; import com.github.f4b6a3.ulid.util.UlidUtil;
import com.github.f4b6a3.ulid.util.UlidValidator; import com.github.f4b6a3.ulid.util.UlidValidator;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
public class UlidCreatorTest { public class UlidCreatorTest {
private static int processors;
private static final int ULID_LENGTH = 26; private static final int ULID_LENGTH = 26;
private static final int DEFAULT_LOOP_MAX = 100_000; private static final int DEFAULT_LOOP_MAX = 100_000;
private static final String DUPLICATE_UUID_MSG = "A duplicate ULID was created";
@BeforeClass
public static void beforeClass() {
processors = Runtime.getRuntime().availableProcessors();
if (processors < 4) {
processors = 4;
}
}
@Test @Test
public void testGetUlid() { public void testGetUlid() {
String[] list = new String[DEFAULT_LOOP_MAX]; String[] list = new String[DEFAULT_LOOP_MAX];
@ -52,9 +35,9 @@ public class UlidCreatorTest {
private void checkNullOrInvalid(String[] list) { private void checkNullOrInvalid(String[] list) {
for (String ulid : list) { for (String ulid : list) {
assertTrue("ULID is null", ulid != null); assertNotNull("ULID is null", ulid);
assertTrue("ULID is empty", !ulid.isEmpty()); assertTrue("ULID is empty", !ulid.isEmpty());
assertTrue("ULID length is wrong ", ulid.length() == ULID_LENGTH); assertEquals("ULID length is wrong", ULID_LENGTH, ulid.length());
assertTrue("ULID is not valid", UlidValidator.isValid(ulid)); assertTrue("ULID is not valid", UlidValidator.isValid(ulid));
} }
} }
@ -67,7 +50,7 @@ public class UlidCreatorTest {
assertTrue(String.format("ULID is duplicated %s", ulid), set.add(ulid)); assertTrue(String.format("ULID is duplicated %s", ulid), set.add(ulid));
} }
assertTrue("There are duplicated ULIDs", set.size() == list.length); assertEquals("There are duplicated ULIDs", set.size(), list.length);
} }
private void checkCreationTime(String[] list, long startTime, long endTime) { private void checkCreationTime(String[] list, long startTime, long endTime) {
@ -87,53 +70,7 @@ public class UlidCreatorTest {
Arrays.sort(other); Arrays.sort(other);
for (int i = 0; i < list.length; i++) { for (int i = 0; i < list.length; i++) {
assertTrue("The ULID list is not ordered", list[i].equals(other[i])); assertEquals("The ULID list is not ordered", list[i], other[i]);
}
}
@Test
public void testGetUlidBasedGuidParallelGeneratorsShouldCreateUniqueUuids() throws InterruptedException {
Thread[] threads = new Thread[processors];
TestThread.clearHashSet();
// Instantiate and start many threads
for (int i = 0; i < processors; i++) {
threads[i] = new TestThread(UlidCreator.getUlidBasedCreator(), DEFAULT_LOOP_MAX);
threads[i].start();
}
// Wait all the threads to finish
for (Thread thread : threads) {
thread.join();
}
// Check if the quantity of unique UUIDs is correct
assertTrue(DUPLICATE_UUID_MSG, TestThread.hashSet.size() == (DEFAULT_LOOP_MAX * processors));
}
private static class TestThread extends Thread {
private static Set<UUID> hashSet = new HashSet<>();
private UlidBasedGuidCreator creator;
private int loopLimit;
public TestThread(UlidBasedGuidCreator creator, int loopLimit) {
this.creator = creator;
this.loopLimit = loopLimit;
}
public static void clearHashSet() {
hashSet = new HashSet<>();
}
@Override
public void run() {
for (int i = 0; i < loopLimit; i++) {
synchronized (hashSet) {
hashSet.add(creator.create());
}
}
} }
} }
} }

View File

@ -22,9 +22,9 @@ public class UlidConverterTest {
UUID uuid1 = UlidCreator.getUlid(); UUID uuid1 = UlidCreator.getUlid();
String ulid = UlidConverter.toString(uuid1); String ulid = UlidConverter.toString(uuid1);
assertTrue("ULID is null", ulid != null); assertNotNull("ULID is null", ulid);
assertTrue("ULID is empty", !ulid.isEmpty()); assertTrue("ULID is empty", !ulid.isEmpty());
assertTrue("ULID length is wrong ", ulid.length() == ULID_LENGTH); assertEquals("ULID length is wrong ", ULID_LENGTH, ulid.length());
assertTrue("ULID is not valid", UlidValidator.isValid(ulid)); assertTrue("ULID is not valid", UlidValidator.isValid(ulid));
UUID uuid2 = UlidConverter.fromString(ulid); UUID uuid2 = UlidConverter.fromString(ulid);

View File

@ -5,10 +5,8 @@ import java.time.Instant;
import org.junit.Test; import org.junit.Test;
import com.github.f4b6a3.util.Base32Util;
import com.github.f4b6a3.util.ByteUtil;
import com.github.f4b6a3.ulid.exception.InvalidUlidException; import com.github.f4b6a3.ulid.exception.InvalidUlidException;
import com.github.f4b6a3.ulid.util.UlidUtil; import static com.github.f4b6a3.ulid.util.UlidUtil.*;
public class UlidUtilTest { public class UlidUtilTest {
@ -21,20 +19,43 @@ public class UlidUtilTest {
private static final String[] EXAMPLE_DATES = { "1970-01-01T00:00:00.000Z", "1985-10-26T01:16:00.123Z", 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" }; "2001-09-09T01:46:40.456Z", "2020-01-15T14:30:33.789Z", "2038-01-19T03:14:07.321Z" };
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" };
@Test(expected = InvalidUlidException.class) @Test(expected = InvalidUlidException.class)
public void testExtractTimestamp() { public void testExtractTimestamp() {
String ulid = "0000000000" + EXAMPLE_RANDOMNESS; String ulid = "0000000000" + EXAMPLE_RANDOMNESS;
long milliseconds = UlidUtil.extractTimestamp(ulid); long milliseconds = extractTimestamp(ulid);
assertEquals(0, milliseconds); assertEquals(0, milliseconds);
ulid = "7ZZZZZZZZZ" + EXAMPLE_RANDOMNESS; ulid = "7ZZZZZZZZZ" + EXAMPLE_RANDOMNESS;
milliseconds = UlidUtil.extractTimestamp(ulid); milliseconds = extractTimestamp(ulid);
assertEquals(TIMESTAMP_MAX, milliseconds); assertEquals(TIMESTAMP_MAX, milliseconds);
ulid = "8ZZZZZZZZZ" + EXAMPLE_RANDOMNESS; ulid = "8ZZZZZZZZZ" + EXAMPLE_RANDOMNESS;
UlidUtil.extractTimestamp(ulid); extractTimestamp(ulid);
fail("Should throw exception: invalid ULID");
} }
@Test @Test
@ -43,12 +64,11 @@ public class UlidUtilTest {
String randomnessComponent = EXAMPLE_RANDOMNESS; String randomnessComponent = EXAMPLE_RANDOMNESS;
for (String i : EXAMPLE_DATES) { for (String i : EXAMPLE_DATES) {
long milliseconds = Instant.parse(i).toEpochMilli(); long milliseconds = Instant.parse(i).toEpochMilli();
String timestampComponent = leftPad(Base32Util.toBase32Crockford(milliseconds)); String timestampComponent = new String(UlidUtil.zerofill(toBase32Crockford(milliseconds), 10));
String ulid = timestampComponent + randomnessComponent; String ulid = timestampComponent + randomnessComponent;
long result = UlidUtil.extractTimestamp(ulid); long result = extractTimestamp(ulid);
assertEquals(milliseconds, result); assertEquals(milliseconds, result);
} }
@ -65,11 +85,11 @@ public class UlidUtilTest {
long milliseconds = Instant.parse(i).toEpochMilli(); long milliseconds = Instant.parse(i).toEpochMilli();
byte[] bytes = new byte[6]; byte[] bytes = new byte[6];
System.arraycopy(ByteUtil.toBytes(milliseconds), 2, bytes, 0, 6); System.arraycopy(toBytes(milliseconds), 2, bytes, 0, 6);
String timestampComponent = leftPad(Base32Util.toBase32Crockford(milliseconds)); String timestampComponent = new String(UlidUtil.zerofill(toBase32Crockford(milliseconds), 10));
String ulid = timestampComponent + randomnessComponent; String ulid = timestampComponent + randomnessComponent;
Instant result = UlidUtil.extractInstant(ulid); Instant result = extractInstant(ulid);
assertEquals(instant, result); assertEquals(instant, result);
} }
@ -79,7 +99,7 @@ public class UlidUtilTest {
public void testExtractTimestampComponent() { public void testExtractTimestampComponent() {
String ulid = EXAMPLE_ULID; String ulid = EXAMPLE_ULID;
String expected = EXAMPLE_TIMESTAMP; String expected = EXAMPLE_TIMESTAMP;
String result = UlidUtil.extractTimestampComponent(ulid); String result = extractTimestampComponent(ulid);
assertEquals(expected, result); assertEquals(expected, result);
} }
@ -87,11 +107,157 @@ public class UlidUtilTest {
public void testExtractRandomnessComponent() { public void testExtractRandomnessComponent() {
String ulid = EXAMPLE_ULID; String ulid = EXAMPLE_ULID;
String expected = EXAMPLE_RANDOMNESS; String expected = EXAMPLE_RANDOMNESS;
String result = UlidUtil.extractRandomnessComponent(ulid); String result = extractRandomnessComponent(ulid);
assertEquals(expected, result); assertEquals(expected, result);
} }
private String leftPad(String unpadded) { @Test
return "0000000000".substring(unpadded.length()) + unpadded; 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);
}
}
@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);
}
} }
} }

View File

@ -9,7 +9,7 @@ import com.github.f4b6a3.ulid.util.UlidValidator;
public class UlidValidatorTest { public class UlidValidatorTest {
@Test @Test
public void testIsValidStrict() { public void testIsValid() {
String ulid = null; // Null String ulid = null; // Null
assertFalse("Null ULID should be invalid.", UlidValidator.isValid(ulid)); assertFalse("Null ULID should be invalid.", UlidValidator.isValid(ulid));
@ -38,7 +38,7 @@ public class UlidValidatorTest {
ulid = "#123456789ABCDEFGHJKMNPQRS"; // Special char ulid = "#123456789ABCDEFGHJKMNPQRS"; // Special char
assertFalse("ULID with special chars should be invalid.", UlidValidator.isValid(ulid)); assertFalse("ULID with special chars should be invalid.", UlidValidator.isValid(ulid));
ulid = "01234-56789-ABCDEFGHJKMNPQRS"; // Hyphens ulid = "01234-56789-ABCDEFGHJKM---NPQRS"; // Hyphens
assertTrue("ULID with hiphens should be valid.", UlidValidator.isValid(ulid)); assertTrue("ULID with hiphens should be valid.", UlidValidator.isValid(ulid));
ulid = "8ZZZZZZZZZABCDEFGHJKMNPQRS"; // timestamp > (2^48)-1 ulid = "8ZZZZZZZZZABCDEFGHJKMNPQRS"; // timestamp > (2^48)-1