
org.seppiko.commons.utils.codec.Base62 Maven / Gradle / Ivy
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.seppiko.commons.utils.codec;
import java.io.ByteArrayOutputStream;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Arrays;
import org.seppiko.commons.utils.CharUtil;
import org.seppiko.commons.utils.Environment;
import org.seppiko.commons.utils.MathUtil;
import org.seppiko.commons.utils.NumberUtil;
import org.seppiko.commons.utils.StringUtil;
/**
* Base62
* A simple base62 encode / decode.
* Similar to base64, typically used in URL shortness.
*
* @see Base62
* @author Leonard Woo
*/
public class Base62 implements Serializable {
@Serial
private static final long serialVersionUID = -6183689559038935787L;
private static final int BASE_COUNT = Environment.DIGIT_ALPHABET_ALL_COUNT;
private static final int STANDARD_BASE = 256;
// {@code "[0-9A-Za-z]"}
private static final char[] ALPHABET_BASE = Environment.DIGIT_ALPHABET_ALL;
private static final byte[] DECODE_TABLE;
static {
DECODE_TABLE = new byte[STANDARD_BASE];
Arrays.fill(DECODE_TABLE, (byte) -1);
for (int i = 0; i < ALPHABET_BASE.length; i++) {
DECODE_TABLE[ALPHABET_BASE[i]] = (byte) i;
}
}
private Base62() {}
/**
* Encode bytes to Base62 string
*
* @param source bytes.
* @return Base62 String.
* @throws IllegalArgumentException source is empty.
*/
public static String encode(final byte[] source) throws IllegalArgumentException {
if (null == source || source.length == 0) {
throw new IllegalArgumentException("source must not be empty.");
}
final byte[] indices = convert(source, STANDARD_BASE, BASE_COUNT);
return new String(translate(indices, ALPHABET_BASE));
}
/**
* Encode number to Base62 string
*
* @param source Number.
* @return Base62 String.
* @throws IllegalArgumentException source is negative.
*/
public static String encode(final BigInteger source) throws IllegalArgumentException {
if (source.compareTo(BigInteger.ZERO) < 0) {
throw new IllegalArgumentException("source must not be negative.");
}
StringBuilder sb = new StringBuilder();
if (BigInteger.ZERO.equals(source)) {
sb.append(ALPHABET_BASE[0]);
} else {
BigInteger l = source;
while (l.compareTo(BigInteger.ZERO) > 0) {
BigInteger[] divmod = l.divideAndRemainder(BigInteger.valueOf(BASE_COUNT)); // divide and mod
l = divmod[0];
sb.append(ALPHABET_BASE[divmod[1].intValue()]);
}
}
return sb.toString();
}
/**
* Decode Base62 string to bytes
*
* @param str Base62 String.
* @return bytes.
* @throws IllegalArgumentException source is {@code null} or empty.
*/
public static byte[] decodeToBytes(final String str) throws IllegalArgumentException {
if (StringUtil.isNullOrEmpty(str)) {
throw new IllegalArgumentException("string must not be empty");
}
final byte[] prepared = translate(str.getBytes(), DECODE_TABLE);
return convert(prepared, BASE_COUNT, STANDARD_BASE);
}
/**
* Decode Base62 string to number
*
* @param str Base62 String.
* @return Number.
* @throws IllegalArgumentException string is empty or {@code null}.
*/
public static BigInteger decode(final String str) throws IllegalArgumentException {
if (StringUtil.isNullOrEmpty(str)) {
throw new IllegalArgumentException("string must not be empty");
}
final char[] items = str.toCharArray();
BigInteger result = BigInteger.ZERO;
int pow = 0;
for (char c: items) {
// char array index multiply 62^time
result = result.add(BigInteger.valueOf(DECODE_TABLE[c]).multiply(MathUtil.pow(BASE_COUNT, pow)));
pow++;
}
return result;
}
/**
* Uses the elements of a byte array as indices to a dictionary and returns the corresponding values
* in form of a byte array.
*/
private static byte[] translate(final byte[] indices, final byte[] dictionary) {
final byte[] translation = new byte[indices.length];
for (int i = 0; i < indices.length; i++) {
translation[i] = dictionary[indices[i]];
}
return translation;
}
private static char[] translate(final byte[] indices, final char[] dictionary) {
final char[] translation = new char[indices.length];
for (int i = 0; i < indices.length; i++) {
translation[i] = dictionary[indices[i]];
}
return translation;
}
/**
* Converts a byte array from a source base to a target base using the alphabet.
*/
private static byte[] convert(final byte[] message, final int sourceBase, final int targetBase) {
// This algorithm is inspired by: http://codegolf.stackexchange.com/a/21672
final int estimatedLength = estimateOutputLength(message.length, sourceBase, targetBase);
final ByteArrayOutputStream out = new ByteArrayOutputStream(estimatedLength);
byte[] source = message;
while (source.length > 0) {
final ByteArrayOutputStream quotient = new ByteArrayOutputStream(source.length);
int remainder = 0;
for (byte b : source) {
final int accumulator = (b & ((int) BaseNCodec.MASK_8BITS)) + remainder * sourceBase;
final int digit = (accumulator - (accumulator % targetBase)) / targetBase;
remainder = accumulator % targetBase;
if (quotient.size() > 0 || digit > 0) {
quotient.write(digit);
}
}
out.write(remainder);
source = quotient.toByteArray();
}
// pad output with zeroes corresponding to the number of leading zeroes in the message
for (int i = 0; i < message.length - 1 && message[i] == 0; i++) {
out.write(0);
}
return reverse(out.toByteArray());
}
/**
* Estimates the length of the output in bytes.
*/
private static int estimateOutputLength(int inputLength, int sourceBase, int targetBase) {
return (int) Math.ceil((Math.log(sourceBase) / Math.log(targetBase)) * inputLength);
}
/**
* Reverses a byte array.
*/
private static byte[] reverse(final byte[] arr) {
final int length = arr.length;
final byte[] reversed = new byte[length];
for (int i = 0; i < length; i++) {
reversed[length - i - 1] = arr[i];
}
return reversed;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy