Implementing the Luhn Algorithm

The Luhn algorithm is widely used to validate credit card numbers. It catches mistyped card numbers before they are sent to a server.

By odd and even, we mean the digit’s position, not its value.

Worked Example

Let’s validate a Visa number: 4003600000000014

Odd positions (1st, 3rd, 5th… from right):

Even positions (2nd, 4th, 6th… from right), each multiplied by 2:

This gives us:

Since , we replace it with :

Final sum:

Since the last digit of 20 is zero (it is divisible by 10), the card number is valid.

Converting Strings to Arrays

We will implement the algorithm using array operations. First, let’s convert the card number string into an array of characters.

const cnumber = "4003600000000014";

let arr = cnumber.split("");

console.log(arr);

The split() method divides a string into an array of substrings. When the separator is an empty string (''), each character becomes a separate array element.

Since the Luhn algorithm works from right to left, we can reverse the array.

arr = arr.reverse();

Transforming Arrays with map

Our array contains strings (characters), but we need numbers. The map() method transforms each element by applying a function.

arr = arr.map(convertToInt);

function convertToInt(element) {
  return parseInt(element);
}

Notice that convertToInt is passed as an argument to map(). That makes map() a higher-order function. A higher-order function is a function that takes another function as an argument, or returns a function.

Arrow Functions and Method Chaining

JavaScript has a shorter syntax for writing functions, called arrow functions. The arrow function element => parseInt(element) does the same thing as our convertToInt function, in less code.

We can also chain methods together, writing one operation after another.

const cnumber = "4003600000000014";

let arr = cnumber
  .split("")
  .reverse()
  .map((element) => parseInt(element));

console.log(arr);

Reducing Arrays to a Single Value

The Luhn algorithm sums digits in specific ways. We need to reduce our array to a single value. The reduce() method does that.

const sum = arr.reduce(reducer, 0);

function reducer(accumulator, currentValue) {
  return accumulator + currentValue;
}

console.log(sum);

The reduce() method takes two arguments:

  1. A reducer function that processes each element
  2. An initial value for the accumulator (here, 0)

The reducer function receives:

Parameter Description
accumulator The running total from previous iterations
currentValue The current array element
currentIndex The element’s index (optional)

Completing the Algorithm

Now let’s implement the full Luhn algorithm in our reducer.

const cnumber = "4003600000000014";

let arr = cnumber
  .split("")
  .reverse()
  .map((element) => parseInt(element));

const sum = arr.reduce(reducer, 0);

function reducer(accumulator, currentValue, currentIndex) {
  currentIndex += 1; // account for 0-based indexing
  if (currentIndex % 2 === 0) {
    // even position
    currentValue *= 2;
    if (currentValue > 9) {
      currentValue -= 9;
    }
  }
  return accumulator + currentValue;
}

if (sum % 10 === 0) {
  console.log("Valid card number");
} else {
  console.log("Invalid card number");
}

Try it with another valid number: 6011329933655299.

Adding a Format Check

Before running the Luhn algorithm, let’s add a quick format check to ensure the card number contains only digits and has the right length (13-16 digits). Update the card validation in handleFormSubmit.

  const cnumber = document.getElementById("cnumber").value;
- if (!isValid(cnumber)) {
+ if (!/^[0-9]{13,16}$/.test(cnumber) || !isValid(cnumber)) {
    window.alert("Invalid card number!");
    return;
  }

The pattern /^[0-9]{13,16}$/ ensures the input is between 13 and 16 digits. This is the valid range for most credit card numbers.

The Complete script.js

Here is our final validation code.

const submitBtn = document.getElementById("submit");
submitBtn.addEventListener("click", handleFormSubmit);

function handleFormSubmit(event) {
  event.preventDefault();

  const month = document.getElementById("month").value;
  const year = document.getElementById("year").value;
  if (new Date() > new Date(year, month - 1)) {
    window.alert("Your card is expired!");
    return;
  }

  const cvv = document.getElementById("cvv").value;
  if (!/^[0-9]{3,4}$/.test(cvv)) {
    window.alert("Invalid CVV. It must be 3 or 4 digits!");
    return;
  }

  const cnumber = document.getElementById("cnumber").value;
  if (!/^[0-9]{13,16}$/.test(cnumber) || !isValid(cnumber)) {
    window.alert("Invalid card number!");
    return;
  }

  window.alert("Thanks for the payment!");
}

function isValid(cnumber) {
  let arr = cnumber
    .split("")
    .reverse()
    .map((element) => parseInt(element));

  const sum = arr.reduce(reducer, 0);

  function reducer(accumulator, currentValue, currentIndex) {
    currentIndex += 1;
    if (currentIndex % 2 === 0) {
      currentValue *= 2;
      if (currentValue > 9) {
        currentValue -= 9;
      }
    }
    return accumulator + currentValue;
  }

  return sum % 10 === 0;
}

Notice the reducer function is defined inside isValid. In JavaScript, you can declare functions inside other functions.

Checkpoint: Commit your progress.

git add .
git commit -m "payment-03: Implement Luhn algorithm for card validation"
git push