exercism

Exercism - Largest Series Product

This post shows you how to get Largest Series Product exercise of Exercism.

Stevinator Stevinator
11 min read
SHARE
exercism dart flutter largest-series-product

Preparation

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

Largest Series Product Exercise

So we need to use the following concepts.

Classes

Classes define blueprints for objects. They can contain methods that work together to solve a problem.

class LargestSeriesProduct {
  int largestProduct(String digits, int span) {
    // Calculate largest product logic
    return 0;
  }
}

void main() {
  LargestSeriesProduct calculator = LargestSeriesProduct();
  int result = calculator.largestProduct("63915", 3);
  print(result); // 162
}

ArgumentError

ArgumentError is thrown when a function receives an invalid argument. You can throw it to indicate that the input doesn’t meet the function’s requirements.

void main() {
  int largestProduct(String digits, int span) {
    // Validate span
    if (span < 0) {
      throw ArgumentError('span must not be negative');
    }
    
    if (span > digits.length) {
      throw ArgumentError('span must be smaller than string length');
    }
    
    // Process...
    return 0;
  }
  
  try {
    largestProduct("123", 5); // Throws ArgumentError
  } catch (e) {
    print(e); // ArgumentError: span must be smaller than string length
  }
}

Regular Expressions (RegExp)

Regular expressions allow you to match patterns in strings. The RegExp class and hasMatch() method are used to validate string formats.

void main() {
  // Create regex pattern for digits only
  RegExp digitPattern = RegExp(r'^\d*$');
  
  // Check if string matches
  print(digitPattern.hasMatch('123')); // true
  print(digitPattern.hasMatch('63915')); // true
  print(digitPattern.hasMatch('12a3')); // false (contains letter)
  print(digitPattern.hasMatch('12 3')); // false (contains space)
  
  // Use for validation
  String digits = "63915";
  if (!digitPattern.hasMatch(digits)) {
    throw ArgumentError('digits input must only contain digits');
  }
}

String substring() Method

The substring() method extracts a portion of a string. It takes a start index and optionally an end index. It’s perfect for extracting series of adjacent digits.

void main() {
  String digits = "63915";
  
  // Get substring from index to end
  String rest = digits.substring(2);
  print(rest); // "915"
  
  // Get substring with start and end
  String series1 = digits.substring(0, 3);
  print(series1); // "639"
  
  String series2 = digits.substring(1, 4);
  print(series2); // "391"
  
  // Extract all series of length 3
  for (int i = 0; i <= digits.length - 3; i++) {
    String series = digits.substring(i, i + 3);
    print(series); // "639", "391", "915"
  }
}

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 series = "639";
  
  // Split into individual characters
  List<String> chars = series.split('');
  print(chars); // ["6", "3", "9"]
  
  // Use with map to convert to integers
  List<int> numbers = series.split('').map(int.parse).toList();
  print(numbers); // [6, 3, 9]
}

List generate() Method

The List.generate() method creates a list by calling a function for each index. It’s perfect for generating all possible series positions.

void main() {
  String digits = "63915";
  int span = 3;
  
  // Generate all starting positions for series
  List<int> positions = List.generate(digits.length - span + 1, (i) => i);
  print(positions); // [0, 1, 2]
  
  // Generate all series
  List<String> series = List.generate(
    digits.length - span + 1,
    (i) => digits.substring(i, i + span)
  );
  print(series); // ["639", "391", "915"]
}

Iterable map() Method

The map() method transforms each element in an iterable. It returns a new iterable with transformed values. It’s used to convert strings to integers and calculate products.

void main() {
  List<String> series = ["639", "391", "915"];
  
  // Convert each series to list of integers
  List<List<int>> numbers = series.map((s) => s.split('').map(int.parse).toList()).toList();
  print(numbers); // [[6, 3, 9], [3, 9, 1], [9, 1, 5]]
  
  // Calculate product of each series
  List<int> products = series.map((s) {
    return s.split('').map(int.parse).fold(1, (p, d) => p * d);
  }).toList();
  print(products); // [162, 27, 45]
}

