baja/obj/CategoryMask.js

/**
 * @copyright 2023 Tridium, Inc. All Rights Reserved.
 */

/**
 * Defines {@link baja.CategoryMask}.
 * @private
 * @module baja/obj/CategoryMask
 */
define([
  'bajaScript/baja/obj/Simple' ], function (
  Simple
) {

  'use strict';

  /**
   * @class
   * @alias module:baja/obj/CategoryMask
   * @private
   * @extends baja.Simple
   * @since Niagara 4.14
   */
  class CategoryMask extends Simple {
    /**
     * @param {string} hex the hex encoding for this mask
     */
    constructor(hex) {
      super();
      validateHex(hex);
      this.$hex = hex;
    }

    /**
     * @param {number} categoryIndex
     * @returns {boolean} true if the bit for that category index is set
     */
    get(categoryIndex) {
      const hex = this.$hex;
      if (hex === '*') { return true; }

      const bitIndex = categoryIndex - 1; // category 1 lives in bit 0
      const bitInChar = bitIndex % 4; // each char holds 4 bits
      const charIndex = Math.floor(bitIndex / 4);
      const hexChar = hex[hex.length - 1 - charIndex];
      return !!(parseInt(hexChar, 16) & (1 << bitInChar));
    }

    /**
     * @returns {number} the number of categories represented by this mask
     */
    size() {
      const hex = this.$hex;
      const largestChar = parseInt(hex[0], 16);
      if (!largestChar) { return 0; }

      const rest = (hex.length - 1) * 4;
      if (largestChar & 8) { return 4 + rest; }
      if (largestChar & 4) { return 3 + rest; }
      if (largestChar & 2) { return 2 + rest; }
      return 1 + rest;
    }

    /**
     * @returns {boolean} true if this is the null/default instance
     */
    isNull() {
      return this === CategoryMask.DEFAULT;
    }

    /**
     * @param {string|number[]} hex the hex string, or an array of category indices (1-indexed)
     * @returns {module:baja/obj/CategoryMask}
     */
    static make(hex) {
      if (Array.isArray(hex)) {
        let bytes = [];
        hex.forEach((categoryIndex) => {
          categoryIndex--; // 1-based to 0-based
          const byteIndex = Math.floor(categoryIndex / 4);
          bytes[byteIndex] = (bytes[byteIndex] || 0) | 1 << categoryIndex % 4;
        });
        for (let i = 0, len = bytes.length; i < len; ++i) {
          bytes[i] = bytes[i] || 0;
        }
        const hexString = bytes.reverse().map((b) => b.toString(16)).join('');
        return CategoryMask.make(hexString);
      }
      return hex ? new CategoryMask(hex) : CategoryMask.DEFAULT;
    }

    /**
     * @param {string|number[]} hex the hex string, or an array of category indices (1-indexed)
     * @returns {module:baja/obj/CategoryMask}
     */
    make(hex) {
      return CategoryMask.make(hex);
    }

    /**
     * @param {string} str
     * @returns {module:baja/obj/CategoryMask}
     */
    decodeFromString(str) {
      return this.make(str);
    }

    /**
     * @returns {string}
     */
    encodeToString() {
      return this.$hex;
    }
  }

  CategoryMask.DEFAULT = new CategoryMask('');

  /** @param {string} hex */
  function validateHex(hex) {
    if (hex === '*') { return; }
    if (hex[0] === '0') { throw new Error('No leading zero allowed'); }
    for (let i = 0, len = hex.length; i < len; ++i) {
      const char = hex[i];
      if (!char.match(/[0-9a-f]/)) { throw new Error('Invalid char ' + char); }
    }
  }

  return CategoryMask;
});