Al programar estamos acostumbrados a realizar distintos tipos de operaciones. Quizá las más comunes sean operaciones matemáticas, manipulación de strings y comparaciones.

Existe un conjunto de operadores que sirven para trabajar con datos a nivel de bit, o a nivel binario. En sistemas electrónicos estos son muy comunes, pero en aplicaciones web tal vez no tanto en comparación con el resto de operadores. Creo que en parte se debe a que en cualquier lenguaje se explica la función de estos operadores, pero no se dan ejemplos de su uso.

En este post se exponen un par de casos de uso con operadores a nivel de bit. Trataré de explicar cada uno lo mejor que pueda.


Manejo de permisos

Manejar permisos en una aplicación no es algo nuevo, ni algo exclusivo de los operadores bit a bit. Una forma común de implementar esta funcionalidad es almacenar un listado de permisos disponibles en una base de datos y relacionarlos con los usuarios de la aplicación para determinar quien tiene permisos sobre cuales recursos: la o las tablas que representan los permisos contienen un registro por cada permiso disponible.

Con operadores bit a bit podemos almacenar distintos permisos en un solo valor decimal, aprovechando que un valor decimal también puede representarse como un número binario. De esta manera, cada uno de los bits o dígitos binarios que componen un número puede representar un permiso.

Para tratar de simplificar el post he utilizado solo seis posiciones en la imagen (hasta 25), pero el número de posiciones máximo depende del sistema operativo sobre el que se ejecute PHP. Por ejemplo, en sistemas de 32 bits un solo valor entero puede representar 32 posiciones/permisos (hasta 231), mientras que en un sistema de 64 bits se puede almacenar 64 posiciones/permisos (hasta 263) en un solo valor entero.

¿Cuáles y qué clase de permisos podemos almacenar de esta manera? Los que deseemos. Utilicemos un ejemplo muy trillado: un blog. Vamos a suponer que deseamos controlar permisos relacionados con posts y comentarios. Podríamos tener los siguientes permisos: crear, editar y eliminar posts; crear, editar y eliminar comentarios.

En términos de variables, podrían quedar de la siguiente manera:

$createPost, $editPost, $deletePost, $createComment, $editComment, $deleteComment

Lo importante es que cada permiso debe ser una potencia de 2 (1, 2, 4...) O en otras palabras, valores que solo tengan un bit con valor 1 (0001, 0010, 0100...) Ya depende de nosotros el valor que asignemos a cada uno. Para fines del post usemos los siguientes valores:

$createPost = 1;
$editPost = 2;
$deletePost = 4;
$createComment = 8;
$editComment = 16;
$deleteComment = 32;

Ya con estos valores asignados y tomando como referencia la imagen anterior, tendríamos que el número entero 37 representa un grupo de permisos en los que están activos los bits correspondientes a las posiciones 1, 3 y 6 (de derecha a izquierda); es decir, están activos los permisos correspondientes a crear post, eliminar post y eliminar comentarios:

// $userPermissions = 1 + 4 + 32
$userPermissions = $createPost + $deletePost  + $deleteComment

El valor 63 de la imagen representaría un grupo de permisos donde todos los bits o permisos están activos:

// $userPermissions = 1 + 2 + 4 + 8 + 16 + 32
$userPermissions = $createPost + $editPost + $deletePost  + $createComment + $editComment + $deleteComment

Podemos ver que para agregar varios permisos simplemente sumamos su valor decimal. Esto es el equivalente a utilizar el primer operador a nivel de bit: OR. Un OR a nivel de bit es similar a un OR booleano. Una diferencia es que la comparación se hace con cada uno de los bits del valor binario; otra diferencia es que el símbolo de OR a nivel de bit es un símbolo pipe |, mientras que el OR booleano son dos símbolos pipe ||.

Por ejemplo, si queremos agregar los permisos para crear y editar posts, tendríamos algo como lo siguiente:

$userPermissions = $createPost  | $editPost;
var_dump($userPermissions); // imprime 3

