Phone Number in Common-lisp: Complete Solution & Deep Dive Guide

text

Mastering Phone Number Validation in Common Lisp: From Raw Input to Clean NANP Format

Cleaning a phone number in Common Lisp involves stripping non-digit characters, handling the optional '1' country code, and validating the remaining 10-digit string against NANP rules. This ensures the area code and exchange code do not start with '0' or '1' for a valid, standardized format.

You've just joined the engineering team at a fast-growing communications startup. The mission is critical: connect people reliably. But there's a problem lurking in the database—a chaotic mess of user-submitted phone numbers. You see formats like (123) 456-7890, 123.456.7890, +1 123 456 7890, and even invalid entries with letters or incorrect lengths. This data inconsistency is causing SMS delivery failures and breaking user profiles.

This isn't just a cleanup task; it's about building a resilient system. You need a robust, intelligent parser that can sift through this chaos, identify valid North American phone numbers, and standardize them into a single, clean format. This guide will walk you through building exactly that, using the power and elegance of Common Lisp to transform messy input into pristine, reliable data.


What is the North American Numbering Plan (NANP) and Why is Validation Crucial?

Before diving into code, it's essential to understand the rules of the system we're building for. The North American Numbering Plan (NANP) is the telephone numbering system for the United States, Canada, and many Caribbean countries. It's the reason why all these countries share the international country code 1.

A standard NANP number is a 10-digit number, structured as follows:

  • Area Code (NPA): The first 3 digits. This code historically couldn't start with a 0 or 1. While rules have relaxed, for many systems, it's a critical validation step. We'll enforce the rule that the first digit (N) must be in the range [2-9].
  • Exchange Code (NXX): The next 3 digits. Similar to the area code, the first digit (N) must also be in the range [2-9].
  • Subscriber Number: The final 4 digits.

So, a valid 10-digit number looks like NXX-NXX-XXXX where the first N of the area code and exchange code is a digit from 2 to 9. Any number that violates this structure is invalid. For example, a number with an area code of 123 or an exchange code of 055 would be rejected.

Data validation isn't just a "nice-to-have" feature. It's the bedrock of system reliability. For a communications platform, invalid phone numbers lead to:

  • Failed Deliveries: Sending an SMS to a malformed number wastes resources and results in a poor user experience.
  • Data Corruption: Storing invalid data pollutes your database, making analytics and future operations difficult.
  • Security Risks: Unsanitized input can be a vector for injection attacks in less secure systems.
  • Increased Costs: API calls to SMS gateways often charge per attempt, meaning failed attempts cost real money.

By implementing a strict validation and cleaning process, you ensure that every phone number entering your system is usable, reliable, and secure.


Why Common Lisp is an Excellent Choice for This Task

Common Lisp, one of the most powerful and expressive programming languages, is uniquely suited for tasks involving data manipulation and transformation. Its strengths shine brightly in this phone number parsing problem.

First, Common Lisp is a multi-paradigm language, but its functional programming capabilities are legendary. This problem can be elegantly solved by creating a pipeline of small, pure functions. Each function has a single responsibility: one strips punctuation, another handles the country code, and others validate specific rules. This approach, known as functional decomposition, makes the code incredibly easy to read, test, and maintain.

Second, its powerful sequence and string manipulation library is second to none. Functions like remove-if-not, subseq, and char provide a high-level, declarative way to work with strings. You describe what you want to do (e.g., "remove all characters that are not digits") rather than writing complex, error-prone loops to do it.

Finally, Common Lisp's interactive development environment, typically via a REPL (Read-Eval-Print Loop), allows for rapid prototyping and testing. You can build and test each small helper function independently before composing them into the final solution, leading to a more robust and bug-free result. This module from the kodikra Common Lisp learning path is a perfect example of these principles in action.


How to Implement the Phone Number Cleaner: A Step-by-Step Walkthrough

