exercism

Exercism - Grade School

This post shows you how to get Grade School exercise of Exercism.

Stevinator Stevinator
13 min read
SHARE
exercism dart flutter grade-school

Preparation

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

Grade School Exercise

So we need to use the following concepts.

Classes and Private Fields

Classes define blueprints for objects. Private fields (prefixed with _) are only accessible within the same library, providing encapsulation.

class GradeSchool {
  // Private field - only accessible in this file
  final Map<int, Set<String>> _grades = {};
  final Set<String> _allStudents = {};
  
  // Public method
  List<String> roster() {
    return [];
  }
}

void main() {
  GradeSchool school = GradeSchool();
  // Can't access _grades or _allStudents from outside
  List<String> students = school.roster();
}

Maps with Sets as Values

Maps can store Sets as values, creating a structure where each key maps to a collection of unique items. This is perfect for organizing students by grade.

void main() {
  // Map from grade (int) to set of student names (Set<String>)
  Map<int, Set<String>> grades = {};
  
  // Add grade 1 with students
  grades[1] = {'Anna', 'Barb', 'Charlie'};
  
  // Add grade 2 with students
  grades[2] = {'Alex', 'Peter', 'Zoe'};
  
  // Access students in grade 1
  Set<String> grade1Students = grades[1]!;
  print(grade1Students); // {Anna, Barb, Charlie}
  
  // Check if grade exists
  if (grades.containsKey(3)) {
    print('Grade 3 exists');
  }
}

Sets

Sets are collections of unique elements. They automatically prevent duplicates, making them perfect for tracking all students in the school.

void main() {
  // Create set
  Set<String> allStudents = {};
  
  // Add students
  allStudents.add('Jim');
  allStudents.add('Anna');
  allStudents.add('Jim'); // Duplicate - won't be added
  
  print(allStudents); // {Jim, Anna}
  print(allStudents.length); // 2 (only unique students)
  
  // Check if student exists
  if (allStudents.contains('Jim')) {
    print('Jim is already in school');
  }
}

Records (Tuples)

Records allow you to group multiple values together. They’re perfect for representing student data as (name, grade) pairs.

void main() {
  // Record syntax: (name, grade)
  var student = ('Jim', 2);
  print(student); // (Jim, 2)
  
  // Destructure records
  var (name, grade) = student;
  print(name); // Jim
  print(grade); // 2
  
  // List of records
  List<(String, int)> students = [
    ('Jim', 2),
    ('Anna', 1),
    ('Alex', 2),
  ];
  
  // Iterate with destructuring
  for (var (name, grade) in students) {
    print('$name is in grade $grade');
  }
}

Map containsKey() Method

The containsKey() method checks if a map contains a specific key. It’s essential for checking if a grade exists before accessing it.

void main() {
  Map<int, Set<String>> grades = {
    1: {'Anna', 'Barb'},
    2: {'Alex', 'Peter'},
  };
  
  // Check if grade exists
  if (grades.containsKey(1)) {
    print('Grade 1 exists');
  }
  
  // Check if grade doesn't exist
  if (!grades.containsKey(3)) {
    print('Grade 3 does not exist');
    // Create new grade
    grades[3] = {};
  }
}

Map keys Property

The keys property returns an iterable of all keys in the map. You can convert it to a list and sort it.

void main() {
  Map<int, Set<String>> grades = {
    2: {'Alex', 'Peter'},
    1: {'Anna', 'Barb'},
    5: {'Jim'},
  };
  
  // Get all grade numbers
  Iterable<int> gradeNumbers = grades.keys;
  print(gradeNumbers); // (2, 1, 5)
  
  // Convert to list and sort
  List<int> sortedGrades = grades.keys.toList()..sort();
  print(sortedGrades); // [1, 2, 5]
}

Set contains() Method

The contains() method checks if a set contains a specific element. It’s used to detect duplicate students.

