Pangram in Common-lisp: Complete Solution & Deep Dive Guide

a close up of a computer screen with code on it

The Complete Guide to Pangram Detection in Common Lisp

A pangram is a sentence containing every letter of the English alphabet at least once. To programmatically detect a pangram in Common Lisp, the most effective method involves normalizing the input string to lowercase, filtering out all non-alphabetic characters, creating a unique set of the remaining letters, and finally, verifying if the count of these unique letters is exactly 26.

Imagine you're building a new feature for a typography website. The goal is to showcase fonts using sentences that are not only witty but also comprehensive, displaying every letter of the alphabet to give designers a full preview. You launch a competition for the best sentences, and submissions pour in by the thousands. How do you efficiently validate which ones are true pangrams without manually checking each one? This is where the power of programming, and specifically the elegance of Common Lisp, comes into play.

This challenge, a classic in computer science, is a perfect gateway to understanding powerful string and sequence manipulation techniques. In this guide, we'll dissect the problem from the ground up, exploring a clean, functional solution in Common Lisp. We will then push further, analyzing its performance and building an even more optimized version, transforming you from a beginner to a proficient problem-solver in the world of Lisp.


What Exactly Is a Pangram?

A pangram, derived from the Greek words pan gramma (meaning "every letter"), is a sentence that utilizes every letter of a given alphabet at least one time. For the context of this programming challenge from the kodikra learning path, we are concerned with the 26-letter English alphabet.

The most famous English pangram is undoubtedly:

The quick brown fox jumps over the lazy dog.

This sentence is a staple for testing typefaces and keyboards because it neatly contains all 26 letters. However, there are many other creative and often humorous pangrams, such as "Waltz, bad nymph, for quick jigs vex" or "Sphinx of black quartz, judge my vow."

When we translate this definition into programming logic, two critical constraints emerge:

  • Case Insensitivity: The check must ignore whether a letter is uppercase (e.g., 'A') or lowercase (e.g., 'a'). Both should be treated as the same letter.
  • Character Filtering: The sentence may contain spaces, numbers, punctuation, or other symbols. Our algorithm must ignore these and focus exclusively on the alphabetic characters.

Why Choose Common Lisp for This Task?

Common Lisp, a standardized and mature dialect of Lisp, is exceptionally well-suited for problems involving symbolic computation and data manipulation. Its strengths make solving the pangram challenge not just possible, but remarkably elegant.

Firstly, Common Lisp's philosophy is built around operating on sequences. Strings, in Lisp, are simply sequences of characters. This means a rich library of powerful, high-level functions like map, reduce, remove-if, and remove-duplicates are available to manipulate strings with minimal boilerplate code. You express *what* you want to do, not *how* to loop through every index.

Secondly, its functional programming paradigm encourages composing functions together in a clean, readable pipeline. As you'll see in our solution, we can chain operations like lowercasing, filtering, and deduplicating into a single, expressive form. This leads to code that is often more concise and easier to reason about than its imperative equivalent in other languages.

Finally, the interactive development style facilitated by the REPL (Read-Eval-Print Loop) allows for rapid prototyping and testing. You can test each part of your logic—from case conversion to character filtering—in real-time, making the development process fluid and efficient. This is a hallmark of the Lisp family of languages and a significant advantage for both beginners and experts.


How to Structure the Pangram Detection Algorithm

Before diving into the code, it's crucial to outline a clear, step-by-step plan. A robust algorithm for pangram detection can be broken down into four distinct stages. This logical separation of concerns is key to writing clean and maintainable code.

We will start with the raw input sentence and progressively refine it until we can make a simple, final determination. The process is a classic example of a data processing pipeline, where the output of one stage becomes the input for the next.

The Algorithmic Blueprint

  1. Normalization: The first step is to eliminate case sensitivity. We achieve this by converting the entire input string to a single case, typically lowercase. This ensures that 'A' and 'a' are not counted as two different letters.
  2. Filtration: Next, we must discard all irrelevant characters. Spaces, commas, periods, numbers, and any other non-alphabetic symbols should be removed, leaving us with a sequence composed purely of letters.
  3. Unification: With a clean sequence of letters, our next task is to find the unique set of letters. For example, in the word "hello", the unique letters are 'h', 'e', 'l', and 'o'. This step removes all duplicates.
  4. Validation: The final step is a simple check. We count the number of unique letters from the previous stage. If this count is exactly 26, the original sentence is a pangram; otherwise, it is not.

