The modulo operation for large integers in SAS

On social media, a SAS user reported that SAS could not compute the modulo of an extremely large integer. In SAS, the modulo operation is usually performed by using the MOD function, which computes the remainder of dividing an integer, N, by another integer, d. (In symbols, the remainder is r = mod(N,d).) When I asked for details, he directed me to a financial regulation that shows how to implement an ISO algorithm to generate (and validate) a Universal Loan Identifier (ULI), which is a unique identifier that is used for financial transactions. One step of the algorithm requires finding the residual of a large integer mod 97.

After reading the regulation, it was clear why the SAS user was having a problem, and it was also clear how to solve the problem. This article shows how to compute the modulo operation on arbitrarily large integers in SAS. In a subsequent article, I implement the complete ISO algorithm to generate a check digit and validate a Universal Loan Identifier (ULI).

The problem with the MOD operation on large integers

Suppose you want to divide an integer N (called the dividend) by a positive integer d (called the divisor). The Quotient-Remainder Theorem (also called the division algorithm) states that there exist unique positive integers q and r (the quotient and remainder) where r < d and N = d*q + r.

In computer languages, we can use the FLOOR function to compute the quotient (floor(N/d)) and the MOD function to compute the remainder (mod(N,d)). The documentation for the MOD function in SAS states, "The computation that is performed by the MOD function is exact if both of the following conditions are true:

Both arguments are exact integers.

All integers that are less than either argument have exact 8-byte floating-point representations."

The last bullet point says that N must be less than or equal to the 'ExactInt' constant, which for SAS is 9007199254740992 ≈ 9.007E15. If N exceeds that value, then MOD returns a missing value. The following DATA step shows the MOD operation for a numbers less than, equal to, and greater than the 'ExactInt' constant:

data Remainder_LargeInt;
format N q best16.;
input N; /* attempt to read dividend as integer */
d = 97; /* divisor */
q = floor(N/d); /* integer part of quotient */
r = mod(N, d); /* remainder */
diff = N - (d*q + r); /* =0 for representable integers */
output;
datalines;
1197488
9007199254740992
1011339391255432926101144229991433300
1011339391255432926101144229991433338
;

proc print noobs; run;

For the first integer, the quotient is 12345 and the remainder is 23. For the second integer, which is the value of 'ExactInt', the remainder is 32. The MOD function cannot compute the remainder for the two very long 37-digit integers. The SAS log alerts you that there is a problem: NOTE: Invalid argument to function MOD(1.0113394E36,97).

The two long 37-digit integers are examples from the ISO algorithm for generating a check digit and validating a generating (and validating) a Universal Loan Identifier (ULI). The problem is that SAS converts these integers to doubles, and the representation is not exact because of the limits of 64-bit finite precision computations, which keeps 16 digits of precision.

Interestingly, when you read the description of the ISO algorithm, you discover that these long integers are not stored as numbers. Rather, they are stored as character strings, where each character is a digit 0-9. The strings are the unique identifier for a specific loan from a specific financial institution. Do you remember typing in "bank routing numbers" and "account numbers" when you transfer money to/from a bank account? Well, the long integers in this example are similar. They represent a concatenation of an identifier for a financial institution and for an account at that institution.

The algorithm in the ISO regulation tells you how to generate these strings. But it also tells you that you must compute the value mod = (N,97) where N is one of these long strings! Don't you need to convert the string to a big number to compute the remainder? No, you don't!

The long-division algorithm

In elementary school, students learn how to find the quotient and remainder by applying the long-division algorithm. The steps in that computation dictate that you move from left to right along the digits of the dividend. You NEVER have to look at the entire dividend! Rather, for each digit, you incrementally build up the quotient by looking at a remainder from the previous step. Each step requires looking at only a small number, which is always less than 10*d. An example is shown to the right for the seven-digit dividend 1197488. The long-division algorithm produced the remainder (and quotient, if desired) in at most seven steps.

My point is this: You can perform long-division on a string of numbers without ever needing to store the full dividend. You can perform the long-division algorithm by traversing the integers in the string from left to right. At each step, you keep track of the remainder, which is the number that you "bring down" when doing long division on paper. At the next step, you multiply the previous remainder by 10 (shift the remainder left) and bring down the next number from the dividend. You then figure out the partial quotient (which we don't need here) and the remainder.

Although the algorithm from elementary school requires multiplication and subtraction, in a computer program you can replace those two steps by a single call to the MOD function which gives the remainder at each step of the algorithm. This is illustrated to the right. You can safely use the MOD operation in this algorithm because the temporary dividends are never more than 10*97.

The following PROC FCMP step defines a function called MOD_FROM_STRING. The function takes two arguments. The first parameter is a string (s) that contains only characters for the digits 0-9. The second parameter is an integer divisor (d), such as d=97. The function applies the long-division algorithm to find the remainder mod(N,d). The only tricky part is that you need to use the INPUT function to convert the digits in the dividend string.

proc fcmp outlib=work.banking.ULI;
/* Compute the remainder of a large integer (represented as a string)
when divided by d.
This implements the standard long-division algorithm,
but at the i_th step the i_th character is converted to an integer.
*/
function mod_from_string(s $, d);
r = 0; /* initialize remainder */
do i = 1 to length(s);
c = substr(s, i, 1); /* i_th digit as character */
digit = input(c, 1.); /* i_th digit as integer */
r = mod(10*r + digit, d); /* running remainder */
end;
return( r ); /* return remainder as an integer */
endsub;
quit;

With this new function defined, you can represent the large integers as strings, which ensures that the modulo operation will be exact for arbitrarily large numbers. The following example reads the same four integers as before, but this time stores the numbers as strings. The call to the mod_from_string function computes the remainder MOD(N,97).

option cmplib=work.banking; /* set search path for the FCMP function(s) */

/* test the mod_from_string function by using the examples in
https://www.consumerfinance.gov/rules-policy/regulations/1003/c/
*/
data Remainder_LargeStr;
length ULI $200; /* "N": represent the integers as a string of character digits */
input ULI;
r = mod_from_string(ULI, 97); /* remainder mod 97 */
datalines;
1197488
9007199254740992
1011339391255432926101144229991433300
1011339391255432926101144229991433338
;

proc print noobs; run;

Success! The output shows that the program can perform the modulo operation on these strings. The remainders for the first two numbers (23 and 32) are the same as are computed by the MOD function. The large integer strings are taken from an example in the ISO regulation, so I know the remainders of 60 and 1 are correct.

Summary

A SAS user noted that he was having problems computing the modulo operation on extremely large integers. The specific application was the compute the remainder mod 97 of a large integer to use as a checksum. The SAS documentation states that the MOD function operates on a numeric integer. The integer must be less than 'ExactInt', which is the value of the largest integer that is exactly representable in a double-precision floating point value. However, instead of storing the long integer as a double-precision value (and therefore losing precision), you can write a short function that carries out the long-division algorithm digit-by-digit on the string. In this way, you never need to deal with a large integer.

You can use SAS to implement the entire algorithm that generates a "check digit" and that validates a ULI. I don't want this post to become too long, so I will implement the rest of the algorithm in a separate blog post.