Podemos ver que es igual a haber sumado los valores de $createPost y $editPost. Pero hay que tener cuidado, esta equivalencia solamente se da cuando utilizamos valores que son potencia de 2 como en este caso. Veamos qué sucede a nivel de bit cuando utilizamos el operador OR a nivel bit.

Primero que nada, cuando se utilizan operadores a nivel de bit con números enteros, PHP convierte los valores a su equivalente binario. Entonces tendríamos que $createPost y $editPost tienen los siguientes valores, respectivamente: 000001 y 000010.

Y si aplicamos un OR para cada bit, tendremos lo siguiente:

000001
000010
------
000011

Se aplica una operación OR por cada bit. Una operación OR da como resultado alguno de los siguientes valores, dependiendo del valor de cada operando (A y B):

primer valor (A) segundo valor (B) resultado (A OR B)
1 0 1
0 1 1
1 1 1
0 0 0

Verificando permisos

Ya sabemos como almacenar varios permisos en una variable usando el operador OR a nivel bit o con una simple suma. Pero ahora, ¿cómo podemos comprobar si un permiso en particular se encuentra en un grupo de permisos? Para este caso utilizaremos el operador a nivel de bit AND.

Supongamos que hemos asignado permisos para crear, editar y eliminar posts a la variable $userPermissions y deseamos saber si contiene el permiso para editar posts. Para ello haríamos lo siguiente:

if ($userPermissions & $editPost) {
    echo "tiene permisos para editar posts";
} else {
    echo "no tiene permisos para editar posts";
}

Procedamos a la explicación. Primero que nada, hay que notar que estamos usando el símbolo & (ampersand). Este es el operador AND a nivel de bit en PHP; el operador AND booleano se representa con dos símbolos ampersand &&.

Al igual que el operador OR a nivel de bit, AND realiza una operación lógica por cada uno de los bits en los operandos, por lo que en nuestro ejemplo tendríamos lo siguiente:

000111
000010
------
000010

El resultado de la operación AND por cada bit, dependerá de los siguientes valores:

primer valor (A) segundo valor (B) resultado (A AND B)
1 0 0
0 1 0
1 1 1
0 0 0

En este caso el resultado es 000010, porque solamente en el segundo bit (de derecha a izquierda) de ambos valores binarios el valor es 1. Y como podemos ver en la tabla anterior, el resultado de la operación AND será 1 solamente cuando se cumpla esa condición.

Ahora probemos con otro ejemplo. Verifiquemos con otro permiso que sabemos que existe en $userPermissions: el permiso para eliminar posts (000100 binario / 4 decimal):

000111
000100
------
000100

El resultado es 000100 porque solamente en el tercer bit (de derecha a izquierda) de ambos operandos el valor es 1. Y como sabemos 1 AND 1 es igual a 1.

Ahora, ¿qué sucede si el permiso a verificar no se encuentra en el grupo de permisos asignados? Probemos con el permiso para crear comentarios (001000 binario / 8 decimal):

000111
001000
------
000000

El valor es resultado es 000000 porque no hay algún bit en ambos operandos donde el valor sea 1.

Aquí se puede notar un patrón al utilizar el operador AND a nivel de bit: cuando el permiso a verificar existe, el resultado es el valor del permiso a verificar; cuando el permiso no existe, el valor es 0:

// el permiso existe, entonces se regresa el valor de $createPost (1)
var_dump($userPermissions & $createPost);

// el permiso existe, entonces se regresa el valor de $editPost (2)
var_dump($userPermissions & $editPost);

// el permiso existe, entonces se regresa el valor de $deletePost (4)
var_dump($userPermissions & $createComments); 

// el permiso no existe, entonces se regresa 0
var_dump($userPermissions & $deletePost); 

Ahora, recordemos que el código de verificación es el siguiente:

if ($userPermissions & $editPost) {
    echo "tiene permisos para editar posts";
} else {
    echo "no tiene permisos para editar posts";
}

