Benchmarking these Smalltalk Luhn implementations:
a _ [ :x | x isValidLuhn] .
b _ [ :x | x isValidLuhn2].
interval _ (50000000000 to: 50000010000).
Time millisecondsToRun: [interval select: a] 18071
Time millisecondsToRun: [interval select: b] 28953
After tinkering around with both algorithms, I realized that keeping a running total is much faster. An even greater speed improvement is achieved by converting the ASCII character to its corresponding integer then subtracting 48, rather than converting to a string then converting the string to a number. So instead of aDigit asString asNumber, doing aDigit asInteger - 48 does better:
Time millisecondsToRun: [interval select: f] 4768
The naive implementation is easier to read, but three to four times less efficient, as it conses up a string from a character. Using (aDigit asInteger - $0 asInteger) might be a slightly more readable compromise. It seems like using a dynamically allocated Collection of any sort seems to be very bad for performance.
Combining the mapped doubled add-the-digits approach with the accumulator approach:
Integer>>isValidLuhn8
| stream accOdd accEven anEven doubleMap |
accOdd _ accEven _ 0.
stream _ ReadStream on: self asString reverse.
doubleMap _ #(0 2 4 6 8 1 3 5 7 9 ).
[stream atEnd]
whileFalse: [accOdd _ accOdd + stream next asInteger - $0 asInteger.
anEven _ stream next.
anEven
ifNotNil: [accEven _ accEven
+ (doubleMap at: anEven asInteger - $0 asInteger + 1)]].
^ accOdd + accEven \ 10 = 0
...seems to yield really good performance (3.5 to 5.5 seconds for 10001 operations) on average, while being only slightly incomprehensible.
11:01:47 AM
|