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
Copyright (c) 2020 f4b6a3
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

140
README.md
View File

@ -12,7 +12,7 @@ Create a ULID as GUID:
UUID ulid = UlidCreator.getUlid();
```
Create a ULID string:
Create a ULID as string:
```java
String ulid = UlidCreator.getUlidString();
@ -28,7 +28,7 @@ Add these lines to your `pom.xml`.
<dependency>
<groupId>com.github.f4b6a3</groupId>
<artifactId>ulid-creator</artifactId>
<version>1.1.1</version>
<version>2.0.0</version>
</dependency>
```
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
------------------------------------------------------
### 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.
@ -72,66 +110,68 @@ Examples of ULIDs:
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.
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:
These are some examples of using the `UlidSpecCreator` to create ULIDs strings:
```java
// with your custom timestamp strategy
TimestampStrategy customStrategy = new CustomTimestampStrategy();
String ulid = UlidCreator.getUlidBasedGuidCreator()
String ulid = UlidCreator.getUlidSpecCreator()
.withTimestampStrategy(customStrategy)
.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
Random random = new Random();
String ulid = UlidCreator.getUlidBasedGuidCreator()
String ulid = UlidCreator.getUlidSpecCreator()
.withRandomGenerator(random)
.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>
<artifactId>ulid-creator</artifactId>
<version>1.1.2-SNAPSHOT</version>
<version>2.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<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>
@ -26,17 +26,12 @@
<properties>
<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.target>${jdk.version}</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.github.f4b6a3</groupId>
<artifactId>util</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
@ -46,10 +41,10 @@
</dependencies>
<scm>
<connection>scm:git:git://github.com/dexecutor/dependent-tasks-executor.git</connection>
<developerConnection>scm:git:git@github.com:dexecutor/dexecutor.git</developerConnection>
<url>https://github.com/dexecutor/dependent-tasks-executor</url>
<tag>ulid-creator-1.0.0</tag>
<url>https://github.com/f4b6a3/ulid-creator</url>
<connection>scm:git:ssh://git@github.com/f4b6a3/ulid-creator.git</connection>
<developerConnection>scm:git:ssh://git@github.com/f4b6a3/ulid-creator.git</developerConnection>
<tag>HEAD</tag>
</scm>
<distributionManagement>

View File