Utilizar el resultado de la operación en la condición if es simplemente un atajo que aprovecha la conversión implícita de PHP. Como podemos ver, el resultado de la operación AND a nivel bit es un número entero. Al usar dicho número en una condición, PHP lo convertirá a su equivalente booleano. En el caso de valores enteros, PHP convierte a true cualquier número entero distinto a 0, mientras que un valor igual a 0 se convertirá al valor booleano false. Por lo tanto, cuando el permiso exista, la condición if resultará verdadera, y cuando no, resultará falsa.

Quitando permisos

Ya vimos como asignar y verificar permisos. En esta sección veremos cómo funcionan otros operadores a nivel de bit: XOR y NOT.

XOR no lo utilizaremos en realidad, pero quise incluirlo por la siguiente razón: hay artículos qué sugieren el uso de este operador para desactivar ciertos bits de un valor binario, pero aunque se puede usar con este fin, su funcionamiento es como el de un interruptor de corriente: si está activo, se desactiva; si está desactivado, se activa. Veamos más a detalle cómo se ve este comportamiento a nivel de bits.

El operador XOR es similar a OR, con la diferencia de que devuelve 0 si ambos operandos son iguales. El operador XOR se representa con el símbolo caret ^ o "conito", si tienen 5 años mentales.

primer valor (A) segundo valor (B) resultado (A XOR B)
1 0 1
0 1 1
1 1 0
0 0 0

Ahora supongamos que usamos XOR para remover un permiso. Como en el ejemplo anterior, tendremos una variable llamada $userPermissions que contendrá permisos para crear, editar y eliminar posts (1 + 2 + 4 = 7). Aplicaremos una operación XOR para remover el permiso para editar posts (000010):

000111 
000010
------
000101

Podemos ver que se remueve el segundo bit (de derecha a izquierda) del resultado, es decir, el bit equivalente a editar posts. En este caso se obtuvo el resultado esperado porque 000111 tenía activo el bit que nos interesaba desactivar, ¿pero qué sucede si tratamos de usar XOR para remover un permiso que no se encuentra otorgado?

000101 
000010
------
000111

Como había mencionado, XOR funciona como un interruptor, por lo que en este caso, el segundo bit se activó. Esto no es lo que esperábamos, nuestra intencion era desactivar el bit correspondiente al permiso, se encontrara activo o no.

Existe una forma de lograr el resultado esperado utilizando XOR, pero requiere verificar previamente si el permiso existe:

if ($userPermissions & $editPost) {
    $userPermissions = $userPermissions ^ $editPost;
} 

Esto funciona, pero podemos aprovechar otro operador que nos puede facilitar el trabajo: el operador NOT.

El símbolo de operador NOT a nivel de bit es la tilde ~. Este operador, al igual que el operador NOT booleano, invierte un valor, pero en este caso lo hace bit por bit. Ejemplo:

~ 00000000000000000000000000101001 
-----------------------------------
  11111111111111111111111111010110

No podemos usar solamente el operador NOT para remover un permiso, pues esto resultaria en activar los bits de permisos no otorgados y desactivar los bits de permisos otorgados. Para lograr nuestro cometido, hay que combinar el operador NOT con el operador AND. Supongamos de nuevo que tenemos una variable llamada $userPermissions con permisos para crear, editar y eliminar posts. Como en el ejemplo de XOR, deseamos eliminar el permiso para editar posts. Utilizando NOT y AND quedaría de la siguiente manera:

// Primero se asignan permisos. Equivalente a 1 + 2 + 4
$userPermissions = $createPost | $editPost | $deletePost;

// removemos el permiso equivalente a editar posts
$userPermissions = $userPermissions &  ~$editPost;

// imprime 5, equivalente a crear y eliminar posts
var_dump($userPermissions); 

// removemos el permiso equivalente a editar posts
$userPermissions = $userPermissions &  ~$editPost;

