¿Cuál es la idea detrás de ^ = 32, que convierte letras minúsculas en mayúsculas y viceversa?

146

Estaba resolviendo algún problema en las fuerzas de código. Normalmente, primero verifico si el carácter es una letra en inglés superior o inferior y luego resto o sumo 32para convertirlo a la letra correspondiente. Pero encontré que alguien hace ^= 32lo mismo. Aquí está:

char foo = 'a';
foo ^= 32;
char bar = 'A';
bar ^= 32;
cout << foo << ' ' << bar << '\n'; // foo is A, and bar is a

He buscado una explicación para esto y no lo descubrí. Entonces, ¿por qué esto funciona?

Devon
fuente
55
en.wikipedia.org/wiki/File:USASCII_code_chart.png Consejo: puedes convertir @a `mediante ^ 32.
KamilCuk
112
FWIW, realmente no "funciona". Funciona para este conjunto de caracteres en particular, pero hay otros conjuntos donde no debería. Debería usar touppery tolowercambiar entre mayúsculas y minúsculas.
NathanOliver
77
en algún momento con concursos en "la idea" es el código de escritura de una manera tan ofuscado que nunca pasaría una revisión seria;)
IDCLEV 463035818
21
^ = está transformando el valor usando XOR. Las letras mayúsculas ASCII tienen un cero en el bit correspondiente, mientras que las minúsculas tienen un uno. Dicho eso, por favor no lo hagas! Utilice las rutinas de caracteres (unicode) adecuadas para convertir entre minúsculas y mayúsculas. La era de solo ASCII ya pasó.
Hans-Martin Mosner
14
No es solo que solo funcione con algunos conjuntos de caracteres. Incluso si asumimos que todo el mundo es UTF-8 (que al menos podría ser un buen objetivo utópico), también solo funciona con las 26 letras Apara Z. Eso está bien siempre y cuando solo te importe el inglés (y no uses ortografía "ingenua", palabras como "café" o nombres con signos diacríticos ...), pero el mundo no es solo inglés.
ilkkachu

Respuestas:

149

Echemos un vistazo a la tabla de códigos ASCII en binario.

A 1000001    a 1100001
B 1000010    b 1100010
C 1000011    c 1100011
...
Z 1011010    z 1111010

Y 32 es 0100000la única diferencia entre letras minúsculas y mayúsculas. Así que alternar ese bit alterna el caso de una carta.

Hanjoung Lee
fuente
49
"cambia el caso" * solo para ASCII
Mooing Duck
39
@ Moooing solo para A-Za-z en ASCII. La minúscula de "[" no es "{".
dbkk
21
@dbkk {es más corto que [, por lo que es un caso "inferior". ¿No? Ok, me mostraré: D
Peter Badida
25
Dato curioso: en el área de 7 bits, las computadoras alemanas habían [] {|} reasignadas a ÄÖÜäöü ya que necesitábamos Umlauts más que esos caracteres, por lo que en ese contexto, {(ä) en realidad era minúscula [(Ä).
Guntram Blohm apoya a Monica
14
@GuntramBlohm Más información curiosa, esta es la razón por la cual los servidores IRC consideran foobar[] y foobar{}son apodos idénticos, ya que los apodos no distinguen entre mayúsculas y minúsculas , e IRC tiene su origen en Escandinavia :)
ZeroKnight
117

Esto utiliza el hecho de que los valores ASCII han sido elegidos por personas realmente inteligentes.

foo ^= 32;

Esto voltea el sexto bit 1 más bajo de foo(el indicador en mayúsculas de ASCII), transformando una mayúscula ASCII en minúscula y viceversa .

+---+------------+------------+
|   | Upper case | Lower case |  32 is 00100000
+---+------------+------------+
| A | 01000001   | 01100001   |
| B | 01000010   | 01100010   |
|            ...              |
| Z | 01011010   | 01111010   |
+---+------------+------------+

Ejemplo

'A' ^ 32

    01000001 'A'
XOR 00100000 32
------------
    01100001 'a'

Así como por bienes de XOR, 'a' ^ 32 == 'A'.

aviso

No se requiere C ++ para usar ASCII para representar caracteres. Otra variante es EBCDIC . Este truco solo funciona en plataformas ASCII. Una solución más portátil sería usar std::tolowery std::toupper, con la bonificación ofrecida para tener en cuenta la ubicación (sin embargo, no resuelve automáticamente todos sus problemas, vea los comentarios):

bool case_incensitive_equal(char lhs, char rhs)
{
    return std::tolower(lhs, std::locale{}) == std::tolower(rhs, std::locale{}); // std::locale{} optional, enable locale-awarness
}

assert(case_incensitive_equal('A', 'a'));

1) Como 32 es 1 << 5(2 a la potencia 5), ​​cambia el sexto bit (contando desde 1).

YSC
fuente
16
EBCDIC también fue elegido por algunas personas muy inteligentes: funciona muy bien en tarjetas perforadas cf. ASCII que es un desastre. Pero esta es una buena respuesta, +1.
Betsabé
65
No sé sobre tarjetas perforadas, pero se usó ASCII en cinta de papel. Es por eso que el carácter Eliminar está codificado como 1111111: para que pueda marcar cualquier carácter como "eliminado" perforando todos los agujeros en su columna en la cinta.
dan04
23
@Bathsheba como alguien que no ha usado una tarjeta perforada, es muy difícil entender la idea de que EBCDIC fue diseñado de manera inteligente.
Lord Farquaad
9
@LordFarquaad En mi humilde opinión, la imagen de Wikipedia de cómo se escriben las letras en una tarjeta perforada es una ilustración obvia de cómo EBCDIC tiene algún sentido (pero no total, ver / vs S) para esta codificación. en.wikipedia.org/wiki/EBCDIC#/media/…
Peteris
11
@ dan04 Nota para mencionar "¿cuál es la forma minúscula de 'MASSE'?". Para aquellos que no saben, hay dos palabras en alemán cuya forma mayúscula es MASSE; uno es "Masse" y el otro es "Maße". Proper toloweren alemán no solo necesita un diccionario, sino que debe poder analizar el significado.
Martin Bonner apoya a Mónica
35

Permítanme decir que esto es, aunque parezca inteligente, un truco muy, muy estúpido. Si alguien te recomienda esto en 2019, golpéalo. Golpéalo tan fuerte como puedas.
Por supuesto, puede hacerlo en su propio software que usted y nadie más usa si sabe que nunca usará ningún idioma que no sea inglés de todos modos. De lo contrario, no te vayas.

El truco fue discutible "OK" hace unos 30-35 años cuando las computadoras realmente no hacían mucho más que inglés en ASCII, y tal vez uno o dos idiomas europeos principales. Pero ... ya no es así.

El truco funciona porque las mayúsculas y minúsculas de EE. UU. Y América están exactamente 0x20separadas entre sí y aparecen en el mismo orden, lo cual es solo un poco de diferencia. Lo que, de hecho, este pequeño truco, alterna.

Ahora, las personas que crean páginas de códigos para Europa occidental, y más tarde el consorcio Unicode, fueron lo suficientemente inteligentes como para mantener este esquema, por ejemplo, para Umlauts alemanes y Vocales con acento francés. No es así para ß que (hasta que alguien convenció al consorcio Unicode en 2017, y una gran revista impresa de Fake News escribió sobre ello, realmente convenció al Duden, sin comentarios al respecto) ni siquiera existe como un versal (se transforma en SS) . Ahora bien, no existe como Versal, pero los dos son 0x1DBFpuestos aparte, no 0x20.

Sin embargo, los implementadores no fueron lo suficientemente considerados para mantener esto en marcha. Por ejemplo, si aplica su truco en algunos idiomas de Europa del Este o similares (no sabría sobre cirílico), obtendrá una desagradable sorpresa. Todos esos caracteres "hacha" son ejemplos de eso, minúsculas y mayúsculas son uno aparte. El truco por lo tanto no funciona correctamente allí.

Hay mucho más que considerar, por ejemplo, algunos caracteres no se transforman simplemente de minúsculas a mayúsculas (se reemplazan con diferentes secuencias), o pueden cambiar de forma (lo que requiere diferentes puntos de código).

Ni siquiera pienses en lo que este truco hará para cosas como tailandés o chino (solo te dará una completa tontería).

Ahorrar un par de cientos de ciclos de CPU puede haber valido la pena hace 30 años, pero hoy en día, realmente no hay excusa para convertir una cadena correctamente. Hay funciones de biblioteca para realizar esta tarea no trivial.
El tiempo necesario para convertir varias docenas de kilobytes de texto correctamente es insignificante hoy en día.

Damon
fuente
2
Estoy totalmente de acuerdo, aunque es una buena idea para todos los programadores saber por qué funciona, incluso podría hacer una buena pregunta de entrevista. ¿Qué hace esto y cuándo debería usarse? :)
Bill K
33

Funciona porque, como sucede, la diferencia entre 'a' y A 'en las codificaciones ASCII y derivadas es 32, y 32 también es el valor del sexto bit. Voltear el sexto bit con un OR exclusivo convierte así entre superior e inferior.

Jack Aidley
fuente
22

Lo más probable es que su implementación del conjunto de caracteres sea ASCII. Si miramos la mesa:

ingrese la descripción de la imagen aquí

Vemos que hay una diferencia exactamente 32entre el valor de un número en minúsculas y mayúsculas. Por lo tanto, si lo hacemos^= 32 (lo que equivale a alternar el sexto bit menos significativo), cambia entre un carácter en minúsculas y mayúsculas.

Tenga en cuenta que funciona con todos los símbolos, no solo con las letras. Alterna un carácter con el carácter respectivo donde el sexto bit es diferente, lo que resulta en un par de caracteres que se alternan de un lado a otro. Para las letras, los respectivos caracteres en mayúscula / minúscula forman dicho par. A NULcambiará a Spacey al revés, y el@ cambia con la tecla de retroceso. Básicamente, cualquier carácter en la primera columna de este gráfico alterna con el carácter una columna sobre, y lo mismo se aplica a la tercera y cuarta columna.

Sin embargo, no usaría este truco, ya que no hay garantía de que funcione en ningún sistema. Simplemente use toupper y tolower en su lugar, y consultas como isupper .

Resplandor
fuente
2
Bueno, no funciona para todas las letras que tienen una diferencia de 32. ¡De lo contrario, funcionaría entre '@' y ''!
Matthieu Brucher
2
@MatthieuBrucher Está funcionando, 32 ^ 32es 0, no 64
NathanOliver
55
'@' y '' no son "letras". Solo [a-z]y [A-Z]son "letras". El resto son coincidencias que siguen la misma regla. Si alguien le pidiera "mayúscula]", ¿cuál sería? seguiría siendo "]" - "}" no es la "mayúscula" de "]".
freedomn-m
44
@MatthieuBrucher: Otra forma de hacer ese punto es que los rangos alfabéticos en minúsculas y mayúsculas no cruzan un %32límite de "alineación" en el sistema de codificación ASCII. Es por eso que bit 0x20es la única diferencia entre las versiones en mayúsculas / minúsculas de la misma letra. Si este no fuera el caso, necesitaría sumar o restar 0x20, no solo alternar, y para algunas letras se realizaría para voltear otros bits más altos. (Y la misma operación no podría alternar, y buscar caracteres alfabéticos en primer lugar sería más difícil porque no se podría |= 0x20forzar a Lcase)
Peter Cordes,
2
+1 por recordarme todas esas visitas a asciitable.com para mirar ese gráfico exacto (¡y la versión ASCII extendida!) Durante el último, no sé, ¿15 o 20 años?
AC
15

Muchas buenas respuestas aquí que describen cómo funciona esto, pero por qué funciona de esta manera es para mejorar el rendimiento. Las operaciones bit a bit son más rápidas que la mayoría de las demás operaciones dentro de un procesador. Puede hacer rápidamente una comparación entre mayúsculas y minúsculas simplemente no mirando el bit que determina el caso o cambiar el caso a mayúsculas / minúsculas simplemente volteando el bit (los tipos que diseñaron la tabla ASCII eran bastante inteligentes).

Obviamente, esto no es tan importante hoy como lo fue en 1960 (cuando comenzó el trabajo en ASCII) debido a los procesadores más rápidos y Unicode, pero todavía hay algunos procesadores de bajo costo que podrían marcar una diferencia significativa siempre que pueda garantizar solo caracteres ASCII.

https://en.wikipedia.org/wiki/Bitwise_operation

En procesadores simples de bajo costo, típicamente, las operaciones bit a bit son sustancialmente más rápidas que la división, varias veces más rápidas que la multiplicación y, a veces, significativamente más rápidas que la suma.

NOTA: recomendaría utilizar bibliotecas estándar para trabajar con cadenas por varias razones (legibilidad, corrección, portabilidad, etc.). Solo use el cambio de bits si ha medido el rendimiento y este es su cuello de botella.

Brian
fuente
14

Así es como funciona ASCII, eso es todo.

Pero al explotar esto, estás renunciando a la portabilidad ya que C ++ no insiste en ASCII como codificación.

Esta es la razón por la cual las funciones std::touppery std::tolowerse implementan en la biblioteca estándar de C ++: en su lugar, debe usarlas.

Betsabé
fuente
66
Sin embargo, existen protocolos que requieren el uso de ASCII, como DNS. De hecho, el "truco 0x20" es utilizado por algunos servidores DNS para insertar entropía adicional en una consulta DNS como un mecanismo anti-spoofing. DNS no distingue entre mayúsculas y minúsculas, pero también se supone que preserva las mayúsculas y minúsculas, por lo que si envía una consulta con mayúsculas y minúsculas y recupera el mismo caso, es una buena indicación de que la respuesta no ha sido falsificada por un tercero.
Alnitak
Vale la pena mencionar que muchas codificaciones todavía tienen la misma representación para los caracteres ASCII estándar (no extendidos). Pero aún así, si realmente le preocupan las diferentes codificaciones, debe usar las funciones adecuadas.
Capitán Man
55
@ CapitánMan: Absolutamente. UTF-8 es una cosa de pura belleza. Esperemos que se "absorba" en el estándar C ++ en la medida en que IEEE754 tiene para coma flotante.
Betsabé
11

Vea la segunda tabla en http://www.catb.org/esr/faqs/things-every-hacker-once-knew/#_ascii , y las siguientes notas, reproducidas a continuación:

El modificador de Control en su teclado básicamente borra los tres bits superiores de cualquier carácter que escriba, dejando los cinco inferiores y asignándolo al rango 0..31. Entonces, por ejemplo, Ctrl-SPACE, Ctrl- @ y Ctrl-`significan lo mismo: NUL.

