2
\$\begingroup\$

The Source code is maintained on GitHub and may be cloned via the following commands. A Live demo is hosted online, thanks to GitHub Pages.

mkdir -vp ~/git/hub/javascript-utilities cd ~/git/hub/javascript-utilities git clone [email protected]:javascript-utilities/decimal-to-base.git 

The build target is ECMAScript version 6, and so far both manual tests and automated JestJS tests show that the decimalToBase function functions as intended, both for Browser and NodeJS environments.

I am aware of (number).toString(radix) for converting numbers to another base. However, the built-in Number.toString() method doesn't seem to have options for prefixing Binary, Octal, or Heximal bases; among other features that I'm implementing.

Example Usage

decimalToBase(540, 16); //> "0x21C" 

I am mostly concerned with improving the JavaScript, and TypeScript. The HTML and CSS are intended to be simple and functional.

Pleas post any suggestion to the JavaScript. Additionally if anyone has a clean way of handling floating point numbers that'd be super, because at this time the current implementation only handles integers.

"use strict"; /** * Converts decimal to another base, eg. hex, octal, or binary * @function decimalToBase * @param {number|string} decimal * @param {number|string} radix - default `16` * @param {boolean} verbose - default `false` * @param {string[]} symbols_list - default `[...'0123456789abcdefghijklmnopqrstuvwxyz']` * @returns {string} * @throws {SyntaxError|RangeError} * @author S0AndS0 * @license AGPL-3.0 * @see {link} - https://www.tutorialspoint.com/how-to-convert-decimal-to-hexadecimal * @see {link} - https://www.ecma-international.org/ecma-262/6.0/#sec-literals-numeric-literals * @example * decimalToBase(540, 16); * //> "0x21C" */ const decimalToBase = (decimal, radix = 16, verbose = false, symbols_list = [...'0123456789abcdefghijklmnopqrstuvwxyz']) => { decimal = Math.floor(Number(decimal)); radix = Number(radix); const max_base = symbols_list.length; if (isNaN(decimal)) { throw new SyntaxError('First argument is Not a Number'); } else if (isNaN(radix)) { throw new SyntaxError('radix is Not a Number'); } else if (radix > max_base) { throw new RangeError(`radix must be less than or equal to max base -> ${max_base}`); } else if (radix < 2) { throw new RangeError(`radix must be greater than 2`); } let prefix = ''; switch (radix) { case 16: // Hexadecimal prefix = '0x'; break; case 8: // Octal prefix = '0o'; break; case 2: // Binary prefix = '0b'; break; } if (radix >= 10 && decimal < 10) { return `${prefix}${symbols_list[decimal]}`; } let converted = ''; let dividend = decimal; while (dividend > 0) { const remainder = dividend % radix; const quotient = (dividend - remainder) / radix; /* istanbul ignore next */ if (verbose) { console.log(`dividend -> ${dividend}`, `remainder -> ${remainder}`, `quotient -> ${quotient}`); } converted = `${symbols_list[remainder]}${converted}`; dividend = quotient; } return `${prefix}${converted.toUpperCase()}`; }; /* istanbul ignore next */ if (typeof module !== 'undefined') { module.exports = decimalToBase; }
*, *::before, *::after { box-sizing: border-box; } .container { max-width: 50%; position: relative; } .row { padding-top: 1rem; } .row::after { content: ''; position: absolute; left: 0; background-color: lightgrey; height: 0.2rem; width: 100%; } .label { font-weight: bold; font-size: 1.2rem; width: 19%; padding-right: 1%; } .text_input { float: right; width: 79%; }
<!DOCTYPE html> <html lang="en" dir="ltr"> <head> <meta charset="utf-8"> <title>Tests Decimal to Base</title> <!-- <link rel="stylesheet" href="assets/css/main.css"> --> <!-- <script type="text/javascript" src="assets/js/modules/decimal-to-base/decimal-to-base.js"></script> --> <script type="text/javascript"> function updateOutput(_event) { const decimal = document.getElementById('decimal').value; const radix = document.getElementById('radix').value; const output_element = document.getElementById('output'); let output_value = 'NaN'; try { output_value = decimalToBase(decimal, radix) } catch (e) { if (e instanceof SyntaxError) { console.error(e); } else if (e instanceof RangeError) { console.error(e); } else { throw e; } } output_element.value = output_value; } window.addEventListener('load', (_event) => { document.getElementById('decimal').addEventListener('input', updateOutput); document.getElementById('radix').addEventListener('input', updateOutput); }); </script> </head> <body> <div class="container"> <div class="row"> <span class="label">Radix: </span> <input type="text" class="text_input" id="radix" value="16"> </div> <br> <div class="row"> <span class="label">Input: </span> <input type="text" class="text_input" id="decimal"> </div> <br> <div class="row"> <span class="label">Output: </span> <input type="text" class="text_input" id="output" readonly> </div> </div> </body> </html>


For completeness here are the JestJS tests.

"use strict"; /** * @author S0AndS0 * @copyright AGPL-3.0 * @example <caption>Jest Tests for decimalToBase</caption> * // Initialize new class instance and run tests * const test_decimalToBase = new decimalToBase_Test(); * test_decimalToBase.runTests(); */ class decimalToBase_Test { constructor() { this.decimalToBase = require('../decimal-to-base.js'); this.decimal_limits = { min: 1, max: 16 }; this.base_configs = [ { base: 2, name: 'Binary' }, { base: 3, name: 'Trinary' }, { base: 4, name: 'Quaternary' }, { base: 5, name: 'Quinary AKA Pental' }, { base: 6, name: 'Senary AKA Heximal or Seximal' }, { base: 7, name: 'Septenary' }, { base: 8, name: 'Octal' }, { base: 9, name: 'Nonary' }, { base: 10, name: 'Decimal AKA Denary' }, { base: 11, name: 'Undecimal' }, { base: 12, name: 'Duodecimal AKA Dozenal or Uncial' }, { base: 13, name: 'Tridecimal' }, { base: 14, name: 'Tetradecimal' }, { base: 15, name: 'Pentadecimal' }, { base: 16, name: 'Hexadecimal' } ]; } /** * Runs all tests for this module */ runTests() { this.testsErrors(); this.testsConversion(); } /** * Uses `(Number).toString()` to check conversions, note this will only work for radix between `2` though `36`, and default `symbols_list` */ static doubleChecker(decimal, radix) { decimal = Number(decimal); radix = Number(radix); let prefix = ''; switch (radix) { case 2: prefix = '0b'; break; case 8: prefix = '0o'; break; case 16: prefix = '0x'; break; } return `${prefix}${(decimal).toString(radix).toUpperCase()}`; } /** * Tests available error states */ testsErrors() { test('Is a `SyntaxError` thrown, when `decimal` parameter is not a number?', () => { expect(() => { this.decimalToBase('spam!', 10); }).toThrow(SyntaxError); }); test('Is a `SyntaxError` thrown, when `radix` parameter is not a number?', () => { expect(() => { this.decimalToBase(42, 'ham'); }).toThrow(SyntaxError); }); test('Is a `RangeError` thrown, when `symbols_list` is not long enough?', () => { expect(() => { this.decimalToBase(42, 37); }).toThrow(RangeError); }); test('Is a `RangeError` thrown, when `radix` parameter is less than `2`?', () => { expect(() => { this.decimalToBase(42, 1); }).toThrow(RangeError); }); } /** * Loops through `this.base_configs` and tests decimal integers between `this.decimal_limits['min']` and `this.decimal_limits['max']` */ testsConversion() { const min = this.decimal_limits['min']; const max = this.decimal_limits['max']; this.base_configs.forEach((config) => { const { base } = config; const { name } = config; for (let decimal = min; decimal <= max; decimal++) { const expected_value = this.constructor.doubleChecker(decimal, base); test(`Base ${base}, does ${decimal} equal "${expected_value}" in ${name}?`, () => { expect(this.decimalToBase(decimal, base)).toEqual(expected_value); }); } }); } } const test_decimalToBase = new decimalToBase_Test(); test_decimalToBase.runTests(); 

Questions

  • Are there any mistakes?
  • Have I missed any test cases?
  • Any suggestions on expanding this implementation to handle floating point numbers?
  • Any suggestions on making the code more readable?
  • Are there any additional features that are wanted?
\$\endgroup\$
5
  • \$\begingroup\$Deleting the radix 16 in the test code field generates an error and thereafter entering any radix number gives an error. Maybe it is the field entry code.\$\endgroup\$CommentedJun 25, 2020 at 11:06
  • \$\begingroup\$Thanks for the heads-up @MohsenAlyafei. By design the decimalToBase function will throw a SyntaxError or RangeError, which should be caught by in-lined JavaScript within the HTML. But for some reason CodeReview has their own catcher which is adding an overlaid element, so it seems to be necessary to use the "Expand snippit" so that errors do not hide elements used for input/output.\$\endgroup\$
    – S0AndS0
    CommentedJun 25, 2020 at 15:47
  • 1
    \$\begingroup\$radix must be less than max base -> ${max_base}. If max-base = 16 then this says "radix cannot be 16."\$\endgroup\$
    – radarbob
    CommentedJun 29, 2020 at 20:17
  • 1
    \$\begingroup\$Not sure... do non-integer decimal parameter values get caught?\$\endgroup\$
    – radarbob
    CommentedJun 29, 2020 at 20:24
  • \$\begingroup\$@radarbob Thanks for pointing out these bugs! I've corrected'em both on GitHub and here, as well as added ya to the list of attribution links.\$\endgroup\$
    – S0AndS0
    CommentedJun 30, 2020 at 0:40

2 Answers 2

2
\$\begingroup\$

From the array variable:symbols_list = [...'0123456789abcdefghijklmnopqrstuvwxyz'], I would assume that the function would permit the use of alternative symbols for representing numbers in formats above the decimal format. I have not come across such formats, like Hexadecimal numbers being represented by letters other than the English (Latin) letters A to F.

However, if you stick with the English letters A to Z, then I would suggest that you could simplify the process by using the built-in (number).toString(radix) (which you are already aware of) and then just add the needed prefix (which is only required for outputs in Binary, Octal, and Hex numbers).

I would do this by making use of the built-in function as follows:

const decimalToBase = (decimal, radix = 16) => { let converted = (decimal).toString(radix); let prefix = ''; switch (radix) { case 16: prefix = '0x'; break; case 8: prefix = '0o'; break; case 2: prefix = '0b'; } return prefix + converted.toUpperCase(); } console.log(decimalToBase(540)); // 0x21C (default radix) console.log(decimalToBase(123, 2)); // 0b1111011 console.log(decimalToBase(123, 8)); // 0o173 console.log(decimalToBase(123, 10)); // 123 console.log(decimalToBase(123, 32)); // 3R console.log(decimalToBase(123, 36)); // 3F

\$\endgroup\$
1
  • \$\begingroup\$Thanks for the feedback! The doubleChecker method within the decimalToBase_Test class does mostly this, and the symbols_list parameter is there for those that want to experiment with bases larger than 36; which is the upper limit for toString() method on Numbers.\$\endgroup\$
    – S0AndS0
    CommentedJun 30, 2020 at 0:40
1
\$\begingroup\$

I really like that you have JSDoc'ed the function, although I’d like to echo that it’s basically recreating Number.prototype.toString, down to the error types.

One thing I would like to address is the [functional-programming]-tag, because there seems to be a fair amount of misconceptions about the term. In the case of prefixing the base, a functional approach might be along the lines

const compose = f => g => a => f(g(a)) const concat = a => b => String.prototype.concat.call(a, b) const toBase = radix => n => Number.prototype.toString.call(n, radix) const toBin = compose (concat ("0b")) (toBase(2)) const toOct = compose (concat ("0o")) (toBase(8)) const toHex = compose (concat ("0x")) (toBase(16)) 

utilizing the fact that functions can be both arguments to, and return values of functions. Which enables things like partial application and function composition, given curried (n-ary to unary) forms of the functions.

Regarding testing, you might consider using property-based testing like fast-check to generate random arguments that you might have not thought of.

\$\endgroup\$

    Start asking to get answers

    Find the answer to your question by asking.

    Ask question

    Explore related questions

    See similar questions with these tags.