// sigue imprimiendo 5 aunque hayamos aplicado la operación dos veces
var_dump($userPermissions); 

En términos de bits lo que sucedería es lo siguiente. Primero el permiso para editar posts aplicándole el operador NOT:

~ 000010
---------
  111101

Y luego este valor invertido lo usamos en la operación AND con los permisos existentes:

000111
111101 &
-------
000101

Como se puede ver en el resultado, el bit correspondiente a editar permisos (segundo bit de derecha a izquierda) se ha desactivado, mientras que los bits correspondientes a crear y eliminar posts (primer y tercer bits de derecha a izquierda) permanecieron activos.

Lo que se hizo aquí se conoce como bit masking1 y sirve para dejar ciertos bits intactos mientras que otros bits se desactivan. Es importante prestar atención pues utilizaremos esta misma técnica más adelante. Lo importante a resaltar es que la "máscara" debe tener como valor 1 en las posiciones de los bits que se desean dejar intactos, valor 0 en los bits que se desean desactivar y se debe realizar una operacion AND.

En nuestro ejemplo el valor de la máscara es 111101, porque deseamos desactivar el segundo bit de derecha a izquierda y conservar los valores del resto. Originalmente el valor de la máscara era 000010 pero para que esta técnica funcionara utilizamos el operador NOT para invertir los valores.


Configuración de errores con operadores bit a bit

En PHP es posible indicar el nivel de errores que deseamos que se muestren. Por ejemplo, en un entorno de desarrollo deseamos que se muestren todos los errores para poder depurar y detectar la fuente de una falla más fácilmente; pero en un ambiente de producción no deseamos que este sea el caso pues estos errores se mostrarían a cualquiera que visite el sitio.

El nivel de errores a mostrar puede configurarse desde el archivo php.ini o de forma dinámica utilizando la función error_reporting2. Por ejemplo, podemos indicar solamente los errores que deseamos que se muestren, utilizando el operador OR, que como vimos en ejemplos anteriores, es el equivalente a sumar los valores que sean potencia de 2. De hecho, si revisan la lista de constantes de error3 verán que, efectivamente, sus valores son potencia de 2.

error_reporting(E_ERROR | E_RECOVERABLE_ERROR);

Otra forma de indicar el nivel de errores es utilizando algo que ya vimos antes: una máscara de bits y los operadores NOT y AND para partir de un valor que represente todos los errores y remover los que no deseemos incluir. En el siguiente ejemplo indicamos que se muestren todos los niveles de error, excepto E_NOTICE.

error_reporting(E_ALL & ~E_NOTICE);

Conversión de colores

Ahora pasemos a otro ejemplo de uso distinto que hará uso de los operadores que ya vimos junto con un par más.

Hay diferentes sistemas para representar colores, siendo de los más comunes el sistema hexadecimal y RGB (Red, Green, Blue). Tomemos como ejemplo el color del rectángulo en la siguiente imagen:

Este color de relleno que es como de un tono naranja se representa en formato hexadecimal con el valor EAB44B. En el sistema RGB se indican tres valores decimales que pueden ir desde el 0 hasta 255 cada uno (desde 00000000 hasta 11111111). Cada valor representa un color: el primero representa la cantidad de color rojo (R); el segundo representa la cantidad de color verde (G); el tercero representa la cantidad de color azul (B). Estos valores combinados forman el color de tono naranja de la imagen.

¿Cómo se obtienen los valores RGB 234, 180 y 75 a partir del valor hexadecimal EAB44B? Para averiguarlo, primero veamos la relación que hay entre este valor hexadecimal con el sistema binario y decimal.

En el sistema hexadecimal, los posibles valores se componen de números y letras. Los números son los mismos que en el sistema decimal: del 0 al 9. Las letras que pueden utilizarse van de la A hasta la F. Esto nos da un total de 16 valores.

