Understanding The Modulo Operator, %, and Its Mirror, //, and Their Usefulness
The modulo operator, %, gives the remainder of division. 1 % 2
would be like dividing 1 by 2, but stopping any further calculation because that's as far as we can go without using decimal notation, and keeping the part that wasn't divided (which is 1).
Using the modulo operator, %
, doesn't conceptually take into account that 1/2 is equal to 2/4. With the expression op1 % op2
, what op2
signifies is a range of values, while op1
signifies a value that can be reduced to a value within the range that op1
represents.
To show the usefulness of this in programming, I'll use a chess board as an example. Suppose I have a 1-dimensional array representing the board. If at some line in my code I need to convert an array index (say 23) of a square to chess notation (file & rank --> h6 for instance), I could do something like this...
>>> board = ['br', 'bn', 'bb', 'bq', 'bk', 'bb', 'bn', 'br', # 8
... 'bp', 'bp', 'bp', 'bp', 'bp', 'bp', 'bp', 'bp', # 7
... None, None, None, None, None, None, None, None, # 6
... None, None, None, None, None, None, None, None, # 5
... None, None, None, None, None, None, None, None, # 4
... None, None, None, None, None, None, None, None, # 3
... 'wp', 'wp', 'wp', 'wp', 'wp', 'wp', 'wp', 'wp', # 2
... 'wr', 'wn', 'wb', 'wq', 'wk', 'wb', 'wn', 'wr'] # 1
... # a b c d e f g h
>>>
>>> def position(sq_index):
... return { 'file': sq_index % 8, 'rank': 7 - sq_index // 8 }
...
>>> position(23)
{'file': 7, 'rank': 5}
>>>
>>> def chess_notation(pos):
... alpha = 'abcdefgh'
... return alpha[pos['file']] + str(pos['rank'] + 1) # + 1 because ranks
... # start at 1 in
... # notation.
...
>>> chess_notation(position(23))
'h6'
(In chess, the columns on the x-axis of the board are referred to as 'files' and the y-axis rows are the 'ranks')
Note that the implementation for position()
has sq_index % 8
("sq_index modulo eight") in it to determine the file value. The files on a chess board go from 'a' to 'h' (columns 0 to 7). If we count squares left to right 0 to 7, we get to the end and then repeat our counting on the next rank. The number of squares we may have counted so far could be 9, but using 9 % 8
, we know our finger is now on the 'b' file (9 % 8 = 1
).
So no matter what index we have within 0 to 63, it can be reduced to a value between 0 and 7. If index n
increments from 0 to 63 sequentially, n % 8
will produce 0, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, .. and so on.
The calculation to get the rank is a similar operation. We have sq_index // 8
. The //
operator (or "floor division operator") produces an integer result with the remainder discarded (essentially the opposite of %
). This gives the same result we'd get if implemented as math.floor( 1 / 8 )
. //
is a Python 3 feature. With Python 3, 1 / 8
produces a float result, while 1 // 8
produces an integer value.
So, say we count squares again, 0 to 7. At each square n_sq // 8
produces the same value, 0
. Then we continue counting up the next rank, 8 to 15. Each of those index values produces 1
(e.g. 11 // 8 = 1
). And so on. The board array in the code has ranks numbered 8 to 1, so to convert to the right rank value it's 8 - (sq_index // 8)
.
Note: For Python 2 the //
operator, and the /
operator both produce an integer value. This difference between Python 2 and 3 can cause some confusion when migrating.