Last active
November 29, 2022 15:01
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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