exercism

Exercism - Luhn

This post shows you how to get Luhn exercise of Exercism.

Stevinator Stevinator
11 min read
SHARE
exercism dart flutter luhn

Preparation

Before we click on our next exercise, let’s see what concepts of DART we need to consider

Luhn Exercise

So we need to use the following concepts.

String ReplaceAll Method

The replaceAll() method replaces all occurrences of a pattern in a string with another string. It’s useful for removing or replacing characters.

void main() {
  String cardNumber = "4539 3195 0343 6467";
  
  // Remove all spaces
  String cleaned = cardNumber.replaceAll(' ', '');
  print(cleaned); // "4539319503436467"
  
  // Remove other characters
  String text = "4539-3195-0343-6467";
  String noDashes = text.replaceAll('-', '');
  print(noDashes); // "4539319503436467"
}

String Length Property

The length property returns the number of characters in a string. It’s useful for validation.

void main() {
  String number = "4539319503436467";
  
  // Check length
  print(number.length); // 16
  
  // Validate minimum length
  if (number.length <= 1) {
    print('Invalid: too short');
  }
  
  // Check if valid length
  bool isValidLength = number.length > 1;
  print(isValidLength); // true
}

Regular Expressions (RegExp)

Regular expressions allow you to match patterns in strings. The hasMatch() method checks if a string matches a pattern.

void main() {
  // Pattern: only digits
  RegExp pattern = RegExp(r'^\d+$');
  
  // Valid patterns
  print(pattern.hasMatch('4539319503436467')); // true
  print(pattern.hasMatch('123')); // true
  
  // Invalid patterns
  print(pattern.hasMatch('4539 3195')); // false (contains space)
  print(pattern.hasMatch('4539-3195')); // false (contains dash)
  print(pattern.hasMatch('abc123')); // false (contains letters)
  
  // Pattern breakdown:
  // ^ - start of string
  // \d+ - one or more digits
  // $ - end of string
}

For Loops

For loops allow you to iterate a specific number of times. They’re perfect for processing each character in a string.

void main() {
  String number = "4539319503436467";
  
  // Iterate through each position
  for (int i = 0; i < number.length; i++) {
    print('Position $i: ${number[i]}');
  }
  
  // Process from right to left
  for (int i = 0; i < number.length; i++) {
    int rightIndex = number.length - 1 - i;
    print('Right to left position $i: ${number[rightIndex]}');
  }
}

String Indexing

You can access individual characters in a string using square brackets with an index. The first character is at index 0.

void main() {
  String number = "4539";
  
  // Access by index
  print(number[0]); // '4'
  print(number[1]); // '5'
  print(number[2]); // '3'
  print(number[3]); // '9'
  
  // Access from right
  int len = number.length;
  print(number[len - 1]); // '9' (last character)
  print(number[len - 2]); // '3' (second from right)
  
  // Access from right using calculation
  for (int i = 0; i < number.length; i++) {
    int rightIndex = number.length - 1 - i;
    print(number[rightIndex]); // 9, 3, 5, 4
  }
}

Integer Parsing

The int.parse() method converts a string to an integer. It’s needed when processing string characters as numbers.

void main() {
  String digit = "5";
  
  // Parse to integer
  int num = int.parse(digit);
  print(num); // 5
  
  // Parse character from string
  String number = "4539";
  int firstDigit = int.parse(number[0]);
  print(firstDigit); // 4
  
  // Process each digit
  for (int i = 0; i < number.length; i++) {
    int digit = int.parse(number[i]);
    print(digit);
  }
}

isOdd Property

The isOdd property returns true if an integer is odd, and false if it’s even. It’s useful for identifying every second element when counting from 0.

void main() {
  // Check if index is odd
  print(0.isOdd); // false (even)
  print(1.isOdd); // true (odd)
  print(2.isOdd); // false (even)
  print(3.isOdd); // true (odd)
  
  // Use to identify every second element
  for (int i = 0; i < 10; i++) {
    if (i.isOdd) {
      print('Index $i is odd - process this');
    }
  }
  // Output: Index 1, 3, 5, 7, 9 are odd
}

