exercism

Exercism - Perfect Numbers

This post shows you how to get Perfect Numbers exercise of Exercism.

Stevinator Stevinator
9 min read
SHARE
exercism dart flutter perfect-numbers

Preparation

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

Perfect Numbers Exercise

So we need to use the following concepts.

Enums

Enums define a set of named constants. They’re useful for representing a fixed set of related values, like classification types.

enum Classification { perfect, abundant, deficient }

void main() {
  // Use enum values
  Classification type = Classification.perfect;
  print(type); // Classification.perfect
  
  // Compare enum values
  if (type == Classification.perfect) {
    print('Perfect number!');
  }
  
  // Switch on enum
  switch (type) {
    case Classification.perfect:
      print('Perfect');
      break;
    case Classification.abundant:
      print('Abundant');
      break;
    case Classification.deficient:
      print('Deficient');
      break;
  }
}

ArgumentError and Exception Handling

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 divide(int a, int b) {
    if (b == 0) {
      throw ArgumentError('Cannot divide by zero');
    }
    return a ~/ b;
  }
  
  int classify(int number) {
    if (number <= 0) {
      throw ArgumentError('Number must be positive');
    }
    // Classification logic...
    return 0;
  }
  
  try {
    classify(-5); // Throws ArgumentError
  } catch (e) {
    print(e); // ArgumentError: Number must be positive
  }
}

Helper Methods

Helper methods (often private methods starting with _) encapsulate reusable logic, making code more organized and maintainable.

class PerfectNumbers {
  // Public method
  Classification classify(int number) {
    final sum = _aliquotSum(number);
    return sum == number ? Classification.perfect : Classification.deficient;
  }
  
  // Private helper method
  int _aliquotSum(int n) {
    // Calculate sum of factors
    return 0;
  }
  
  // Another helper method
  List<int> _factors(int n) {
    // Find all factors
    return [];
  }
}

Finding Factors Efficiently

To find all factors of a number efficiently, you only need to check up to the square root of the number. For each divisor i found, you also get its pair n / i.

void main() {
  List<int> findFactors(int n) {
    final factors = <int>[1]; // 1 is always a factor
    
    // Only check up to sqrt(n)
    for (int i = 2; i * i <= n; i++) {
      if (n % i == 0) {
        factors.add(i); // Add the divisor
        if (i != n ~/ i) {
          factors.add(n ~/ i); // Add its pair
        }
      }
    }
    return factors;
  }
  
  // Example: factors of 12
  // i=2: 12 % 2 == 0, add 2 and 6
  // i=3: 12 % 3 == 0, add 3 and 4
  // i=4: 4*4 > 12, stop
  // Result: [1, 2, 6, 3, 4]
  print(findFactors(12)); // [1, 2, 6, 3, 4]
}

Modulo Operator

The modulo operator (%) returns the remainder of a division operation. It’s essential for checking divisibility.

void main() {
  // Check if a number is divisible by another
  int n = 12;
  
  print(12 % 2); // 0 (12 is divisible by 2)
  print(12 % 3); // 0 (12 is divisible by 3)
  print(12 % 5); // 2 (12 is not divisible by 5)
  
  // Check divisibility
  if (n % i == 0) {
    print('$i is a factor of $n');
  }
}

Integer Division

The integer division operator (~/) divides two numbers and returns an integer result, truncating any decimal part.

void main() {
  int n = 12;
  
  // Get the pair factor
  int i = 3;
  int pair = n ~/ i;
  print(pair); // 4 (12 / 3 = 4)
  
  // Avoid adding duplicate factors
  if (i != n ~/ i) {
    print('Different factors');
  }
  
  // Example: for n=16, i=4
  // 4 == 16 ~/ 4 (both are 4), so don't add duplicate
}

Reduce Method

The reduce() method combines all elements of a collection into a single value using a combining function. It’s perfect for summing a list of numbers.

void main() {
  List<int> factors = [1, 2, 3, 4, 6];
  
  // Sum all factors
  int sum = factors.reduce((a, b) => a + b);
  print(sum); // 16
  
  // Product of all factors
  int product = factors.reduce((a, b) => a * b);
  print(product); // 144
  
  // Find maximum
  int max = factors.reduce((a, b) => a > b ? a : b);
  print(max); // 6
}

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 sum = 6;
  int number = 6;
  
  // Simple ternary
  String result = sum == number ? 'equal' : 'not equal';
  print(result); // 'equal'
  
  // Nested ternary
  String classification = sum == number 
      ? 'perfect'
      : sum > number 
          ? 'abundant' 
          : 'deficient';
  print(classification); // 'perfect'
  
  // With return
  Classification classify(int sum, int number) {
    return sum == number 
        ? Classification.perfect
        : sum > number 
            ? Classification.abundant 
            : Classification.deficient;
  }
}

Lists

Lists are ordered collections of items. You can add elements, iterate over them, and perform various operations.

