Состояние в битовых картах


В повседневном коде с недесятичными форматами чисел сталкиваться приходится редко. Но есть кейс, когда двоичный формат приходится в тему — это битовые карты. То есть можно решить, что определённое число — это не просто число, а обозначение какого-то состояния. Например, у нас есть три слота, они могут быть заполнены или нет. Тогда битовые карты будут выглядеть так:

// все три слота свободны
000;
// первый слот занят, остальные свободны
100;
// первый и третий слоты заняты, второй свободен
101;

Ок, идём дальше. То, что состояния теперь заданы в виде двоичных чисел, даёт возможность применять к ним двоичные операции! Например, это могут быть двоичные И и ИЛИ. Обозначаются они операторами & и |. Двоичные числа сравниваются побитово (первый бит одного числа с первым битом другого, второй бит одного числа со вторым битом другого…).

В случае логического И оба бита должны быть равными 1, чтобы в результате получить 1, в остальных случаях будет 0:

0 & (0 === 0);
0 & (1 === 0);
1 & (0 === 0);
1 & (1 === 1);

В случае логического ИЛИ любой из битов может быть равен 1, чтобы получить в результате 1:

0 | (0 === 0);
0 | (1 === 1);
1 | (0 === 1);
1 | (1 === 1);

И теперь, если вернуться к примеру со слотами, мы можем «складывать» два состояния с помощью побитового ИЛИ: 100 | 001 === 101 (первый занятый слот ИЛИ третий занятый слот — это занятые 1 и 3 слот). А из «суммы» двух состояний можно убрать все состояния, кроме определённого, с помощью побитового И: 101 & 100 === 100 (первый с третьим занятые слоты И первый занятый слот — это первый занятый слот).

Теперь к юзкейсу. Вся мякотка маппинга состояний на битовые карты раскрывается в кейсе, когда побитовое ИЛИ используется для создания композиции нескольких отдельных состояний. Представьте, что у нас есть некие состояния:

const MOBILE = 0b0001;
const TABLET = 0b0010;
const LAPTOP = 0b0100;
const DESKTOP = 0b1000;

Можно использовать как просто отдельное состояние, например, MOBILE, так и композицию нескольких состояний, к примеру, MOBILE | LAPTOP (что буквально так и считывается мобайл ИЛИ лаптоп, спасибо TS👋).

Это можно использовать для задания динамических ключей объектов:

const objectMap = {
[MOBILE]: "mobile stuff",
[LAPTOP]: "laptop stuff",
[MOBILE | LAPTOP]: "mobile or laptop stuff",
};

Как вы уже, возможно, догадались, распаковать этот ключ можно с помощью логического И (&), обратной операции. Предположим, что нужно отфильтровать только те ключи, в которых есть MOBILE, то есть нужно ко всем ключам применить & MOBILE:

MOBILE & (MOBILE === MOBILE);
// у 0b0001 и 0b0001 есть общая 1, получаем 0b0001
LAPTOP &
(MOBILE ===
0(
// у 0b0100 и 0b0001 нет общей 1, получаем 0b0000
MOBILE | LAPTOP
)) &
(MOBILE === MOBILE);
// у 0b0101 и 0b0001 есть общая 1, получаем 0b0001

Дальше отфильтровываем те значения, ключи которых выдали нули и готово!

Вот как выглядит функция-фильтровщик:

function getFilteredValues(objectMap, filterKey) {
return Object.entries(objectMap)
.map(([key, value]) => {
const shouldInclude =
(key & MOBILE) === filterKey ||
(key & TABLET) === filterKey ||
(key & LAPTOP) === filterKey ||
(key & DESKTOP) === filterKey;
return shouldInclude ? value : null;
})
.filter(Boolean);
}

Полный код примера тут https://codepen.io/juwain/pen/KKEGRrB?editors=0011

Кстати, небольшой оффтоп: обратите внимание на ссылку. Codepen тоже использует битовую карту, чтобы обозначить, какие редакторы по умолчанию скрыть (0), а какие показать (1) 👾

Я использовал такой подход для динамического создания стилей для каждого типа девайса. Иногда стили для соседних девайсов повторялись, не хотелось их дублировать и получилось сгруппировать по типу MOBILE | LAPTOP.

Пример (это Styled Components):

${setResponsiveStyles({
[MOBILE]: css`
// some only mobile styles
`,
[MOBILE | TABLET]: css`
// some mobile or tablet styles
`,
[LAPTOP]: css`
// some only laptop styles
`,
[DESKTOP]: css`
// some only desktop styles
`
})}