Bootstrap

java测试示例-生成ULID

ULID全称Universally Unique Lexicographically Sortable Identifier,直译就是通用唯一按字典排序的标识符,原始仓库是https://github.com/ulid/javascript,由前端开发者alizain发起,基于JavaScript语言。从项目中的commit历史来看已超5年,得到充分的实践验证。ULID出现的原因是认为主流的UUID方案在许多场景下可能不是最优的,存在问题:

  • UUID不是128 bit随机编码(由128 bit随机数通过编码生成字符串)的最高效实现方式
  • UUID的v1/v2实现在许多环境中是不切实际的,因为这两个版本的的实现需要访问唯一的、稳定的MAC地址
  • UUID的v3/v5实现需要唯一的种子,并且产生随机分布的ID,这可能会导致在许多数据结构中出现碎片
  • UUID的v4除了随机性之外不需要提供其他信息,随机性可能会在许多数据结构中导致碎片

概括一下是:UUID的v1/v2实现依赖唯一稳定MAC地址不现实,v3/v4/v5实现因为随机性产生的ID会"碎片化"。

ULID的特点如下:

  • 设计为128 bit大小,与UUID兼容
  • 每毫秒生成1.21e+24个唯一的ULID(高性能)
  • 按字典顺序(字母顺序)排序
  • 标准编码为26个字符的字符串,而不是像UUID那样需要36个字符
  • 使用Crockford的base32算法来提高效率和可读性(每个字符5 bit)
  • 不区分大小写
  • 没有特殊字符串(URL安全,不需要进行二次URL编码)
  • 可单调排序(正确地检测并处理相同的毫秒,所谓单调性,就是毫秒数相同的情况下,能够确保新的ULID随机部分的在最低有效位上加1位)

ULID规范在ULID/javascript类库中实现

# 格式
 01GKJNSEJZ      A44X2DPXGSK303WP
|----------|    |----------------|
 Timestamp          Randomness
   48bits             80bits

# 所有字符必须使用默认的ASCII字符集
ttttttttttrrrrrrrrrrrrrrrr
# 共占据26个字符
where
# 时间戳占据高(左边)10个(编码后的)字符
t is Timestamp (10 characters)
# 随机数占据低(右边)16个(编码后的)字符
r is Randomness (16 characters)

# 使用Crockford Base32编码算法,排除了I、 L、O、U字母,避免混淆和滥用,字母表
0123456789ABCDEFGHJKMNPQRSTVWXYZ
# 二进制布局的多个部分被编码为16 byte,每个部分都以最高字节优先
0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      32_bit_uint_time_high                    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     16_bit_uint_time_low      |       16_bit_uint_random      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       32_bit_uint_random                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       32_bit_uint_random                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

基于Base32编码能生成的最大合法ULID是7ZZZZZZZZZZZZZZZZZZZZZZZZZ,使用的时间戳为epoch time的281474976710655或者说2 ^ 48 - 1。对于任何大于此值的ULID进行解码或编码的尝试都应被拒绝,以防止溢出错误

参考

ULID规范解读与实现原理

java测试

参考jar库

<dependency>
    <groupId>com.github.f4b6a3</groupId>
    <artifactId>ulid-creator</artifactId>
    <version>5.1.0</version>
</dependency>
<dependency>
    <groupId>de.huxhorn.sulky</groupId>
    <artifactId>de.huxhorn.sulky.ulid</artifactId>
    <version>8.3.0</version>
</dependency>

demo


import java.security.SecureRandom;

/**
 * Time32Main 类说明:
 *
 * @author z.y.l
 * @version v1.0
 * @date 2022/12/5
 */
public class Time32Main {
    public static void main(String[] args) {
        final long time = System.currentTimeMillis();
        //32进制编码
        final char[] alphabet = {
                '0','1','2','3','4','5','6','7','8','9',
                'A','B','C','D','E','F','G','H','J','K',
                'M','N','P','Q','R','S','T','V','W','X',
                'Y','Z',};
        // 时间戳 转 32编码
        t1(time,alphabet);
        t2(time,alphabet);
        //ulId 生成
        toUlIdStr(time,alphabet);
    }
    public static void t1(long time,char[] char32s){
        int count = 10, maskBits = 5, mask = 0x1F, offset = 0 , i1;
        // 31 对应二进制 00011111
        char[] buffer = new char[count];
        long l1;
        System.out.print("位数:" + count);
        System.out.print(",32进制 每位对应 二进制个数:" + maskBits);
        System.out.print(",求 32进制编码 运算数:" + mask + "(二进制" + Integer.toBinaryString(mask) + ")");
        System.out.println(",偏移:" + offset);
        System.out.println("时间戳:" + time + "(二进制" + Long.toBinaryString(time) + ")");
        for(int i = 0; i < count; i++) {
            i1 = (count - i - 1) * maskBits;
            l1 = time >>> ((count - i - 1) * maskBits);
            int index = (int)( l1 & mask);
            buffer[offset+i] = char32s[index];
            System.out.print("时间戳 >>> " + i1 + " = " + l1 + " & " + mask + " = " + index + " ~ " + char32s[index]);
            System.out.print( ",时间戳 >>> " + Integer.toBinaryString(i1) + " = " + Long.toBinaryString(l1) );
            System.out.println( " & 00011111 = " + Integer.toBinaryString(index) );
        }
        System.out.println(time + " = [转32编码] = " + new String(buffer));
    }
    public static void t2(long time,char[] alphabet){
        char[] chars = new char[10];
        genMsd(alphabet,chars,time);
        System.out.println(">>> "+new String(chars));
    }