This logical flow is visualized in the diagram below, showing how data is transformed at each step of the pipeline.

    ● Start: Input Sentence
    │        "The quick brown fox..."
    ▼
  ┌───────────────────┐
  │ 1. Normalize Case │
  │ (string-downcase) │
  └─────────┬─────────┘
    │        "the quick brown fox..."
    ▼
  ┌───────────────────┐
  │ 2. Filter Chars   │
  │ (remove-if-not)   │
  └─────────┬─────────┘
    │        "thequickbrownfox..."
    ▼
  ┌───────────────────┐
  │ 3. Unify Letters  │
  │ (remove-duplicates)│
  └─────────┬─────────┘
    │        "thequickbrownfxjmpsvlazydg"
    ▼
  ┌───────────────────┐
  │ 4. Count Unique   │
  │      (length)     │
  └─────────┬─────────┘
    │        26
    ▼
  ◆ Is count == 26?
   ╱           ╲
 True          False
  │             │
  ▼             ▼
 ● Is a Pangram   ● Not a Pangram

A Detailed Code Walkthrough of the Common Lisp Solution

With our algorithm defined, let's translate it into working Common Lisp code. The solution provided in the kodikra.com curriculum is a masterpiece of functional composition, showcasing the language's expressive power. We'll break it down line by line.

The Complete Code

Here is the full solution from the exclusive kodikra.com module for context. We will analyze each part in detail below.


(defpackage :pangram
  (:use :cl)
  (:export :pangramp))

(in-package :pangram)

(defun pangramp (sentence)
  "Determines if a sentence is a pangram."
  (let ((filtered (remove-if-not #'alpha-char-p
                                 (string-downcase sentence))))
    (= 26 (length (remove-duplicates filtered)))))

Dissecting the Code

Part 1: Package Definition


(defpackage :pangram
  (:use :cl)
  (:export :pangramp))
  • (defpackage :pangram ...): This form defines a new package named pangram. In Common Lisp, packages are namespaces that prevent symbol name collisions. Think of them like modules or libraries in other languages.
  • (:use :cl): This tells our new package to inherit all the standard symbols from the default :common-lisp package (aliased as :cl). This gives us access to core functions like defun, let, string-downcase, etc., without needing to prefix them (e.g., cl:defun).
  • (:export :pangramp): This makes the symbol pangramp public. Any other package that wishes to use our code can import this function. It's the public API of our module.

Part 2: Setting the Current Package


(in-package :pangram)

This command switches the current working namespace to the :pangram package we just defined. Any new definitions from this point forward will belong to this package.

Part 3: The Main Function


(defun pangramp (sentence)
  "Determines if a sentence is a pangram."
  ...)
  • (defun pangramp (sentence)): This defines a function named pangramp that accepts one argument, sentence. By convention, predicate functions in Lisp (functions that return true or false) often end with a 'p'.
  • "Determines if a sentence is a pangram.": This is a docstring. It's crucial for documentation and can be accessed interactively in the REPL, making the code self-describing.

Part 4: The Core Logic with let