hexadecimal decimal binario
0 0 0000
1 1 0001
2 2 0010
3 3 0011
4 4 0100
5 5 0101
6 6 0110
7 7 0111
8 8 1000
9 9 1001
A 10 1010
B 11 1011
C 12 1100
D 13 1101
E 14 1110
F 15 1111

Los primeros dos caracteres del valor hexadecimal representan la cantidad de rojo (R), el tercer y cuarto caracter la cantidad de verde (G) y el quinto y sexto la cantidad de azul (B). Para obtener los valores decimales de cada color, primero hay que obtener el valor binario de cada uno. Por ejemplo, la parte correspondiente a rojo es EA. La letra E corresponde al valor binario 1110, mientras que la letra A corresponde al valor binario 1010. Entonces tenemos que el valor hexadecimal EA equivale al valor binario 11101010. El valor 11101010 equivale al valor 234 en decimal, que es la cantidad de rojo (R) en RGB.

Muy bien, ¿pero cómo obtenemos estos valores utilizando operadores a nivel de bit? Para esto usaremos la misma técnica de bit masking que en la sección de permisos. En este caso nos servirá para "aislar" los bits que corresponden al color que nos interesa y descartar el resto.

Primero tomemos el valor completo en hexadecimal y su equivalente en binario. EAB44B equivale a 111010101011010001001011.

Los primeros ocho dígitos binarios representan el color rojo. Usando la técnica de bit masking, tendríamos que usar el operador AND y el siguiente valor binario como máscara para conservar los valores de los primeros ocho dígitos y descartar el resto: 111111110000000000000000.

Entonces tendríamos la siguiente operación:

111010101011010001001011  &
111111110000000000000000
-------------------------
111010100000000000000000

Vemos que los primeros ocho dígitos del resultado son 11101010, mientras que el resto quedó en ceros. ¡Pero no tan rápido! Hay un pequeño problema, el equivalente decimal del resultado debería ser 234, pero 111010100000000000000000 equivale a 15335424 decimal. Esto se debe a que el valor de cada dígito depende de su posición. En este caso los valores 1 están en las posiciones 16, 18, 20, 21 y 22 (de derecha a izquierda y considerando que la primera posición se eleva a 0, la segunda a 1 y así sucesivamente), por lo que tenemos 217 + 219 + 221 + 222 + 223 = 131072 + 524288 + 2097152 + 4194304 + 8388608 = 15335424.

Para solucionar este problema hay que recorrer los dígitos hacia la derecha para que ocupen las posiciones del 1 al 8 (potencias del 0 al 7) y así obtener el valor decimal correcto. Para esto podemos usar otro operador a nivel de bit que precisamente se encarga de mover a la derecha el número de posiciones que le indiquemos: el operador shift right >>.

Necesitamos recorrer 16 posiciones hacia la derecha para que el resultado quede en las posiciones correctas. Entonces tendríamos lo siguiente:

111010101011010001001011  &
111111110000000000000000
-------------------------
111010100000000000000000 >> 16
-------------------------
000000000000000011101010 = 234 decimal

Las posiciones "libres" a la izquierda se rellenan automáticamente con ceros cuando se usa este tipo de operación. Ahora, ¿cómo quedaría en código PHP? Primero que nada, para representar un valor hexadecimal en PHP hay que usar el prefijo 0x. Con esto representaremos los valores del color y la máscara:

$color = 0xEAB44B;

$mask = 0xFF0000; // equivale a 111111110000000000000000

$redColor = ($color & $mask) >> 16;

var_dump($redColor); //imprime 234

Hay que resaltar un par de cosas importantes en este código: al utilizar operador a nivel de bit, PHP convierte automáticamente los operandos en su equivalente binario para poder realizar la operación. Es por ello que la operación funciona aunque los valores de $color y $mask no sean binarios sino hexadecimales. De hecho, el valor de $mask se convierte al valor binario 111111110000000000000000 que es el valor binario de la máscara mostrada en los ejemplos anteriores.