    private static void genMsd(char[] alphabet,char[] chars,long time){
        // 31 的 二进制 0b11111
        chars[0x00] = alphabet[(int) (time >>> 45 & 0b11111)];
        chars[0x01] = alphabet[(int) (time >>> 40 & 0b11111)];
        chars[0x02] = alphabet[(int) (time >>> 35 & 0b11111)];
        chars[0x03] = alphabet[(int) (time >>> 30 & 0b11111)];
        chars[0x04] = alphabet[(int) (time >>> 25 & 0b11111)];
        chars[0x05] = alphabet[(int) (time >>> 20 & 0b11111)];
        chars[0x06] = alphabet[(int) (time >>> 15 & 0b11111)];
        chars[0x07] = alphabet[(int) (time >>> 10 & 0b11111)];
        chars[0x08] = alphabet[(int) (time >>> 5 & 0b11111)];
        chars[0x09] = alphabet[(int) (time & 0b11111)];
    }
    public static void toUlIdStr(long time,char[] alphabet){
        SecureRandom random = new SecureRandom();
        long msb = (time << 16) | (random.nextLong() & 0xffffL);
        long lsb = random.nextLong();
        System.out.println("ulId = "+toUlIdStr(msb,lsb,alphabet));
    }
    public static String toUlIdStr(long msb,long lsb,char[] alphabet) {
        final char[] chars = new char[26];
        // msb 初始化的是 左移16位 << 16,需要恢复 就要 右移16位
        long time = msb >>> 16;
        long random0 = ((msb & 0xffffL) << 24) | (lsb >>> 40);
        long random1 = (lsb & 0xffffffffffL);
        // 第一段 10位,时间戳 转 32编码
        genMsd(alphabet,chars,time);
        // 第二段 16位,分两次 生成
        genLsd(alphabet,chars,random0,0x0a);
        genLsd(alphabet,chars,random1,0x12);

        return new String(chars);
    }
    private static void genLsd(char[] alphabet,char[] chars,long random,int offset){
        // 31 的 二进制 0b11111
        chars[offset] = alphabet[(int) (random >>> 35 & 0b11111)];
        chars[offset+0x01] = alphabet[(int) (random >>> 30 & 0b11111)];
        chars[offset+0x02] = alphabet[(int) (random >>> 25 & 0b11111)];
        chars[offset+0x03] = alphabet[(int) (random >>> 20 & 0b11111)];
        chars[offset+0x04] = alphabet[(int) (random >>> 15 & 0b11111)];
        chars[offset+0x05] = alphabet[(int) (random >>> 10 & 0b11111)];
        chars[offset+0x06] = alphabet[(int) (random >>> 5 & 0b11111)];
        chars[offset+0x07] = alphabet[(int) (random & 0b11111)];
    }
}

测试

位数:10,32进制 每位对应 二进制个数:5,求 32进制编码 运算数:31(二进制11111),偏移:0
时间戳:1670295370335(二进制11000010011100101010111001011101001011111)
时间戳 >>> 45 = 0 & 31 = 0 ~ 0,时间戳 >>> 101101 = 0 & 00011111 = 0
时间戳 >>> 40 = 1 & 31 = 1 ~ 1,时间戳 >>> 101000 = 1 & 00011111 = 1
时间戳 >>> 35 = 48 & 31 = 16 ~ G,时间戳 >>> 100011 = 110000 & 00011111 = 10000
时间戳 >>> 30 = 1555 & 31 = 19 ~ K,时间戳 >>> 11110 = 11000010011 & 00011111 = 10011
时间戳 >>> 25 = 49778 & 31 = 18 ~ J,时间戳 >>> 11001 = 1100001001110010 & 00011111 = 10010
时间戳 >>> 20 = 1592917 & 31 = 21 ~ N,时间戳 >>> 10100 = 110000100111001010101 & 00011111 = 10101
时间戳 >>> 15 = 50973369 & 31 = 25 ~ S,时间戳 >>> 1111 = 11000010011100101010111001 & 00011111 = 11001
时间戳 >>> 10 = 1631147822 & 31 = 14 ~ E,时间戳 >>> 1010 = 1100001001110010101011100101110 & 00011111 = 1110
时间戳 >>> 5 = 52196730322 & 31 = 18 ~ J,时间戳 >>> 101 = 110000100111001010101110010111010010 & 00011111 = 10010
时间戳 >>> 0 = 1670295370335 & 31 = 31 ~ Z,时间戳 >>> 0 = 11000010011100101010111001011101001011111 & 00011111 = 11111
1670295370335 = [转32编码] = 01GKJNSEJZ
01GKJNSEJZ
01GKJNSEJZA44X2DPXGSK303WP

在这里插入图片描述

库源码

注释做了部分清理

ulid-creator


package de.huxhorn.sulky.ulid;

import java.io.Serializable;
import java.security.SecureRandom;
import java.util.Objects;
import java.util.Optional;
import java.util.Random;

@SuppressWarnings("PMD.ShortClassName")
public class ULID
{
	private static final char[] ENCODING_CHARS = {
			'0','1','2','3','4','5','6','7','8','9',
			'A','B','C','D','E','F','G','H','J','K',
			'M','N','P','Q','R','S','T','V','W','X',
			'Y','Z',
	};

	private static final byte[] DECODING_CHARS = {
			// 0
			-1, -1, -1, -1, -1, -1, -1, -1,
			// 8
			-1, -1, -1, -1, -1, -1, -1, -1,
			// 16
			-1, -1, -1, -1, -1, -1, -1, -1,
			// 24
			-1, -1, -1, -1, -1, -1, -1, -1,
			// 32
			-1, -1, -1, -1, -1, -1, -1, -1,
			// 40
			-1, -1, -1, -1, -1, -1, -1, -1,
			// 48
			0, 1, 2, 3, 4, 5, 6, 7,
			// 56
			8, 9, -1, -1, -1, -1, -1, -1,
			// 64
			-1, 10, 11, 12, 13, 14, 15, 16,
			// 72
			17, 1, 18, 19, 1, 20, 21, 0,
			// 80
			22, 23, 24, 25, 26, -1, 27, 28,
			// 88
			29, 30, 31, -1, -1, -1, -1, -1,
			// 96
			-1, 10, 11, 12, 13, 14, 15, 16,
			// 104
			17, 1, 18, 19, 1, 20, 21, 0,
			// 112
			22, 23, 24, 25, 26, -1, 27, 28,
			// 120
			29, 30, 31,
	};

	private static final int MASK = 0x1F;
	private static final int MASK_BITS = 5;
	private static final long TIMESTAMP_OVERFLOW_MASK = 0xFFFF_0000_0000_0000L;
	private static final long TIMESTAMP_MSB_MASK = 0xFFFF_FFFF_FFFF_0000L;
	private static final long RANDOM_MSB_MASK = 0xFFFFL;

	private final Random random;

	public ULID()
	{
		this(new SecureRandom());
	}