void main() {
  Set<String> allStudents = {'Jim', 'Anna', 'Alex'};
  
  // Check if student exists
  if (allStudents.contains('Jim')) {
    print('Jim is already enrolled');
  }
  
  // Check before adding
  String newStudent = 'Peter';
  if (!allStudents.contains(newStudent)) {
    allStudents.add(newStudent);
    print('Added Peter');
  } else {
    print('Peter already exists');
  }
}

Set add() Method

The add() method adds an element to a set. It returns true if the element was added (new), false if it already existed.

void main() {
  Set<String> students = {};
  
  // Add new student
  bool added = students.add('Jim');
  print(added); // true (was added)
  print(students); // {Jim}
  
  // Try to add duplicate
  bool addedAgain = students.add('Jim');
  print(addedAgain); // false (already exists)
  print(students); // {Jim} (unchanged)
}

List toList() Method

The toList() method converts an iterable (like a Set or Map keys) to a List. This is needed for sorting.

void main() {
  Set<String> students = {'Zoe', 'Alex', 'Peter'};
  
  // Convert set to list
  List<String> studentList = students.toList();
  print(studentList); // [Zoe, Alex, Peter] (order may vary)
  
  // Convert and sort
  List<String> sorted = students.toList()..sort();
  print(sorted); // [Alex, Peter, Zoe]
}

Cascade Operator (..)

The cascade operator (..) allows you to perform multiple operations on the same object in sequence. It’s perfect for method chaining when methods modify the object but return void.

void main() {
  List<String> students = ['Zoe', 'Alex', 'Peter'];
  
  // Without cascade: sort() returns void
  // List<String> sorted = students.sort(); // Error!
  
  // With cascade: sort() modifies list, then returns it
  List<String> sorted = (students.toList()..sort());
  print(sorted); // [Alex, Peter, Zoe]
  
  // Common pattern: toList() then sort()
  Set<String> studentSet = {'Zoe', 'Alex', 'Peter'};
  List<String> sortedList = studentSet.toList()..sort();
  print(sortedList); // [Alex, Peter, Zoe]
}

List sort() Method

The sort() method sorts a list in-place. For strings, it sorts alphabetically. It returns void, so use cascade operator to chain.

void main() {
  List<String> students = ['Zoe', 'Alex', 'Peter'];
  
  // Sort in-place
  students.sort();
  print(students); // [Alex, Peter, Zoe]
  
  // With cascade operator
  List<String> sorted = (students.toList()..sort());
  print(sorted); // [Alex, Peter, Zoe]
}

List addAll() Method

The addAll() method adds all elements from an iterable to a list. It’s perfect for combining multiple lists.

void main() {
  List<String> result = [];
  
  // Add single elements
  result.add('Anna');
  result.add('Barb');
  
  // Add multiple elements at once
  List<String> grade1 = ['Anna', 'Barb', 'Charlie'];
  result.addAll(grade1);
  print(result); // [Anna, Barb, Anna, Barb, Charlie]
  
  // Build result from multiple grades
  List<String> grade2 = ['Alex', 'Peter'];
  result.addAll(grade2);
  print(result); // [Anna, Barb, Anna, Barb, Charlie, Alex, Peter]
}

For-In Loops with Destructuring

For-in loops can destructure records directly in the loop variable, making it easy to iterate over (name, grade) pairs.

void main() {
  List<(String, int)> students = [
    ('Jim', 2),
    ('Anna', 1),
    ('Alex', 2),
  ];
  
  // Iterate with destructuring
  for (var (name, grade) in students) {
    print('$name is in grade $grade');
    // Jim is in grade 2
    // Anna is in grade 1
    // Alex is in grade 2
  }
}

Conditional Logic

Conditional logic (if, continue) is used to check for duplicates and skip invalid operations. It’s essential for validation.