(let ((filtered (remove-if-not #'alpha-char-p
                                 (string-downcase sentence))))
  ...)
  • (let ((filtered ...))): The let special form creates a local variable binding. Here, we create a variable named filtered that will exist only within the scope of the let block. This improves readability by breaking a complex operation into named, intermediate steps.
  • (string-downcase sentence): This is the first step in our pipeline (Normalization). It takes the input sentence and returns a new string with all characters converted to lowercase.
  • (remove-if-not #'alpha-char-p ...): This is the Filtration step. It's a powerful higher-order function that iterates over the sequence provided (the lowercased string). It keeps only the elements for which the predicate function returns true.
    • #'alpha-char-p: The #' is shorthand for (function ...). It retrieves the function object associated with the symbol alpha-char-p. alpha-char-p is a built-in Common Lisp function that returns true if a character is an alphabet letter, and nil (false) otherwise.

After this expression, the filtered variable holds a string containing only the lowercase letters from the original sentence.

Part 5: The Final Validation


(= 26 (length (remove-duplicates filtered)))

This is the final chain of operations, executing the Unification and Validation steps. Since it's the last expression in the function, its result will be the function's return value.

  • (remove-duplicates filtered): This function takes the filtered sequence and returns a new sequence containing only the unique elements. For example, if filtered was "aabbc", this would return "abc" (the order is not guaranteed but is irrelevant here).
  • (length ...): This simply returns the number of elements in the sequence of unique letters.
  • (= 26 ...): Finally, this compares the length of the unique letters to 26. It returns T (true) if they are equal, and NIL (false) otherwise. This boolean value is the final output of our pangramp function.

An Alternative, High-Performance Approach

The functional solution is elegant and highly readable, but is it the most performant? For very large inputs (megabytes of text), creating multiple intermediate strings (`string-downcase`, `remove-if-not`) and then running `remove-duplicates` can lead to unnecessary memory allocation and processing time. `remove-duplicates` on a long sequence has a time complexity that can be around O(n log n) or O(n^2) depending on the implementation details.

A more performant approach, especially in lower-level languages, is to use a fixed-size boolean array (or bit vector) to track which letters have been seen. We can adapt this efficient technique to Common Lisp.

The Bit Vector Algorithm

The logic is as follows:

  1. Create a boolean array (or a bit vector) of size 26, with all values initialized to NIL (false). Each index from 0 to 25 corresponds to a letter ('a' to 'z').
  2. Iterate through the input sentence character by character.
  3. For each character, convert it to lowercase.
  4. If it's an alphabetic character, calculate its corresponding index (e.g., 'a' -> 0, 'b' -> 1).
  5. Set the value at that index in our boolean array to T (true).
  6. After the loop finishes, check if all 26 values in the array are T.

This approach processes the string in a single pass (O(n) time complexity) and uses constant extra space (the 26-element array), making it highly efficient.

    ● Start: Input Sentence
    │
    ▼
  ┌───────────────────────────┐
  │ Create `seen` array[26]   │
  │ all initialized to false  │
  └─────────────┬─────────────┘
                │
    ╭───────────▼───────────╮
    │ Loop over each char c │
    ╰───────────┬───────────╯
                │
                ▼
          ◆ Is c a letter?
         ╱                ╲
       Yes                 No
        │                  │
        ▼                  │
┌──────────────────┐       │
│ Normalize c to   │       │
│ lowercase, get   │       │
│ index `i`        │       │
└────────┬─────────┘       │
         │                 │
         ▼                 │
┌──────────────────┐       │
│ Mark `seen[i]`   │       │
│ as true          │       │
└────────┬─────────┘       │
         │                 │
         ╰─────────┬───────╯
                   │
    ╭──────────────▼──────────────╮
    │ End of loop? (next char)    │
    ╰──────────────┬──────────────╯
                   │
                   ▼
        ◆ Are all 26 elements
        │ in `seen` array true?
       ╱                       ╲
     Yes                        No
      │                         │
      ▼                         ▼
 ● Is a Pangram             ● Not a Pangram

Optimized Code Implementation


(defun pangramp-optimized (sentence)
  "Determines if a sentence is a pangram using a boolean vector for efficiency."
  (let ((seen (make-array 26 :element-type 'boolean :initial-element nil)))
    (loop for char across (string-downcase sentence)
          when (alpha-char-p char)
          do (setf (aref seen (- (char-code char) (char-code #\a))) t))
    (every #'identity seen)))

Code Breakdown:

  • (let ((seen (make-array 26 ...)))): We create a local variable seen. make-array 26 creates an array of size 26. :element-type 'boolean is an optimization hint, and :initial-element nil sets all slots to false.
  • (loop for char across ... do ...): The powerful loop macro iterates through each character of the lowercased sentence.
  • when (alpha-char-p char): This clause ensures the body of the loop only executes for alphabetic characters.
  • (setf (aref seen ...) t): This is the core update logic.
    • (char-code char) gets the integer ASCII/Unicode value of the character.
    • (- (char-code char) (char-code #\a)) calculates the 0-based index. For 'a', this is 0; for 'b', it's 1, and so on.
    • (aref seen ...) accesses the array at that index.
    • setf updates the value at that position to t.
  • (every #'identity seen): After the loop, this function checks if every element in the seen array is true. #'identity is a function that simply returns its argument. Since only nil is false in Common Lisp, this elegantly checks if any element is still nil. If all are t, it returns t.

Pros & Cons of Each Approach

Choosing between these two solutions depends on the specific context, such as the expected input size and the priority of readability versus raw performance.

Aspect Functional Approach (Initial Solution) Optimized Approach (Bit Vector)
Readability Excellent. The code reads like a description of the algorithm's steps. Good, but requires understanding of loop, aref, and character code arithmetic.
Conciseness Extremely concise and idiomatic for Lisp. Slightly more verbose due to array setup and explicit loop.
Performance (Large Inputs) Slower. Creates multiple intermediate sequences and has a less optimal complexity for deduplication. Excellent. Single-pass O(n) time complexity with constant extra space.
Memory Usage Higher. Allocates memory for several new strings/sequences. Minimal. Allocates a single, small, fixed-size array.

Frequently Asked Questions (FAQ)

1. Is the pangram check case-sensitive?
No, it is not. Both solutions explicitly use (string-downcase sentence) at the beginning of their logic pipeline. This ensures that 'A' and 'a' are treated as the same character before any further processing occurs.

2. What happens if the input sentence contains numbers or punctuation?
They are completely ignored. The functional solution uses (remove-if-not #'alpha-char-p ...), which filters out any character that is not a letter. The optimized solution uses a when (alpha-char-p char) clause inside its loop to achieve the same result, processing only the letters.

3. Can this function handle non-English (Unicode) characters?
The current logic is specifically tailored for the 26-letter English alphabet. Functions like alpha-char-p might recognize letters from other languages depending on the Common Lisp implementation's Unicode support. However, the core validation (checking for 26 letters and using char-code #\a as a base) would need to be significantly modified to support other alphabets.

4. What does the `#'` syntax mean in `#'alpha-char-p`?
The hash-quote syntax, #', is a reader macro and a shorthand for the (function ...) special operator. It is used to retrieve the actual function object from a symbol. When you pass a function as an argument to another function (like remove-if-not or every), you need to pass the function object itself, not just its name (the symbol).

5. Why use `let` to create an intermediate variable in the first solution?
While you could nest all the function calls into one giant form, using let to create a named variable like filtered significantly improves code readability. It breaks the logic into a clear, sequential step. It also makes debugging easier, as you can inspect the value of the intermediate variable.

6. Is there a built-in function in Common Lisp to check for a pangram?
No, there is no single built-in function for this. The pangram problem is a classic programming challenge found in many learning curricula, including the kodikra.com Common Lisp track, because it effectively tests a developer's understanding of fundamental string, sequence, and data manipulation skills.

7. How can I quickly test this function?
Once you've loaded the code into your Common Lisp REPL, you can call the function directly. For example:

  (pangramp "The quick brown fox jumps over the lazy dog")
  ;; Expected output: T

  (pangramp "This is not a pangram")
  ;; Expected output: NIL
  

Conclusion: From Theory to Elegant Code

We've journeyed from a simple definition to two complete, robust implementations for detecting pangrams in Common Lisp. The first solution highlighted the beauty of the functional style, where chaining high-level sequence operations leads to code that is both concise and deeply expressive. The second, optimized solution demonstrated how to prioritize performance for large-scale applications by using a more traditional iterative approach with a bit vector, all while leveraging Lisp's powerful loop macro.

This exploration serves as a perfect example of the trade-offs in software development—readability versus performance, elegance versus raw speed. Common Lisp provides the tools to excel at both. By mastering these fundamental concepts of string manipulation, higher-order functions, and algorithmic thinking, you are well on your way to tackling more complex challenges.

Disclaimer: The code presented in this article is written against modern Common Lisp standards and has been tested with the Steel Bank Common Lisp (SBCL) implementation, version 2.4.4. While it is expected to work on other compliant implementations, minor adjustments may be necessary.

Ready to apply these skills to more problems? Explore our complete Common Lisp Learning Path to continue your journey. For more guides and resources, check out our central hub for everything you need to know about Common Lisp.


Published by Kodikra — Your trusted Common-lisp learning resource.