	public ULID(Random random)
	{
		Objects.requireNonNull(random, "random must not be null!");
		this.random = random;
	}

	public void appendULID(StringBuilder stringBuilder)
	{
		Objects.requireNonNull(stringBuilder, "stringBuilder must not be null!");
		internalAppendULID(stringBuilder, System.currentTimeMillis(), random);
	}

	public String nextULID()
	{
		return nextULID(System.currentTimeMillis());
	}

	public String nextULID(long timestamp)
	{
		return internalUIDString(timestamp, random);
	}

	public Value nextValue()
	{
		return nextValue(System.currentTimeMillis());
	}

	public Value nextValue(long timestamp)
	{
		return internalNextValue(timestamp, random);
	}

	public Value nextMonotonicValue(Value previousUlid)
	{
		return nextMonotonicValue(previousUlid, System.currentTimeMillis());
	}

	public Value nextMonotonicValue(Value previousUlid, long timestamp)
	{
		Objects.requireNonNull(previousUlid, "previousUlid must not be null!");
		if(previousUlid.timestamp() == timestamp)
		{
			return previousUlid.increment();
		}
		return nextValue(timestamp);
	}

	public Optional<Value> nextStrictlyMonotonicValue(Value previousUlid)
	{
		return nextStrictlyMonotonicValue(previousUlid, System.currentTimeMillis());
	}

	public Optional<Value> nextStrictlyMonotonicValue(Value previousUlid, long timestamp)
	{
		Value result = nextMonotonicValue(previousUlid, timestamp);
		if(result.compareTo(previousUlid) < 1)
		{
			return Optional.empty();
		}
		return Optional.of(result);
	}

	public static Value parseULID(String ulidString)
	{
		Objects.requireNonNull(ulidString, "ulidString must not be null!");
		if(ulidString.length() != 26)
		{
			throw new IllegalArgumentException("ulidString must be exactly 26 chars long.");
		}

		String timeString = ulidString.substring(0, 10);
		long time = internalParseCrockford(timeString);
		if ((time & TIMESTAMP_OVERFLOW_MASK) != 0)
		{
			throw new IllegalArgumentException("ulidString must not exceed '7ZZZZZZZZZZZZZZZZZZZZZZZZZ'!");
		}
		String part1String = ulidString.substring(10, 18);
		String part2String = ulidString.substring(18);
		long part1 = internalParseCrockford(part1String);
		long part2 = internalParseCrockford(part2String);

		long most = (time << 16) | (part1 >>> 24);
		long least = part2 | (part1 << 40);
		return new Value(most, least);
	}

	public static Value fromBytes(byte[] data)
	{
		Objects.requireNonNull(data, "data must not be null!");
		if(data.length != 16)
		{
			throw new IllegalArgumentException("data must be 16 bytes in length!");
		}
		long mostSignificantBits = 0;
		long leastSignificantBits = 0;
		for (int i=0; i<8; i++)
		{
			mostSignificantBits = (mostSignificantBits << 8) | (data[i] & 0xff);
		}
		for (int i=8; i<16; i++)
		{
			leastSignificantBits = (leastSignificantBits << 8) | (data[i] & 0xff);
		}
		return new Value(mostSignificantBits, leastSignificantBits);
	}

	public static class Value
		implements Comparable<Value>, Serializable
	{
		private static final long serialVersionUID = -3563159514112487717L;

		/*
		 * The most significant 64 bits of this ULID.
		 */
		private final long mostSignificantBits;

		/*
		 * The least significant 64 bits of this ULID.
		 */
		private final long leastSignificantBits;

		public Value(long mostSignificantBits, long leastSignificantBits)
		{
			this.mostSignificantBits = mostSignificantBits;
			this.leastSignificantBits = leastSignificantBits;
		}

		public long getMostSignificantBits() {
			return mostSignificantBits;
		}

		public long getLeastSignificantBits() {
			return leastSignificantBits;
		}


		public long timestamp()
		{
			return mostSignificantBits >>> 16;
		}

		public byte[] toBytes()
		{
			byte[] result=new byte[16];
			for (int i=0; i<8; i++)
			{
				result[i] = (byte)((mostSignificantBits >> ((7-i)*8)) & 0xFF);
			}
			for (int i=8; i<16; i++)
			{
				result[i] = (byte)((leastSignificantBits >> ((15-i)*8)) & 0xFF);
			}

			return result;
		}

		public Value increment()
		{
			long lsb = leastSignificantBits;
			if(lsb != 0xFFFF_FFFF_FFFF_FFFFL)
			{
				return new Value(mostSignificantBits, lsb+1);
			}
			long msb = mostSignificantBits;
			if((msb & RANDOM_MSB_MASK) != RANDOM_MSB_MASK)
			{
				return new Value(msb + 1, 0);
			}
			return new Value(msb & TIMESTAMP_MSB_MASK, 0);
		}

		@Override
		public int hashCode() {
			long hilo = mostSignificantBits ^ leastSignificantBits;
			return ((int)(hilo >> 32)) ^ (int) hilo;
		}

		@Override
		public boolean equals(Object o)
		{
			if (this == o) return true;
			if (o == null || getClass() != o.getClass()) return false;

			Value value = (Value) o;

			return mostSignificantBits == value.mostSignificantBits
					&& leastSignificantBits == value.leastSignificantBits;
		}

		@Override
		public int compareTo(Value val)
		{
			return (this.mostSignificantBits < val.mostSignificantBits ? -1 :
					(this.mostSignificantBits > val.mostSignificantBits ? 1 :
							(this.leastSignificantBits < val.leastSignificantBits ? -1 :
									(this.leastSignificantBits > val.leastSignificantBits ? 1 :
											0))));
		}

		@Override
		public String toString()
		{
			char[] buffer = new char[26];

			internalWriteCrockford(buffer, timestamp(), 10, 0);
			long value = ((mostSignificantBits & 0xFFFFL) << 24);
			long interim = (leastSignificantBits >>> 40);
			value = value | interim;
			internalWriteCrockford(buffer, value, 8, 10);
			internalWriteCrockford(buffer, leastSignificantBits, 8, 18);

			return new String(buffer);
		}
	}

