Allow converting multiple values simultaneously

And document a whole load of code.
This commit is contained in:
Florine W. Dekker 2022-11-23 14:30:52 +01:00
parent 7789363797
commit 7ec5171adc
Signed by: FWDekker
GPG Key ID: D3DCFAA8A4560BE0
3 changed files with 141 additions and 24 deletions

View File

@ -13,4 +13,6 @@
#inputs textarea {
display: block;
width: 25em;
height: 5em;
}

View File

@ -40,7 +40,10 @@
<header class="fwd-header">
<hgroup>
<h1><a href=".">Converter</a></h1>
<h2>Convert numbers to and from various bases.</h2>
<h2>
Convert numbers to and from various bases.
Separate values by commas to convert multiple at the same time.
</h2>
</hgroup>
</header>
</section>

View File

@ -1,6 +1,6 @@
// noinspection JSUnresolvedVariable
const {$, doAfterLoad, stringToHtml} = window.fwdekker;
import bigInt from "big-integer"
import bigInt from "big-integer";
/**
@ -42,7 +42,17 @@ function stringReplaceAlls(str, targets, replacements) {
}
/**
* A numeral system, expressing a conversion from decimal to this system.
*/
class NumeralSystem {
/**
* Constructs a new numeral system.
*
* @param base the base
* @param alphabet the symbols representing values in this system
* @param caseSensitive `true` if and only if capitalization affects the value of a number in this system
*/
constructor(base, alphabet, caseSensitive) {
this.base = base;
this.alphabet = alphabet;
@ -50,39 +60,87 @@ class NumeralSystem {
}
/**
* Converts a decimal number to a number in this system.
*
* @param decimalNumber {bigInt} the decimal number to convert
* @returns {string} the representation of `decimalNumber` in this system
*/
decimalToBase(decimalNumber) {
return decimalNumber.toString(this.base, this.alphabet);
}
/**
* Converts multiple decimal numbers to numbers in this system.
*
* @param decimalNumbers {bigInt[]} the decimal numbers to convert
* @returns {string[]} the representations of `decimalNumbers` in this system
*/
decimalsToBases(decimalNumbers) {
return decimalNumbers.map((it) => this.decimalToBase(it));
}
/**
* Converts a number from this system to decimal.
*
* @param baseString {string} the number in this system to convert
* @returns {bigInt} the decimal representation of `baseString`
*/
baseToDecimal(baseString) {
return bigInt(baseString, this.base, this.alphabet, this.caseSensitive);
}
filterBaseString(baseString) {
// Regex from https://stackoverflow.com/a/3561711/
const alphabet = this.caseSensitive
? this.alphabet
: this.alphabet.toLowerCase() + this.alphabet.toUpperCase();
const regexSafeAlphabet = alphabet.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
/**
* Converts numbers from this system to decimal.
*
* @param baseStrings {string[]} the numbers in this system to convert
* @returns {bigInt[]} the decimal representations of `baseStrings`
*/
basesToDecimals(baseStrings) {
return baseStrings.map((it) => this.baseToDecimal(it));
}
return baseString.replace(new RegExp(`[^${regexSafeAlphabet}]`, "g"), "");
/**
* Removes disallowed symbols from `baseString`.
*
* @param baseString {string} the string representation of a number in this system
* @returns {string} the filtered base string
*/
filterBaseString(baseString) {
const alphabet = this.caseSensitive
? (this.alphabet + ",")
: (this.alphabet.toLowerCase() + this.alphabet.toUpperCase() + ",");
// Regex from https://stackoverflow.com/a/3561711/
const alphabetRegex = alphabet.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
return baseString.replace(new RegExp(`[^${alphabetRegex}]`, "g"), "");
}
}
/**
* An input field that contains a value in some numeral system.
*/
class NumeralSystemInput {
/**
* Constructs a new numeral system input, including HTML elements.
*
* @param name {string} the human-readable name of the system
* @param numeralSystem {NumeralSystem} the numeral system in which values in this input are expressed
*/
constructor(name, numeralSystem) {
this.name = name;
this.numeralSystem = numeralSystem;
const base = this.numeralSystem.base;
this.label = stringToHtml(`<label for="${this.name}-input">${this.name}</label>`);
this.label = stringToHtml(`<label for="b${base}-input">${this.name} <small>(base ${base})</small></label>`);
this.textarea = stringToHtml(`<textarea id="${this.name}-input" class="number-input"></textarea>`);
this.textarea = stringToHtml(`<textarea id="b${base}-input" class="number-input"></textarea>`);
this.textarea.oninput = () => {
if (this.textarea.value == null || this.textarea.value === "")
return;
this.textarea.value = this.numeralSystem.filterBaseString(this.textarea.value);
updateAllInputs(this, this.numeralSystem.baseToDecimal(this.textarea.value));
updateAllInputs(this, this.numeralSystem.basesToDecimals(this.textarea.value.split(",")));
};
this.wrapper = document.createElement("article");
@ -91,33 +149,55 @@ class NumeralSystemInput {
}
/**
* Appends this input's elements to `parent.`
*
* @param parent {HTMLElement} the element to add this input to
*/
addToParent(parent) {
parent.appendChild(this.wrapper);
}
update(decimalNumber) {
this.textarea.value = this.numeralSystem.decimalToBase(decimalNumber);
/**
* Updates the input's value to contain this input's numeral system's representation of `decimalNumbers`.
*
* @param decimalNumbers {bigInt[]} the decimal numbers to represent in the input
*/
update(decimalNumbers) {
this.textarea.value = this.numeralSystem.decimalsToBases(decimalNumbers).join(",");
}
}
/**
* The base-64 `NumeralSystem`.
*/
class Base64NumeralSystem extends NumeralSystem {
// TODO Convert static methods to static properties once supported by Firefox
/**
* @returns {string} the default base-64 alphabet, including padding
*/
static defaultAlphabet() {
return "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
}
/**
* Constructs a new base 64 numeral system.
* Constructs a new base-64 numeral system.
*
* @param alphabet the 64 characters to encode numbers with, and the padding character at the end
* @param alphabet {string} the 64 characters to encode numbers with, and the padding character at the end
*/
constructor(alphabet) {
super(64, alphabet, true);
}
/**
* Converts a decimal number to base-64.
*
* @param decimalNumber {bigInt} the decimal number to convert to base-64
* @returns {string} the base-64 representation of `decimalNumber`
*/
decimalToBase(decimalNumber) {
const hex = decimalNumber.toString(16);
const b64 = Array.from(hex.padStart(hex.length + hex.length % 2))
@ -131,6 +211,12 @@ class Base64NumeralSystem extends NumeralSystem {
return stringReplaceAlls(btoa(b64), Base64NumeralSystem.defaultAlphabet(), this.alphabet);
}
/**
* Converts a base-64 number to decimal.
*
* @param baseString {string} the base-64 number to convert to decimal
* @returns {bigInt} the decimal representation of `baseString`
*/
baseToDecimal(baseString) {
if (baseString.length % 4 === 1) throw new Error("Invalid input string length.");
@ -141,17 +227,28 @@ class Base64NumeralSystem extends NumeralSystem {
}
}
/**
* An input field for base-64 numbers.
*/
class Base64NumeralSystemInput extends NumeralSystemInput {
/**
* @returns {Object.<string, [string, string]>} the variants for the last two characters in the base-64 alphabet
*/
// TODO Convert static methods to static properties once supported by Firefox
static dropdownOptions() {
return {"Standard": ['+', '/'], "Filename": ['-', '_'], "IMAP": ['+', ',']};
}
/**
* Constructs a new base-64 input, including HTML elements.
*
* @param name {string} the human-readable name of the system
*/
constructor(name) {
super(name, new Base64NumeralSystem(Base64NumeralSystem.defaultAlphabet()));
this.dropdown = stringToHtml(`<select id="${this.name}-dropdown"></select>`);
this.dropdown = stringToHtml(`<select id="${this.numeralSystem.base}-dropdown"></select>`);
this.dropdown.onchange = () => {
const selectedOption = Base64NumeralSystemInput.dropdownOptions()[this.dropdown.value];
this.setLastDigits(selectedOption[0], selectedOption[1]);
@ -169,18 +266,27 @@ class Base64NumeralSystemInput extends NumeralSystemInput {
}
/**
* Changes the last two symbols of the alphabet.
*
* @param c62 the new 62nd (0-indexed) symbol of the alphabet
* @param c63 the new 63rd (0-indexed) symbol of the alphabet
*/
setLastDigits(c62, c63) {
const oc62 = this.numeralSystem.alphabet[62];
const oc63 = this.numeralSystem.alphabet[63];
this.numeralSystem.alphabet =
stringReplaceAt(stringReplaceAt(this.numeralSystem.alphabet, 62, c62), 63, c63);
this.textarea.value =
stringReplaceAll(stringReplaceAll(this.textarea.value, oc62, c62), oc63, c63);
this.numeralSystem.alphabet = stringReplaceAt(stringReplaceAt(this.numeralSystem.alphabet, 62, c62), 63, c63);
this.textarea.value = stringReplaceAll(stringReplaceAll(this.textarea.value, oc62, c62), oc63, c63);
}
}
/**
* All the inputs to display.
*
* @type {NumeralSystemInput[]} all the inputs to display
*/
const inputs = [
new NumeralSystemInput("Binary", new NumeralSystem(2, "01")),
new NumeralSystemInput("Octal", new NumeralSystem(8, "01234567")),
@ -198,10 +304,16 @@ const inputs = [
),
];
function updateAllInputs(source, newValue) {
/**
* Updates the values of all inputs to represent `newValue`.
*
* @param source {NumeralSystemInput} the input that triggered the update
* @param newDecimalValues {bigInt[]} the decimal representation of the new value
*/
function updateAllInputs(source, newDecimalValues) {
for (const input of inputs)
if (input !== source)
input.update(newValue);
input.update(newDecimalValues);
}
@ -210,6 +322,6 @@ doAfterLoad(() => {
for (const input of inputs)
input.addToParent(form);
updateAllInputs(undefined, bigInt(42));
updateAllInputs(undefined, [bigInt(42), bigInt(17)]);
inputs[0].textarea.focus();
});