All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.seppiko.commons.utils.codec.Base62 Maven / Gradle / Ivy

There is a newer version: 2.11.0
Show newest version
/*
 * 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