	/*
	 * http://crockford.com/wrmg/base32.html
	 */
	static void internalAppendCrockford(StringBuilder builder, long value, int count)
	{
		for(int i = count-1; i >= 0; i--)
		{
			int index = (int)((value >>> (i * MASK_BITS)) & MASK);
			builder.append(ENCODING_CHARS[index]);
		}
	}

	static long internalParseCrockford(String input)
	{
		Objects.requireNonNull(input, "input must not be null!");
		int length = input.length();
		if(length > 12)
		{
			throw new IllegalArgumentException("input length must not exceed 12 but was "+length+"!");
		}

		long result = 0;
		for(int i=0;i<length;i++)
		{
			char current = input.charAt(i);
			byte value = -1;
			if(current < DECODING_CHARS.length)
			{
				value = DECODING_CHARS[current];
			}
			if(value < 0)
			{
				throw new IllegalArgumentException("Illegal character '"+current+"'!");
			}
			result |= ((long)value) << ((length - 1 - i)*MASK_BITS);
		}
		return result;
	}

	/*
	 * http://crockford.com/wrmg/base32.html
	 */
	static void internalWriteCrockford(char[] buffer, long value, int count, int offset)
	{
		for(int i = 0; i < count; i++)
		{
			int index = (int)((value >>> ((count - i - 1) * MASK_BITS)) & MASK);
			buffer[offset+i] = ENCODING_CHARS[index];
		}
	}

	static String internalUIDString(long timestamp, Random random)
	{
		checkTimestamp(timestamp);

		char[] buffer = new char[26];

		internalWriteCrockford(buffer, timestamp, 10, 0);
		internalWriteCrockford(buffer, random.nextLong(), 8, 10);
		internalWriteCrockford(buffer, random.nextLong(), 8, 18);

		return new String(buffer);
	}

	static void internalAppendULID(StringBuilder builder, long timestamp, Random random)
	{
		checkTimestamp(timestamp);

		internalAppendCrockford(builder, timestamp, 10);
		internalAppendCrockford(builder, random.nextLong(), 8);
		internalAppendCrockford(builder, random.nextLong(), 8);
	}

	static Value internalNextValue(long timestamp, Random random)
	{
		checkTimestamp(timestamp);
		long mostSignificantBits = random.nextLong();
		long leastSignificantBits = random.nextLong();
		mostSignificantBits &= 0xFFFF;
		mostSignificantBits |= (timestamp << 16);
		return new Value(mostSignificantBits, leastSignificantBits);
	}

	private static void checkTimestamp(long timestamp)
	{
		if((timestamp & TIMESTAMP_OVERFLOW_MASK) != 0)
		{
			throw new IllegalArgumentException("ULID does not support timestamps after +10889-08-02T05:31:50.655Z!");
		}
	}
}

de.huxhorn.sulky.ulid

package com.github.f4b6a3.ulid;

public final class UlidCreator {
	private UlidCreator() {
	}
	public static Ulid getUlid() {
		return UlidFactoryHolder.INSTANCE.create();
	}
	public static Ulid getUlid(final long time) {
		return UlidFactoryHolder.INSTANCE.create(time);
	}
	public static Ulid getMonotonicUlid() {
		return MonotonicFactoryHolder.INSTANCE.create();
	}
	public static Ulid getMonotonicUlid(final long time) {
		return MonotonicFactoryHolder.INSTANCE.create(time);
	}
	private static class UlidFactoryHolder {
		static final UlidFactory INSTANCE = UlidFactory.newInstance();
	}
	private static class MonotonicFactoryHolder {
		static final UlidFactory INSTANCE = UlidFactory.newMonotonicInstance();
	}
}
package com.github.f4b6a3.ulid;

import java.security.SecureRandom;
import java.time.Clock;
import java.util.Random;
import java.util.function.IntFunction;
import java.util.function.LongFunction;
import java.util.function.LongSupplier;

public final class UlidFactory {
	private final Clock clock;
	private final LongFunction<Ulid> ulidFunction;

	public UlidFactory() {
		this(new UlidFunction(IRandom.newInstance()));
	}
	private UlidFactory(LongFunction<Ulid> ulidFunction) {
		this(ulidFunction, null);
	}
	private UlidFactory(LongFunction<Ulid> ulidFunction, Clock clock) {
		this.ulidFunction = ulidFunction;
		this.clock = clock != null ? clock : Clock.systemUTC();
	}
	public static UlidFactory newInstance() {
		return new UlidFactory(new UlidFunction(IRandom.newInstance()));
	}
	public static UlidFactory newInstance(Random random) {
		return new UlidFactory(new UlidFunction(IRandom.newInstance(random)));
	}
	public static UlidFactory newInstance(LongSupplier randomFunction) {
		return new UlidFactory(new UlidFunction(IRandom.newInstance(randomFunction)));
	}
	public static UlidFactory newInstance(IntFunction<byte[]> randomFunction) {
		return new UlidFactory(new UlidFunction(IRandom.newInstance(randomFunction)));
	}
	public static UlidFactory newMonotonicInstance() {
		return new UlidFactory(new MonotonicFunction(IRandom.newInstance()));
	}
	public static UlidFactory newMonotonicInstance(Random random) {
		return new UlidFactory(new MonotonicFunction(IRandom.newInstance(random)));
	}
	public static UlidFactory newMonotonicInstance(LongSupplier randomFunction) {
		return new UlidFactory(new MonotonicFunction(IRandom.newInstance(randomFunction)));
	}
	public static UlidFactory newMonotonicInstance(IntFunction<byte[]> randomFunction) {
		return new UlidFactory(new MonotonicFunction(IRandom.newInstance(randomFunction)));
	}
	static UlidFactory newMonotonicInstance(LongSupplier randomFunction, Clock clock) {
		return new UlidFactory(new MonotonicFunction(IRandom.newInstance(randomFunction)), clock);
	}
	static UlidFactory newMonotonicInstance(IntFunction<byte[]> randomFunction, Clock clock) {
		return new UlidFactory(new MonotonicFunction(IRandom.newInstance(randomFunction)), clock);
	}
	public synchronized Ulid create() {
		return this.ulidFunction.apply(clock.millis());
	}
	public synchronized Ulid create(final long time) {
		return this.ulidFunction.apply(time);
	}