Modulo Operator

The modulo operator (%) returns the remainder of a division operation. It’s used to check if a number is evenly divisible by another.

void main() {
  int sum = 80;
  
  // Check if divisible by 10
  int remainder = sum % 10;
  print(remainder); // 0 (80 is divisible by 10)
  
  // Validate Luhn
  bool isValid = sum % 10 == 0;
  print(isValid); // true
  
  // Another example
  int sum2 = 36;
  bool isValid2 = sum2 % 10 == 0;
  print(isValid2); // false (36 is not divisible by 10)
}

Conditional (Ternary) Operator

The ternary operator (? :) provides a concise way to write if-else statements. It’s useful for simple conditional assignments.

void main() {
  int digit = 7;
  
  // Double the digit
  int doubled = digit * 2;
  print(doubled); // 14
  
  // If > 9, subtract 9; otherwise use doubled value
  int result = doubled > 9 ? doubled - 9 : doubled;
  print(result); // 5 (14 - 9 = 5)
  
  // One-liner
  int processed = digit * 2 > 9 ? digit * 2 - 9 : digit * 2;
  print(processed); // 5
  
  // Equivalent to:
  if (digit * 2 > 9) {
    processed = digit * 2 - 9;
  } else {
    processed = digit * 2;
  }
}

Introduction

At the Global Verification Authority, you’ve just been entrusted with a critical assignment. Across the city, from online purchases to secure logins, countless operations rely on the accuracy of numerical identifiers like credit card numbers, bank account numbers, transaction codes, and tracking IDs. The Luhn algorithm is a simple checksum formula used to help identify mistyped numbers.

A batch of identifiers has just arrived on your desk. All of them must pass the Luhn test to ensure they’re legitimate. If any fail, they’ll be flagged as invalid, preventing mistakes such as incorrect transactions or failed account verifications.

Can you ensure this is done right? The integrity of many services depends on you.

Instructions

Determine whether a number is valid according to the Luhn formula.

The number will be provided as a string.

Validating a Number

Strings of length 1 or less are not valid. Spaces are allowed in the input, but they should be stripped before checking. All other non-digit characters are disallowed.

Example 1: Valid Credit Card Number

The number to be checked is 4539 3195 0343 6467.

The first step of the Luhn algorithm is to start at the end of the number and double every second digit, beginning with the second digit from the right and moving left.

4539 3195 0343 6467
↑ ↑  ↑ ↑  ↑ ↑  ↑ ↑  (double these)

If the result of doubling a digit is greater than 9, we subtract 9 from that result. We end up with:

8569 6195 0383 3437

Finally, we sum all digits. If the sum is evenly divisible by 10, the original number is valid.

8 + 5 + 6 + 9 + 6 + 1 + 9 + 5 + 0 + 3 + 8 + 3 + 3 + 4 + 3 + 7 = 80

80 is evenly divisible by 10, so number 4539 3195 0343 6467 is valid!

Example 2: Invalid Canadian SIN

The number to be checked is 066 123 478.

We start at the end of the number and double every second digit, beginning with the second digit from the right and moving left.

066 123 478
 ↑  ↑ ↑  ↑  (double these)

If the result of doubling a digit is greater than 9, we subtract 9 from that result. We end up with:

036 226 458

We sum the digits:

0 + 3 + 6 + 2 + 2 + 6 + 4 + 5 + 8 = 36

36 is not evenly divisible by 10, so number 066 123 478 is not valid!

What is the Luhn algorithm?

The Luhn algorithm, also known as the “modulus 10” algorithm, is a simple checksum formula used to validate a variety of identification numbers, including credit card numbers, IMEI numbers, and Canadian Social Insurance Numbers. It was created by IBM scientist Hans Peter Luhn and described in U.S. Patent No. 2,950,048, filed on January 6, 1954, and granted on August 23, 1960.

— Wikipedia

How can we validate using the Luhn algorithm?

