exercism

Exercism - ISBN Verifier

This post shows you how to get ISBN Verifier exercise of Exercism.

Stevinator Stevinator
10 min read
SHARE
exercism dart flutter isbn-verifier

Preparation

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

ISBN Verifier 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 isbn = "3-598-21508-8";
  
  // Remove all dashes
  String cleaned = isbn.replaceAll('-', '');
  print(cleaned); // "3598215088"
  
  // Replace characters
  String text = "hello world";
  String replaced = text.replaceAll(' ', '-');
  print(replaced); // "hello-world"
}

String Length Property

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

void main() {
  String isbn = "3598215088";
  
  // Check length
  print(isbn.length); // 10
  
  // Validate length
  if (isbn.length != 10) {
    print('Invalid length');
  }
}

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: 9 digits followed by a digit or X
  RegExp pattern = RegExp(r'^\d{9}[\dX]$');
  
  // Valid ISBN patterns
  print(pattern.hasMatch('3598215088')); // true
  print(pattern.hasMatch('359821507X')); // true
  print(pattern.hasMatch('359821508')); // false (too short)
  print(pattern.hasMatch('35982150888')); // false (too long)
  print(pattern.hasMatch('359821508A')); // false (invalid character)
  
  // Pattern breakdown:
  // ^ - start of string
  // \d{9} - exactly 9 digits
  // [\dX] - one digit or X
  // $ - end of string
}

String Split Method

The split() method divides a string into a list of substrings. When called with an empty string '', it splits the string into individual characters.

void main() {
  String isbn = "3598215088";
  
  // Split into individual characters
  List<String> chars = isbn.split('');
  print(chars); // [3, 5, 9, 8, 2, 1, 5, 0, 8, 8]
  
  // Process each character
  for (var char in chars) {
    print(char);
  }
}

Fold Method

The fold() method reduces a collection to a single value by iteratively combining elements. It’s perfect for calculating sums with multipliers.

void main() {
  List<String> digits = ['3', '5', '9', '8'];
  int multiplier = 4;
  
  // Sum with decreasing multiplier
  int sum = digits.fold<int>(
    0,
    (total, char) {
      int value = int.parse(char);
      int result = total + value * multiplier;
      multiplier--; // Decrease multiplier
      return result;
    },
  );
  
  print(sum); // 3*4 + 5*3 + 9*2 + 8*1 = 12 + 15 + 18 + 8 = 53
}

Post-Decrement Operator

The post-decrement operator (--) decreases a variable by 1 after using its current value. It’s useful for decreasing multipliers in loops.

void main() {
  int multiplier = 10;
  
  // Post-decrement: use value, then decrease
  int current = multiplier--;
  print(current); // 10
  print(multiplier); // 9
  
  // In fold
  List<String> chars = ['3', '5', '9'];
  int mult = 3;
  int sum = chars.fold<int>(0, (total, char) => 
    total + int.parse(char) * mult--
  );
  // mult starts at 3, decreases: 3, 2, 1
  print(sum); // 3*3 + 5*2 + 9*1 = 9 + 10 + 9 = 28
}

Integer Parsing

The int.parse() method converts a string to an integer. It throws an exception if the string is not a valid integer.

void main() {
  // Parse digits
  int num1 = int.parse('5');
  print(num1); // 5
  
  // Handle X as 10
  String char = 'X';
  int value = char == 'X' ? 10 : int.parse(char);
  print(value); // 10
  
  // Conditional parsing
  String digit = '8';
  int val = digit == 'X' ? 10 : int.parse(digit);
  print(val); // 8
}

Modulo Operator

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

void main() {
  int sum = 330;
  
  // Check if divisible by 11
  int remainder = sum % 11;
  print(remainder); // 0 (330 is divisible by 11)
  
  // Validate ISBN
  bool isValid = sum % 11 == 0;
  print(isValid); // true
}

Conditional Logic

Conditional statements allow you to execute different code based on conditions. The ternary operator provides a concise way to write simple conditionals.

void main() {
  String char = 'X';
  
  // Ternary operator
  int value = char == 'X' ? 10 : int.parse(char);
  print(value); // 10
  
  // Multiple conditions
  String isbn = "3598215088";
  bool valid = isbn.length == 10 && RegExp(r'^\d{9}[\dX]$').hasMatch(isbn);
  print(valid); // true
}

Introduction

The ISBN-10 verification process is used to validate book identification numbers. These normally contain dashes and look like: 3-598-21508-8

ISBN

The ISBN-10 format is 9 digits (0 to 9) plus one check character (either a digit or an X only). In the case the check character is an X, this represents the value ‘10’. These may be communicated with or without hyphens, and can be checked for their validity by the following formula:

(d₁ × 10 + d₂ × 9 + d₃ × 8 + d₄ × 7 + d₅ × 6 + d₆ × 5 + d₇ × 4 + d₈ × 3 + d₉ × 2 + d₁₀ × 1) mod 11 == 0

If the result is 0, then it is a valid ISBN-10, otherwise it is invalid.

Example

Let’s take the ISBN-10 3-598-21508-8. We plug it in to the formula, and get:

(3 × 10 + 5 × 9 + 9 × 8 + 8 × 7 + 2 × 6 + 1 × 5 + 5 × 4 + 0 × 3 + 8 × 2 + 8 × 1) mod 11 == 0

Since the result is 0, this proves that our ISBN is valid.