	static final class UlidFunction implements LongFunction<Ulid> {
		private final IRandom random;
		public UlidFunction(IRandom random) {
			this.random = random;
		}
		@Override
		public Ulid apply(final long time) {
			if (this.random instanceof ByteRandom) {
				return new Ulid(time, this.random.nextBytes(Ulid.RANDOM_BYTES));
			} else {
				final long msb = (time << 16) | (this.random.nextLong() & 0xffffL);
				final long lsb = this.random.nextLong();
				return new Ulid(msb, lsb);
			}
		}
	}

	static final class MonotonicFunction implements LongFunction<Ulid> {
		private Ulid lastUlid;
		private final IRandom random;
		protected static final int CLOCK_DRIFT_TOLERANCE = 10_000;
		public MonotonicFunction(IRandom random) {
			this.random = random;
			this.lastUlid = new Ulid(0L, this.random.nextBytes(Ulid.RANDOM_BYTES));
		}
		@Override
		public synchronized Ulid apply(final long time) {
			final long lastTime = lastUlid.getTime();
			if ((time > lastTime - CLOCK_DRIFT_TOLERANCE) && (time <= lastTime)) {
				this.lastUlid = this.lastUlid.increment();
			} else {
				if (this.random instanceof ByteRandom) {
					this.lastUlid = new Ulid(time, this.random.nextBytes(Ulid.RANDOM_BYTES));
				} else {
					final long msb = (time << 16) | (this.random.nextLong() & 0xffffL);
					final long lsb = this.random.nextLong();
					this.lastUlid = new Ulid(msb, lsb);
				}
			}
			return new Ulid(this.lastUlid);
		}
	}

	static interface IRandom {
		public long nextLong();
		public byte[] nextBytes(int length);
		static IRandom newInstance() {
			return new ByteRandom();
		}
		static IRandom newInstance(Random random) {
			if (random == null) {
				return new ByteRandom();
			} else {
				if (random instanceof SecureRandom) {
					return new ByteRandom(random);
				} else {
					return new LongRandom(random);
				}
			}
		}
		static IRandom newInstance(LongSupplier randomFunction) {
			return new LongRandom(randomFunction);
		}
		static IRandom newInstance(IntFunction<byte[]> randomFunction) {
			return new ByteRandom(randomFunction);
		}
	}

	static class LongRandom implements IRandom {
		private final LongSupplier randomFunction;
		public LongRandom() {
			this(newRandomFunction(null));
		}
		public LongRandom(Random random) {
			this(newRandomFunction(random));
		}
		public LongRandom(LongSupplier randomFunction) {
			this.randomFunction = randomFunction != null ? randomFunction : newRandomFunction(null);
		}
		@Override
		public long nextLong() {
			return randomFunction.getAsLong();
		}
		@Override
		public byte[] nextBytes(int length) {
			int shift = 0;
			long random = 0;
			final byte[] bytes = new byte[length];
			for (int i = 0; i < length; i++) {
				if (shift < Byte.SIZE) {
					shift = Long.SIZE;
					random = randomFunction.getAsLong();
				}
				shift -= Byte.SIZE; // 56, 48, 40...
				bytes[i] = (byte) (random >>> shift);
			}
			return bytes;
		}
		static LongSupplier newRandomFunction(Random random) {
			final Random entropy = random != null ? random : new SecureRandom();
			return entropy::nextLong;
		}
	}
	static class ByteRandom implements IRandom {

		private final IntFunction<byte[]> randomFunction;
		public ByteRandom() {
			this(newRandomFunction(null));
		}
		public ByteRandom(Random random) {
			this(newRandomFunction(random));
		}
		public ByteRandom(IntFunction<byte[]> randomFunction) {
			this.randomFunction = randomFunction != null ? randomFunction : newRandomFunction(null);
		}
		@Override
		public long nextLong() {
			long number = 0;
			byte[] bytes = this.randomFunction.apply(Long.BYTES);
			for (int i = 0; i < Long.BYTES; i++) {
				number = (number << 8) | (bytes[i] & 0xff);
			}
			return number;
		}
		@Override
		public byte[] nextBytes(int length) {
			return this.randomFunction.apply(length);
		}
		static IntFunction<byte[]> newRandomFunction(Random random) {
			final Random entropy = random != null ? random : new SecureRandom();
			return (final int length) -> {
				final byte[] bytes = new byte[length];
				entropy.nextBytes(bytes);
				return bytes;
			};
		}
	}
}
package com.github.f4b6a3.ulid;

import java.io.Serializable;
import java.time.Instant;
import java.util.SplittableRandom;
import java.util.UUID;

public final class Ulid implements Serializable, Comparable<Ulid> {

    private static final long serialVersionUID = 2625269413446854731L;

    private final long msb; // most significant bits
    private final long lsb; // least significant bits

    public static final int ULID_CHARS = 26;
    public static final int TIME_CHARS = 10;
    public static final int RANDOM_CHARS = 16;
    public static final int ULID_BYTES = 16;
    public static final int TIME_BYTES = 6;
    public static final int RANDOM_BYTES = 10;

    private static final char[] ALPHABET_UPPERCASE = //
            { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', //
                    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', //
                    'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z' };

    private static final char[] ALPHABET_LOWERCASE = //
            { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', //
                    'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', //
                    'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z' };

