exercism

Exercism - Rotational Cipher

This post shows you how to get Rotational Cipher exercise of Exercism.

Stevinator Stevinator
12 min read
SHARE
exercism dart flutter rotational-cipher

Preparation

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

Rotational Cipher 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 RotationalCipher {
  String rotate({required String text, required int shiftKey}) {
    // Rotate text by shiftKey positions
    return text;
  }
}

void main() {
  RotationalCipher cipher = RotationalCipher();
  String result = cipher.rotate(text: "hello", shiftKey: 13);
  print(result); // "uryyb"
}

Required Named Parameters

Required named parameters must be provided when calling a function. They’re marked with the required keyword and make function calls more explicit.

void main() {
  String rotate({required String text, required int shiftKey}) {
    return text;
  }
  
  // Must provide both parameters
  String result = rotate(text: "hello", shiftKey: 13);
  
  // Error: missing required parameter
  // String result2 = rotate(text: "hello");
}

Modulo Operator (%)

The modulo operator (%) returns the remainder after division. It’s essential for wrapping character shifts within the alphabet range (0-25).

void main() {
  // Normalize shift key to 0-25 range
  int shiftKey = 27;
  int shift = shiftKey % 26;
  print(shift); // 1 (27 % 26 = 1)
  
  // Shift within alphabet
  int code = 97; // 'a'
  int shifted = ((code - 97 + 13) % 26) + 97;
  print(shifted); // 110 ('n')
  
  // ROT26 is same as ROT0
  int shift26 = 26 % 26;
  print(shift26); // 0
}

StringBuffer

StringBuffer is an efficient way to build strings by appending characters or strings. It’s more efficient than string concatenation in loops.

void main() {
  // Create StringBuffer
  final result = StringBuffer();
  
  // Append characters
  result.writeCharCode(97); // 'a'
  result.writeCharCode(98); // 'b'
  result.write('c'); // 'c'
  
  // Convert to string
  String text = result.toString();
  print(text); // "abc"
  
  // Use in loops
  final buffer = StringBuffer();
  for (int i = 0; i < 5; i++) {
    buffer.write('$i');
  }
  print(buffer.toString()); // "01234"
}

Character Code Arithmetic

Characters have numeric codes (Unicode values). You can convert between characters and their codes, and perform arithmetic to shift letters.

void main() {
  // Get character code
  int codeA = 'A'.codeUnitAt(0);
  print(codeA); // 65
  
  int codea = 'a'.codeUnitAt(0);
  print(codea); // 97
  
  // Convert code to character
  String letterA = String.fromCharCode(65);
  print(letterA); // 'A'
  
  // Shift letter
  int codeB = codeA + 1;
  String letterB = String.fromCharCode(codeB);
  print(letterB); // 'B'
  
  // Shift within alphabet range
  int code = 122; // 'z'
  int shifted = ((code - 97 + 1) % 26) + 97;
  String letter = String.fromCharCode(shifted);
  print(letter); // 'a' (wraps around)
}

Character Code Ranges

ASCII/Unicode character codes have specific ranges for letters. Lowercase letters are 97-122 (a-z), uppercase letters are 65-90 (A-Z).

void main() {
  // Lowercase range: 97-122 (a-z)
  int codeA = 'a'.codeUnitAt(0); // 97
  int codeZ = 'z'.codeUnitAt(0); // 122
  
  // Uppercase range: 65-90 (A-Z)
  int codeAUpper = 'A'.codeUnitAt(0); // 65
  int codeZUpper = 'Z'.codeUnitAt(0); // 90
  
  // Check if character is lowercase
  int code = 'm'.codeUnitAt(0);
  bool isLowercase = code >= 97 && code <= 122;
  print(isLowercase); // true
  
  // Check if character is uppercase
  int codeUpper = 'M'.codeUnitAt(0);
  bool isUppercase = codeUpper >= 65 && codeUpper <= 90;
  print(isUppercase); // true
}

String Indexing

String indexing allows you to access individual characters by position. You can iterate through a string character by character.

void main() {
  String text = "hello";
  
  // Access character by index
  String first = text[0];
  print(first); // 'h'
  
  // Get string length
  int length = text.length;
  print(length); // 5
  
  // Iterate through characters
  for (int i = 0; i < text.length; i++) {
    String char = text[i];
    print(char); // h, e, l, l, o
  }
}

codeUnitAt() Method

The codeUnitAt() method returns the Unicode code unit (character code) at a specific index in a string.