void main() {
  // Create empty list
  List<int> factors = <int>[];
  
  // Add elements
  factors.add(1);
  factors.add(2);
  factors.add(3);
  
  // List literal with type
  List<int> numbers = <int>[1, 2, 3];
  
  // Iterate
  for (var factor in factors) {
    print(factor);
  }
}

Introduction

Determine if a number is perfect, abundant, or deficient based on Nicomachus’ (60 - 120 CE) classification scheme for positive integers.

The Greek mathematician Nicomachus devised a classification scheme for positive integers, identifying each as belonging uniquely to the categories of perfect, abundant, or deficient based on their aliquot sum. The aliquot sum is defined as the sum of the factors of a number not including the number itself. For example, the aliquot sum of 15 is 1 + 3 + 5 = 9.

Perfect

A number is perfect when it equals its aliquot sum. For example:

  • 6 is a perfect number because 1 + 2 + 3 = 6
  • 28 is a perfect number because 1 + 2 + 4 + 7 + 14 = 28

Abundant

A number is abundant when it is less than its aliquot sum. For example:

  • 12 is an abundant number because 1 + 2 + 3 + 4 + 6 = 16
  • 24 is an abundant number because 1 + 2 + 3 + 4 + 6 + 8 + 12 = 36

Deficient

A number is deficient when it is greater than its aliquot sum. For example:

  • 8 is a deficient number because 1 + 2 + 4 = 7
  • Prime numbers are deficient

Task

Implement a way to determine whether a given number is perfect. Depending on your language track, you may also need to implement a way to determine whether a given number is abundant or deficient.

What is an aliquot sum?

The aliquot sum of a positive integer is the sum of all proper divisors of the number (all divisors except the number itself). For example, the aliquot sum of 12 is 1 + 2 + 3 + 4 + 6 = 16. This concept is fundamental to number theory and was used by ancient mathematicians like Nicomachus to classify numbers.

— Number Theory

How can we classify numbers?

To classify a number:

  1. Find all factors: Find all divisors of the number (excluding the number itself)
  2. Calculate aliquot sum: Sum all the factors
  3. Compare:
    • If sum == number → Perfect
    • If sum > number → Abundant
    • If sum < number → Deficient
  4. Handle edge cases: Throw an error for non-positive numbers

The key insight is efficiently finding factors by only checking up to the square root of the number, since for each divisor i, we also get its pair n / i.

For example, for number 12:

  • Factors (excluding 12): 1, 2, 3, 4, 6
  • Aliquot sum: 1 + 2 + 3 + 4 + 6 = 16
  • 16 > 12 → Abundant

Solution

enum Classification { perfect, abundant, deficient }

class PerfectNumbers {
  Classification classify(int number) {
    if (number <= 0) throw ArgumentError();

    final sum = _aliquotSum(number);

    return sum == number
        ? Classification.perfect
        : sum > number
            ? Classification.abundant
            : Classification.deficient;
  }

  int _aliquotSum(int n) =>
      n == 1 ? 0 : _factors(n).reduce((a, b) => a + b);

  List<int> _factors(int n) {
    final factors = <int>[1];
    for (int i = 2; i * i <= n; i++) {
      if (n % i == 0) {
        factors.add(i);
        if (i != n ~/ i) factors.add(n ~/ i);
      }
    }
    return factors;
  }
}

Let’s break down the solution:

  1. enum Classification - Defines the three possible classifications:

    • perfect - Number equals its aliquot sum
    • abundant - Number is less than its aliquot sum
    • deficient - Number is greater than its aliquot sum
  2. List<int> _factors(int n) - Helper method that finds all factors of a number:

    • Initialize: Starts with [1] since 1 is always a factor
    • Efficient loop: Only checks up to sqrt(n) (using i * i <= n)
    • Find divisors: For each i where n % i == 0, i is a divisor
    • Add pair: Also adds n ~/ i (the pair factor), unless it’s a perfect square (avoiding duplicates)
    • Example: For n=12, finds 1, 2, 6, 3, 4 (excluding 12 itself)
  3. int _aliquotSum(int n) - Helper method that calculates the aliquot sum:

    • Edge case: Returns 0 for n=1 (1 has no proper divisors)
    • Calculate sum: Uses reduce() to sum all factors found by _factors(n)
    • Returns the sum of all proper divisors
  4. Classification classify(int number) - Main method that classifies a number:

    • Validation: Throws ArgumentError if number is not positive
    • Calculate sum: Gets the aliquot sum using _aliquotSum(number)
    • Classify: Uses nested ternary operators to determine classification:
      • If sum == numberClassification.perfect
      • Else if sum > numberClassification.abundant
      • Else → Classification.deficient

The solution efficiently finds factors by only checking up to the square root, making it much faster than checking all numbers up to n. The helper methods keep the code organized and the main classify method clean and readable.


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.