    private static final long[] ALPHABET_VALUES = new long[128];
    static {
        for (int i = 0; i < ALPHABET_VALUES.length; i++) {
            ALPHABET_VALUES[i] = -1;
        }
        // Numbers
        ALPHABET_VALUES['0'] = 0x00;
        ALPHABET_VALUES['1'] = 0x01;
        ALPHABET_VALUES['2'] = 0x02;
        ALPHABET_VALUES['3'] = 0x03;
        ALPHABET_VALUES['4'] = 0x04;
        ALPHABET_VALUES['5'] = 0x05;
        ALPHABET_VALUES['6'] = 0x06;
        ALPHABET_VALUES['7'] = 0x07;
        ALPHABET_VALUES['8'] = 0x08;
        ALPHABET_VALUES['9'] = 0x09;
        // Lower case
        ALPHABET_VALUES['a'] = 0x0a;
        ALPHABET_VALUES['b'] = 0x0b;
        ALPHABET_VALUES['c'] = 0x0c;
        ALPHABET_VALUES['d'] = 0x0d;
        ALPHABET_VALUES['e'] = 0x0e;
        ALPHABET_VALUES['f'] = 0x0f;
        ALPHABET_VALUES['g'] = 0x10;
        ALPHABET_VALUES['h'] = 0x11;
        ALPHABET_VALUES['j'] = 0x12;
        ALPHABET_VALUES['k'] = 0x13;
        ALPHABET_VALUES['m'] = 0x14;
        ALPHABET_VALUES['n'] = 0x15;
        ALPHABET_VALUES['p'] = 0x16;
        ALPHABET_VALUES['q'] = 0x17;
        ALPHABET_VALUES['r'] = 0x18;
        ALPHABET_VALUES['s'] = 0x19;
        ALPHABET_VALUES['t'] = 0x1a;
        ALPHABET_VALUES['v'] = 0x1b;
        ALPHABET_VALUES['w'] = 0x1c;
        ALPHABET_VALUES['x'] = 0x1d;
        ALPHABET_VALUES['y'] = 0x1e;
        ALPHABET_VALUES['z'] = 0x1f;
        // Lower case OIL
        ALPHABET_VALUES['o'] = 0x00;
        ALPHABET_VALUES['i'] = 0x01;
        ALPHABET_VALUES['l'] = 0x01;
        // Upper case
        ALPHABET_VALUES['A'] = 0x0a;
        ALPHABET_VALUES['B'] = 0x0b;
        ALPHABET_VALUES['C'] = 0x0c;
        ALPHABET_VALUES['D'] = 0x0d;
        ALPHABET_VALUES['E'] = 0x0e;
        ALPHABET_VALUES['F'] = 0x0f;
        ALPHABET_VALUES['G'] = 0x10;
        ALPHABET_VALUES['H'] = 0x11;
        ALPHABET_VALUES['J'] = 0x12;
        ALPHABET_VALUES['K'] = 0x13;
        ALPHABET_VALUES['M'] = 0x14;
        ALPHABET_VALUES['N'] = 0x15;
        ALPHABET_VALUES['P'] = 0x16;
        ALPHABET_VALUES['Q'] = 0x17;
        ALPHABET_VALUES['R'] = 0x18;
        ALPHABET_VALUES['S'] = 0x19;
        ALPHABET_VALUES['T'] = 0x1a;
        ALPHABET_VALUES['V'] = 0x1b;
        ALPHABET_VALUES['W'] = 0x1c;
        ALPHABET_VALUES['X'] = 0x1d;
        ALPHABET_VALUES['Y'] = 0x1e;
        ALPHABET_VALUES['Z'] = 0x1f;
        // Upper case OIL
        ALPHABET_VALUES['O'] = 0x00;
        ALPHABET_VALUES['I'] = 0x01;
        ALPHABET_VALUES['L'] = 0x01;
    }

    private static final long INCREMENT_OVERFLOW = 0x0000000000000000L;

    public Ulid(Ulid ulid) {
        this.msb = ulid.msb;
        this.lsb = ulid.lsb;
    }


    public Ulid(long mostSignificantBits, long leastSignificantBits) {
        this.msb = mostSignificantBits;
        this.lsb = leastSignificantBits;
    }

    public Ulid(long time, byte[] random) {

        // The time component has 48 bits.
        if ((time & 0xffff000000000000L) != 0) {
            // ULID specification:
            // "Any attempt to decode or encode a ULID larger than this (time > 2^48-1)
            // should be rejected by all implementations, to prevent overflow bugs."
            throw new IllegalArgumentException("Invalid time value"); // overflow or negative time!
        }
        // The random component has 80 bits (10 bytes).
        if (random == null || random.length != RANDOM_BYTES) {
            throw new IllegalArgumentException("Invalid random bytes"); // null or wrong length!
        }

        long long0 = 0;
        long long1 = 0;

        long0 |= time << 16;
        long0 |= (long) (random[0x0] & 0xff) << 8;
        long0 |= (long) (random[0x1] & 0xff);

        long1 |= (long) (random[0x2] & 0xff) << 56;
        long1 |= (long) (random[0x3] & 0xff) << 48;
        long1 |= (long) (random[0x4] & 0xff) << 40;
        long1 |= (long) (random[0x5] & 0xff) << 32;
        long1 |= (long) (random[0x6] & 0xff) << 24;
        long1 |= (long) (random[0x7] & 0xff) << 16;
        long1 |= (long) (random[0x8] & 0xff) << 8;
        long1 |= (long) (random[0x9] & 0xff);

        this.msb = long0;
        this.lsb = long1;
    }

    public static Ulid fast() {
        final long time = System.currentTimeMillis();
        final SplittableRandom random = new SplittableRandom();
        return new Ulid((time << 16) | (random.nextLong() & 0xffffL), random.nextLong());
    }

    public static Ulid from(UUID uuid) {
        return new Ulid(uuid.getMostSignificantBits(), uuid.getLeastSignificantBits());
    }

    public static Ulid from(byte[] bytes) {

        if (bytes == null || bytes.length != ULID_BYTES) {
            throw new IllegalArgumentException("Invalid ULID bytes"); // null or wrong length!
        }

        long msb = 0;
        long lsb = 0;

        msb |= (bytes[0x0] & 0xffL) << 56;
        msb |= (bytes[0x1] & 0xffL) << 48;
        msb |= (bytes[0x2] & 0xffL) << 40;
        msb |= (bytes[0x3] & 0xffL) << 32;
        msb |= (bytes[0x4] & 0xffL) << 24;
        msb |= (bytes[0x5] & 0xffL) << 16;
        msb |= (bytes[0x6] & 0xffL) << 8;
        msb |= (bytes[0x7] & 0xffL);

        lsb |= (bytes[0x8] & 0xffL) << 56;
        lsb |= (bytes[0x9] & 0xffL) << 48;
        lsb |= (bytes[0xa] & 0xffL) << 40;
        lsb |= (bytes[0xb] & 0xffL) << 32;
        lsb |= (bytes[0xc] & 0xffL) << 24;
        lsb |= (bytes[0xd] & 0xffL) << 16;
        lsb |= (bytes[0xe] & 0xffL) << 8;
        lsb |= (bytes[0xf] & 0xffL);

        return new Ulid(msb, lsb);
    }

