Reducing code for readability and speed

In a conversation a couple of days ago, somebody mentioned that they needed to determine the missing digit(checksum) of an ISBN-10 number. This was interesting, specially because this was a perfect example for reducing the lines of code written to resolve this.

The specification of ISBN-10 checksum

The final character of a ten-digit International Standard Book Number is a check digit computed so that multiplying each digit by its position in the number (counting from the right) and taking the sum of these products modulo 11 is 0.

The same with an example if you did not understand it, is:
Take for instance the number ISBN 0201-53082-1 where 1 is the checksum and how we determine it is correct is as follows,

The sum of products of 0x9 + 2×8 + 1×7 + 5×6 + 3×5 + 0x4 + 8×3 + 2×2 + 1×1 = 99
and 99 mod 11 is 0 (so this number is correct/valid)

Let’s find the checksum

say we have another ISBN number as 0-306-40615 and we need to determine the checksum number.

let number = "0-306-40615"
// first remove non numbers
var numbersOnly = ""
for i in number {
    if i != "-" {
        numbersOnly.append(i)
    }
}

This sanitizes the input and removes all of the dashes in the number, rather than iterate through a loop, one can also use the in-built function called replaceOccurances as follows

let numbersOnly = number.replacingOccurances(of: "-" with :"")

compact, and more readable than in the loop where we are checking that the number is not a dash and then we add it to the array.

Getting the product

Now that we have the numbers sanitized, we can proceed to generate the product as specified

var total = 0
var index = 10

for i in numbersOnly {
    let aDigit = Int(String(i)) ?? 0
    total += (index * aDigit)
    index -= 1
}

the total would now contain the product of all the numbers

the checksum digit can be determined as

let checksumDigit = (11 - (total % 11))

where we get the modulus 11 of total and that we subtract from 11 to get the actual number.

Putting it all together

let number = "0-306-40615"
let numbersOnly = number.replacingOccurances(of: "-" with :"")

var total = 0
var index = 10

for i in numbersOnly {
    let aDigit = Int(String(i)) ?? 0
    total += (index * aDigit)
    index -= 1
}

let checksumDigit = (11 - (total % 11))

NOTE: we can also add other checks to determine the length of the string to add to the validation

Can we make this any better?

What if someone entered the number as ISBN 0-306-046l5″ accidentally, where there is ISBN preceding the number and a l instead of 1 in the string. The replaceOccurances function will only remove the dash, and then when we generate the aDigit by casting an Int from the String, if it fails we simply use the value of 0 as default.

Let’s see if we can try and use lesser code for the same,

When the digit is not a number, the Int function fails and returns a nil. This can break our code – OR if you spotted it, can help us.

let number = "0-306-40615"
let processed = number.compactMap { Int(String($0)) }

Now let’s determine the total as per the algorithm.

var total = 0
for (index, val) in processed.enumerated() {
    total += ((10-index) * val)
}

and the checksum remains as earlier

let checksumDigit = (11 - (total % 11))

Let’s take on UPCA

Now that we can solve this, we find that it is fun to try and attempt other formats, let’s take on UPCA, and the specifications for the checksum digit UPCA


The final digit of a Universal Product Code is a check digit computed as follows:
1. Add the digits in the odd-numbered positions (first, third, fifth, etc.) together and multiply by three.
2. Add the digits (up to but not including the check digit) in the even-numbered positions (second, fourth, sixth, etc.) to the result.
3. Take the remainder of the result divided by 10 (modulo operation). If the remainder is equal to 0 then use 0 as the check digit, and if not 0 subtract the remainder from 10 to derive the check digit.

So for example,
say the UPC-A code is “036000241457”, the last digit (check sum) is 7 and we can calculate it as

1. Add the odd number digits: 0+6+0+2+1+5 = 14
2. Multiply this result by 3: 14 x 3 = 42
3. Add the even number digits: 3+0+0+4+4 = 11
4. Add the two results together: 42 + 11 = 53
4. Calculate the check digit, take the modulus 10 of (53/10) which is 3, and 10 - 3 = 7

So the checksum we calculated is the same as the last digit, so the UPC-A code is correct/valid.

Let’s work on the code now,
first we need to get the checksum digit which is the last digit and process the rest of the numbers (less the last number)

Luckily with Swift, we have functions that allow us to get the characters using remove functions, like removeFirst or removeLast which returns the character accordingly and it also removes the character from the original string. If you were to use the drop functions, then they would simply return but not affect the original string. The other difference is that the remove functions return a character and the drop functions return a SubString.

var upca = "036000241457"
let checksumDigit = String(upca.removeLast())
let upcaInt = upca.compactMap { Int(String($0)) }

var oddSum = 0
var evenSum = 0
for i in 0..< upcaInt.count {
    if i % 2 == 0 {
      oddSum += upcaInt[i]
    } else {
      evenSum += upcaInt[i]
    }
}
var result = ((oddsum * 3) + evenSum)
var checksum = 10 - (result % 10)

I am not sure if there is a way to get an index in any of the functional methods, because that could make this more into one line of code.

Summary

In summary, the idea for this article is to generate lesser code that is faster and more readable. Faster because the code generated is optimised for that functionality.

so take the example (again)

let numbers = "0318876523"
func getArray(numbers:String) -> [Int] {

var result: [Int] = []
for i in numbers {
    if let _num = Int(String(i)) {
        result.append(_num)
    }
}
return result

}

print(getArray(numbers: numbers))

This entire function can be reduced to a single line as

let result = numbers.compactMap { Int(String($0)) }

Which does exactly what we are after and strips out all of the non numbers. In fact it can further be used to transform it to other formats back to a sanitized string using

let resultx = numbers.compactMap{ Int(String($0)) }.map { String($0) }.joined()

Timing of methodologies

I tried the line above vs the replacingOccurences method a 1000 times to get a sense of timing.
The replacing method took 0.0216690 seconds for the 1000 runs where as the functional method took 0.49249, however it only replaces the dashes (hyphens) into blanks. If we had to replace more than one character, we would have to run it a couple of times for each character. This would soon become irrelevant because it is easier to know the characters we require than those that we should remove.

taking that further, we can also build a simple function that replaces a set of characters from a string as

var numbers = "0318876523"
let charsToRemove = "35678"

let numbers2 = charsToRemove.map{$0}
numbers2.reduce(into: numx) {$0 = $0.replacingOccurrences(of: String($1), with: "")}

this will remove the digits 3, 5, 6, 7 and 8 from the string numbers

So much for now, next time we shall look at transforming dictionaries, arrays etc using some functions methods.

Views All Time
Views All Time
1020
Views Today
Views Today
1
Posted in Article, Basics, Tip, Tutorial.