Task

Given a string the program should check if the provided string is a valid ISBN-10. Putting this into place requires some thinking about preprocessing/parsing of the string prior to calculating the check digit for the ISBN.

The program should be able to verify ISBN-10 both with and without separating dashes.

Caveats

Converting from strings to numbers can be tricky in certain languages. Now, it’s even trickier since the check digit of an ISBN-10 may be ‘X’ (representing ‘10’). For instance 3-598-21507-X is a valid ISBN-10.

What is ISBN?

The International Standard Book Number (ISBN) is a numeric commercial book identifier which is intended to be unique. Publishers purchase ISBNs from an affiliate of the International ISBN Agency. An ISBN is assigned to each separate edition and variation (except reprintings) of a publication. For example, an e-book, a paperback and a hardback edition of the same book would each have a different ISBN.

— Wikipedia

How can we verify an ISBN-10?

To verify an ISBN-10:

  1. Remove dashes: Strip all hyphens from the input string
  2. Validate format: Check that the string has exactly 10 characters and matches the pattern (9 digits followed by a digit or X)
  3. Calculate weighted sum: For each character, multiply by a decreasing multiplier (10, 9, 8, …, 1)
    • If the character is ‘X’, use 10
    • Otherwise, parse the character as an integer
  4. Check validity: The sum modulo 11 must equal 0

For example, with “3-598-21508-8”:

  • Remove dashes: “3598215088”
  • Validate: 10 characters, matches pattern ✓
  • Calculate: 3×10 + 5×9 + 9×8 + 8×7 + 2×6 + 1×5 + 5×4 + 0×3 + 8×2 + 8×1 = 330
  • Check: 330 % 11 = 0 → Valid!

⚠️ Old Solution (No Longer Works)

Previously, the solution had issues with regex pattern matching and validation. Here’s what the old solution looked like:

bool isValid(String isbnString) {
  int i = 10;
  isbnString = isbnString.replaceAll('-', '');
  if (!isbnString.contains(RegExp(r'^\d{9}{\d|X}$'))) return false;
  return isbnString.split('').fold<int>(0, (pr, cu) => pr + i-- * (int.tryParse(cu) ?? 10)) % 11 == 0;
}

Why This Solution Doesn’t Work Anymore

The old solution has several issues:

  1. Incorrect regex pattern: The pattern r'^\d{9}{\d|X}$' is malformed. The { should be [ for a character class. The correct pattern should be r'^\d{9}[\dX]$' to match 9 digits followed by either a digit or X.

  2. Using contains() instead of hasMatch(): The contains() method checks if a substring matches anywhere in the string, not if the entire string matches the pattern. This can lead to false positives.

  3. Missing length validation: The solution doesn’t explicitly check that the ISBN has exactly 10 characters after removing dashes, which could allow invalid inputs.

  4. Using int.tryParse() with null coalescing: While this works, it’s less explicit than checking for ‘X’ directly, making the code harder to understand.

The exercise now requires:

  • Correct regex pattern using character class [\dX] instead of {\d|X}
  • Using hasMatch() to ensure the entire string matches the pattern
  • Explicit length validation for better error handling
  • Clearer handling of the ‘X’ character as 10

Solution

bool isValid(String isbnString) {
  final isbn = isbnString.replaceAll('-', '');

  if (isbn.length != 10 || !RegExp(r'^\d{9}[\dX]$').hasMatch(isbn)) {
    return false;
  }

  int multiplier = 10;
  final sum = isbn.split('').fold<int>(
    0,
    (total, char) => total + (char == 'X' ? 10 : int.parse(char)) * multiplier--,
  );

  return sum % 11 == 0;
}

Let’s break down the solution:

  1. final isbn = isbnString.replaceAll('-', '') - Removes all dashes from the input:

    • Converts “3-598-21508-8” to “3598215088”
    • Uses final to indicate the cleaned string won’t be reassigned
  2. if (isbn.length != 10 || !RegExp(r'^\d{9}[\dX]$').hasMatch(isbn)) - Validates the format:

    • Length check: Ensures exactly 10 characters after removing dashes
    • Pattern check: Uses regex r'^\d{9}[\dX]$' to verify:
      • ^ - Start of string
      • \d{9} - Exactly 9 digits
      • [\dX] - One digit or X (character class)
      • $ - End of string
    • hasMatch(): Checks if the entire string matches the pattern (not just contains it)
    • Returns false if validation fails
  3. int multiplier = 10 - Initializes the multiplier:

    • Starts at 10 and decreases for each character
    • Used in the weighted sum calculation
  4. isbn.split('').fold<int>(...) - Calculates the weighted sum:

    • split(''): Converts string to list of characters
    • fold<int>(0, ...): Starts with 0 and accumulates the sum
    • For each character:
      • If char == 'X', use 10
      • Otherwise, parse the character as an integer
      • Multiply by the current multiplier value
      • Use post-decrement (multiplier--) to decrease multiplier for next iteration
  5. return sum % 11 == 0 - Checks validity:

    • If the sum modulo 11 equals 0, the ISBN is valid
    • Returns true for valid ISBNs, false otherwise

The solution correctly validates ISBN-10 format and calculates the weighted sum, handling the special case of ‘X’ representing 10.


You can watch this tutorial on YouTube. So don’t forget to like and subscribe. 😉

Watch on YouTube
Stevinator

Stevinator

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