int.parse() Method

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 single digit
  int digit = int.parse('6');
  print(digit); // 6
  
  // Parse multiple digits
  int number = int.parse('639');
  print(number); // 639
  
  // Use with map to convert list of strings
  List<String> chars = ['6', '3', '9'];
  List<int> numbers = chars.map(int.parse).toList();
  print(numbers); // [6, 3, 9]
}

List fold() Method

The fold() method reduces a list to a single value by applying a function to each element and an accumulator. It’s perfect for calculating products.

void main() {
  List<int> numbers = [6, 3, 9];
  
  // Calculate product using fold
  int product = numbers.fold(1, (acc, value) => acc * value);
  print(product); // 162 (6 × 3 × 9)
  
  // Calculate sum using fold
  int sum = numbers.fold(0, (acc, value) => acc + value);
  print(sum); // 18 (6 + 3 + 9)
  
  // Use with string conversion
  String series = "639";
  int product2 = series.split('').map(int.parse).fold(1, (p, d) => p * d);
  print(product2); // 162
}

Iterable reduce() Method

The reduce() method reduces an iterable to a single value by applying a function to each element. It’s similar to fold() but doesn’t require an initial value.

void main() {
  List<int> numbers = [162, 27, 45];
  
  // Find maximum using reduce
  int max = numbers.reduce((a, b) => a > b ? a : b);
  print(max); // 162
  
  // Find minimum using reduce
  int min = numbers.reduce((a, b) => a < b ? a : b);
  print(min); // 27
}

max() Function from dart:math

The max() function from dart:math returns the larger of two numbers. It’s more concise than using reduce() for finding maximum values.

import 'dart:math';

void main() {
  List<int> numbers = [162, 27, 45];
  
  // Find maximum using max function
  int maximum = numbers.reduce(max);
  print(maximum); // 162
  
  // Compare two numbers
  int larger = max(162, 27);
  print(larger); // 162
}

Conditional (Ternary) Operator

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

void main() {
  int span = 0;
  
  // Ternary operator
  int result = span == 0 ? 1 : 0;
  print(result); // 1
  
  // Equivalent if-else
  int result2;
  if (span == 0) {
    result2 = 1;
  } else {
    result2 = 0;
  }
  
  // Use in return statement
  int calculate(int span) {
    return span == 0 ? 1 : calculateProduct(span);
  }
}

Method Chaining

Method chaining allows you to call multiple methods in sequence. Each method returns a value that can be used by the next method.

void main() {
  String digits = "63915";
  int span = 3;
  
  // Chain methods together
  int largest = List.generate(digits.length - span + 1, (i) => digits.substring(i, i + span))
      .map((s) => s.split('').map(int.parse).fold(1, (p, d) => p * d))
      .reduce(max);
  
  print(largest); // 162
  
  // Break down the chain:
  // 1. Generate all series
  // 2. Map each series to its product
  // 3. Reduce to find maximum
}

Introduction

You work for a government agency that has intercepted a series of encrypted communication signals from a group of bank robbers. The signals contain a long sequence of digits. Your team needs to use various digital signal processing techniques to analyze the signals and identify any patterns that may indicate the planning of a heist.

Instructions

Your task is to look for patterns in the long sequence of digits in the encrypted signal.

The technique you’re going to use here is called the largest series product.

Let’s define a few terms, first.

  • input: the sequence of digits that you need to analyze
  • series: a sequence of adjacent digits (those that are next to each other) that is contained within the input
  • span: how many digits long each series is
  • product: what you get when you multiply numbers together

Example

Let’s work through an example, with the input “63915”.

To form a series, take adjacent digits in the original input.

If you are working with a span of 3, there will be three possible series:

  • “639”
  • “391”
  • “915”

Then we need to calculate the product of each series:

  • The product of the series “639” is 162 (6 × 3 × 9 = 162)
  • The product of the series “391” is 27 (3 × 9 × 1 = 27)
  • The product of the series “915” is 45 (9 × 1 × 5 = 45)

162 is bigger than both 27 and 45, so the largest series product of “63915” is from the series “639”. So the answer is 162.

How do we find the largest series product?