Our goal is to create a function, clean, that takes a raw string and returns either a clean 10-digit phone number or indicates an error (in this case, by returning an invalid value, which we'll handle). We will achieve this by breaking the problem down into smaller, manageable pieces.

Here is the complete solution from the kodikra.com module, which we will analyze in detail.

The Complete Code Structure

(defpackage :phone-number
  (:use :common-lisp)
  (:export :clean))

(in-package :phone-number)

(defun strip-non-digits (string)
  "Removes any character that is not a digit from the input string."
  (remove-if-not #'digit-char-p string))

(defun trim-leading-one (string)
  "If the string is 11 digits and starts with '1', it removes the leading '1'."
  (if (and (= 11 (length string))
           (char= #\1 (char string 0)))
      (subseq string 1)
      string))

(defun starts-with-0-or-1-p (string)
  "Checks if the first character of the string is '0' or '1'."
  (member (char string 0) '(#\0 #\1) :test #'char=))

(defun valid-length-p (string)
  "Checks if the string has exactly 10 digits."
  (= 10 (length string)))

(defun valid-area-code-p (string)
  "Checks if the area code (first 3 digits) is valid (doesn't start with 0 or 1)."
  (not (starts-with-0-or-1-p (subseq string 0 3))))

(defun valid-exchange-code-p (string)
  "Checks if the exchange code (digits 3-5) is valid (doesn't start with 0 or 1)."
  (not (starts-with-0-or-1-p (subseq string 3 6))))

(defun clean (phone-number-string)
  "Cleans and validates a phone number string according to NANP rules."
  (let* ((digits-only (strip-non-digits phone-number-string))
         (trimmed (trim-leading-one digits-only))
         (invalid-number "0000000000"))
    (if (and (valid-length-p trimmed)
             (valid-area-code-p trimmed)
             (valid-exchange-code-p trimmed))
        trimmed
        invalid-number)))

Code Walkthrough: Deconstructing the Logic

1. Package Definition

(defpackage :phone-number
  (:use :common-lisp)
  (:export :clean))

(in-package :phone-number)

This is standard Common Lisp boilerplate for creating a new module. defpackage defines a new package named phone-number. This acts as a namespace, preventing function names from clashing with other code.

  • (:use :common-lisp): This line imports all the standard symbols from the Common Lisp language package, so we can use functions like defun, if, and length without a prefix.
  • (:export :clean): This makes the clean function public. Only exported functions can be called from outside this package (e.g., phone-number:clean). All other functions we define are private helpers.
  • (in-package :phone-number): This command switches the current working namespace to our newly defined package.

2. The Cleaning Pipeline

The core of the solution is a two-step cleaning process before validation. This process is represented visually in the following diagram.

    ● Start (Raw Input String)
    │ e.g., "+1 (223) 456-7890"
    ▼
  ┌─────────────────────────┐
  │ strip-non-digits      │
  │ (remove-if-not #'digit-char-p) │
  └───────────┬─────────────┘
              │
              ▼
    ● Intermediate Result
    │ e.g., "12234567890"
    │
    ▼
  ┌─────────────────────────┐
  │ trim-leading-one        │
  │ (check length 11 & starts with '1') │
  └───────────┬─────────────┘
              │
              ▼
    ● Cleaned Digit String
    │ e.g., "2234567890"
    │
    ▼
  ┌─────────────────────────┐
  │ Pass to Validation Logic │
  └─────────────────────────┘

Let's examine the functions that implement this pipeline.

strip-non-digits

(defun strip-non-digits (string)
  "Removes any character that is not a digit from the input string."
  (remove-if-not #'digit-char-p string))

This function is a perfect example of Lisp's declarative style. It takes a string and uses remove-if-not, a higher-order function. It keeps only the elements of the sequence (the string) for which the predicate function returns true. The predicate here is #'digit-char-p, a built-in Common Lisp function that returns true if a character is a digit (0-9).

trim-leading-one

(defun trim-leading-one (string)
  "If the string is 11 digits and starts with '1', it removes the leading '1'."
  (if (and (= 11 (length string))
           (char= #\1 (char string 0)))
      (subseq string 1)
      string))

This function handles the optional NANP country code. It first checks two conditions using and:

  1. Is the length of the string exactly 11?
  2. Is the first character (at index 0) equal to #\1?

If both are true, it returns a new string containing everything *except* the first character, using (subseq string 1). If either condition is false, it returns the original string unmodified.

3. The Validation Predicates

After cleaning, we have a string of digits. Now we need to validate it against NANP rules using a series of predicate functions (functions that return true or false).

    ● Start (Cleaned 10-Digit String)
    │
    ▼
  ┌──────────────────┐
  │ valid-length-p   │
  │ (is length == 10?) │
  └─────────┬────────┘
            │
      No ───┼───► Invalid Number
            │
           Yes
            ▼
  ┌───────────────────┐
  │ valid-area-code-p │
  │ (pos 0 not '0'/'1'?) │
  └─────────┬─────────┘
            │
      No ───┼───► Invalid Number
            │
           Yes
            ▼
  ┌───────────────────────┐
  │ valid-exchange-code-p │
  │ (pos 3 not '0'/'1'?)  │
  └─────────┬─────────────┘
            │
      No ───┼───► Invalid Number
            │
           Yes
            ▼
    ● Final Result: Valid Number

Let's look at the functions that implement this validation logic.

valid-length-p

(defun valid-length-p (string)
  "Checks if the string has exactly 10 digits."
  (= 10 (length string)))

This is the simplest check. It returns true only if the string's length is exactly 10, the standard for NANP numbers after the country code is removed.

starts-with-0-or-1-p

(defun starts-with-0-or-1-p (string)
  "Checks if the first character of the string is '0' or '1'."
  (member (char string 0) '(#\0 #\1) :test #'char=))

This is a crucial helper for our other validation functions. It extracts the first character of a string with (char string 0) and checks if it's a member of the list '(#\0 #\1). This tells us if a code starts with an invalid digit.

valid-area-code-p and valid-exchange-code-p

(defun valid-area-code-p (string)
  "Checks if the area code (first 3 digits) is valid (doesn't start with 0 or 1)."
  (not (starts-with-0-or-1-p (subseq string 0 3))))

(defun valid-exchange-code-p (string)
  "Checks if the exchange code (digits 3-5) is valid (doesn't start with 0 or 1)."
  (not (starts-with-0-or-1-p (subseq string 3 6))))

These two functions enforce the NANP rule that neither the area code nor the exchange code can begin with a 0 or a 1. They use subseq to slice the appropriate 3-digit part of the number and pass it to our starts-with-0-or-1-p helper. The not inverts the result, so the function returns true if the code is valid (i.e., it does *not* start with 0 or 1).

4. The Main clean Function

Finally, the clean function orchestrates the entire process.

(defun clean (phone-number-string)
  "Cleans and validates a phone number string according to NANP rules."
  (let* ((digits-only (strip-non-digits phone-number-string))
         (trimmed (trim-leading-one digits-only))
         (invalid-number "0000000000"))
    (if (and (valid-length-p trimmed)
             (valid-area-code-p trimmed)
             (valid-exchange-code-p trimmed))
        trimmed
        invalid-number)))

It uses let* to create a sequence of local bindings. This is more readable than nested function calls.

  1. digits-only: The raw input is passed to strip-non-digits.
  2. trimmed: The result of the first step is passed to trim-leading-one.
  3. invalid-number: A constant is defined to represent an invalid number. In a real application, you might return nil or signal an error.

The if statement then uses and to chain all our validation predicates together. If trimmed passes all three checks (length, area code, and exchange code), the function returns the clean trimmed string. If any check fails, the and short-circuits and returns false, causing the if to return the invalid-number string.

Running the Code

To use this code, you would save it as a file (e.g., phone-number.lisp) and load it in your Common Lisp environment. Using a popular implementation like SBCL (Steel Bank Common Lisp), you can test it from your terminal.

# Load the file, call the function with a valid number, and exit
$ sbcl --noinform --load phone-number.lisp \
       --eval '(format t "~a~%" (phone-number:clean "+1 (223) 456-7890"))' \
       --quit
2234567890

# Test with an invalid area code
$ sbcl --noinform --load phone-number.lisp \
       --eval '(format t "~a~%" (phone-number:clean "(123) 456-7890"))' \
       --quit
0000000000

Where This Logic Fits in a Real-World Application

This phone number cleaning module is not an isolated piece of code; it's a vital component in a larger system. Its primary use case is as a data sanitization layer that sits between raw user input and your application's core logic or database.

Consider these common integration points:

  • API Endpoints: When a new user signs up via an API call, their provided phone number should immediately be passed through the phone-number:clean function. If it returns an invalid result, the API should return a 400 Bad Request error with a clear message like "Invalid phone number format."
  • User Profile Forms: In a web application, when a user updates their profile, the backend server would use this module to validate the phone number before saving it to the database.
  • Data Migration Scripts: If you're importing a large dataset of user records from another system, you would run every phone number through this cleaner as part of the ETL (Extract, Transform, Load) process to ensure data quality in your new system.
  • Messaging Services: Before attempting to send an SMS or place a call via a third-party gateway like Twilio, the number should be validated. This prevents failed API calls and saves money.

By centralizing this logic into a well-defined package, you create a reusable and testable asset that ensures consistency across your entire application stack.


Pros and Cons of This Functional Approach

Every implementation has trade-offs. This functional decomposition approach is elegant but it's worth considering its strengths and weaknesses.

Pros Cons / Risks
Highly Readable: Each function has a clear, single purpose. The main clean function reads like a set of business rules. Potential for Inefficiency: Multiple intermediate strings are created (e.g., after stripping digits, after trimming). For extremely high-performance scenarios, a single-pass state machine might be faster, though much more complex.
Easily Testable: You can write unit tests for each small helper function in isolation, making it easy to pinpoint bugs. Limited Error Reporting: The current implementation returns a generic "invalid" string. It doesn't tell the caller *why* the number was invalid (bad length, bad area code, etc.). A more advanced version might return multiple values or a condition object.
Composable and Reusable: The helper functions (like strip-non-digits) can be reused in other parts of the application that require similar logic. Strictly NANP-focused: This code is specifically designed for NANP and will incorrectly handle or reject valid international numbers from other regions. It is not a general-purpose phone number library.
Maintainable: If a business rule changes (e.g., area codes can now start with '1'), you only need to update one small function (valid-area-code-p). No External Library Use: While a strength for simplicity, it means you have to maintain the validation rules yourself. A library like Google's `libphonenumber` would be more comprehensive but adds an external dependency.

For its intended purpose within the kodikra.com learning curriculum, this approach is ideal as it perfectly balances clarity, correctness, and idiomatic Lisp style.


Frequently Asked Questions (FAQ)

Why does the code return "0000000000" for invalid numbers?

Returning a fixed-length string of zeros is a simple way to signal an error while maintaining the same data type (a string of 10 digits). In a real-world application, it might be better to return NIL (Lisp's null value) or to signal a specific error condition that can be caught and handled by the calling code. This choice makes the function's behavior explicit for this particular module.

Can this code handle international numbers outside the NANP?

No, this implementation is specifically tailored to the rules of the North American Numbering Plan. It will incorrectly reject many valid international numbers that do not conform to the 10-digit structure or the area/exchange code rules. For global phone number validation, using a comprehensive, dedicated library like Google's libphonenumber is recommended.

What is the difference between `let` and `let*` in the `clean` function?

In Common Lisp, let binds all variables in parallel. This means you cannot use the result of one binding to define another in the same let block. let*, on the other hand, binds variables sequentially. This allows us to define trimmed using the value of digits-only, which was defined just one line above. It's perfect for creating processing pipelines.

How could I modify this code to provide more specific error messages?

You could refactor the clean function to use a cond statement instead of if. Each clause in the cond could test one validation rule and, if it fails, return a specific error message string like "Error: Number must be 10 digits" or "Error: Invalid area code". This would make the function more user-friendly for API responses.

Is using regular expressions a better alternative?

Regular expressions could also solve this problem, potentially in a more compact way. A single regex could capture a valid NANP number. However, the functional approach used here is often more readable and easier to debug for developers who are not regex experts. Each step is explicit. The performance difference is likely negligible for most applications.

What does the `#` character mean in `#'digit-char-p`?

The #' syntax is a shorthand for the function special operator in Common Lisp. It tells the compiler that you are referring to the function object itself, rather than trying to call the function. This is necessary when passing a function as an argument to another function, like we do with remove-if-not.

Why is it important to check that area codes and exchange codes don't start with 0 or 1?

Historically, a '0' in the second position of an area code indicated a special code for a whole state or region (e.g., 800 for toll-free), and a '1' was used to signal a long-distance call. Similarly, '0' and '1' were reserved for operator and system use in exchange codes. While some of these rules have been relaxed, they remain a standard part of validating a "classic" NANP number and help filter out many obviously invalid inputs.


Conclusion: The Power of Clean Data

We have successfully built a robust phone number validation and cleaning utility in Common Lisp. By breaking a complex problem into a series of small, single-purpose functions, we created a solution that is not only correct but also exceptionally clear, maintainable, and testable. This module demonstrates the power of functional programming for data transformation tasks, turning chaotic, unreliable user input into a standardized, valuable asset.

The principles applied here—understanding the domain rules, sanitizing input, and validating data against strict criteria—are fundamental to building reliable software. Whether you are working with phone numbers, email addresses, or any other form of user data, this disciplined approach will save you from countless bugs and operational headaches down the line.

Disclaimer: The code in this article is based on modern Common Lisp standards and is expected to work with current implementations like SBCL 2.4+ or CCL. The NANP rules themselves can evolve, so always verify against the latest specifications for production systems.

Ready to tackle more challenges? Explore the full Common Lisp learning path on kodikra.com and continue your journey to mastery. Or, see where this module fits in our broader curriculum by checking out the complete kodikra learning roadmap.


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