void main() {
  String text = "ABC";
  
  // Get code unit at index 0
  int codeA = text.codeUnitAt(0);
  print(codeA); // 65 ('A')
  
  // Get code unit at index 1
  int codeB = text.codeUnitAt(1);
  print(codeB); // 66 ('B')
  
  // Use in loops
  for (int i = 0; i < text.length; i++) {
    int code = text.codeUnitAt(i);
    print('${text[i]}: $code');
    // A: 65
    // B: 66
    // C: 67
  }
}

writeCharCode() Method

The writeCharCode() method appends a character to a StringBuffer using its Unicode code unit.

void main() {
  final buffer = StringBuffer();
  
  // Write character by code
  buffer.writeCharCode(65); // 'A'
  buffer.writeCharCode(66); // 'B'
  buffer.writeCharCode(67); // 'C'
  
  String result = buffer.toString();
  print(result); // "ABC"
  
  // Use in cipher
  int shiftedCode = 110; // 'n'
  buffer.writeCharCode(shiftedCode);
  print(buffer.toString()); // "ABCn"
}

For Loops

For loops allow you to iterate through a sequence, processing each element. They’re essential for transforming each character in a string.

void main() {
  String text = "hello";
  
  // Iterate through string indices
  for (int i = 0; i < text.length; i++) {
    String char = text[i];
    int code = char.codeUnitAt(0);
    print('$char: $code');
    // h: 104
    // e: 101
    // l: 108
    // l: 108
    // o: 111
  }
}

Conditional Logic

Conditional logic (if, else if, else) is used to check character types and apply different transformations. It’s essential for handling letters vs non-letters.

void main() {
  int code = 'M'.codeUnitAt(0);
  
  // Check if lowercase
  if (code >= 97 && code <= 122) {
    print('Lowercase letter');
  }
  // Check if uppercase
  else if (code >= 65 && code <= 90) {
    print('Uppercase letter');
  }
  // Not a letter
  else {
    print('Not a letter');
  }
}

Arithmetic Operations

Arithmetic operations like addition (+), subtraction (-), and modulo (%) are used to calculate shifted character codes within the alphabet range.

void main() {
  int code = 'a'.codeUnitAt(0); // 97
  int shift = 13;
  
  // Shift within lowercase alphabet
  // 1. Subtract base (97) to get position in alphabet (0-25)
  // 2. Add shift
  // 3. Modulo 26 to wrap around
  // 4. Add base back to get final code
  int shifted = ((code - 97 + shift) % 26) + 97;
  print(shifted); // 110 ('n')
  
  // Example: 'z' + 1 wraps to 'a'
  int codeZ = 'z'.codeUnitAt(0); // 122
  int shiftedZ = ((codeZ - 97 + 1) % 26) + 97;
  print(shiftedZ); // 97 ('a')
}

Comparison Operators

Comparison operators (>=, <=) are used to check if a character code falls within a specific range (lowercase or uppercase letters).

void main() {
  int code = 'm'.codeUnitAt(0);
  
  // Check if lowercase (97-122)
  if (code >= 97 && code <= 122) {
    print('Lowercase letter');
  }
  
  // Check if uppercase (65-90)
  if (code >= 65 && code <= 90) {
    print('Uppercase letter');
  }
  
  // Check if not a letter
  if (code < 65 || (code > 90 && code < 97) || code > 122) {
    print('Not a letter');
  }
}

Introduction

Create an implementation of the rotational cipher, also sometimes called the Caesar cipher.

The Caesar cipher is a simple shift cipher that relies on transposing all the letters in the alphabet using an integer key between 0 and 26. Using a key of 0 or 26 will always yield the same output due to modular arithmetic. The letter is shifted for as many values as the value of the key.

The general notation for rotational ciphers is ROT + . The most commonly used rotational cipher is ROT13.

A ROT13 on the Latin alphabet would be as follows:

Plain: abcdefghijklmnopqrstuvwxyz

Cipher: nopqrstuvwxyzabcdefghijklm

It is stronger than the Atbash cipher because it has 27 possible keys, and 25 usable keys.

Ciphertext is written out in the same formatting as the input including spaces and punctuation.

Examples

  • ROT5 omg gives trl
  • ROT0 c gives c
  • ROT26 Cool gives Cool
  • ROT13 The quick brown fox jumps over the lazy dog. gives Gur dhvpx oebja sbk whzcf bire gur ynml qbt.
  • ROT13 Gur dhvpx oebja sbk whzcf bire gur ynml qbt. gives The quick brown fox jumps over the lazy dog.

How do we solve the rotational cipher?

