Source: index.js

/*!
 * @tobydeieso/BinaryData
 * Copyright (C) 2023  Toby De Ieso
 */


/**
 * A binary number represented in string format, containing only the characters 1 and 0.
 * @typedef {string} binaryString
 * @example
 * '1001101' // The decimal value of 77
 */

/**
 * A binary number represented in string format, containing only the characters 0-9 and A-F.
 * @typedef {string} hexString
 * @example
 * '0x4D' // The decimal value of 77
 */

/**
 * A binary number represented as an array of numbers with either the value of 1 or 0.
 * @typedef {number[]} binaryArray
 * @example
 * [1, 1, 0, 1, 0, 0, 1] // The decimal value of 77
 */

/**
 * An Object representing a single word value (e.g. 4 bits) in 3 different formats.
 * @typedef {Object} word
 * @property {binaryString} binary - A string that represents a binary value, consisting of 1's and 0's.
 * @property {hexString} hex - A string that represents a hexadecimal value, consisting of the characters 0-9 and A-F.
 * @property {decimal} decimal - A decimal number value.
 * @example
 * { binary: '0101', hex: '5', decimal: 5 }
 * 
 */

/**
 * A integer number value, anywhere from 0 to 4,294,967,295.
 * @typedef {number} decimal
 */


/**
 * Toby De Ieso's BinaryData Module
 * @module @tobydeieso/binarydata
 */

/**
 * A custom binary data type, where the binary data ({@link binaryArray}) is stored as an array of 
 * integer numbers (0's and 1's) with a bit length rounded up to the nearest 4 bit (word).
 * The class has methods to return the binary value in other formats, including 
 * a {@link binaryString}, {@link hexString} or {@link decimal} number value.
 */
class BinaryData {