Los teclados muy antiguos solían hacer Shift simplemente alternando 32 o 16 bits, dependiendo de la tecla; Esta es la razón por la cual la relación entre letras minúsculas y mayúsculas en ASCII es tan regular, y la relación entre números y símbolos, y algunos pares de símbolos, es más o menos regular si se mira de reojo. El ASR-33, que era un terminal todo en mayúscula, incluso le permitía generar algunos caracteres de puntuación para los que no tenía claves al cambiar los 16 bits; así, por ejemplo, Shift-K (0x4B) se convirtió en un [(0x5B)

ASCII se diseñó de tal manera que las teclas shifty del ctrlteclado se pudieran implementar sin mucha ctrllógica (o tal vez ninguna ), shiftprobablemente solo requirieron unas pocas puertas. Probablemente tenía al menos tanto sentido almacenar el protocolo de cable como cualquier otra codificación de caracteres (no se requiere conversión de software).

El artículo vinculado también explica muchas convenciones de hackers extrañas como And control H does a single character and is an old^H^H^H^H^H classic joke.(que se encuentra aquí ).

Iiridayn
fuente
1
Podría implementar un cambio de alternancia para más ASCII w / foo ^= (foo & 0x60) == 0x20 ? 0x10 : 0x20, aunque esto es solo ASCII y, por lo tanto, imprudente por las razones indicadas en otras respuestas. Probablemente también se pueda mejorar con una programación sin ramificación.
Iiridayn
1
Ah, foo ^= 0x20 >> !(foo & 0x40)sería más simple. También es un buen ejemplo de por qué el código breve a menudo se considera ilegible ^ _ ^.
Iiridayn
8

Xoring con 32 (00100000 en binario) establece o restablece el sexto bit (desde la derecha). Esto es estrictamente equivalente a sumar o restar 32.

Yves Daoust
fuente
2
Otra forma de decir esto es que XOR es add-without-carry.
Peter Cordes
7

Los rangos alfabéticos en minúsculas y mayúsculas no cruzan un %32límite de "alineación" en el sistema de codificación ASCII.

Es por eso que bit 0x20es la única diferencia entre las versiones en mayúsculas / minúsculas de la misma letra.

Si este no fuera el caso, necesitaría sumar o restar 0x20, no solo alternar, y para algunas letras se realizaría para voltear otros bits más altos. (Y no habría una sola operación que pudiera alternar, y buscar caracteres alfabéticos en primer lugar sería más difícil porque no podría | = 0x20 forzar lcase).


Trucos relacionados solo con ASCII: puede verificar si hay un carácter ASCII alfabético forzando minúsculas con c |= 0x20y luego verificando si (sin signo) c - 'a' <= ('z'-'a'). Entonces, solo 3 operaciones: OR + SUB + CMP contra una constante 25. Por supuesto, los compiladores saben cómo optimizar (c>='a' && c<='z') en asm como este para usted , por lo que a lo sumo debe hacer la c|=0x20parte usted mismo. Es bastante inconveniente hacer todo el casting necesario usted mismo, especialmente para evitar promociones enteras predeterminadas para firmar int.

unsigned char lcase = y|0x20;
if (lcase - 'a' <= (unsigned)('z'-'a')) {   // lcase-'a' will wrap for characters below 'a'
    // c is alphabetic ASCII
}
// else it's not

Consulte también Convertir una cadena en C ++ a mayúsculas (cadena SIMDtoupper solo para ASCII, enmascarando el operando para XOR usando esa verificación).

Y también Cómo acceder a una matriz de caracteres y cambiar las letras minúsculas a mayúsculas, y viceversa (C con intrínsecos SIMD y mayúscula escalar x86 asm para caracteres alfabéticos ASCII, dejando otros sin modificar).


La mayoría de estos trucos solo son útiles si se optimiza manualmente el procesamiento de texto con SIMD (por ejemplo, SSE2 o NEON), después de comprobar que ninguno de los chars en un vector tiene su bit alto establecido. (Y, por lo tanto, ninguno de los bytes forma parte de una codificación UTF-8 de varios bytes para un solo carácter, que podría tener diferentes inversas en mayúsculas / minúsculas). Si encuentra alguno, puede recurrir al escalar para este fragmento de 16 bytes, o para el resto de la cadena.

Incluso hay algunas configuraciones regionales donde toupper()o tolower()en algunos caracteres en el rango ASCII producen caracteres fuera de ese rango, especialmente en turco donde I ↔ ı e İ ↔ i. En esos entornos locales, necesitaría una verificación más sofisticada, o probablemente no intente utilizar esta optimización en absoluto.


Pero en algunos casos, se le permite asumir ASCII en lugar de UTF-8, por ejemplo, utilidades Unix con LANG=C(la ubicación POSIX), no en_CA.UTF-8o lo que sea.

Pero si puede verificar que es seguro, puede hacer touppercadenas de longitud media mucho más rápido que llamar toupper()en un bucle (como 5x), y la última vez que probé con Boost 1.58 , mucho más rápido de lo boost::to_upper_copy<char*, std::string>()que hace una estupidez dynamic_castpor cada personaje.

Peter Cordes
fuente