void main() {
  Set<String> allStudents = {'Jim', 'Anna'};
  String newStudent = 'Jim';
  
  // Check if student already exists
  if (allStudents.contains(newStudent)) {
    print('Student already exists');
    // Skip adding
    return;
  }
  
  // Add new student
  allStudents.add(newStudent);
  print('Added student');
}

Null Assertion Operator (!)

The null assertion operator (!) tells Dart that a value is definitely not null. It’s used when accessing map values that we know exist.

void main() {
  Map<int, Set<String>> grades = {
    1: {'Anna', 'Barb'},
  };
  
  // After checking containsKey, we know the value exists
  if (grades.containsKey(1)) {
    Set<String> students = grades[1]!; // Safe to use !
    print(students); // {Anna, Barb}
  }
}

Introduction

Given students’ names along with the grade they are in, create a roster for the school.

In the end, you should be able to:

  1. Add a student’s name to the roster for a grade:

    • “Add Jim to grade 2.”
    • “OK.”
  2. Get a list of all students enrolled in a grade:

    • “Which students are in grade 2?”
    • “We’ve only got Jim right now.”
  3. Get a sorted list of all students in all grades. Grades should be sorted as 1, 2, 3, etc., and students within a grade should be sorted alphabetically by name.

    • “Who is enrolled in school right now?”
    • “Let me think. We have Anna, Barb, and Charlie in grade 1, Alex, Peter, and Zoe in grade 2, and Jim in grade 5. So the answer is: Anna, Barb, Charlie, Alex, Peter, Zoe, and Jim.”

Note that all our students only have one name (it’s a small town, what do you want?), and each student cannot be added more than once to a grade or the roster. If a test attempts to add the same student more than once, your implementation should indicate that this is incorrect.

How do we solve the grade school problem?

To solve the grade school problem:

  1. Data Structure: Use a Map<int, Set<String>> to store grades and their students

    • Key: grade number (int)
    • Value: set of student names (Set) - ensures uniqueness per grade
  2. Duplicate Tracking: Use a Set<String> to track all students across all grades

    • Prevents adding the same student to multiple grades
    • Quick lookup with contains()
  3. Add Students:

    • Check if student already exists in _allStudents
    • If exists, return false (duplicate)
    • If new, add to grade’s set and _allStudents, return true
  4. Get Grade:

    • Check if grade exists with containsKey()
    • Return sorted list of students in that grade
  5. Get Roster:

    • Sort grades numerically
    • For each grade, get students sorted alphabetically
    • Combine all students in order

The key insight is using Sets to ensure uniqueness and Maps to organize students by grade, with proper sorting for the final roster.

Solution

class GradeSchool {
  final Map<int, Set<String>> _grades = {};
  final Set<String> _allStudents = {};

  List<bool> add(List<(String, int)> students) {
    final results = <bool>[];
    
    for (final (name, grade) in students) {
      // Check if student already exists anywhere in the school
      if (_allStudents.contains(name)) {
        results.add(false);
        continue;
      }
      
      // Add student to the grade
      if (!_grades.containsKey(grade)) {
        _grades[grade] = {};
      }
      
      _grades[grade]!.add(name);
      _allStudents.add(name);
      results.add(true);
    }
    
    return results;
  }

  List<String> roster() {
    final result = <String>[];
    
    // Sort grades numerically
    final sortedGrades = _grades.keys.toList()..sort();
    
    // For each grade, get students sorted alphabetically
    for (final grade in sortedGrades) {
      final students = _grades[grade]!.toList()..sort();
      result.addAll(students);
    }
    
    return result;
  }

  List<String> grade(int gradeNumber) {
    if (!_grades.containsKey(gradeNumber)) {
      return [];
    }
    
    // Return students in this grade, sorted alphabetically
    final students = _grades[gradeNumber]!.toList()..sort();
    return students;
  }
}