  /**
   * A quick access table for word objects (e.g. 4 bit binary values), from 0000 to 1111.
   * @type {word[]}
   * @static
   */
  static #wordTable = [
    { binary: '0000', hex: '0', decimal: 0 },
    { binary: '0001', hex: '1', decimal: 1 },
    { binary: '0010', hex: '2', decimal: 2 },
    { binary: '0011', hex: '3', decimal: 3 },
    { binary: '0100', hex: '4', decimal: 4 },
    { binary: '0101', hex: '5', decimal: 5 },
    { binary: '0110', hex: '6', decimal: 6 },
    { binary: '0111', hex: '7', decimal: 7 },
    { binary: '1000', hex: '8', decimal: 8 },
    { binary: '1001', hex: '9', decimal: 9 },
    { binary: '1010', hex: 'A', decimal: 10 },
    { binary: '1011', hex: 'B', decimal: 11 },
    { binary: '1100', hex: 'C', decimal: 12 },
    { binary: '1101', hex: 'D', decimal: 13 },
    { binary: '1110', hex: 'E', decimal: 14 },
    { binary: '1111', hex: 'F', decimal: 15 }
  ];

  /**
   * An array of bit values for each position upto 32 bit.
   * @type {number[]}
   * @static
   */
  static #bitTable = [ 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576, 2097152, 4194304, 8388608, 16777216, 33554432, 67108864, 134217728, 268435456, 536870912, 1073741824, 2147483648, 4294967296 ];

  /**
   * A string of characters that can be contained within a valid binary string.
   * @type {string}
   * @static
   */
  static #validBinaryChars = '01';

  /**
   * A string of characters that can be contained within a valid hexadecimal string.
   * @type {string}
   * @static
   */
  static #validHexChars = '0123456789ABCDEF';

  /**
   * A prefix that signifies a text string contains a hexadecimal value.
   * @type {string}
   * @static
   */
  static #hexPrefix = '0x';

  /**
   * The minimum bit resolution for all internal binary objects.
   * @type {number}
   */
  static #maxPrecision = 32;

  /**
   * A copy of the raw data passed into the constructor.
   * @type {decimal|hexString|binaryString|binaryArray}
   */
  #_raw;

  /**
   * The detected type of data that was passed into the constructor, represented as a string.
   * It can be one of the following 5 values:
   * * 'decimal' : {@link decimal}
   * * 'hexString' : {@link hexString}
   * * 'binaryString' : {@link binaryString}
   * * 'binaryArray' : {@link binaryArray}
   * * 'error' : The data type could not be detected OR the conversion failed.
   * @type {string}
   */
  #_type;

  /**
   * The binary value that all internal methods will be run from.
   * @type {binaryArray}
   */
  #_value;

  /**
   * The minimum bit resolution for all internal binary objects.
   * @type {number}
   */
  #resolution = 4;


  //////////////////
  // External API //
  //////////////////

  /**
   * Creates a new BinaryData object
   * @param {decimal|hexString|binaryString|binaryArray} data
   * @param {number} precision
   */
  constructor(data, precision) {
    this.set(data, precision);
  }


  /**
   * TBA
   * @param {decimal|hexString|binaryString|binaryArray} data
   * @param {number} [precision]
   * @returns {boolean}
   */
  set(data, precision) {
    this.#_raw = Array.isArray(data) ? data.slice() : data;
    this.#_value = [];

    if (typeof data === 'number') {

      // Convert a decimal number
      this.#_type = 'decimal';
      this.#_value = this.#decimalToBinary(data);

      if (!this.#_value) {
        // Error
        console.error(`Invalid decimal number (input: ${this.#_raw})`);
        this.#_type = 'error';
        return false;
      }

    } else if (typeof data === 'string') {
      // Trim the string so that it doesn't effect the transform
      data = data.trim();

      if (data.startsWith(BinaryData.#hexPrefix)) {

        // Convert a hexadecimal string, beginging with '0x'
        this.#_type = 'hexString';
        this.#_value = this.#hexStringToBinary(data);

        if (!this.#_value) {
          // Error
          console.error(`Invalid hexadecimal string (input: ${this.#_raw})`);
          this.#_type = 'error';
          return false;
        }

      } else {

        // Convert a string of 1's and 0's
        this.#_type = 'binaryString';
        this.#_value = this.#binaryStringToBinary(data);

        if (!this.#_value) {
          // Error
          console.error(`Invalid binary string (input: ${this.#_raw})`);
          this.#_type = 'error';
          return false;
        }

      }
    } else if (Array.isArray(data)) {

      // Convert an array of 0's and 1's
      this.#_type = 'binaryArray';
      this.#_value = this.#binaryArrayToBinary(data);

      if (!this.#_value) {
        // Error
        console.error(`Invalid binary array (input: ${this.#_raw})`);
        this.#_type = 'error';
        return false;
      }

    } else {

      // Error
      console.error(`Invalid data type, must be a 'number', 'string' or 'array' (type: ${typeof data})`);
      this.#_type = 'error';
      return false;

    }

    // Increase the precision of the provided binary data value, unless the value already required a higher
    // precision to be stored (up to the max precision).
    if (precision && (typeof precision === 'number')) {
      precision = Math.max(0, Math.min(BinaryData.#maxPrecision, Math.ceil(precision / this.#resolution) * this.#resolution));
      if (precision > this.getPrecision()) {
        this.leftAdd(Array(precision - this.getPrecision()).fill(0));
      }
    } 

    return true;
  }


  /**
   * Returns the bit length of the value stored
   * @returns {number}
   */
  getPrecision() {
    if (this.#_type !== 'error') {
      return this.#_value.length;
    }
    return 0;
  }


  /**
   * Returns the bit length of the value stored
   * @deprecated Since version 1.1.0
   * @returns {number}
   */
  getLength() {
    this.getPrecision();
  }


  /**
   * Returns the raw data that was passed in when the instance was created OR when the `set` method was called.
   * @returns {decimal|hexString|binaryString|binaryArray}
   */
  getRaw() {
    return this.#_raw;
  }


  /**
   * Returns the internal detection routine used when converting input data, represented as a string.
   * It can be one of the following 5 values:
   * * 'decimal' : {@link decimal}
   * * 'hexString' : {@link hexString}
   * * 'binaryString' : {@link binaryString}
   * * 'binaryArray' : {@link binaryArray}
   * * 'error' : The data type could not be detected OR the conversion failed.
   * @returns {string}
   */
  getConversionType() {
    return this.#_type;
  }


  /**
   * Returns the binaryString reprsentation of the data stored within the BinaryData instance.
   * @returns {binaryString|undefined}
   */
  getString() {
    if (this.#_type !== 'error') {
      return this.#_value.join('');
    }
    return undefined;
  }

  /**
   * Returns the binaryString reprsentation of the data stored within the BinaryData instance.
   * @returns {binaryString|undefined}
   */
  get() {
    if (this.#_type !== 'error') {
      return this.#_value.join('');
    }
    return undefined;
  }


  /**
   * Returns a duplicated version of the internal binaryArray.
   * @returns {binaryArray|undefined}
   */
  getArray() {
    if (this.#_type !== 'error') {
      return this.#_value.slice();
    }
    return undefined;
  }


  /**
   * Returns the hexString reprsentation of the data stored within the BinaryData instance.
   * This can be with or without the hexadecimal string prefix of `0x`.
   * @param {boolean} [removePrefix=false]
   * @returns {hexString|undefined}
   */
  getHex(removePrefix) {
    if (this.#_type !== 'error') {
      let hex = removePrefix ? '' : BinaryData.#hexPrefix;
      let binaryArray = this.getArray();

      while (binaryArray.length) {
        hex += this.#getWordFromBinary(binaryArray.splice(0, 4).join('')).hex;
      }

      return hex;
    }
    return undefined;
  }


  /**
   * Returns the decimal value of the data stored within the BinaryData instance.
   * @returns {decimal|undefined}
   */
  getDecimal() {
    if (this.#_type !== 'error') {
      return this.#getDecimalInternal(this.#_value);
    }
    return undefined;
  }
  

  ////////////////////////
  // Bitwise Operations //
  ////////////////////////

  // TODO: Needs limits to fix operations to the bit depth in use (currently swaps to the
  // system depth due to using the core ECMA Script Bitwise operators

  /**
   * AND method returns a 1 in each bit position for which the corresponding bits (e.g. from the 
   * input data) of both are 1s.
   * @param {decimal|hexString|binaryString|binaryArray} data
   * @returns {string|undefined}
   */
  and(data) {
    if (this.#_type !== 'error') {
      this.set(this.#convertToDecimal(data) & this.getDecimal());
      return this.get();
    }
    return undefined;
  }


  /**
   * NOT method inverts the bits wihtin the internal binaryArray.
   * @returns {string|undefined}
   */
  not() {
    if (this.#_type !== 'error') {
      let bitValues = this.getArray();
      bitValues.forEach((value, index) => {
        bitValues[index] = value ? 0 : 1;
      });
      this.set(bitValues);
      return this.get();
    }
    return undefined;
  }


  /**
   * OR method returns a 1 in each bit position for which the corresponding bits (e.g. from the 
   * input data) of either or both are 1s.
   * @param {decimal|hexString|binaryString|binaryArray} data
   * @returns {string|undefined}
   */
  or(data) {
    if (this.#_type !== 'error') {
      this.set(this.#convertToDecimal(data) | this.getDecimal());
      return this.get();
    }
    return undefined;
  }


  /**
   * XOR method returns a 1 in each bit position for which the corresponding bits (e.g. from the 
   * input data) of either but not both are 1s.
   * @param {decimal|hexString|binaryString|binaryArray} data
   * @returns {string|undefined}
   */
  xor(data) {
    if (this.#_type !== 'error') {
      this.set(this.#convertToDecimal(data) ^ this.getDecimal());
      return this.get();
    }
    return undefined;
  }


  //////////////////////////
  // Binary Modifications //
  //////////////////////////

  /**
   * Manually replace the value of a single bit within the existing internal binaryArray at an 
   * index from `0` to `getPrecision() - 1`.
   * @param {number} index
   * @param {boolean} value 
   * @returns {string|undefined}
   */
  setBit(index, value) {
    if (typeof index === 'number') {
      if (typeof value === 'boolean') {
        if ((value >= 0) && (value < this.getPrecision())) {
          this.#_value[this.getPrecision() - (index + 1)] = value ? 1 : 0;
          return this.get();
        }
        console.error('Index out of range.');
      } else {
        console.error('Invalid value type, must be a boolean.');
      }
    } else {
      console.error('Invalid index type, must be a number.');
    }
    return undefined;
  }


  /**
   * Add bit values to the left of the binary data.
   * @param {decimal|hexString|binaryString|binaryArray} data 
   * @returns {string|undefined}
   */
  leftAdd(data) {
    let tempData = new BinaryData(data);
    if (tempData.getConversionType !== 'error') {
      this.#_value.unshift(...tempData.getArray());
      return this.get();
    }
    return undefined;
  }


  /**
   * Add bit values to the right of the binary data.
   * @param {decimal|hexString|binaryString|binaryArray} data 
   * @returns {string|undefined}
   */
  rightAdd(data) {
    let tempData = new BinaryData(data);
    if (tempData.getConversionType !== 'error') {
      this.#_value.push(...tempData.getArray());
      return this.get();
    }
    return undefined;
  }


  /**
   * Shifts the internal binary data a specified number of bits to the left, within the precision
   * of the BinaryData instance. Excess bits shifted off to the left are discarded. Zero bits 
   * are shifted in from the right.
   * @param {number} offset
   * @returns {string|undefined}
   */
  leftShift(offset) {
    if (typeof offset === 'number') {
      offset = Math.max(0, Math.min(BinaryData.#maxPrecision, offset));
      this.#_value.push(...Array(offset).fill(0));
      this.#_value.splice(0, offset);
      return this.get();
    }
    console.error('Invalid shift balue, must be number.');
    return undefined;
  }


  /**
   * Shifts the internal binary data a specified number of bits to the right, within the precision
   * of the BinaryData instance. Excess bits shifted off to the right are discarded. Zero bits 
   * are shifted in from the left.
   * @param {number} offset 
   * @returns {string|undefined}
   */
  rightShift(offset) {
    if (typeof offset === 'number') {
      offset = Math.max(0, Math.min(BinaryData.#maxPrecision, offset));
      this.#_value.unshift(...Array(offset).fill(0));
      this.#_value.splice(-offset, offset);
      return this.get();
    }
    console.error('Invalid shift balue, must be number.');
    return undefined;
  }


  /////////////////////////////////
  // Internal binary conversions //
  /////////////////////////////////

  /**
   * TBA
   * @private
   * @param {string} binary 
   * @returns {word}
   */
  #getWordFromBinary(binary) {
    // TODO: Add support for partial words (e.g. less than 4 bits)
    return BinaryData.#wordTable.find(cell => binary === cell.binary) || BinaryData.#wordTable[0];
  }

  /**
   * TBA
   * @private
   * @param {string} hex 
   * @returns {word}
   */
  #getWordFromHex(hex) {
    return BinaryData.#wordTable.find(cell => hex === cell.hex) || BinaryData.#wordTable[0];
  }

  /**
   * TBA
   * @private
   * @param {decimal} data 
   * @returns {binaryArray}
   */
  #decimalToBinary(data) {
    let realBits = 0;
    let precisionBits = 0;
    let bitValues = [];

    // Find largest bit value
    for (let index = 0; index < BinaryData.#bitTable.length; index++) {
      if ((data >= BinaryData.#bitTable[index]) && (data < BinaryData.#bitTable[index + 1])) {
        realBits = index;
        break;
      }
    }

    // Convert TRUE bits to 1's and FALSE to 0's and append to bit array
    while (realBits >= 0) {
      let bitValue = BinaryData.#bitTable[realBits];
      if (data >= bitValue) {
        data -= bitValue;
        bitValues.push(1);
      } else {
        bitValues.push(0);
      }
      realBits--;
    }

    // Pad the bit array to the required precision
    precisionBits = Math.ceil(bitValues.length / this.#resolution) * this.#resolution;
    while (bitValues.length < precisionBits) {
      bitValues.unshift(0);
    }

    return bitValues;
  }

  /**
   * TBA
   * @private
   * @param {hexString} data 
   * @returns {binaryArray}
   */
  #hexStringToBinary(data) {
    let success = true;
    let bitValues = [];

    // Convert to uppercase to prevent issues going forward
    data = data.toUpperCase();

    // Check for valid hex values, find relevant word object, then create bit array from the binaryString split
    data.substring(2, data.length).split('').forEach((str) => {
      if (BinaryData.#validHexChars.includes(str)) {
        this.#getWordFromHex(str).binary.split('').forEach((value) => {
          bitValues.push(parseInt(value, 2));
        });
      } else {
        success = false;
      }
    });

    if (success) { return bitValues; }
    return false;
  }

  /**
   * TBA
   * @private
   * @param {binaryString} data 
   * @returns {binaryArray}
   */
  #binaryStringToBinary(data) {
    let success = true;
    let realBits = 0;
    let precisionBits = 0;
    let bitValues = [];

    realBits = data.length;

    // Pad the string to the required precision, then create bit array from the split
    precisionBits = Math.ceil(realBits / this.#resolution) * this.#resolution;
    data.padStart(precisionBits, '0').split('').forEach((str) => {
      if (BinaryData.#validBinaryChars.includes(str)) {
        bitValues.push(parseInt(str, 2));
      } else {
        success = false;
      }
    });

    if (success) { return bitValues; }
    return false;
  }

  /**
   * TBA
   * @private
   * @param {binaryArray} data 
   * @returns {binaryArray}
   */
  #binaryArrayToBinary(data) {
    let success = true;
    let realBits = 0;
    let precisionBits = 0;
    let bitValues = [];

    realBits = data.length;

    // Pad the incomming array to the required precision
    precisionBits = Math.ceil(realBits / this.#resolution) * this.#resolution;
    while (data.length < precisionBits) {
      data.unshift(0);
    }

    // Create bit array from data, checking f0r 1's and 0's
    data.forEach((value) => {
      if (value === 1 || value === 0) {
        bitValues.push(value);
      } else {
        success = false;
      }
    });

    if (success) { return bitValues; }
    return false;
  }

  //////////////////////////////////
  // Internal decimal conversions //
  //////////////////////////////////

  /**
   * TBA
   * @private
   * @param {binaryArray} data 
   * @returns {decimal}
   */
  #getDecimalInternal(data) {
    let decimal = 0;

    data.slice().reverse().forEach((value, bit) => {
      if (value) { decimal += Math.pow(2, bit); }
    });

    return decimal;
  }

  /**
   * TBA
   * @private
   * @param {decimal|binaryArray|binaryString} data 
   * @returns {decimal}
   */
  #convertToDecimal(data) {
    if (typeof data === 'number') {

      // Return the raw decimal number
      return data;

    } else if (typeof data === 'string') {

      // Convert a string of 1's and 0's
      return this.#getDecimalInternal(this.#binaryStringToBinary(data));

    } else if (Array.isArray(data)) {

      // Convert an array of 0's and 1's
      return this.#getDecimalInternal(this.#binaryArrayToBinary(data));

    }

    return 0;
  }
}


export { BinaryData as default };