PHP Numbers
Calculating Using Numbers in Bases Other Than Decimal
Problem
You want to perform mathematical operations with numbers formatted not in decimal, but in octal or hexadecimal. For example, you want to calculate web-safe colors in hexadecimal.Solution
Prefix the number with a leading symbol, so PHP knows it isn’t in base 10. The leading symbol 0b indicates binary (base 2), the leading symbol 0 indicates octal (base 8) and the leading symbol 0x indicates hexadecimal (base 16). If $a = 100 and $b = 0144 and $c = 0x64 and $d = 0b1100100, PHP considers $a, $b, $c, and $d to be equal.
Here’s how to count from decimal 1 to 15 using hexadecimal notation:
for ($i = 0x1; $i < 0x10; $i++) {
print "$i\n";
}
Discussion
Even if you use hexadecimally formatted numbers in a for loop, by default all numbers are printed in decimal. In other words, the code in the Solution doesn’t print out …, 8, 9, a, b, …. Here’s an example:for ($i = 0x1; $i < 0x10; $i++) { print dechex($i) . "\n"; }
For most calculations, it’s easier to use decimal. Sometimes, however, it’s more logical to switch to another base—for example, when doing byte arithmetic. Dan Bernstein’s popular “times 33” hash is a convenient and fast way to hash a string of arbitrary length to an integer value.
To compute the “times 33” hash, you start with the magic number 5381 as your hash value. Then, for each byte in the string you want to hash, you add the byte and the previous hash value times 32 to the hash value. Translating that directly to PHP produces code that looks like this:
function times_33_hash($str) {
$h = 5381;
for ($i = 0, $j = strlen($str); $i < $j; $i++) {
// Shifting $h left by 5 bits is a quick way to multiply by 32
$h += ($h << 5) + ord($str[$i]);
}
return $h;
}
That code isn’t completely correct, however. It produces some strange results. For example, times_33_hash("Once, I ate a papaya.") returns a float, not an integer, with a really, really large value (about 2.28375 x 1019).
The repeated multiplications and additions, once for each byte in the string, have overflowed PHP’s maximum integer value so PHP’s autoconversion to float (with loss of precision) kicked in. To fix this, all you have to do is logical-AND the hash value with a mask of the significant bits you want to keep in the hash value.
Expressing those significant bits is a lot more understandable in hexadecimal rather than decimal. For example, if you want 32 bits in the hashed value, add a masking line inside the loop as follows:
function times_33_hash($str) {
$h = 5381;
for ($i = 0, $j = strlen($str); $i < $j; $i++) {
// Shifting $h left by 5 bits is a quick way to multiply by 32
$h += ($h << 5) + ord($str[$i]);
// Only keep the lower 32 bits of $h
$h = $h & 0xFFFFFFFF;
}
return $h;
}
Each hexadecimal F represents four bits, so masking with eight of them produces a 32-bit mask. You could use 4294967295 in your code as the mask value instead of 0xFFFFFFFF, but it wouldn’t be as clear.
No comments:
Post a Comment