Let’s break down the solution:

  1. class GradeSchool - Main class:

    • Encapsulates the grade school roster system
    • Manages students and their grades
  2. final Map<int, Set<String>> _grades = {} - Grade storage:

    • Maps grade numbers to sets of student names
    • Set ensures no duplicate students per grade
    • Private field (prefixed with _) for encapsulation
  3. final Set<String> _allStudents = {} - All students tracker:

    • Tracks all students across all grades
    • Prevents adding same student to multiple grades
    • Private field for encapsulation
  4. List<bool> add(List<(String, int)> students) - Add method:

    • Takes list of (name, grade) pairs
    • Returns list of booleans indicating success/failure for each
    • Handles multiple students in one call
  5. final results = <bool>[] - Results list:

    • Stores success/failure for each student addition
    • true = successfully added, false = duplicate
  6. for (final (name, grade) in students) - Iterate students:

    • Destructures each (name, grade) record
    • Processes each student addition
  7. if (_allStudents.contains(name)) - Check duplicate:

    • Checks if student already exists anywhere in school
    • Prevents adding same student twice
  8. results.add(false) and continue - Handle duplicate:

    • Adds false to results (duplicate detected)
    • Skips to next student (doesn’t add)
  9. if (!_grades.containsKey(grade)) - Check grade exists:

    • Checks if grade already has a set of students
    • If not, creates new empty set for that grade
  10. _grades[grade] = {} - Create grade set:

    • Initializes empty set for new grade
    • Allows adding students to this grade
  11. _grades[grade]!.add(name) - Add to grade:

    • Adds student name to grade’s set
    • Uses ! because we know grade exists (checked above)
    • Set automatically prevents duplicates within grade
  12. _allStudents.add(name) - Track student:

    • Adds student to global tracking set
    • Ensures we can detect duplicates in future additions
  13. results.add(true) - Success:

    • Adds true to results (successfully added)
    • Student is now in the roster
  14. return results - Return results:

    • Returns list of booleans for each student
    • Caller can see which additions succeeded/failed
  15. List<String> roster() - Get all students:

    • Returns sorted list of all students in all grades
    • Grades sorted numerically, students alphabetically
  16. final result = <String>[] - Result list:

    • Builds final sorted roster
    • Will contain all students in order
  17. final sortedGrades = _grades.keys.toList()..sort() - Sort grades:

    • Gets all grade numbers
    • Converts to list
    • Uses cascade operator to sort in-place
    • Result: [1, 2, 3, 5, …] (numerically sorted)
  18. for (final grade in sortedGrades) - Iterate grades:

    • Processes each grade in numerical order
    • Ensures grades appear 1, 2, 3, etc.
  19. final students = _grades[grade]!.toList()..sort() - Get sorted students:

    • Gets students in current grade
    • Converts set to list
    • Uses cascade operator to sort alphabetically
    • Result: [‘Alex’, ‘Peter’, ‘Zoe’] (alphabetically sorted)
  20. result.addAll(students) - Add to result:

    • Adds all students from current grade to result
    • Maintains order: grade 1 students, then grade 2, etc.
  21. return result - Return roster:

    • Returns complete sorted roster
    • All students in order: grades sorted, students within grade sorted
  22. List<String> grade(int gradeNumber) - Get grade students:

    • Returns sorted list of students in specific grade
    • Handles non-existent grades
  23. if (!_grades.containsKey(gradeNumber)) return [] - Check grade exists:

    • If grade doesn’t exist, return empty list
    • Early return for efficiency
  24. final students = _grades[gradeNumber]!.toList()..sort() - Get sorted students:

    • Gets students in requested grade
    • Converts set to list
    • Sorts alphabetically using cascade operator
  25. return students - Return grade list:

    • Returns sorted list of students in that grade
    • Empty list if grade doesn’t exist

The solution efficiently manages a school roster using Maps and Sets. The Map organizes students by grade, while the Set ensures uniqueness. The add() method prevents duplicates by checking the global student set, and both roster() and grade() methods return properly sorted results. The cascade operator enables clean method chaining for sorting operations.


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.