Para obtener los valores del color verde y azul usamos el mismo procedimiento pero usando diferentes valores para la máscara. Por ejemplo, para obtener el color verde hay que recorrer 8 posiciones a la derecha y el valor de la máscara es 000000001111111100000000:

111010101011010001001011  &
000000001111111100000000
-------------------------
000000001011010000000000 >> 8
-------------------------
000000000000000010110100 = 180 decimal

Y en codigo tenemos que:

$color = 0xEAB44B;

$mask = 0x00FF00; // equivale a 000000001111111100000000

$greenColor = ($color & $mask) >> 8;

var_dump($greenColor); // imprime 180

El color azul tiene una peculiaridad, y es que no hay necesidad de recorrer posiciones. Esto se debe a que los dígitos correspondientes al color azul ya se encuentran en las posiciones correctas. Para obtener su valor tendríamos lo siguiente:

111010101011010001001011  &
000000000000000011111111
-------------------------
000000000000000001001011 = 75 decimal

Lo cual en codigo sería:

$color = 0xEAB44B;

$mask = 0x0000FF; // equivale a 000000000000000011111111

$blueColor = ($color & $mask);

var_dump($blueColor); // imprime 75

Y ya con el código para todos los colores:

$color = 0xEAB44B;

$redMask = 0xFF0000; // equivale a 111111110000000000000000
$greenMask = 0x00FF00; // equivale a 000000001111111100000000
$blueMask = 0x0000FF; // equivale a 000000000000000011111111

$redColor = ($color & $redMask) >> 16;
$greenColor = ($color & $greenMask) >> 8;
$blueColor = ($color & $blueMask);

var_dump($redColor); // imprime int(234)
var_dump($greenColor); // imprime int(180)
var_dump($blueColor); // imprime int(75)

Tengo algo qué confesar: hay una forma más "directa" de hacer estas conversiones. En el código anterior utilizamos una máscara distinta para cada color, cada máscara contiene activos los bits para las posiciones correspondientes al color a extraer. Si invertimos el orden de las operaciones y usamos 0x0000FF (000000000000000011111111 binario) como máscara para todos los colores obtendremos el mismo resultado, puesto que al recorrer primero los bits ya los colocamos o "alineamos" con los valores de la máscara:

$color = 0xEAB44B;

$mask = 0x0000FF; // equivale a 000000000000000011111111

$redColor = ($color >> 16) & $mask;
$greenColor = ($color >> 8) & $mask;
$blueColor = ($color & $mask);

var_dump($redColor); // imprime int(234)
var_dump($greenColor); // imprime int(180)
var_dump($blueColor); // imprime int(75)

¿Y si queremos obtener el color en hexadecimal a partir de RGB? Primero hacemos el proceso inverso en los colores rojo y verde usando el operador llamado shift left que recorre posiciones hacia la izquierda en lugar de hacia la derecha. A partir de ahí combinamos los colores con el operador OR:

000000000000000011101010 // rojo << 16
000000000000000010110100 // verde << 8
000000000000000001001011 // azul no necesita recorrerse
-------------------------
111010100000000000000000 OR 
000000001011010000000000 OR
000000000000000001001011
-------------------------
111010101011010001001011

Que en código quedaría de la siguiente forma:

$hexColor = dechex(($redColor << 16) | ($greenColor << 8) | $blueColor);

var_dump($hexColor); // imprime string(6) "eab44b"

Noten que se utiliza la función dechex() puesto que si tratamos de imprimir directamente el valor de la operación con var_dump() nos mostrará el equivalente en decimal.


Y así termina este pequeño post, tutorial o como le quieran llamar. Algo extenso pero quería cubrir el uso de estos operadores y con un poco de suerte ayudar a entender a alguien que los haya usado antes sin saber realmente su funcionamiento o algún ejemplo de su uso.

Si leyeron el post y hay algo que no queda claro, no duden en preguntar. Si creen que no tengo idea de lo que hablo, también pueden comentar. Si les ayudó en algo, me doy por bien servido.