    public static Ulid from(String string) {

        final char[] chars = toCharArray(string);

        long time = 0;
        long random0 = 0;
        long random1 = 0;

        time |= ALPHABET_VALUES[chars[0x00]] << 45;
        time |= ALPHABET_VALUES[chars[0x01]] << 40;
        time |= ALPHABET_VALUES[chars[0x02]] << 35;
        time |= ALPHABET_VALUES[chars[0x03]] << 30;
        time |= ALPHABET_VALUES[chars[0x04]] << 25;
        time |= ALPHABET_VALUES[chars[0x05]] << 20;
        time |= ALPHABET_VALUES[chars[0x06]] << 15;
        time |= ALPHABET_VALUES[chars[0x07]] << 10;
        time |= ALPHABET_VALUES[chars[0x08]] << 5;
        time |= ALPHABET_VALUES[chars[0x09]];

        random0 |= ALPHABET_VALUES[chars[0x0a]] << 35;
        random0 |= ALPHABET_VALUES[chars[0x0b]] << 30;
        random0 |= ALPHABET_VALUES[chars[0x0c]] << 25;
        random0 |= ALPHABET_VALUES[chars[0x0d]] << 20;
        random0 |= ALPHABET_VALUES[chars[0x0e]] << 15;
        random0 |= ALPHABET_VALUES[chars[0x0f]] << 10;
        random0 |= ALPHABET_VALUES[chars[0x10]] << 5;
        random0 |= ALPHABET_VALUES[chars[0x11]];

        random1 |= ALPHABET_VALUES[chars[0x12]] << 35;
        random1 |= ALPHABET_VALUES[chars[0x13]] << 30;
        random1 |= ALPHABET_VALUES[chars[0x14]] << 25;
        random1 |= ALPHABET_VALUES[chars[0x15]] << 20;
        random1 |= ALPHABET_VALUES[chars[0x16]] << 15;
        random1 |= ALPHABET_VALUES[chars[0x17]] << 10;
        random1 |= ALPHABET_VALUES[chars[0x18]] << 5;
        random1 |= ALPHABET_VALUES[chars[0x19]];

        final long msb = (time << 16) | (random0 >>> 24);
        final long lsb = (random0 << 40) | (random1 & 0xffffffffffL);

        return new Ulid(msb, lsb);
    }

    public UUID toUuid() {
        return new UUID(this.msb, this.lsb);
    }

    public byte[] toBytes() {

        final byte[] bytes = new byte[ULID_BYTES];

        bytes[0x0] = (byte) (msb >>> 56);
        bytes[0x1] = (byte) (msb >>> 48);
        bytes[0x2] = (byte) (msb >>> 40);
        bytes[0x3] = (byte) (msb >>> 32);
        bytes[0x4] = (byte) (msb >>> 24);
        bytes[0x5] = (byte) (msb >>> 16);
        bytes[0x6] = (byte) (msb >>> 8);
        bytes[0x7] = (byte) (msb);

        bytes[0x8] = (byte) (lsb >>> 56);
        bytes[0x9] = (byte) (lsb >>> 48);
        bytes[0xa] = (byte) (lsb >>> 40);
        bytes[0xb] = (byte) (lsb >>> 32);
        bytes[0xc] = (byte) (lsb >>> 24);
        bytes[0xd] = (byte) (lsb >>> 16);
        bytes[0xe] = (byte) (lsb >>> 8);
        bytes[0xf] = (byte) (lsb);

        return bytes;
    }

    @Override
    public String toString() {
        return toString(ALPHABET_UPPERCASE);
    }

    public String toLowerCase() {
        return toString(ALPHABET_LOWERCASE);
    }

    public Ulid toRfc4122() {

        // set the 4 most significant bits of the 7th byte to 0, 1, 0 and 0
        final long msb4 = (this.msb & 0xffffffffffff0fffL) | 0x0000000000004000L; // RFC-4122 version 4
        // set the 2 most significant bits of the 9th byte to 1 and 0
        final long lsb4 = (this.lsb & 0x3fffffffffffffffL) | 0x8000000000000000L; // RFC-4122 variant 2

        return new Ulid(msb4, lsb4);
    }

    public Instant getInstant() {
        return Instant.ofEpochMilli(this.getTime());
    }

    public static Instant getInstant(String string) {
        return Instant.ofEpochMilli(getTime(string));
    }

    public long getTime() {
        return this.msb >>> 16;
    }

    public static long getTime(String string) {

        final char[] chars = toCharArray(string);

        long time = 0;

        time |= ALPHABET_VALUES[chars[0x00]] << 45;
        time |= ALPHABET_VALUES[chars[0x01]] << 40;
        time |= ALPHABET_VALUES[chars[0x02]] << 35;
        time |= ALPHABET_VALUES[chars[0x03]] << 30;
        time |= ALPHABET_VALUES[chars[0x04]] << 25;
        time |= ALPHABET_VALUES[chars[0x05]] << 20;
        time |= ALPHABET_VALUES[chars[0x06]] << 15;
        time |= ALPHABET_VALUES[chars[0x07]] << 10;
        time |= ALPHABET_VALUES[chars[0x08]] << 5;
        time |= ALPHABET_VALUES[chars[0x09]];

        return time;
    }

    public byte[] getRandom() {

        final byte[] bytes = new byte[RANDOM_BYTES];

        bytes[0x0] = (byte) (msb >>> 8);
        bytes[0x1] = (byte) (msb);

        bytes[0x2] = (byte) (lsb >>> 56);
        bytes[0x3] = (byte) (lsb >>> 48);
        bytes[0x4] = (byte) (lsb >>> 40);
        bytes[0x5] = (byte) (lsb >>> 32);
        bytes[0x6] = (byte) (lsb >>> 24);
        bytes[0x7] = (byte) (lsb >>> 16);
        bytes[0x8] = (byte) (lsb >>> 8);
        bytes[0x9] = (byte) (lsb);

        return bytes;
    }