To find the largest series product:

  1. Validate inputs: Check that span is non-negative, span ≤ length, and digits contain only digits
  2. Handle edge case: If span is 0, return 1 (empty product)
  3. Generate all series: Create all possible adjacent series of the given span
  4. Calculate products: For each series, convert to integers and multiply all digits
  5. Find maximum: Return the largest product among all series

The key insight is using List.generate() to create all starting positions, then using substring() to extract each series, map() and fold() to calculate products, and reduce(max) to find the maximum.

For example, with “63915” and span 3:

  • Generate positions: [0, 1, 2]
  • Extract series: [“639”, “391”, “915”]
  • Calculate products: [162, 27, 45]
  • Find maximum: 162

Solution

import 'dart:math';

class LargestSeriesProduct {
  int largestProduct(String digits, int span) {
    if (span < 0) throw ArgumentError('span must not be negative');
    if (span > digits.length) throw ArgumentError('span must be smaller than string length');
    if (!RegExp(r'^\d*$').hasMatch(digits)) throw ArgumentError('digits input must only contain digits');
    
    return span == 0
        ? 1
        : List.generate(digits.length - span + 1, (i) => digits.substring(i, i + span))
            .map((s) => s.split('').map(int.parse).fold(1, (p, d) => p * d))
            .reduce(max);
  }
}

Let’s break down the solution:

  1. import 'dart:math' - Import max function:

    • Imports max() function for finding maximum value
    • Used with reduce() to find largest product
  2. class LargestSeriesProduct - Main class:

    • Encapsulates the largest product calculation
    • Contains the main calculation method
  3. int largestProduct(String digits, int span) - Main method:

    • Takes string of digits and span length
    • Returns the largest product of any series
    • Validates inputs before processing
  4. if (span < 0) throw ArgumentError(...) - Validate span:

    • Checks that span is not negative
    • Throws descriptive error if invalid
    • Prevents negative span values
  5. if (span > digits.length) throw ArgumentError(...) - Validate span length:

    • Checks that span doesn’t exceed string length
    • Throws error if span is too large
    • Example: span=5 for “123” is invalid
  6. if (!RegExp(r'^\d*$').hasMatch(digits)) - Validate digits:

    • Regular expression ^\d*$ matches strings with only digits
    • ^ = start of string
    • \d* = zero or more digits
    • $ = end of string
    • Throws error if string contains non-digits
  7. throw ArgumentError('digits input must only contain digits') - Error for invalid digits:

    • Provides clear error message
    • Indicates what went wrong
  8. return span == 0 ? 1 : ... - Handle edge case:

    • Ternary operator for concise conditional
    • If span is 0, return 1 (empty product convention)
    • Otherwise, calculate largest product
  9. List.generate(digits.length - span + 1, (i) => ...) - Generate all series:

    • Creates list of all possible starting positions
    • digits.length - span + 1 = number of possible series
    • Example: “63915” (length 5), span 3 → 5-3+1 = 3 positions [0, 1, 2]
  10. digits.substring(i, i + span) - Extract series:

    • Gets substring starting at index i with length span
    • Example: i=0, span=3 → “639”
    • Example: i=1, span=3 → “391”
    • Example: i=2, span=3 → “915”
  11. .map((s) => ...) - Transform to products:

    • Maps each series string to its product
    • Transforms list of strings to list of products
  12. s.split('') - Split to characters:

    • Converts string to list of single characters
    • Example: “639” → [“6”, “3”, “9”]
  13. .map(int.parse) - Convert to integers:

    • Parses each character string to integer
    • Example: [“6”, “3”, “9”] → [6, 3, 9]
  14. .fold(1, (p, d) => p * d) - Calculate product:

    • Folds list to single product value
    • Starts with 1 (multiplicative identity)
    • Multiplies each digit with accumulator
    • Example: [6, 3, 9] → 1×6×3×9 = 162
  15. .reduce(max) - Find maximum:

    • Reduces list of products to maximum value
    • Uses max() function from dart:math
    • Example: [162, 27, 45] → 162

The solution efficiently finds the largest series product using method chaining to generate all series, calculate their products, and find the maximum. Input validation ensures the function handles edge cases and invalid inputs gracefully.


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.