-
Notifications
You must be signed in to change notification settings - Fork 8
/
Base62.java
110 lines (96 loc) · 3.86 KB
/
Base62.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
package com.github.ksuid;
import java.math.BigInteger;
import java.util.stream.IntStream;
import static java.lang.Math.abs;
import static java.lang.Math.ceil;
import static java.lang.Math.log;
import static java.math.BigInteger.ZERO;
import static java.math.BigInteger.valueOf;
/**
* Utility class to encode/decode bytes into Base62 strings in the same form
* as <a href="https://github.com/segmentio/ksuid/blob/master/base62.go">https://github.com/segmentio/ksuid/blob/master/base62.go</a>
* <p>
* Unless otherwise noted, passing a {@code null} argument to a method of this class
* will cause a {@link java.lang.NullPointerException NullPointerException} to be thrown.
* <p>
* See <a href="https://github.com/segmentio/ksuid">https://github.com/segmentio/ksuid</a>.
*/
final class Base62 {
// VisibleForTesting
static final char[] BASE_62_CHARACTERS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".toCharArray();
// VisibleForTesting
static final BigInteger BASE = valueOf(BASE_62_CHARACTERS.length);
private static final int BYTE_BITS = 8;
private static final double DIGIT_BITS = log(BASE_62_CHARACTERS.length) / log(2);
private Base62() {
throw new AssertionError("static utility class");
}
/**
* Encode bytes to Base62 string.
*
* @param bytes bytes to encode
* @return a Base62 string
*/
static String base62Encode(final byte[] bytes) {
return base62Encode(bytes, 0);
}
/**
* Encode bytes to Base62 string.
* <p>
* If length is greater than the size of the Base62 string,
* the returned string will be padded with zeros up to length size.
* <p>
* See <a href="https://stackoverflow.com/questions/14110010/base-n-encoding-of-a-byte-array">stack overflow comments</a>
* for the inspiration of this method.
*
* @param bytes bytes to encode
* @param length length to which to pad the returned string with zeros
* @return a Base62 string
*/
static String base62Encode(final byte[] bytes, final int length) {
final int size = (int) ceil((bytes.length * BYTE_BITS) / DIGIT_BITS);
final StringBuilder sb = new StringBuilder(size);
// Allocate an extra zero value byte in the first position, so that the unsigned 32bit UTC
// timestamp value is not treated as negative value and thus being encoded as
// "000000000000000000000000000"
final byte[] tempBuffer = new byte[bytes.length + 1];
System.arraycopy(bytes, 0, tempBuffer, 1, bytes.length);
BigInteger value = new BigInteger(tempBuffer);
while (value.compareTo(ZERO) > 0) {
final BigInteger[] quotientAndRemainder = value.divideAndRemainder(BASE);
sb.append(BASE_62_CHARACTERS[abs(quotientAndRemainder[1].intValue())]);
value = quotientAndRemainder[0];
}
while (length > 0 && sb.length() < length) {
sb.append('0');
}
return sb.reverse().toString();
}
/**
* Decode a Base62 string into bytes.
*
* @param s base62 string to decode
* @return decoded bytes
*/
static byte[] base62Decode(final String s) {
return IntStream.range(0, s.length())
.mapToObj(s::charAt)
.map(Base62::indexOf)
.map(BigInteger::valueOf)
.reduce(ZERO, (result, index) -> result.multiply(BASE).add(index))
.toByteArray();
}
// VisibleForTesting
static int indexOf(final char c) {
if (c >= '0' && c <= '9') {
return (c - 48);
}
if (c >= 'A' && c <= 'Z') {
return (c - 55);
}
if (c >= 'a' && c <= 'z') {
return (c - 61);
}
throw new IllegalArgumentException("'" + c + "' is not a valid Base62 character");
}
}