    public static byte[] getRandom(String string) {

        final char[] chars = toCharArray(string);

        long random0 = 0;
        long random1 = 0;

        random0 |= ALPHABET_VALUES[chars[0x0a]] << 35;
        random0 |= ALPHABET_VALUES[chars[0x0b]] << 30;
        random0 |= ALPHABET_VALUES[chars[0x0c]] << 25;
        random0 |= ALPHABET_VALUES[chars[0x0d]] << 20;
        random0 |= ALPHABET_VALUES[chars[0x0e]] << 15;
        random0 |= ALPHABET_VALUES[chars[0x0f]] << 10;
        random0 |= ALPHABET_VALUES[chars[0x10]] << 5;
        random0 |= ALPHABET_VALUES[chars[0x11]];

        random1 |= ALPHABET_VALUES[chars[0x12]] << 35;
        random1 |= ALPHABET_VALUES[chars[0x13]] << 30;
        random1 |= ALPHABET_VALUES[chars[0x14]] << 25;
        random1 |= ALPHABET_VALUES[chars[0x15]] << 20;
        random1 |= ALPHABET_VALUES[chars[0x16]] << 15;
        random1 |= ALPHABET_VALUES[chars[0x17]] << 10;
        random1 |= ALPHABET_VALUES[chars[0x18]] << 5;
        random1 |= ALPHABET_VALUES[chars[0x19]];

        final byte[] bytes = new byte[RANDOM_BYTES];

        bytes[0x0] = (byte) (random0 >>> 32);
        bytes[0x1] = (byte) (random0 >>> 24);
        bytes[0x2] = (byte) (random0 >>> 16);
        bytes[0x3] = (byte) (random0 >>> 8);
        bytes[0x4] = (byte) (random0);

        bytes[0x5] = (byte) (random1 >>> 32);
        bytes[0x6] = (byte) (random1 >>> 24);
        bytes[0x7] = (byte) (random1 >>> 16);
        bytes[0x8] = (byte) (random1 >>> 8);
        bytes[0x9] = (byte) (random1);

        return bytes;
    }

    public long getMostSignificantBits() {
        return this.msb;
    }

    public long getLeastSignificantBits() {
        return this.lsb;
    }

    public Ulid increment() {

        long newMsb = this.msb;
        long newLsb = this.lsb + 1; // increment the LEAST significant bits

        if (newLsb == INCREMENT_OVERFLOW) {
            newMsb += 1; // increment the MOST significant bits
        }

        return new Ulid(newMsb, newLsb);
    }

    public static boolean isValid(String string) {
        return string != null && isValidCharArray(string.toCharArray());
    }

    @Override
    public int hashCode() {
        final long bits = msb ^ lsb;
        return (int) (bits ^ (bits >>> 32));
    }

    @Override
    public boolean equals(Object other) {
        if (other == null)
            return false;
        if (other.getClass() != Ulid.class)
            return false;
        Ulid that = (Ulid) other;
        if (lsb != that.lsb)
            return false;
        else if (msb != that.msb)
            return false;
        return true;
    }

    @Override
    public int compareTo(Ulid that) {

        final long min = 0x8000000000000000L;

        final long a = this.msb + min;
        final long b = that.msb + min;

        if (a > b)
            return 1;
        else if (a < b)
            return -1;

        final long c = this.lsb + min;
        final long d = that.lsb + min;

        if (c > d)
            return 1;
        else if (c < d)
            return -1;

        return 0;
    }

    String toString(char[] alphabet) {

        final char[] chars = new char[ULID_CHARS];

        long time = this.msb >>> 16;
        long random0 = ((this.msb & 0xffffL) << 24) | (this.lsb >>> 40);
        long random1 = (this.lsb & 0xffffffffffL);

        chars[0x00] = alphabet[(int) (time >>> 45 & 0b11111)];
        chars[0x01] = alphabet[(int) (time >>> 40 & 0b11111)];
        chars[0x02] = alphabet[(int) (time >>> 35 & 0b11111)];
        chars[0x03] = alphabet[(int) (time >>> 30 & 0b11111)];
        chars[0x04] = alphabet[(int) (time >>> 25 & 0b11111)];
        chars[0x05] = alphabet[(int) (time >>> 20 & 0b11111)];
        chars[0x06] = alphabet[(int) (time >>> 15 & 0b11111)];
        chars[0x07] = alphabet[(int) (time >>> 10 & 0b11111)];
        chars[0x08] = alphabet[(int) (time >>> 5 & 0b11111)];
        chars[0x09] = alphabet[(int) (time & 0b11111)];

        chars[0x0a] = alphabet[(int) (random0 >>> 35 & 0b11111)];
        chars[0x0b] = alphabet[(int) (random0 >>> 30 & 0b11111)];
        chars[0x0c] = alphabet[(int) (random0 >>> 25 & 0b11111)];
        chars[0x0d] = alphabet[(int) (random0 >>> 20 & 0b11111)];
        chars[0x0e] = alphabet[(int) (random0 >>> 15 & 0b11111)];
        chars[0x0f] = alphabet[(int) (random0 >>> 10 & 0b11111)];
        chars[0x10] = alphabet[(int) (random0 >>> 5 & 0b11111)];
        chars[0x11] = alphabet[(int) (random0 & 0b11111)];

        chars[0x12] = alphabet[(int) (random1 >>> 35 & 0b11111)];
        chars[0x13] = alphabet[(int) (random1 >>> 30 & 0b11111)];
        chars[0x14] = alphabet[(int) (random1 >>> 25 & 0b11111)];
        chars[0x15] = alphabet[(int) (random1 >>> 20 & 0b11111)];
        chars[0x16] = alphabet[(int) (random1 >>> 15 & 0b11111)];
        chars[0x17] = alphabet[(int) (random1 >>> 10 & 0b11111)];
        chars[0x18] = alphabet[(int) (random1 >>> 5 & 0b11111)];
        chars[0x19] = alphabet[(int) (random1 & 0b11111)];

        return new String(chars);
    }

    static char[] toCharArray(String string) {
        char[] chars = string == null ? null : string.toCharArray();
        if (!isValidCharArray(chars)) {
            throw new IllegalArgumentException(String.format("Invalid ULID: \"%s\"", string));
        }
        return chars;
    }

    static boolean isValidCharArray(final char[] chars) {
        if (chars == null || chars.length != ULID_CHARS) {
            return false;
        }
        if ((ALPHABET_VALUES[chars[0]] & 0b11000) != 0) {
            return false;
        }
        for (int i = 0; i < chars.length; i++) {
            if (ALPHABET_VALUES[chars[i]] == -1) {
                return false;
            }
        }
        return true;
    }
}

END

;