🎼

TypeScriptを使用したビットフラグでの簡単な状態管理メモ

2024/09/09に公開


TypeScript(JavaScript)でビットフラグを用いた簡単な状態管理というか状態の重ね合わせを実装したのでその備忘メモです。

前提

  • 状態は正の整数で表現し、指定も正の整数で行う
  • 状態は数が大きい方が優先度が高いことにする
  • 状態は簡単化のためnumber型を用いて表現する
  • JavaScriptのビット演算仕様により31通りの状態表現が上限
    • ビット演算時に強制的にsigned 32bit integerに変換される模様
  • 状態の実体はRecord型であれば何でも可能 (例ではRecord<string, number>)
  • 状態の重ね合わせはRecord型のプロパティ上書きで表現する

実装

code
type State = Record<string, number>;
type EachState = Record<number, State>;

class CascadeState {
  static ENABLE_ALL = 2147483647  // Math.pow(2, 31) - 1
  #eachState: EachState = {};

  constructor(eachState: EachState) {
    for (const [key, state] of Object.entries(eachState)) {
      const status = Number.parseInt(key);
      if (CascadeState.isPositive31BitInteger(status) && CascadeState.isPowerOfTwo(status)) {
        this.#eachState[status] = state;
      }
    }
  }

  getCascadeState(num: number, mask?: number): State {
    if (!CascadeState.isPositive31BitInteger(mask)) { mask = CascadeState.ENABLE_ALL; }
    return this.#layerState(num & mask!);
  }
  #layerState(num: number): State {
    let layered: State = {};
    if (!CascadeState.isPositive31BitInteger(num)) { return layered; }

    const maxStatus = CascadeState.#getStatusMax(this.#eachState);
    const max = maxStatus < num ? maxStatus : CascadeState.getMSBNumber(num);
    for (let bit=1; bit<=max; bit<<=1) {
      if (num & bit) {
        layered = { ...layered, ...(this.#eachState[bit] ?? {}) };
      }
    }
    return layered;
  }

  static isPositive31BitInteger(num?: number): boolean {
    if (!Number.isInteger(num)) { return false; }
    if (num! <= 0 || num! > CascadeState.ENABLE_ALL) { return false; }
    return true;
  }
  static isPowerOfTwo(num: number): boolean {
    if (num <= 0) { return false; }
    return (num & (num - 1)) === 0;
  }
  static extractPowersOfTwo(num: number): number[] {
    const ary: number[] = [];
    if (!CascadeState.isPositive31BitInteger(num)) { return ary; }
    for (let bit=1; bit<=CascadeState.getMSBNumber(num); bit<<=1) {
      if (num & bit) { ary.push(bit); }
    }
    return ary;
  }
  static getMSBNumber(num: number): number {  // get Most Significant Bit Number
    if (!CascadeState.isPositive31BitInteger(num)) { return 0; }
    return 1 << (31 - Math.clz32(num));
  }
  static #getStatusMax(eachState: EachState): number {
    return Object.keys(eachState).map(x => Number.parseInt(x)).reduce((acc, x) => acc>x ? acc : x, 0);
  }
  static getBinaryString(num: number): string {
    return num.toString(2);
  }
}

使い方

重ね合わせる状態を定義する

const STATUS = {
  PRIORITY1: 0b0001,
  PRIORITY2: 0b0010,
  PRIORITY3: 0b0100,
  PRIORITY4: 0b1000,
} as const;

const state = new CascadeState(
  {
    [STATUS.PRIORITY1]: {a: 1, b: 1, c: 1, d: 1},
    [STATUS.PRIORITY2]: {a: 2,       c: 2,       e: 2},
    [STATUS.PRIORITY3]: {a: 3,             d: 3},
    [STATUS.PRIORITY4]: {a: 4},
  }
);

重ね合わせ状態を取得する

単純に取得する

console.log(state.getCascadeState(0b1111));
// {
//   "a": 4,
//   "b": 1,
//   "c": 2,
//   "d": 3,
//   "e": 2
// }
console.log(state.getCascadeState(0b1010));
// {
//   "a": 4,
//   "c": 2,
//   "e": 2
// }

適用する状態を制限して取得する

console.log(state.getCascadeState(0b1111, STATUS.PRIORITY2 | STATUS.PRIORITY3));
// {
//   "a": 3,
//   "c": 2,
//   "e": 2,
//   "d": 3
// }
console.log(state.getCascadeState(0b1010, STATUS.PRIORITY2 | STATUS.PRIORITY3));
// {
//   "a": 2,
//   "c": 2,
//   "e": 2
// }

簡単な解説

  static ENABLE_ALL = 2147483647  // Math.pow(2, 31) - 1
  • ENABLE_ALLは2進数で31桁が1の数値
    console.log(CascadeState.getBinaryString(CascadeState.ENABLE_ALL));
    // "1111111111111111111111111111111"
    

  static isPositive31BitInteger(num?: number): boolean {
    if (!Number.isInteger(num)) { return false; }
    if (num! <= 0 || num! > CascadeState.ENABLE_ALL) { return false; }
    return true;
  }
  • 符号なし31ビット整数上限超過数は取り扱わない

  static extractPowersOfTwo(num: number): number[] {
    const ary: number[] = [];
    if (!CascadeState.isPositive31BitInteger(num)) { return ary; }
    for (let bit=1; bit<=CascadeState.getMSBNumber(num); bit<<=1) {
      if (num & bit) { ary.push(bit); }
    }
    return ary;
  }
  • ビット列から2のべき乗(EachStateのキー)群を取得する
    CascadeState.extractPowersOfTwo(0b10101).forEach(x => console.log(x.toString(2)));
    // "1"
    // "100"
    // "10000"
    

  static getMSBNumber(num: number): number {  // get Most Significant Bit Number
    if (!CascadeState.isPositive31BitInteger(num)) { return 0; }
    return 1 << (31 - Math.clz32(num));
  }
  • 最上位ビットだけが1の数値を取得する
    console.log(CascadeState.getMSBNumber(0b10101).toString(2));
    // "10000"
    

雑記

コンポーネントを作成する際に状態によって適用するCSSを切り替えたかったので作成したのですが、結局この通りには使わず簡略化して使用しています。状態は頻繁に切り替わることが想定され、都度重ね合わせ計算を実行するのはコストに見合わない気がした事と、各状態を全て重ね合わせなくとも標準状態と各種状態の2種類を組み合わせるだけで十分な気がしたからです。今後何かしらの時にこういう考え方と実装は役に立ちそうだったので備忘記事にしました。しかしこういうことをする時はnumberが無条件で8byte使用するのは少々ヘビーだと感じてしまいますね。

参考文献

Discussion