To validate a number using the Luhn algorithm:

  1. Clean the input: Remove spaces (spaces are allowed but should be removed)
  2. Validate format: Check that length > 1 and contains only digits
  3. Process from right to left:
    • Start at the rightmost digit
    • For every second digit (odd indices when counting from right), double it
    • If doubling results in > 9, subtract 9
  4. Sum all digits: Add up all the processed digits
  5. Check validity: If the sum is divisible by 10, the number is valid

The key insight is processing digits from right to left and doubling every second digit, with special handling for digits that become > 9 after doubling.

For example, with “4539 3195 0343 6467”:

  • Remove spaces: “4539319503436467”
  • Process from right (indices 0, 1, 2, 3… from right):
    • Index 0 (rightmost): 7 → 7
    • Index 1: 6 → 6 × 2 = 12 → 12 - 9 = 3
    • Index 2: 4 → 4
    • Index 3: 3 → 3 × 2 = 6
    • … and so on
  • Sum: 80
  • 80 % 10 == 0 → Valid!

Solution

class Luhn {
  bool valid(String value) {
    final d = value.replaceAll(' ', '');
    if (d.length <= 1 || !RegExp(r'^\d+$').hasMatch(d)) return false;
    
    int sum = 0;
    for (int i = 0; i < d.length; i++) {
      int n = int.parse(d[d.length - 1 - i]);
      if (i.isOdd) n = n * 2 > 9 ? n * 2 - 9 : n * 2;
      sum += n;
    }
    return sum % 10 == 0;
  }
}

Let’s break down the solution:

  1. bool valid(String value) - Main method that validates a number:

    • Takes a string (may contain spaces) as input
    • Returns true if valid according to Luhn, false otherwise
  2. final d = value.replaceAll(' ', '') - Remove spaces:

    • Removes all spaces from the input string
    • Spaces are allowed but must be removed before processing
    • Example: “4539 3195 0343 6467” → “4539319503436467”
  3. if (d.length <= 1 || !RegExp(r'^\d+$').hasMatch(d)) return false - Validate format:

    • Length check: d.length <= 1 - Strings of length 1 or less are invalid
    • Pattern check: !RegExp(r'^\d+$').hasMatch(d) - Must contain only digits
      • ^\d+$ means: start to end, one or more digits
    • Returns false immediately if validation fails
  4. int sum = 0 - Initialize sum:

    • Will accumulate the sum of all processed digits
  5. for (int i = 0; i < d.length; i++) - Loop through all positions:

    • i represents the position from the right (0 = rightmost, 1 = second from right, etc.)
    • Iterates through all digits in the number
  6. int n = int.parse(d[d.length - 1 - i]) - Get digit from right:

    • d.length - 1 - i: Calculates the index from the left
      • When i=0: gets rightmost digit (index length-1)
      • When i=1: gets second from right (index length-2)
      • And so on
    • int.parse(...): Converts the character to an integer
    • Example: For “4539”, when i=0: gets ‘9’ (rightmost)
  7. if (i.isOdd) n = n * 2 > 9 ? n * 2 - 9 : n * 2 - Double every second digit:

    • i.isOdd: Checks if position from right is odd (1, 3, 5, …)
      • These are the positions that should be doubled (second, fourth, sixth from right)
    • n * 2 > 9 ? n * 2 - 9 : n * 2: Ternary operator
      • If doubling results in > 9: subtract 9
      • Otherwise: use the doubled value
    • Example: If n=6 and i=1 (odd): 6 × 2 = 12 > 9 → 12 - 9 = 3
  8. sum += n - Add to sum:

    • Accumulates the processed digit value
    • After the loop, sum contains the total of all processed digits
  9. return sum % 10 == 0 - Check validity:

    • If sum modulo 10 equals 0, the number is valid
    • Returns true for valid numbers, false otherwise

The solution efficiently implements the Luhn algorithm by processing digits from right to left, doubling every second digit, and checking if the sum is divisible by 10.


A video tutorial for this exercise is coming soon! In the meantime, check out my YouTube channel for more Dart and Flutter tutorials. 😉

Visit My YouTube Channel
Stevinator

Stevinator

Stevinator is a software engineer passionate about clean code and best practices. Loves sharing knowledge with the developer community.