Skip to content

Instantly share code, notes, and snippets.

@GregWWalters
Created May 10, 2023 19:44
Show Gist options
  • Save GregWWalters/ecf4cd74f049519f576ec748d821cbda to your computer and use it in GitHub Desktop.
Save GregWWalters/ecf4cd74f049519f576ec748d821cbda to your computer and use it in GitHub Desktop.
BitFlag generator class in JS
/* eslint-disable no-bitwise,no-plusplus,no-underscore-dangle */
/* eslint-disable no-func-assign,no-return-assign */
const util = require('../authorization/util');
// TODO: move utils
const MAX_BIT = Math.log2(Number.MAX_SAFE_INTEGER);
/**
* Get the value of the next bit in the series and increment the next bit
* value. If the maximum safe bit has already been claimed, it will throw
* a RangeError.
* @memberOf BitFlag
* @returns {Integer} bit
*/
const ZERO_BIT = 0 >>> 0;
/**
* Creates and returns a new BitFlag constructor
* @returns {Constructor:BitFlag} constructor
*/
function newBitFlagSeries() {
// force _bit to be an unsigned integer
let _bit = ZERO_BIT;
const _generated = [];
function claimBit() {
// claimBit will return 1 the first time, then increment every time
// thereafter.
// eslint-disable-next-line no-shadow
claimBit = function claimBit() {
if (_bit >= MAX_BIT) {
throw new RangeError(
'maximum number of BitFlags exceeded for this generator'
);
}
return (_bit <<= 1);
};
return (_bit = 1 >>> 0);
}
/**
* If called as a constructor with the `new` operator, create a BitFlag for
* the next bit in the BitFlag series. If called as a regular function,
* convert an integer to a BitFlag instance.
* @constructor
* @property {String} label
* @property {Integer} value
* @function
* @param {...unknown} args
* @param {?String} args[0] - label
* @param {?Integer} args[0] - value
*/
function BitFlag(...args) {
// if called as a converter
if (!new.target) return BitFlag.from(...args);
// if called as constructor with `new`
const [label = '', value = undefined] = args;
if (!label || !util.isStringLike(label))
throw new SyntaxError('must provide a label to BitFlag constructor');
if (util.isSet(value))
throw new SyntaxError(
'cannot specify BitFlag value when calling with new.'
);
Object.defineProperties(this, {
constructor: { value: BitFlag },
bit: { value: claimBit() },
label: { value: String(label) }
});
_generated.push(this);
return this;
}
Object.defineProperties(BitFlag, {
/**
* Defines the maximum allowed value for a flag
* @memberOf BitFlag
*/
MAX_BIT: { value: MAX_BIT, enumerable: true },
/**
* The number of BitFlags generated
* @memberOf BitFlag
* @type {Integer} count
*/
length: {
get() {
return _generated.length;
},
enumerable: true
},
/**
* Returns the greatest bit value assigned
* @memberOf BitFlag
* @type {Integer} bit
*/
greatestBit: {
get() {
return _bit;
},
enumerable: true
},
/**
* the array of generated flags
* @memberOf BitFlag
* @type {BitFlag[]} flags
*/
flags: {
get() {
return [..._generated];
},
enumerable: true
},
parseFromString: {
value: function parseFromString(str) {
if (str?.constructor !== String) {
throw new TypeError('argument must be a String');
}
// TODO
},
enumerable: false
},
/**
* Creates a new BitFlag with the specified properties, skipping the
* generator and automatically incremented value. You probably shouldn't
* be using it.
* @memberOf BitFlag
* @function
* @param {(String|Number|Object)} object - value to convert from
* @param {?String} object.label - optional for object literals
* @param {?Number} object.value - required for object literals
*/
from: {
value: function from(...args) {
let object;
if (args.length > 1) {
const [label, bit] = args;
object = { label, bit };
} else if (args.length > 0) {
[object] = args;
}
const { constructor: ctor, label, bit } = object ?? {};
switch (true) {
case ctor === BitFlag:
return object;
case ctor === String || object instanceof String:
return new BitFlag(object);
case ctor === Number || object instanceof Number:
if (!Number.isSafeInteger(object))
throw new TypeError(
`BitFlag value must be an integer less than ${MAX_BIT} (${MAX_BIT.toString(
2
)})`
);
return Object.create(BitFlag.prototype, {
constructor: { value: BitFlag },
label: { value: '', enumerable: true },
bit: { value: object >>> 0, enumerable: true }
});
case ctor instanceof Object:
if (!Number.isSafeInteger(bit))
throw new TypeError(
'must provide a safe integer value when converting to BitFlag'
);
if (util.isSet(label) && !util.isStringLike(label))
throw new TypeError('label must be a String if provided');
return Object.create(BitFlag.prototype, {
constructor: { value: BitFlag },
label: {
value: label ? String(label) : '',
enumerable: true
},
bit: { value: Number(bit) >>> 0 }
});
default:
throw new TypeError(
`can't create a BitFlag from ${util.classOf(object)}`
);
}
},
enumerable: true
},
/**
* returns whether all the required flags are set on the test BitFlag
* @memberof bitflag
* @function
* @param {(BitFlag|Integer)} require - flags to require
* @param {BitFlag} test - BiFlag value to test
* @returns {boolean}
*/
all: {
value: function all(require, test) {
return (require & test) === require;
},
enumerable: true
},
/**
* returns whether any of the required flags are set on the test BitFlag
* @memberof bitflag
* @function
* @param {(BitFlag|Integer)} require - flags to require
* @param {BitFlag} test - BiFlag value to test
* @returns {boolean}
*/
any: {
value: function any(require, test) {
return (test & require) > 0;
},
enumerable: true
},
/**
* Combines the BitFlags into a single value (a logical and) that can be used
* for simpler testing
* @memberof bitflag
* @function
* @param {BitFlag[]} flags - the BitFlags to combine
* @returns {BitFlag} - the combined value as a BitFlag
*/
combine: {
value: function combine(...flags) {
return BitFlag(
flags.reduce(
(combined, current) => ({
label:
combined.label && current.label
? `${combined.label}+${current.label}`
: combined.label || current.label || '',
bit: combined.bit | current.bit
}),
{ bit: 0 >>> 0 }
)
);
},
enumerable: true
}
});
Object.defineProperties(BitFlag.prototype, {
constructor: { value: BitFlag },
/**
* @memberOf BitFlag#
* @type {Integer} bit
*/
bit: {
value: 0,
enumerable: true
},
/**
* @memberOf BitFlag#
* @type {String} label
*/
label: {
value: '<label>',
enumerable: true
},
/**
* @memberOf BitFlag#
* @type {Integer} bit
* @function
*/
valueOf: {
value: function valueOf() {
return this.bit;
}
},
/**
* @memberOf BitFlag#
* @type {String}
* @function
*/
toString: {
value: function toString() {
return `${this.label || '[BitFlag]'}(${this.bit.toString(2)})`;
}
},
/**
* return the logical "and" of this BitFlag and all BitFlags passed as
* arguments
* @memberOf BitFlag#
* @function
* @param {...BitFlag} bitFlag
* @returns {BitFlag} combined
*/
and: {
value: function and(...bitFlag) {
return BitFlag.from({
label: [this.label, ...bitFlag].join('+'),
value: bitFlag.reduce(
(previous, current) => previous | current.bit,
this.bit
)
});
}
},
/**
* returns whether this BitFlag has all the bits set of the BitFlags
* passed as arguments
* @memberOf BitFlag#
* @function
* @param {...BitFlag} bitFlag
* @returns {Boolean}
*/
hasAll: {
value: function hasAll(...bitFlag) {
const require = bitFlag.reduce(
(previous, current) => previous | current.bit,
0 >>> 0
);
return BitFlag.all(require, this);
},
enumerable: true
},
/**
* returns whether this BitFlag has any of the bits set of the BitFlags
* passed as arguments
* @memberOf BitFlag#
* @function
* @param {...BitFlag} bitFlag
* @returns {Boolean}
*/
hasAny: {
value: function hasAll(...bitFlag) {
const require = bitFlag.reduce(
(previous, current) => previous | current.bit,
0 >>> 0
);
return BitFlag.any(require, this);
},
enumerable: true
}
});
return BitFlag;
}
module.exports = newBitFlagSeries;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment