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 git@github.com: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\$ Jun 25 '20 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
    Jun 25 '20 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
    Jun 29 '20 at 20:17
  • 1
    \$\begingroup\$ Not sure... do non-integer decimal parameter values get caught? \$\endgroup\$
    – radarbob
    Jun 29 '20 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
    Jun 30 '20 at 0:40
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
    Jun 30 '20 at 0:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.