Skip to content

Instantly share code, notes, and snippets.

@rainbowdashlabs
Last active November 29, 2022 15:01
Show Gist options
  • Save rainbowdashlabs/dffe884125d29a0ac754e6ec6f8c382b to your computer and use it in GitHub Desktop.
Save rainbowdashlabs/dffe884125d29a0ac754e6ec6f8c382b to your computer and use it in GitHub Desktop.
A custom radix notation class. Allows to encode and decode numbers created with a custom radix notation.
/*
* SPDX-License-Identifier: MIT
*
* Copyright (C) 2022 RainbowDashLabs
*/
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.IntStream;
/**
* Class to encode and decode numbers to a custom radix representation.
* <p>
* Allows to encode numbers based on user defined charsets. Can add padding. Has default notations for binary, decimal and hexadecimal.
* <p>
* <a href="https://en.wikipedia.org/wiki/Radix">Wikipedia</a>
*/
public class CustomRadixNotation {
private static final CustomRadixNotation BASE_2 = builder().add("0", "1").build();
private static final CustomRadixNotation BASE_2_PAD4 = builder().add("0", "1").padding(4).build();
private static final CustomRadixNotation BASE_2_PAD8 = builder().add("0", "1").padding(8).build();
private static final CustomRadixNotation BASE_8 = builder().add('0', '7').build();
private static final CustomRadixNotation BASE_10 = builder().numbers().build();
private static final CustomRadixNotation BASE_16 = builder().numbers().add('A', 'F').build();
private static final CustomRadixNotation BASE_20_IJ = builder().numbers().add('A', 'H').add("I", "J").build();
private static final CustomRadixNotation BASE_20_JK = builder().numbers().add('A', 'H').add("J", "K").build();
private final Map<Long, String> numberIndex = new HashMap<>();
private final Map<String, Long> charIndex = new HashMap<>();
private final long base;
private final int padding;
private CustomRadixNotation(String[] numbers, int padding) {
var num = 0L;
for (var number : numbers) {
numberIndex.put(num, number);
charIndex.put(number, num);
num++;
}
this.padding = padding;
base = numbers.length;
}
private CustomRadixNotation(Set<String> chars, int padding) {
this(chars.toArray(String[]::new), padding);
}
/**
* Converts a number in one notation into another notation
*
* @param number number to convert
* @param source current notation
* @param target target notation
* @return number encoded with the target notation
*/
public static String convert(String number, CustomRadixNotation source, CustomRadixNotation target) {
return target.encode(source.decode(number));
}
/**
* Decode a number represented by an encoded string.
*
* @param number number as string
* @return number
* @throws IllegalArgumentException when the string contains an unkown character not present in the notation.
*/
public long decode(String number) {
var result = 0L;
var index = 0L;
for (var character : new StringBuilder(number).reverse().toString().split("")) {
var value = getValue(character);
result += (long) Math.pow(base, index) * value;
index++;
}
return result;
}
/**
* Allows to decode a number based on a radix notation into another radix notation.
*
* @param number number to decode with the current notation
* @param encoder encoder which defines the target encoding.
* @return number encoded with the target encoding
*/
public String decode(String number, CustomRadixNotation encoder) {
return convert(number, this, encoder);
}
/**
* Encode a number to a string based on the defined radix.
*
* @param number number
* @return number as string
*/
public String encode(long number) {
var currNum = number;
var index = 0;
// find max index
while (currNum >= base) {
currNum = (currNum - currNum % base) / base;
index++;
}
var result = new StringBuilder();
currNum = number;
// build number
while (index >= 0) {
var value = currNum % base;
result.append(getChar(value));
currNum = (currNum - value) / base;
index--;
}
// apply padding
if (padding != 0 && result.length() % padding != 0) {
result.append(getChar(0).repeat(padding - (result.length() % padding)));
}
return result.reverse().toString();
}
private String getChar(long value) throws IllegalArgumentException {
String character = numberIndex.get(value);
if (character == null) {
throw new IllegalArgumentException("Could not encode number " + value);
}
return character;
}
private long getValue(String character) throws IllegalArgumentException {
Long value = charIndex.get(character);
if (value == null) {
throw new IllegalArgumentException("Could not decode character " + character);
}
return value;
}
/**
* Gets a radix notation based on {@code binary}.
* <p>
* Base: 2
* <p>
* <a href="https://en.wikipedia.org/wiki/Binary_numeral_system">Wikipedia</a>
*
* @return custom radix notation for binary
*/
public static CustomRadixNotation binary() {
return BASE_2;
}
/**
* Gets a radix notation based on {@code binary}. A padding to {@code 8 bits} will be added.
* <p>
* Base: 2
* <p>
* <a href="https://en.wikipedia.org/wiki/Binary_numeral_system">Wikipedia</a>
*
* @return custom radix notation for binary
*/
public static CustomRadixNotation binaryPad4() {
return BASE_2_PAD4;
}
/**
* Gets a radix notation based on {@code binary}. A padding to {@code 8 bits} will be added.
* <p>
* Base: 2
* <p>
* <a href="https://en.wikipedia.org/wiki/Binary_numeral_system">Wikipedia</a>
*
* @return custom radix notation for binary
*/
public static CustomRadixNotation binaryPad8() {
return BASE_2_PAD8;
}
/**
* Gets a radix notation based on {@code octal}.
* <p>
* Base: 8
* <p>
* <a href="https://en.wikipedia.org/wiki/Octal">Wikipedia</a>
*
* @return custom radix notation for decimal
*/
public static CustomRadixNotation octal() {
return BASE_8;
}
/**
* Gets a radix notation based on {@code decimal}.
* <p>
* Base: 10
* <p>
* <a href="https://en.wikipedia.org/wiki/Decimal">Wikipedia</a>
*
* @return custom radix notation for decimal
*/
public static CustomRadixNotation decimal() {
return BASE_10;
}
/**
* Gets a radix notation based on {@code hexadecimal}.
* <p>
* Base: 16
* <p>
* <a href="https://en.wikipedia.org/wiki/Hexadecimal">Wikipedia</a>
*
* @return custom radix notation for hexadecimal
*/
public static CustomRadixNotation hexadecimal() {
return BASE_16;
}
/**
* Gets a radix notation based on {@code vigestimal} with {@code J} and {@code K} for {@code 19} and {@code 20}.
* <p>
* Base: 20
*
* <a href="https://en.wikipedia.org/wiki/Vigesimal">Wikipedia</a>
*
* @return custom radix notation for decimal
*/
public static CustomRadixNotation vigesimalJK() {
return BASE_20_JK;
}
/**
* Gets a radix notation based on {@code vigestimal} with {@code I} and {@code J} for {@code 19} and {@code 20}.
* <p>
* Base: 20
* <p>
* <a href="https://en.wikipedia.org/wiki/Vigesimal">Wikipedia</a>
*
* @return custom radix notation for decimal
*/
public static CustomRadixNotation vigesimalIJ() {
return BASE_20_IJ;
}
/**
* Get a new builder for a custom radix notation.
*
* @return new builder instance
*/
public static Builder builder() {
return new Builder();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof CustomRadixNotation that)) return false;
if (base != that.base) return false;
if (padding != that.padding) return false;
if (!numberIndex.equals(that.numberIndex)) return false;
return charIndex.equals(that.charIndex);
}
@Override
public int hashCode() {
int result = numberIndex.hashCode();
result = 31 * result + charIndex.hashCode();
result = 31 * result + (int) (base ^ (base >>> 32));
result = 31 * result + padding;
return result;
}
@Override
public String toString() {
return "CustomRadixNotation{" +
"characters=[" + String.join(",", numberIndex.values()) +
"], base=" + base +
", padding=" + padding +
'}';
}
public static class Builder {
private final Set<String> chars = new LinkedHashSet<>();
private int padding = 0;
/**
* Adds the listed characters in the given order.
*
* @param chars chars to add
* @return builder instance
* @throws IllegalArgumentException when any input is not 1 character long
*/
public Builder add(String... chars) {
for (String currChar : chars) {
if (currChar.length() != 1) throw new IllegalArgumentException("Inputs must be exact 1 char long");
}
this.chars.addAll(Arrays.asList(chars));
return this;
}
/**
* Adds all characters between these two characters. Inclusive.
*
* @param from first character
* @param to last character
* @return builder instance
*/
public Builder add(char from, char to) {
chars.addAll(IntStream.rangeClosed(from, to).mapToObj(Character::toString).toList());
return this;
}
/**
* Adds the numbers {@code 0} to {@code 9}.
*
* @return builder instance
*/
public Builder numbers() {
return add('0', '9');
}
/**
* Adds the lower characters {@code a} to {@code z}.
*
* @return builder instance
*/
public Builder lowerChars() {
return add('a', 'z');
}
/**
* Adds the upper characters {@code A} to {@code Z}.
*
* @return builder instance
*/
public Builder upperChars() {
return add('A', 'Z');
}
/**
* Adds padding to the number. The padding will be done with the first entered char. This is primarily useful for binary notation.
*
* @param padding padding amount
* @return builder instance
*/
public Builder padding(int padding) {
if (padding < 0) {
throw new IllegalArgumentException("Padding can not be negative.");
}
this.padding = padding;
return this;
}
/**
* Creates a new custom radix notation instance.
*
* @return new custom radix notation instance.
*/
public CustomRadixNotation build() {
return new CustomRadixNotation(chars, padding);
}
}
}
public class CustomRadixNotationExample {
public static void main(String[] args) {
// Create a new radix notation for binary with 8 bit padding
var binary = CustomRadixNotation.binaryPad8();
System.out.println("Binary");
System.out.println(binary.encode(5486)); // 0001010101101110
System.out.println(binary.encode(8452)); // 0010000100000100
System.out.println(binary.encode(142)); // 10001110
// Create a new radix notation for decimal.
// That's what you know and will always output exactly what u gave in just as a string.
// That's more like a proof that this actually works
var decimal = CustomRadixNotation.decimal();
System.out.println("Decimal");
System.out.println(decimal.encode(5486)); // 5486
System.out.println(decimal.encode(102)); // 102
System.out.println(decimal.encode(142)); // 142
// Create a new radix notation for hexadecimal.
var hexadecimal = CustomRadixNotation.hexadecimal();
System.out.println("Hexadecimal");
System.out.println(hexadecimal.encode(5486)); // 156E
System.out.println(hexadecimal.encode(102)); // 66
System.out.println(hexadecimal.encode(142)); // 8E
// Some other interesting stuff:
System.out.println("Octal");
System.out.println(CustomRadixNotation.octal().encode(5486)); // 12556
System.out.println("Vigesimal");
System.out.println(CustomRadixNotation.vigesimalIJ().encode(5486)); // DE6
// Building your own radix notation
// This will be something between decimal and hexadecimal
var custom = CustomRadixNotation.builder().numbers().add('A', 'C').build();
System.out.println("Custom notation with base 13");
System.out.println(custom);
System.out.println(custom.encode(5486)); // 2660
// This is something between binary and octal with a base of 5
custom = CustomRadixNotation.builder().add('1', '5').build();
System.out.println("Custom notation with base 5");
System.out.println(custom);
System.out.println(custom.encode(5486)); // 244532
// Decoding works the same way. It will always work as long as the number was encoded with the same notation.
System.out.println(binary.decode(binary.encode(8452))); // 8452
System.out.println(decimal.decode(decimal.encode(8452))); // 8452
System.out.println(hexadecimal.decode(hexadecimal.encode(8452))); // 8452
// Instead of first encoding and then decoding you can simply use the decode method with an encoder.
// convert a binary into hexadecimal
System.out.println(binary.decode("1010", hexadecimal)); // A
// convert a binary into adecimal
System.out.println(binary.decode("1010", decimal)); // 10
// As an alternative you can also use the convert method.
System.out.println(CustomRadixNotation.convert("1010", binary, decimal)); // 10
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment