The comparison used is numeric
These are numbers not strings of characters. You can see this by looking at the code in the 2009 main.cpp of the Bitcoin reference implementation:
uint256 hashTarget = CBigNum().SetCompact(pblock->nBits).getuint256();
if (hash <= hashTarget)
pblock->nNonce = tmp.block.nNonce;
assert(hash == pblock->GetHash());
//// debug print
printf("proof-of-work found \n hash: %s \ntarget: %s\n", hash.GetHex().c_str(), hashTarget.GetHex().c_str());
if (hash <= hashTarget) is a numeric comparison. Both
hashTarget are type
uint256 - an unsigned integer.
Numbers expressed in hexadecimal are still numbers
There is a choice of visual representations but the choice made does not change the underlying nature of the number or the way in which numbers are compared arithmetically or at a machine level in a computer.
Your example, 00005fad, is a number expressed in hexadecimal (base 16), the same number can be written in normal decimal (base 10) as 24493. Anyone unfamiliar with non-decimal representations such as hexadecimal, octal and binary can check this using something like the Windows 10 calculator, in the menu choose "Programmer Mode" then click on "hex" and enter 5fad - it shows the same value in several different representations.
The notion that Bitcoin cares about the number of leading zeroes in, say, a hexadecimal representation, is a commonly repeated mistake (don't ask me how I know this).
If you insist on writing numbers with leading zeroes it is still obviously true that 000015 (fifteen) with four leading zeroes is smaller than 000150 (a hundred and fifty) with only three leading zeroes. It would however be a mistake to think that smaller numbers always have more leading zeroes. Both you and Bitcoin know that 000017 (seventeen) is smaller than 000019 (nineteen) even though both have the same number of leading zeroes.
It is true that
a is less than
b in exactly the same way that
7 is less than
8 or that
2 is less than
3. But it is probably a mistake to start comparing individual digits in a particular visual representation. The hash and hash targets are ordinary numbers (though large) that are compared in an ordinary way.
So where does this talk of leading zeroes come from? It is part of a method of encoding the target value as a difficulty in a compact representation. Bitcoin has some rather arcane methods of saving space in data that are transmitted.
Lets look at some recent blocks (most recent at top, reverse chronological order)
Notice that the difficulty increased after block 669311 but the number of leading zeroes in the hashes has not increased (not in hexadecimal and not in binary).
If you want to see exact details you could look at early versions of the Bitcoin reference implementation in C++. However I would suggest instead looking at the current BTCD implementation in go-lang because that is well commented and, in my opinion, an easier language to read.
// TargetTimespan is the desired amount of time that should elapse
// before the block difficulty requirement is examined to determine how
// it should be changed in order to maintain the desired block
// generation rate.
// TargetTimePerBlock is the desired amount of time to generate each
// Calculate new target difficulty as:
// currentDifficulty * (adjustedTimespan / targetTimespan)
// The result uses integer division which means it will be slightly
// rounded down. Bitcoind also uses integer division to calculate this
oldTarget := CompactToBig(lastNode.bits)
newTarget := new(big.Int).Mul(oldTarget, big.NewInt(adjustedTimespan))
targetTimeSpan := int64(b.chainParams.TargetTimespan / time.Second)
Calculating the hash target