I have the requirement to validate product codes which include EAN-8, EAN-13 and UPC-A encoded as EAN-13. I have implemented a use-case to perform this check by comparing the actual check digit with the computed one. I followed this explanation for EAN-13 and that one for EAN-8.
The reason I'm asking for a review is because I haven't worked with EANs before and also noticed that my previous versions that had logical flaws still passed the tests with some codes, so the error wasn't obvious.
Example: in an earlier version, I did not drop the last digit (which is the check digit), so it was mistakenly used in the calculation. The result was that the tests passed for the UPC-A (wrapped as EAN-13) code that can be seen in the test, but failed for a "real" EAN-13 and EAN-8, adding to my confusion.
Another example of a bad idea was giving ChatGPT a shot on solving the problem because the thing often contradicted itself and produced code that worked with some codes and didn't work with others - so be careful with that one because you might go from "I somewhat know what I'm doing and want to save time" to "I have no idea, throw everything away and start from scratch".
Note: the code can be seen in action on the Kotlin Playground
Description: the code format is valid if it consists of exactly 8 or 13 digits.
- take the last digit - this is the reference check digit
- take the input digits
- remove the check digit
- convert the rest to a List of Ints
- compute the check digit
In the sumForEAN8()
and sumForEAN13()
extensions, you may notice that the calculation is exactly opposite of the specification, e.g. for EAN-13, I multiply odd digits by 3, whereby the spec says one should do that for even digits. The reason is that the spec divides all digits in even and odd based on the complete number including the check digit. Since we remove the check digit before proceeding with the calculation, the "odd" and "even" numbering basically shifts by one. now I realize that this explanation is wrong (because digits are iterated left-to-right, so dropping the last one cannot affect even/odd): the swap is still needed, but the real reason is that we have 0-based indices in programming, while the spec uses a 1-based index)
internal class ValidateEANUseCase() {
private val eanFormatRegex = Regex("^\\d{8}|\\d{13}$")
fun run(ean: String): Boolean = when {
ean.hasInvalidFormat() -> false
else -> {
val actualCheckDigit = ean.last()
.digitToInt()
val computedCheckDigit = ean
.dropLast(1)
.map { char -> char.digitToInt() }
.computeEANCheckDigit()
actualCheckDigit == computedCheckDigit
}
}
private fun String.hasInvalidFormat(): Boolean = !this.matches(eanFormatRegex)
private fun List<Int>.computeEANCheckDigit(): Int {
val sum = if (size == 7) sumForEAN8() else sumForEAN13()
return (10 - (sum % 10)) % 10
}
private fun List<Int>.sumForEAN8(): Int = withIndex()
.sumOf { (index, digit) ->
if (index % 2 == 0) digit * 3 else digit
}
private fun List<Int>.sumForEAN13(): Int = withIndex()
.sumOf { (index, digit) ->
if (index % 2 == 0) digit else digit * 3
}
}
Below are the unit tests (all passing; tested
refers to the instance of the class being tested). Any further inputs to consider?
@Test
fun `GIVEN a valid EAN-8 number THEN the validation result is TRUE`() {
val result = tested.run(VALID_EAN_8)
assertThat(result).isTrue
}
@Test
fun `GIVEN a valid EAN-13 number THEN the validation result is TRUE`() {
val result = tested.run(VALID_EAN_13)
assertThat(result).isTrue
}
@Test
fun `GIVEN a valid UPC-A number encoded as EAN-13 THEN the validation result is TRUE`() {
val result = tested.run(VALID_UPC_A_ENCODED_AS_EAN_13)
assertThat(result).isTrue
}
@Test
fun `GIVEN ANY invalid input THEN the validation result is FALSE`() {
val results = mutableListOf<Boolean>()
invalidInputs.forEach {
results.add(tested.run(it))
}
assertThat(results).allMatch { it == false }
}
private companion object {
const val VALID_EAN_13 = "5060762541604"
const val VALID_EAN_8 = "42307167"
const val VALID_UPC_A_ENCODED_AS_EAN_13 = "0670367004760"
val invalidInputs = listOf(
"", " ", "\n\t", "\\§$%&()=?ß`+´ü-!äö'#*´;¿\"@<>", "123", "12345678",
"5060962541604", "00123456789012", "hello EAN", "привет EAN", "你好 EAN"
)
}
Thank you.