@ -26,19 +26,29 @@ package com.github.f4b6a3.ulid;
import java.util.UUID;
import com.github.f4b6a3.util.random.Xorshift128PlusRandom;
import com.github.f4b6a3.ulid.creator.UlidBasedGuidCreator;
import com.github.f4b6a3.ulid.creator.UlidSpecCreator;
import com.github.f4b6a3.ulid.util.UlidConverter;
/**
* A factory for Universally Unique Lexicographically Sortable Identifiers.
*
* See the ULID spec: https://github.com/ulid/spec
*/
public class UlidCreator {
public final class 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.
*
@ -48,7 +58,7 @@ public class UlidCreator {
* @return a UUID
*/
public static UUID getUlid() {
return UlidBasedGuidCreatorHolder.INSTANCE.create();
return UlidSpecCreatorHolder.INSTANCE.create();
}
/**
@ -62,49 +72,19 @@ public class UlidCreator {
* @return a ULID
*/
public static String getUlidString() {
return UlidBasedGuidCreatorHolder.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 UlidSpecCreatorHolder.INSTANCE.createString();
}
/**
* Return a GUID creator for direct use.
*
* @return a {@link UlidBasedGuidCreator}
* @return a {@link UlidSpecCreator}
*/
public static UlidBasedGuidCreator getUlidBasedCreator() {
return new UlidBasedGuidCreator();
public static UlidSpecCreator getUlidSpecCreator() {
return new UlidSpecCreator();
}
private static class UlidBasedGuidCreatorHolder {
static final UlidBasedGuidCreator INSTANCE = getUlidBasedCreator();
}
private static class FastUlidBasedGuidCreatorHolder {
static final UlidBasedGuidCreator INSTANCE = getUlidBasedCreator().withFastRandomGenerator();
private static class UlidSpecCreatorHolder {
static final UlidSpecCreator INSTANCE = getUlidSpecCreator();
}
}

View File

@ -27,13 +27,14 @@ package com.github.f4b6a3.ulid.creator;
import java.util.Random;
import java.util.UUID;
import com.github.f4b6a3.ulid.util.UlidConverter;
import com.github.f4b6a3.util.random.Xorshift128PlusRandom;
import com.github.f4b6a3.util.FingerprintUtil;
import com.github.f4b6a3.util.RandomUtil;
import com.github.f4b6a3.ulid.strategy.RandomStrategy;
import com.github.f4b6a3.ulid.strategy.random.DefaultRandomStrategy;
import com.github.f4b6a3.ulid.strategy.random.OtherRandomStrategy;
import com.github.f4b6a3.ulid.exception.UlidCreatorException;
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;
/**
* 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
*/
public class UlidBasedGuidCreator {
public class UlidSpecCreator {
protected long randomMsb = 0;
protected long randomLsb = 0;
protected long random1 = 0;
protected long random2 = 0;
protected long randomLsbMax;
protected long randomMsbMax;
protected long randomMax2;
protected long randomMax1;
protected static final long HALF_RANDOM_COMPONENT = 0x000000ffffffffffL;
protected static final long INCREMENT_MAX = 0x0000010000000000L;
protected long previousTimestamp;
protected Random random;
protected static final String OVERRUN_MESSAGE = "The system overran the generator by requesting too many GUIDs.";
protected static final String OVERRUN_MESSAGE = "The system overran the generator by requesting too many ULIDs.";
protected TimestampStrategy timestampStrategy;
protected RandomStrategy randomStrategy;
public UlidBasedGuidCreator() {
this.reset();
public UlidSpecCreator() {
this.timestampStrategy = new DefaultTimestampStrategy();
this.randomStrategy = new DefaultRandomStrategy();
this.reset();
}
/**
@ -128,11 +129,11 @@ public class UlidBasedGuidCreator {
final long timestamp = this.getTimestamp();
final long randomHi = truncate(randomMsb);
final long randomLo = truncate(randomLsb);
final long rnd1 = random1 & HALF_RANDOM_COMPONENT;
final long rnd2 = random2 & HALF_RANDOM_COMPONENT;
final long msb = (timestamp << 16) | (randomHi >>> 24);
final long lsb = (randomHi << 40) | randomLo;
final long msb = (timestamp << 16) | (rnd1 >>> 24);
final long lsb = (rnd1 << 40) | rnd2;
return new UUID(msb, lsb);
}
@ -142,7 +143,10 @@ public class UlidBasedGuidCreator {
*
* 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() {
return UlidConverter.toString(create());
@ -173,17 +177,13 @@ public class UlidBasedGuidCreator {
protected synchronized void reset() {
// Get random values
if (random == null) {
this.randomMsb = truncate(RandomUtil.get().nextLong());
this.randomLsb = truncate(RandomUtil.get().nextLong());
} else {
this.randomMsb = truncate(random.nextLong());
this.randomLsb = truncate(random.nextLong());
}
final byte[] bytes = new byte[10];
this.randomStrategy.nextBytes(bytes);
this.random1 = UlidUtil.toNumber(bytes, 0, 5);
this.random2 = UlidUtil.toNumber(bytes, 5, 10);
// Save the random values
this.randomMsbMax = this.randomMsb | INCREMENT_MAX;
this.randomLsbMax = this.randomLsb | INCREMENT_MAX;
this.randomMax1 = this.random1 | INCREMENT_MAX;
this.randomMax2 = this.random2 | INCREMENT_MAX;
}
/**
@ -196,7 +196,7 @@ public class UlidBasedGuidCreator {
*/
protected synchronized void increment() {
if ((++this.randomLsb == this.randomLsbMax) && (++this.randomMsb == this.randomMsbMax)) {
if ((++this.random2 > this.randomMax2) && (++this.random1 > this.randomMax1)) {
this.reset();
throw new UlidCreatorException(OVERRUN_MESSAGE);
}
@ -206,73 +206,58 @@ public class UlidBasedGuidCreator {
* Used for changing the timestamp strategy.
*
* @param timestampStrategy a timestamp strategy
* @return {@link UlidBasedGuidCreator}
* @return {@link UlidSpecCreator}
*/
@SuppressWarnings("unchecked")
public synchronized <T extends UlidBasedGuidCreator> T withTimestampStrategy(TimestampStrategy timestampStrategy) {
public synchronized <T extends UlidSpecCreator> T withTimestampStrategy(TimestampStrategy timestampStrategy) {
this.timestampStrategy = timestampStrategy;
return (T) this;
}
/**
* Replace the default random generator, in a fluent way, to another that
* extends {@link Random}.
* Replaces the default random strategy with another.
*
* The default random generator is {@link java.security.SecureRandom}.
*
* For other faster pseudo-random generators, see {@link XorshiftRandom} and its
* variations.
* The default random strategy uses {@link java.security.SecureRandom}.
*
* See {@link Random}.
*
* @param random a random generator
* @return {@link UlidBasedGuidCreator}
* @param <T> the type parameter
* @return {@link AbstractRandomBasedUuidCreator}
*/
@SuppressWarnings("unchecked")
public synchronized <T extends UlidBasedGuidCreator> T withRandomGenerator(Random random) {
this.random = random;
public synchronized <T extends UlidSpecCreator> T withRandomStrategy(RandomStrategy randomStrategy) {
this.randomStrategy = randomStrategy;
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
* generator.
* It replaces the internal {@link DefaultRandomStrategy} with
* {@link OtherRandomStrategy}.
*
* See {@link Xorshift128PlusRandom} and
* {@link FingerprintUtil#getFingerprint()}
*
* @return {@link UlidBasedGuidCreator}
* @param random a random generator
* @return {@link UlidSpecCreator}
*/
@SuppressWarnings("unchecked")
public synchronized <T extends UlidBasedGuidCreator> T withFastRandomGenerator() {
final int salt = (int) FingerprintUtil.getFingerprint();
this.random = new Xorshift128PlusRandom(salt);
public synchronized <T extends UlidSpecCreator> T withRandomGenerator(Random random) {
this.randomStrategy = new OtherRandomStrategy(random);
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
*/
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;
}
/**
* 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;
public class InvalidUlidException extends RuntimeException {
public final class InvalidUlidException extends RuntimeException {
private static final long serialVersionUID = 1L;

View File

@ -24,7 +24,7 @@
package com.github.f4b6a3.ulid.exception;
public class UlidCreatorException extends RuntimeException {
public final class UlidCreatorException extends RuntimeException {
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;
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
public long getTimestamp() {

View File

@ -26,9 +26,9 @@ package com.github.f4b6a3.ulid.strategy.timestamp;
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) {
this.timestamp = timestamp;

View File

@ -26,10 +26,11 @@ package com.github.f4b6a3.ulid.util;
import java.util.UUID;
import com.github.f4b6a3.util.Base32Util;
import com.github.f4b6a3.util.ByteUtil;
import com.github.f4b6a3.ulid.exception.InvalidUlidException;
public class UlidConverter {
import static com.github.f4b6a3.ulid.util.UlidUtil.*;
public final class UlidConverter {
private UlidConverter() {
}
@ -39,8 +40,6 @@ public class UlidConverter {
*
* The returning string is encoded to Crockford's base32.
*
* The timestamp and random components are encoded separated.
*
* @param uuid a UUID
* @return a ULID
*/
@ -49,19 +48,20 @@ public class UlidConverter {
final long msb = uuid.getMostSignificantBits();
final long lsb = uuid.getLeastSignificantBits();
// Extract timestamp component
final long timeNumber = (msb >>> 16);
String timestampComponent = leftPad(Base32Util.toBase32Crockford(timeNumber));
final long time = ((msb & 0xffffffffffff0000L) >>> 16);
final long random1 = ((msb & 0x000000000000ffffL) << 24) | ((lsb & 0xffffff0000000000L) >>> 40);
final long random2 = (lsb & 0x000000ffffffffffL);
// Extract randomness component
byte[] randBytes = new byte[10];
randBytes[0] = (byte) (msb >>> 8);
randBytes[1] = (byte) (msb);
byte[] lsbBytes = ByteUtil.toBytes(lsb);
System.arraycopy(lsbBytes, 0, randBytes, 2, 8);
String randomnessComponent = Base32Util.toBase32Crockford(randBytes);
final char[] timeComponent = zerofill(toBase32Crockford(time), 10);
final char[] randomComponent1 = zerofill(toBase32Crockford(random1), 8);
final char[] randomComponent2 = zerofill(toBase32Crockford(random2), 8);
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
* specification.
*
* The timestamp and random components are decoded separated.
*
* An exception is thrown if the ULID string is invalid.
*
* @param ulid a ULID
* @return a UUID if valid
* @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);
// Extract timestamp component
final String timestampComponent = ulid.substring(0, 10);
final long timeNumber = Base32Util.fromBase32CrockfordAsLong(timestampComponent);
final long time = fromBase32Crockford(timeComponent);
final long random1 = fromBase32Crockford(randomComponent1);
final long random2 = fromBase32Crockford(randomComponent2);
// Extract randomness component
final String randomnessComponent = ulid.substring(10, 26);
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);
final long msb = ((time & 0x0000ffffffffffffL) << 16) | ((random1 & 0x000000ffff000000L) >>> 24);
final long lsb = ((random1 & 0x0000000000ffffffL) << 40) | (random2 & 0x000000ffffffffffL);
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 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() {
}
@ -50,11 +55,196 @@ public class UlidUtil {
public static String extractRandomnessComponent(String ulid) {
UlidValidator.validate(ulid);
return ulid.substring(10, 26);
return ulid.substring(10, ULID_CHAR_LENGTH);
}
protected static long extractUnixMilliseconds(String ulid) {
String milliseconds = ulid.substring(0, 10);
return Base32Util.fromBase32CrockfordAsLong(milliseconds);
return fromBase32Crockford(extractTimestampComponent(ulid).toCharArray());
}
/**
* 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;
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;
private UlidValidator() {
@ -42,14 +42,12 @@ public class UlidValidator {
* A valid ULID string is a sequence of 26 characters from Crockford's base 32
* alphabet.
*
* Dashes are ignored by this validator.
*
* <pre>
* Examples of valid ULID strings:
* - 0123456789ABCDEFGHJKMNPKRS (26 alphanumeric, case insensitive, except iI, lL, oO and uU)
* - 0123456789ABCDEFGHIJKLMNOP (26 alphanumeric, case insensitive, except uU)
* - 0123456789-ABCDEFGHJK-MNPKRS (26 alphanumeric, case insensitive, except iI, lL, oO and uU)
* - 0123456789-ABCDEFGHIJ-KLMNOP (26 alphanumeric, case insensitive, except uU, with dashes)
* - 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)
* </pre>
*
* @param ulid a ULID
@ -57,16 +55,20 @@ public class UlidValidator {
*/
public static boolean isValid(String ulid) {
if (ulid == null || ulid.isEmpty()) {
if (ulid == null) {
return false;
}
String u = ulid.replaceAll("-", "");
if (!u.matches(ULID_PATTERN)) {
char[] chars = removeHyphens(ulid.toCharArray());
if (chars.length != ULID_CHAR_LENGTH || !isCrockfordBase32(chars)) {
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;
}

View File

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

View File

@ -4,7 +4,7 @@ import java.util.HashSet;
import java.util.UUID;
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.strategy.timestamp.FixedTimestampStretegy;
@ -27,7 +27,7 @@ public class UniquenessTest {
private boolean verbose; // Show progress or not
// GUID creator based on ULID spec
private UlidBasedGuidCreator creator;
private UlidSpecCreator creator;
/**
* Initialize the test.
@ -36,7 +36,7 @@ public class UniquenessTest {
* @param requestCount
* @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.requestCount = requestCount;
this.creator = creator;
@ -125,7 +125,7 @@ public class UniquenessTest {
}
public static void execute(boolean verbose, int threadCount, int requestCount) {
UlidBasedGuidCreator creator = UlidCreator.getUlidBasedCreator()
UlidSpecCreator creator = UlidCreator.getUlidSpecCreator()
.withTimestampStrategy(new FixedTimestampStretegy(System.currentTimeMillis()));
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;
import org.junit.BeforeClass;
import org.junit.Test;
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.UlidValidator;
import static org.junit.Assert.*;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
public class UlidCreatorTest {
private static int processors;
private static final int ULID_LENGTH = 26;
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
public void testGetUlid() {
String[] list = new String[DEFAULT_LOOP_MAX];
@ -52,9 +35,9 @@ public class UlidCreatorTest {
private void checkNullOrInvalid(String[] list) {
for (String ulid : list) {
assertTrue("ULID is null", ulid != null);
assertNotNull("ULID is null", ulid);
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));
}
}
@ -67,7 +50,7 @@ public class UlidCreatorTest {
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) {
@ -87,53 +70,7 @@ public class UlidCreatorTest {
Arrays.sort(other);
for (int i = 0; i < list.length; i++) {
assertTrue("The ULID list is not ordered", list[i].equals(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());
}
}
assertEquals("The ULID list is not ordered", list[i], other[i]);
}
}
}

View File

@ -22,9 +22,9 @@ public class UlidConverterTest {
UUID uuid1 = UlidCreator.getUlid();
String ulid = UlidConverter.toString(uuid1);
assertTrue("ULID is null", ulid != null);
assertNotNull("ULID is null", ulid);
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));
UUID uuid2 = UlidConverter.fromString(ulid);

View File

@ -5,10 +5,8 @@ import java.time.Instant;
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.util.UlidUtil;
import static com.github.f4b6a3.ulid.util.UlidUtil.*;
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",
"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)
public void testExtractTimestamp() {
String ulid = "0000000000" + EXAMPLE_RANDOMNESS;
long milliseconds = UlidUtil.extractTimestamp(ulid);
long milliseconds = extractTimestamp(ulid);
assertEquals(0, milliseconds);
ulid = "7ZZZZZZZZZ" + EXAMPLE_RANDOMNESS;
milliseconds = UlidUtil.extractTimestamp(ulid);
milliseconds = extractTimestamp(ulid);
assertEquals(TIMESTAMP_MAX, milliseconds);
ulid = "8ZZZZZZZZZ" + EXAMPLE_RANDOMNESS;
UlidUtil.extractTimestamp(ulid);
fail("Should throw exception: invalid ULID");
extractTimestamp(ulid);
}
@Test
@ -43,12 +64,11 @@ public class UlidUtilTest {
String randomnessComponent = EXAMPLE_RANDOMNESS;
for (String i : EXAMPLE_DATES) {
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;
long result = UlidUtil.extractTimestamp(ulid);
long result = extractTimestamp(ulid);
assertEquals(milliseconds, result);
}
@ -65,11 +85,11 @@ public class UlidUtilTest {
long milliseconds = Instant.parse(i).toEpochMilli();
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;
Instant result = UlidUtil.extractInstant(ulid);
Instant result = extractInstant(ulid);
assertEquals(instant, result);
}
@ -79,7 +99,7 @@ public class UlidUtilTest {
public void testExtractTimestampComponent() {
String ulid = EXAMPLE_ULID;
String expected = EXAMPLE_TIMESTAMP;
String result = UlidUtil.extractTimestampComponent(ulid);
String result = extractTimestampComponent(ulid);
assertEquals(expected, result);
}
@ -87,11 +107,157 @@ public class UlidUtilTest {
public void testExtractRandomnessComponent() {
String ulid = EXAMPLE_ULID;
String expected = EXAMPLE_RANDOMNESS;
String result = UlidUtil.extractRandomnessComponent(ulid);
String result = extractRandomnessComponent(ulid);
assertEquals(expected, result);
}
private String leftPad(String unpadded) {
return "0000000000".substring(unpadded.length()) + unpadded;
@Test
public void testToUpperCase() {
String string = "Aq7zmxKxPc61QKiGRu8Y3PdYMer64lrRxfb9A5JAJuDeEhXSrbsxsaUoHrFzmEJUYBKJPgV+1rAd";
char[] chars1 = string.toCharArray();
char[] chars2 = UlidUtil.toUpperCase(chars1);
assertEquals(new String(string).toUpperCase(), new String(chars2));
string = "kL9zTzZfzlwKYCEmWKPxFYxINf6JZCSmSqykyG5ONWZcFkJG2WGc7gq71YCEzt2hYcsTvfQqEmn0";
chars1 = string.toCharArray();
chars2 = UlidUtil.toUpperCase(chars1);
assertEquals(new String(string).toUpperCase(), new String(chars2));
string = "XXEOUV3jJb3f+wpRPDVke9NgWwEgdkzChnKnpZZWS/mCSqTi757GmmqYdzuDGOa5ftqHI3/zqKrS";
chars1 = string.toCharArray();
chars2 = UlidUtil.toUpperCase(chars1);
assertEquals(new String(string).toUpperCase(), new String(chars2));
string = "t9LVRQZbCxTQgaxlajNE/VYpLpKiHtKt7jHrtxSDIJ2hrHaJI2UPF1zA7I35m9cKz01lHYD1IXlM";
chars1 = string.toCharArray();
chars2 = UlidUtil.toUpperCase(chars1);
assertEquals(new String(string).toUpperCase(), new String(chars2));
string = "jyS52J42LLT6GY+Zywo1R4tQv4bTfAqpFB6aiKEuA3yDxFkuXzuKe8PaGlUTaXD5WgRFMnO9nRLU";
chars1 = string.toCharArray();
chars2 = UlidUtil.toUpperCase(chars1);
assertEquals(new String(string).toUpperCase(), new String(chars2));
}
@Test
public void testZerofill() {
assertEquals("001", new String(UlidUtil.zerofill("1".toCharArray(), 3)));
assertEquals("000123", new String(UlidUtil.zerofill("123".toCharArray(), 6)));
assertEquals("0000000000", new String(UlidUtil.zerofill("".toCharArray(), 10)));
assertEquals("9876543210", new String(UlidUtil.zerofill("9876543210".toCharArray(), 10)));
assertEquals("0000000000123456", new String(UlidUtil.zerofill("123456".toCharArray(), 16)));
}
@Test
public void testLpad() {
String string = "";
char[] chars1 = string.toCharArray();
char[] chars2 = UlidUtil.lpad(chars1, 8, 'x');
assertEquals("xxxxxxxx", new String(chars2));
string = "";
chars1 = string.toCharArray();
chars2 = UlidUtil.lpad(chars1, 12, 'W');
assertEquals("WWWWWWWWWWWW", new String(chars2));
string = "TCgpYATMlK9BmSzX";
chars1 = string.toCharArray();
chars2 = UlidUtil.lpad(chars1, 13, '0');
assertEquals(string, new String(chars2));
string = "2kgy3m9U646L6TJ5";
chars1 = string.toCharArray();
chars2 = UlidUtil.lpad(chars1, 16, '0');
assertEquals(string, new String(chars2));
string = "2kgy3m9U646L6TJ5";
chars1 = string.toCharArray();
chars2 = UlidUtil.lpad(chars1, 17, '0');
assertEquals("0" + string, new String(chars2));
string = "LH6hfYcGJu06xSNF";
chars1 = string.toCharArray();
chars2 = UlidUtil.lpad(chars1, 25, '0');
assertEquals("000000000" + string, new String(chars2));
string = "t9LVRQZbCxTQgaxlajNE/VYpLpKiHtKt7jHrtxSDIJ2hrHaJI2UPF1zA7I35m9cKz01lHYD1IXlM";
chars1 = string.toCharArray();
chars2 = UlidUtil.lpad(chars1, 80, '0');
assertEquals("0000" + string, new String(chars2));
}
@Test
public void testRemoveHyphens() {
String string = "-ZGQ8yCsza-RFxlYyA-FaXa4wd-k4Owa-/ITDvqWOl4-Do3/NwW--Lawx6GcO-LmSRDsd3af3Zt-VMNnvLgIw9-";
char[] chars1 = string.toCharArray();
char[] chars2 = UlidUtil.removeHyphens(chars1);
assertEquals(string.replace("-", ""), new String(chars2));
string = "qi3q-EMvc1-Kk7XzYMj----SUnwf-lp0K7-Ucj-W-cDplP-2dG-3x+5y-r9JBc-ZT-0e--cRHoMbU/lBzZsJ6rcJ5zT/J";
chars1 = string.toCharArray();
chars2 = UlidUtil.removeHyphens(chars1);
assertEquals(string.replace("-", ""), new String(chars2));
string = "RXMD0DJV---Zf3Jqcv39uGjzBuiLkLNL-IvPnTyfMteEet-I7u-Z8oyE+BIUBf/OPi30iICP1TnQpMve4j";
chars1 = string.toCharArray();
chars2 = UlidUtil.removeHyphens(chars1);
assertEquals(string.replace("-", ""), new String(chars2));
string = "---dFH-b-ylQPA60-kuRxZ9-6q5MLd1-qLTKdma-rF2yEABt-t6mJg0U-ibIYcVnt-Guqdn-z-G-43Ob-W/-Gxah1+53a-";
chars1 = string.toCharArray();
chars2 = UlidUtil.removeHyphens(chars1);
assertEquals(string.replace("-", ""), new String(chars2));
string = "-------------------------------";
chars1 = string.toCharArray();
chars2 = UlidUtil.removeHyphens(chars1);
assertEquals(string.replace("-", ""), new String(chars2));
}
@Test
public void testTransliterate() {
char[] alphabetCrockford = "0123456789ABCDEFGHJKMNPQRSTVWXYZ".toCharArray();
char[] alphabetDefault = "0123456789abcdefghijklmnopqrstuv".toCharArray();
char[] chars2 = UlidUtil.transliterate(alphabetCrockford, alphabetCrockford, alphabetDefault);
assertEquals(new String(alphabetDefault), new String(chars2));
chars2 = UlidUtil.transliterate(alphabetDefault, alphabetDefault, alphabetCrockford);
assertEquals(new String(alphabetCrockford), new String(chars2));
}
@Test
public void testIsBase32Crockford() {
assertTrue(UlidUtil.isCrockfordBase32("16JD".toCharArray()));
assertTrue(UlidUtil.isCrockfordBase32("01BX5ZZKBKACTAV9WEVGEMMVRY".toCharArray()));
assertTrue(UlidUtil.isCrockfordBase32(UlidUtil.ALPHABET_CROCKFORD));
assertFalse(UlidUtil.isCrockfordBase32("U6JD".toCharArray()));
assertFalse(UlidUtil.isCrockfordBase32("*1BX5ZZKBKACTAV9WEVGEMMVRY".toCharArray()));
assertFalse(UlidUtil.isCrockfordBase32("u".toCharArray()));
assertFalse(UlidUtil.isCrockfordBase32("U".toCharArray()));
}
@Test
public void testToBase32Crockford() {
assertEquals("7ZZZZZZZZZ", new String(UlidUtil.toBase32Crockford(281474976710655L)));
// Encode from long to base 32
for (int i = 0; i < NUMBERS.length; i++) {
String result = new String(UlidUtil.toBase32Crockford(NUMBERS[i]));
assertEquals(NUMBERS_BASE_32_CROCKFORD[i].length(), result.length());
assertEquals(NUMBERS_BASE_32_CROCKFORD[i], result);
}
}
@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 {
@Test
public void testIsValidStrict() {
public void testIsValid() {
String ulid = null; // Null
assertFalse("Null ULID should be invalid.", UlidValidator.isValid(ulid));
@ -38,7 +38,7 @@ public class UlidValidatorTest {
ulid = "#123456789ABCDEFGHJKMNPQRS"; // Special char
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));
ulid = "8ZZZZZZZZZABCDEFGHJKMNPQRS"; // timestamp > (2^48)-1