To solve the rotational cipher:

  1. Normalize shift key: Use modulo 26 to ensure shift is in range 0-25
  2. Handle zero shift: If shift is 0, return original text (no change)
  3. Process each character:
    • Get character code using codeUnitAt()
    • Check if lowercase (97-122): shift within lowercase range
    • Check if uppercase (65-90): shift within uppercase range
    • Otherwise: keep character as-is (spaces, punctuation)
  4. Shift formula: ((code - base + shift) % 26) + base
    • For lowercase: base = 97
    • For uppercase: base = 65
  5. Build result: Use StringBuffer to efficiently build the result string

The key insight is that we shift letters within their case range, wrapping around using modulo arithmetic, while preserving all non-letter characters.

Solution

class RotationalCipher {
  String rotate({required String text, required int shiftKey}) {
    // Normalize the shift key to be within 0-25
    final shift = shiftKey % 26;
    
    // If shift is 0, return the original text
    if (shift == 0) {
      return text;
    }
    
    final result = StringBuffer();
    
    for (int i = 0; i < text.length; i++) {
      final char = text[i];
      final code = char.codeUnitAt(0);
      
      // Check if it's a lowercase letter (a-z: 97-122)
      if (code >= 97 && code <= 122) {
        // Shift within lowercase range
        final shifted = ((code - 97 + shift) % 26) + 97;
        result.writeCharCode(shifted);
      }
      // Check if it's an uppercase letter (A-Z: 65-90)
      else if (code >= 65 && code <= 90) {
        // Shift within uppercase range
        final shifted = ((code - 65 + shift) % 26) + 65;
        result.writeCharCode(shifted);
      }
      // Not a letter, keep it as is
      else {
        result.write(char);
      }
    }
    
    return result.toString();
  }
}

Let’s break down the solution:

  1. class RotationalCipher - Main class:

    • Encapsulates the rotational cipher implementation
    • Provides the rotate method for encryption/decryption
  2. String rotate({required String text, required int shiftKey}) - Main method:

    • Takes text to encrypt/decrypt and shift key
    • Returns rotated (encrypted/decrypted) text
    • Uses required named parameters for clarity
  3. final shift = shiftKey % 26 - Normalize shift key:

    • Uses modulo 26 to ensure shift is in range 0-25
    • ROT26 becomes ROT0 (same as no shift)
    • ROT27 becomes ROT1, etc.
  4. if (shift == 0) return text - Early return:

    • If shift is 0 (or 26, 52, etc.), no transformation needed
    • Returns original text immediately for efficiency
  5. final result = StringBuffer() - Create buffer:

    • StringBuffer for efficient string building
    • More efficient than string concatenation in loops
  6. for (int i = 0; i < text.length; i++) - Iterate through text:

    • Processes each character in the input string
    • Uses index-based iteration
  7. final char = text[i] - Get character:

    • Accesses character at current index
    • Used for checking and writing non-letters
  8. final code = char.codeUnitAt(0) - Get character code:

    • Gets Unicode code unit (ASCII value) of character
    • Used to determine if letter and calculate shift
  9. if (code >= 97 && code <= 122) - Check lowercase:

    • Lowercase letters have codes 97-122 (a-z)
    • If true, character is a lowercase letter
  10. final shifted = ((code - 97 + shift) % 26) + 97 - Shift lowercase:

    • Subtracts 97 (base for ‘a’) to get position 0-25
    • Adds shift amount
    • Modulo 26 wraps around alphabet (z+1 → a)
    • Adds 97 back to get final character code
    • Example: ‘a’ (97) + 13 → ‘n’ (110)
  11. result.writeCharCode(shifted) - Write shifted lowercase:

    • Appends shifted character to result buffer
    • Uses character code to write character
  12. else if (code >= 65 && code <= 90) - Check uppercase:

    • Uppercase letters have codes 65-90 (A-Z)
    • If true, character is an uppercase letter
  13. final shifted = ((code - 65 + shift) % 26) + 65 - Shift uppercase:

    • Subtracts 65 (base for ‘A’) to get position 0-25
    • Adds shift amount
    • Modulo 26 wraps around alphabet (Z+1 → A)
    • Adds 65 back to get final character code
    • Example: ‘A’ (65) + 13 → ‘N’ (78)
  14. result.writeCharCode(shifted) - Write shifted uppercase:

    • Appends shifted character to result buffer
    • Preserves case of original letter
  15. else result.write(char) - Keep non-letters:

    • For spaces, punctuation, numbers, etc.
    • Writes character as-is (no transformation)
    • Preserves formatting of original text
  16. return result.toString() - Return result:

    • Converts StringBuffer to String
    • Returns the fully transformed text

The solution efficiently encrypts/decrypts text by shifting letters within their case range while preserving all non-letter characters. The modulo arithmetic ensures the shift wraps around the alphabet correctly, and the early return for zero shift optimizes the common case. The StringBuffer provides efficient string building for the result.


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.